@octane-xplat/macos-renderer 0.0.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Alec Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,233 @@
1
- # @octane-xplat/macos-renderer
1
+ # Render Octane components in AppKit
2
2
 
3
- This is a placeholder stub published to reserve the package name ahead of the
4
- first automated release. It has no supported API.
3
+ `@octane-xplat/macos-renderer` mounts Octane components into an existing
4
+ `NSView`. It is the experimental macOS AppKit renderer, using the
5
+ `@nativescript/macos-node-api` runtime and the CLI's JavaScriptCore host.
6
+ The supported packaging target is Apple Silicon with macOS 13.5 or later.
7
+
8
+ Your application owns startup, windows, menus, host services, and custom font
9
+ assets. The renderer owns views, layout, events, accessibility, and its hosted
10
+ popups and sheets. It does not load web stylesheets or implement every
11
+ NativeScript widget. See the [macOS harness](../../apps/macos/README.md) for
12
+ the current component and service limits. The image-crop integration test compiles and mounts the public UI barrel.
13
+ The full application harness has not been reverified by that test. A raw AppKit button
14
+ `performClick` check also exposes an existing action-selector mismatch. Native
15
+ click delivery needs separate repair and verification; the maintained consumer
16
+ checks direct handler dispatch.
17
+
18
+ ## Configure an app
19
+
20
+ Declare the renderer, `octane`, and `@nativescript/macos-node-api` as application
21
+ dependencies. The CLI also requires the runtime to be directly declared, even
22
+ though the renderer depends on it. Use one copy of Octane per application.
23
+ Declare `@octane-xplat/cli`, `@octanejs/vite-plugin`, and `vite` as development
24
+ dependencies; the preset resolves these from your app, not from the CLI.
25
+ The maintained consumer uses Octane 0.6.3, the Vite plugin 0.1.61, Vite 8.3.0,
26
+ and the runtime `0.4.4-next.2026-08-09-31292056208`.
27
+
28
+ Apply the framework's [patch setup](../cli/README.md)
29
+ before building. For TSX checking, install TypeScript; for TSRX source, also
30
+ install `@tsrx/typescript-plugin` and use `tsrx-tsc`.
31
+
32
+ Use this production Vite config:
33
+
34
+ ```js
35
+ import { defineConfig } from 'vite'
36
+ import { xplatMacOS } from '@octane-xplat/cli/macos/vite'
37
+
38
+ export default defineConfig(({ mode }) => xplatMacOS(mode))
39
+ ```
40
+
41
+ The async preset bundles `src/main.mjs` into `dist/package-build/main.cjs`,
42
+ selects macOS platform files, registers the universal renderer, aliases
43
+ NativeScript compatibility imports, and externalizes only the native runtime
44
+ and `node:` host imports. Unsupported host imports still fail CLI packaging.
45
+ No repository paths or harness fonts are needed.
46
+
47
+ ```js
48
+ import { xplatMacOS } from '@octane-xplat/cli/macos/vite'
49
+
50
+ export default await xplatMacOS('production', { entry: 'src/main.mjs' })
51
+ ```
52
+
53
+ Set these TypeScript options:
54
+
55
+ ```json
56
+ {
57
+ "compilerOptions": {
58
+ "target": "ESNext",
59
+ "module": "ESNext",
60
+ "moduleResolution": "Bundler",
61
+ "jsx": "react-jsx",
62
+ "jsxImportSource": "@octane-xplat/macos-renderer",
63
+ "customConditions": ["macos"],
64
+ "moduleSuffixes": [".macos", ""],
65
+ "lib": ["ESNext"],
66
+ "strict": true,
67
+ "skipLibCheck": true,
68
+ "noEmit": true
69
+ },
70
+ "include": ["src/**/*"]
71
+ }
72
+ ```
73
+
74
+ JSX runtime subpaths contain compiler typing declarations; use the Octane Vite
75
+ compiler to produce runnable code. The preset compiles reachable TSX/TSRX
76
+ source by default. `rules` can override that selection.
77
+
78
+ ```js
79
+ import { xplatMacOS } from '@octane-xplat/cli/macos/vite'
80
+
81
+ export default await xplatMacOS('production', {
82
+ rules: [{ include: '**/*.tsx', renderer: 'macos' }],
83
+ })
84
+ ```
85
+
86
+ ## Mount and dispose a root
87
+
88
+ In your existing host bootstrap, pass its content view to the renderer:
89
+
90
+ ```js
91
+ import { createMacOSRoot } from '@octane-xplat/macos-renderer'
92
+ import App from './App.macos.tsx'
93
+
94
+ const root = createMacOSRoot(contentView)
95
+ root.render(App, {})
96
+
97
+ // Call when the owning window or application closes.
98
+ root.unmount()
99
+ ```
100
+
101
+ `contentView` is the `NSView` supplied by your window host. Keep one root for
102
+ that view and call `unmount()` before disposing the host. Root creation does
103
+ not create a window or start the application run loop. The package also
104
+ re-exports Octane's universal native runtime for compiler and hook imports.
105
+
106
+ Use [the packaging guide](../../docs/start/toolchain.md#experimental-appkit-target)
107
+ for `xplat.targets.macos` metadata, signing, and CLI commands. The
108
+ [independent fixture](test/fixtures/main.mjs) demonstrates a complete host
109
+ entry, including startup and shutdown, without importing the harness.
110
+
111
+ ## Render local images and overlays
112
+
113
+ Image sources can be base64 data URIs, local file paths, or `file://` URLs.
114
+ An empty or unsupported source clears the old image. Remote URLs require a
115
+ separate download; image loading does not perform synchronous network work.
116
+ For centered contain-fit, pass `style={{ objectFit: 'contain' }}` to `Image`.
117
+ This scales smaller images up while preserving their aspect ratio.
118
+
119
+ `style={{ pointerEvents: 'none' }}` makes `Image` and `View` (the flexbox host)
120
+ pass hit-testing through to siblings below them. Use it for decorative crop
121
+ source/guide layers; keep gesture controls interactive. The AppKit crop test
122
+ checks those native hit-test boundaries separately from pan handler dispatch.
123
+
124
+ ## Choose fonts
125
+
126
+ Roots default to Apple's system font. Sizes and CSS numeric or named weights
127
+ map to weighted AppKit fonts. `system-ui`, `-apple-system`, and `sans-serif`
128
+ select the system font; unavailable families fall through the family stack,
129
+ then fall back to the system font.
130
+
131
+ ```tsx
132
+ // Text.macos.tsx — the AppKit renderer is configured above.
133
+ export function Title() {
134
+ return <label text="Trip notes" style={{ fontFamily: 'system-ui', fontWeight: 'bold' }} />
135
+ }
136
+ ```
137
+
138
+ For an installed family, set `style.fontFamily` on text or choose a root
139
+ default with `createMacOSRoot(contentView, { fontFamily: 'Helvetica Neue' })`.
140
+ Renderer-hosted popups and sheets inherit that root default.
141
+
142
+ ```js
143
+ // contentView is your existing window's NSView, as in the host example above.
144
+ import { createMacOSRoot } from '@octane-xplat/macos-renderer'
145
+
146
+ const root = createMacOSRoot(contentView, { fontFamily: 'Helvetica Neue' })
147
+ ```
148
+
149
+ For bundled fonts, the application loads its assets and license and creates
150
+ native descriptors. Register those descriptors before mounting:
151
+
152
+ ```js
153
+ import { createMacOSRoot, registerFontFamily } from '@octane-xplat/macos-renderer'
154
+
155
+ // regularFace and boldFace are NSFontDescriptors created from your font asset.
156
+ registerFontFamily('Acme Sans', [
157
+ { weight: 400, descriptor: regularFace },
158
+ { weight: 700, descriptor: boldFace },
159
+ ])
160
+ const root = createMacOSRoot(contentView, { fontFamily: 'Acme Sans' })
161
+ ```
162
+
163
+ Registration does not change the renderer default. Registered weights resolve
164
+ to the first face at or above the requested weight, or the heaviest face.
165
+ Invalid names, empty face lists, and missing descriptors or invalid weights are
166
+ rejected. The renderer does not read assets, register Geist automatically, or
167
+ write a font cache. The [harness font setup](../../apps/macos/src/fonts.mjs)
168
+ demonstrates app-owned descriptors and packaged font bytes.
169
+
170
+ ```js
171
+ // regularFace is an NSFontDescriptor created by the app from its loaded font.
172
+ registerFontFamily('Acme Sans', [{ weight: 400, descriptor: regularFace }])
173
+ ```
174
+
175
+ ## Development bundles
176
+
177
+ Call `xplatMacOS(mode, { packaged: false, hmr: true, entry: 'src/App.macos.tsx' })`
178
+ for the component bundle. It retains renderer and Octane imports so the
179
+ CLI-managed shell can share their live instances across edits. Build the
180
+ shell with `packaged: true`, and populate `__xplatDevModules` with the renderer
181
+ and Octane modules. The [independent development shell](test/fixtures/dev-shell.mjs)
182
+ shows the module mapping and retained-root HMR lifecycle.
183
+
184
+ ```js
185
+ // vite.component.config.mjs
186
+ import { xplatMacOS } from '@octane-xplat/cli/macos/vite'
187
+
188
+ export default await xplatMacOS('development', {
189
+ packaged: false,
190
+ hmr: true,
191
+ entry: 'src/App.macos.tsx',
192
+ })
193
+ ```
194
+
195
+ Other preset options are `root` (defaults to cwd), `entry`, `outDir`, and
196
+ `rules`. Packaged output defaults to CommonJS; component output defaults to
197
+ ES modules. Component builds preserve the output directory so they do not
198
+ delete a shell sharing it; shell configurations should also preserve that
199
+ directory when used for development. Applications may override the component format to CommonJS for
200
+ the CLI watcher, as the maintained fixture does.
201
+
202
+ ```js
203
+ import { xplatMacOS } from '@octane-xplat/cli/macos/vite'
204
+
205
+ const config = await xplatMacOS('development', {
206
+ root: process.cwd(),
207
+ entry: 'src/App.macos.tsx',
208
+ outDir: 'dist/dev',
209
+ packaged: false,
210
+ })
211
+ config.build = {
212
+ ...config.build,
213
+ lib: { entry: 'src/App.macos.tsx', formats: ['cjs'], fileName: 'app' },
214
+ }
215
+ export default config
216
+ ```
217
+
218
+ ## Verify the package
219
+
220
+ From this repository:
221
+
222
+ ```sh
223
+ pnpm --filter @octane-xplat/macos-renderer test
224
+ pnpm --filter @octane-xplat/macos-renderer test:packed
225
+ pnpm --filter @xplat/macos test:fonts
226
+ ```
227
+
228
+ The packed check uses the renderer and CLI tarballs in a temporary app outside
229
+ the checkout. It checks declarations and production compilation on every
230
+ host; on Apple Silicon macOS it also packages and launches the app and verifies
231
+ retained-root HMR. Programmatic action dispatch establishes handler behavior,
232
+ not OS input or hit-testing. Publication requires the repository's normal
233
+ [npm bootstrap and trusted-publisher setup](../../.agents/docs/releases.md).
package/package.json CHANGED
@@ -1,11 +1,46 @@
1
1
  {
2
2
  "name": "@octane-xplat/macos-renderer",
3
- "version": "0.0.1",
4
- "description": "Placeholder stub for @octane-xplat/macos-renderer — no supported API yet.",
3
+ "version": "0.10.0",
4
+ "description": "Experimental AppKit renderer for Octane on macOS",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/octane-xplat/octane-xplat",
9
9
  "directory": "packages/macos-renderer"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "LICENSE"
14
+ ],
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./src/index.d.ts",
19
+ "import": "./src/index.ts",
20
+ "default": "./src/index.ts"
21
+ },
22
+ "./intrinsics": "./src/intrinsics.d.ts",
23
+ "./jsx-runtime": {
24
+ "types": "./src/jsx-runtime.d.ts"
25
+ },
26
+ "./jsx-dev-runtime": {
27
+ "types": "./src/jsx-runtime.d.ts"
28
+ },
29
+ "./compat/native-core": "./src/native-core-shim.ts",
30
+ "./compat/escape-props": "./src/native-escape-props.ts"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "dependencies": {
36
+ "@nativescript/macos-node-api": "0.4.4-next.2026-08-09-31292056208"
37
+ },
38
+ "peerDependencies": {
39
+ "octane": ">=0.6.3 <1"
40
+ },
41
+ "scripts": {
42
+ "test": "node --test test/*.test.mjs",
43
+ "test:packed": "node test/packed-consumer.mjs",
44
+ "typecheck": "tsc --noEmit -p tsconfig.json"
10
45
  }
11
- }
46
+ }
@@ -0,0 +1,199 @@
1
+ // Ambient host globals injected by @nativescript/macos-node-api and the
2
+ // embedding Node process. The prerelease ships no generated AppKit metadata
3
+ // types, so bridge classes expose members loosely while the type names stay
4
+ // usable in our own signatures.
5
+
6
+ type NSClass<T> = {
7
+ new (...args: any[]): T
8
+ alloc(): T
9
+ [key: string]: any
10
+ }
11
+
12
+ // Objective-C classes. Instances expose bridged members as `any`; the type
13
+ // name is real so parameters and node structs can reference them.
14
+ declare class NSObject {
15
+ [key: string]: any
16
+ static alloc(): any
17
+ static new(): any
18
+ static extend(methods: object, options?: object): any
19
+ }
20
+
21
+ interface NSAttributedString extends NSObject {}
22
+ declare const NSAttributedString: NSClass<NSAttributedString>
23
+
24
+ interface NSBundle extends NSObject {}
25
+ declare const NSBundle: NSClass<NSBundle>
26
+
27
+ interface NSError extends NSObject {}
28
+ declare const NSError: NSClass<NSError>
29
+
30
+ interface NSURLRequest extends NSObject {}
31
+ declare const NSURLRequest: NSClass<NSURLRequest>
32
+
33
+ // WebKit is dlopen'd at runtime by the webview leaf.
34
+ interface WKWebView extends NSView {}
35
+ declare const WKWebView: NSClass<WKWebView>
36
+
37
+ interface WKWebViewConfiguration extends NSObject {}
38
+ declare const WKWebViewConfiguration: NSClass<WKWebViewConfiguration>
39
+
40
+ interface NSButton extends NSView {}
41
+ declare const NSButton: NSClass<NSButton>
42
+
43
+ interface NSClickGestureRecognizer extends NSObject {}
44
+ declare const NSClickGestureRecognizer: NSClass<NSClickGestureRecognizer>
45
+
46
+ interface NSColor extends NSObject {}
47
+ declare const NSColor: NSClass<NSColor>
48
+
49
+ interface NSColorSpace extends NSObject {}
50
+ declare const NSColorSpace: NSClass<NSColorSpace>
51
+
52
+ interface NSData extends NSObject {}
53
+ declare const NSData: NSClass<NSData>
54
+
55
+ interface NSDate extends NSObject {}
56
+ declare const NSDate: NSClass<NSDate>
57
+
58
+ interface NSDatePicker extends NSView {}
59
+ declare const NSDatePicker: NSClass<NSDatePicker>
60
+
61
+ interface NSEdgeInsets extends NSObject {}
62
+ declare const NSEdgeInsets: NSClass<NSEdgeInsets>
63
+
64
+ interface NSEvent extends NSObject {}
65
+ declare const NSEvent: NSClass<NSEvent>
66
+
67
+ interface NSFont extends NSObject {}
68
+ declare const NSFont: NSClass<NSFont>
69
+
70
+ interface NSFontDescriptor extends NSObject {}
71
+ declare const NSFontDescriptor: NSClass<NSFontDescriptor>
72
+
73
+ interface NSFontManager extends NSObject {}
74
+ declare const NSFontManager: NSClass<NSFontManager>
75
+
76
+ interface NSImage extends NSObject {}
77
+ declare const NSImage: NSClass<NSImage>
78
+
79
+ interface NSImageView extends NSView {}
80
+ declare const NSImageView: NSClass<NSImageView>
81
+
82
+ interface NSMenu extends NSObject {}
83
+ declare const NSMenu: NSClass<NSMenu>
84
+
85
+ interface NSMenuItem extends NSObject {}
86
+ declare const NSMenuItem: NSClass<NSMenuItem>
87
+
88
+ interface NSMutableParagraphStyle extends NSObject {}
89
+ declare const NSMutableParagraphStyle: NSClass<NSMutableParagraphStyle>
90
+
91
+ interface NSNotification extends NSObject {}
92
+ declare const NSNotification: NSClass<NSNotification>
93
+
94
+ interface NSNotificationCenter extends NSObject {}
95
+ declare const NSNotificationCenter: NSClass<NSNotificationCenter>
96
+
97
+ interface NSNumber extends NSObject {}
98
+ declare const NSNumber: NSClass<NSNumber>
99
+
100
+ interface NSPanGestureRecognizer extends NSObject {}
101
+ declare const NSPanGestureRecognizer: NSClass<NSPanGestureRecognizer>
102
+
103
+ interface NSPopover extends NSObject {}
104
+ declare const NSPopover: NSClass<NSPopover>
105
+
106
+ interface NSScrollView extends NSView {}
107
+ declare const NSScrollView: NSClass<NSScrollView>
108
+
109
+ interface NSSlider extends NSView {}
110
+ declare const NSSlider: NSClass<NSSlider>
111
+
112
+ interface NSStackView extends NSView {}
113
+ declare const NSStackView: NSClass<NSStackView>
114
+
115
+ interface NSTextField extends NSView {}
116
+ declare const NSTextField: NSClass<NSTextField>
117
+
118
+ interface NSTextView extends NSView {}
119
+ declare const NSTextView: NSClass<NSTextView>
120
+
121
+ interface NSTrackingArea extends NSObject {}
122
+ declare const NSTrackingArea: NSClass<NSTrackingArea>
123
+
124
+ interface NSURL extends NSObject {}
125
+ declare const NSURL: NSClass<NSURL>
126
+
127
+ interface NSView extends NSObject {}
128
+ declare const NSView: NSClass<NSView>
129
+
130
+ interface NSViewController extends NSObject {}
131
+ declare const NSViewController: NSClass<NSViewController>
132
+
133
+ interface NSWindow extends NSObject {}
134
+ declare const NSWindow: NSClass<NSWindow>
135
+
136
+ // Enum containers. Members resolve to `number`, so bitwise masks compose.
137
+ declare const NSBackingStoreType: { [key: string]: number }
138
+ declare const NSBezelStyle: { [key: string]: number }
139
+ declare const NSBorderType: { [key: string]: number }
140
+ declare const NSButtonType: { [key: string]: number }
141
+ declare const NSDatePickerElementFlags: { [key: string]: number }
142
+ declare const NSDatePickerStyle: { [key: string]: number }
143
+ declare const NSLayoutAttribute: { [key: string]: number }
144
+ declare const NSPopoverBehavior: { [key: string]: number }
145
+ declare const NSRectEdge: { [key: string]: number }
146
+ declare const NSStackViewDistribution: { [key: string]: number }
147
+ declare const NSStackViewGravity: { [key: string]: number }
148
+ declare const NSTextAlignment: { [key: string]: number }
149
+ declare const NSTrackingAreaOptions: { [key: string]: number }
150
+ declare const NSUserInterfaceLayoutOrientation: { [key: string]: number }
151
+ declare const NSWindowStyleMask: { [key: string]: number }
152
+
153
+ // Standalone constants.
154
+ declare const NSImageScaleProportionallyDown: number
155
+ declare const NSImageScaleProportionallyUpOrDown: number
156
+
157
+ // NSString notification names and attributed-string keys.
158
+ declare const NSPopoverDidCloseNotification: string
159
+ declare const NSViewBoundsDidChangeNotification: string
160
+ declare const NSViewFrameDidChangeNotification: string
161
+ declare const NSFontAttributeName: string
162
+ declare const NSForegroundColorAttributeName: string
163
+ declare const NSParagraphStyleAttributeName: string
164
+ declare const NSURLErrorFailingURLErrorKey: string
165
+
166
+ // Protocol objects (opaque bridge references used in ObjCProtocols arrays).
167
+ declare const NSTextViewDelegate: any
168
+ declare const NSWindowDelegate: any
169
+ declare const WKNavigationDelegate: any
170
+
171
+ // Runtime framework loader injected by the bridge host.
172
+ declare function dlopen(path: string, flags: number): unknown
173
+
174
+ // Cross-package bridge published by this renderer for AppKit-specific leaves
175
+ // (hover, popups, menus, sheets) and the VirtualList offset channel.
176
+ declare var __xplatAppKit: {
177
+ observeHover?: (view: NSView, handlers: any) => void
178
+ showAnchoredPopup?: (options: any) => void
179
+ attachContextMenu?: (view: NSView, options: any, onSelect: any) => void
180
+ attachDatePicker?: (view: NSView, options: any, onChange: any) => void
181
+ presentSheet?: (view: NSView, options: any) => void
182
+ [key: string]: any
183
+ }
184
+
185
+ declare var __xplatVlistOffsets: { [key: string]: number } | undefined
186
+
187
+ // Node host globals.
188
+ declare const performance: { now(): number }
189
+ declare function setTimeout(callback: () => void, ms?: number): unknown
190
+ declare function clearTimeout(timer: unknown): void
191
+ declare function queueMicrotask(callback: () => void): void
192
+ declare const console: {
193
+ log(...args: any[]): void
194
+ warn(...args: any[]): void
195
+ error(...args: any[]): void
196
+ [key: string]: any
197
+ }
198
+
199
+ declare const process: { env: { [key: string]: string | undefined } }
package/src/fonts.ts ADDED
@@ -0,0 +1,100 @@
1
+ interface FontFace {
2
+ weight: number
3
+ descriptor: NSFontDescriptor
4
+ }
5
+
6
+ const families = new Map<string, FontFace[]>()
7
+ const weights = [
8
+ [100, -0.8, 1],
9
+ [200, -0.6, 2],
10
+ [300, -0.4, 3],
11
+ [400, 0, 5],
12
+ [500, 0.23, 6],
13
+ [600, 0.3, 8],
14
+ [700, 0.4, 9],
15
+ [800, 0.56, 10],
16
+ [900, 0.62, 12],
17
+ ]
18
+
19
+ const aliases = new Map([
20
+ ['thin', 100],
21
+ ['extralight', 200],
22
+ ['light', 300],
23
+ ['normal', 400],
24
+ ['regular', 400],
25
+ ['medium', 500],
26
+ ['semibold', 600],
27
+ ['bold', 700],
28
+ ['extrabold', 800],
29
+ ['black', 900],
30
+ ])
31
+
32
+ const systemFamilies = new Set(['system-ui', '-apple-system', 'sans-serif'])
33
+
34
+ /** Associate an app-owned family with native descriptors, without loading assets. */
35
+ export function registerFontFamily(family: string, faces: readonly FontFace[]) {
36
+ if (!family?.trim() || !faces?.length) {
37
+ throw new Error('A font family needs a name and at least one face')
38
+ }
39
+
40
+ const sorted = faces
41
+ .map(({ weight, descriptor }) => {
42
+ if (!Number.isFinite(weight) || !descriptor) {
43
+ throw new Error('A font face needs a finite weight and a native descriptor')
44
+ }
45
+
46
+ return { weight, descriptor }
47
+ })
48
+ .sort((left, right) => left.weight - right.weight)
49
+
50
+ const name = family.trim().toLowerCase()
51
+ if (systemFamilies.has(name)) {
52
+ throw new Error('System font family names cannot be registered')
53
+ }
54
+
55
+ families.set(name, sorted)
56
+ }
57
+
58
+ /** Resolve an explicit family stack, falling back to the weighted system font. */
59
+ export function resolveFont(size: number, weight: number | string = 400, family?: string): NSFont {
60
+ const parsed = aliases.get(String(weight).toLowerCase()) ?? Number(weight)
61
+ const numeric = Number.isFinite(parsed) ? Math.min(900, Math.max(100, parsed)) : 400
62
+ const [, nativeWeight, managerWeight] = weights.find(([limit]) => numeric <= limit)!
63
+ const systemFont = () => {
64
+ const font = NSFont.systemFontOfSizeWeight(Number(size), nativeWeight)
65
+ if (!font) {
66
+ throw new Error('Failed to create the AppKit system font face')
67
+ }
68
+
69
+ return font
70
+ }
71
+
72
+ for (const name of String(family ?? '').split(',')) {
73
+ const trimmed = name.trim().replace(/^['"]|['"]$/g, '')
74
+ if (!trimmed) {
75
+ continue
76
+ }
77
+
78
+ if (systemFamilies.has(trimmed.toLowerCase())) {
79
+ return systemFont()
80
+ }
81
+
82
+ const faces = families.get(trimmed.toLowerCase())
83
+ if (faces) {
84
+ const face = faces.find((face) => numeric <= face.weight) ?? faces.at(-1)!
85
+ const font = NSFont.fontWithDescriptorSize(face.descriptor, Number(size))
86
+ if (font) {
87
+ return font
88
+ }
89
+ } else {
90
+ const manager = NSFontManager.sharedFontManager
91
+ const font = manager.fontWithFamilyTraitsWeightSize(trimmed, 0, managerWeight, Number(size))
92
+
93
+ if (font) {
94
+ return font
95
+ }
96
+ }
97
+ }
98
+
99
+ return systemFont()
100
+ }
package/src/image.ts ADDED
@@ -0,0 +1,23 @@
1
+ /** Local and embedded sources avoid synchronous network work on AppKit's main thread. */
2
+ export function loadImage(source: unknown): NSImage | null {
3
+ const src = String(source ?? '')
4
+ const match = /^data:[^,]*;base64,(.+)$/s.exec(src)
5
+ if (match) {
6
+ const data = NSData.alloc().initWithBase64EncodedStringOptions(match[1], 0)
7
+ return data ? NSImage.alloc().initWithData(data) : null
8
+ }
9
+
10
+ if (src.startsWith('file://')) {
11
+ return NSImage.alloc().initWithContentsOfURL(NSURL.URLWithString(src))
12
+ }
13
+
14
+ if (src && !/^[a-z][a-z\d+.-]*:/i.test(src)) {
15
+ return NSImage.alloc().initWithContentsOfFile(src)
16
+ }
17
+
18
+ if (src) {
19
+ console.warn('[macos-image] Unsupported source; use a base64 data URI or local file')
20
+ }
21
+
22
+ return null
23
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import '@nativescript/macos-node-api'
2
+
3
+ export * from 'octane/universal/native'
4
+ export interface MacOSRootOptions {
5
+ /** Default font family for this root and its renderer-hosted overlays. */
6
+ fontFamily?: string
7
+ }
8
+
9
+ /** Map an application-registered family to its weighted native faces. */
10
+ export declare function registerFontFamily(
11
+ family: string,
12
+ faces: readonly { weight: number; descriptor: NSFontDescriptor }[],
13
+ ): void
14
+
15
+ export declare function createMacOSRoot(
16
+ hostView: NSView,
17
+ options?: MacOSRootOptions,
18
+ ): import('octane/universal/native').UniversalRoot