@octane-xplat/macos-renderer 0.11.0 → 0.11.1
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/README.md +152 -8
- package/package.json +4 -3
- package/src/appearance.ts +56 -0
- package/src/appkit.d.ts +303 -30
- package/src/image.ts +92 -0
- package/src/index.ts +1112 -390
- package/src/intrinsics.d.ts +8 -2
- package/src/{layer.mjs → layer.ts} +74 -22
- package/src/presentation.d.mts +29 -0
- package/src/{presentation.mjs → presentation.ts} +102 -17
package/README.md
CHANGED
|
@@ -10,10 +10,11 @@ assets. The renderer owns views, layout, events, accessibility, and its hosted
|
|
|
10
10
|
popups and sheets. It does not load web stylesheets or implement every
|
|
11
11
|
NativeScript widget. See the [macOS harness](../../apps/macos/README.md) for
|
|
12
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.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
The full application harness has not been reverified by that test. Registered
|
|
14
|
+
action selectors match the assigned target/action strings, and the maintained
|
|
15
|
+
consumer checks direct handler dispatch — but OS-level input delivery (a live
|
|
16
|
+
AppKit `performClick`, control edits, gestures) has not been verified end to
|
|
17
|
+
end.
|
|
17
18
|
|
|
18
19
|
## Configure an app
|
|
19
20
|
|
|
@@ -38,7 +39,7 @@ import { xplatMacOS } from '@octane-xplat/cli/macos/vite'
|
|
|
38
39
|
export default defineConfig(({ mode }) => xplatMacOS(mode))
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
The async preset bundles `src/main.mjs` into `dist/package-build/main.cjs`,
|
|
42
|
+
The async preset bundles the packaged entry (`src/main.mjs` by default) into `dist/package-build/main.cjs`,
|
|
42
43
|
selects macOS platform files, registers the universal renderer, aliases
|
|
43
44
|
NativeScript compatibility imports, and externalizes only the native runtime
|
|
44
45
|
and `node:` host imports. Unsupported host imports still fail CLI packaging.
|
|
@@ -47,7 +48,7 @@ No repository paths or harness fonts are needed.
|
|
|
47
48
|
```js
|
|
48
49
|
import { xplatMacOS } from '@octane-xplat/cli/macos/vite'
|
|
49
50
|
|
|
50
|
-
export default await xplatMacOS('production', { entry: 'src/main.
|
|
51
|
+
export default await xplatMacOS('production', { entry: 'src/main.ts' })
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
Set these TypeScript options:
|
|
@@ -105,7 +106,7 @@ re-exports Octane's universal native runtime for compiler and hook imports.
|
|
|
105
106
|
|
|
106
107
|
Use [the packaging guide](../../docs/start/toolchain.md#experimental-appkit-target)
|
|
107
108
|
for `xplat.targets.macos` metadata, signing, and CLI commands. The
|
|
108
|
-
[independent fixture](test/fixtures/main.
|
|
109
|
+
[independent fixture](test/fixtures/main.ts) demonstrates a complete host
|
|
109
110
|
entry, including startup and shutdown, without importing the harness.
|
|
110
111
|
|
|
111
112
|
## Render local images and overlays
|
|
@@ -113,6 +114,8 @@ entry, including startup and shutdown, without importing the harness.
|
|
|
113
114
|
Image sources can be base64 data URIs, local file paths, or `file://` URLs.
|
|
114
115
|
An empty or unsupported source clears the old image. Remote URLs require a
|
|
115
116
|
separate download; image loading does not perform synchronous network work.
|
|
117
|
+
Uncached decodes resolve on a later run-loop turn, so a source change keeps
|
|
118
|
+
the previous image until its replacement is ready.
|
|
116
119
|
For centered contain-fit, pass `style={{ objectFit: 'contain' }}` to `Image`.
|
|
117
120
|
This scales smaller images up while preserving their aspect ratio.
|
|
118
121
|
|
|
@@ -172,13 +175,148 @@ demonstrates app-owned descriptors and packaged font bytes.
|
|
|
172
175
|
registerFontFamily('Acme Sans', [{ weight: 400, descriptor: regularFace }])
|
|
173
176
|
```
|
|
174
177
|
|
|
178
|
+
## Keep labels inside their rows
|
|
179
|
+
|
|
180
|
+
Use a single-line label with tail ellipsis for a long tab title or suggestion.
|
|
181
|
+
The application assigns the frame; AppKit redraws truncation as its width changes.
|
|
182
|
+
The full string remains in the native value for copying through application code
|
|
183
|
+
and accessibility; the renderer never replaces it with shortened text.
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
export function TabTitle(props: { title: string; width: number }) {
|
|
187
|
+
return (
|
|
188
|
+
<label
|
|
189
|
+
text={props.title}
|
|
190
|
+
maxLines={1}
|
|
191
|
+
whiteSpace="nowrap"
|
|
192
|
+
textOverflow="ellipsis"
|
|
193
|
+
style={{ width: props.width, height: 28 }}
|
|
194
|
+
/>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The supported subset is `whiteSpace="normal"` (word wrapping) or `"nowrap"`,
|
|
200
|
+
`textOverflow="clip"` or `"ellipsis"`, and non-negative integer `maxLines`.
|
|
201
|
+
`nowrap` takes precedence over a multiline limit; `maxLines={1}` also forces
|
|
202
|
+
one line. For wrapping labels, ellipsis appears on the last visible line when
|
|
203
|
+
the line limit or frame height is reached. `maxLines={0}` means unlimited
|
|
204
|
+
lines. Other values fail with an explicit renderer error; CSS whitespace
|
|
205
|
+
collapse/preservation modes are not implemented.
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
export function Suggestion(props: { title: string }) {
|
|
209
|
+
return (
|
|
210
|
+
<label
|
|
211
|
+
text={props.title}
|
|
212
|
+
whiteSpace="normal"
|
|
213
|
+
maxLines={2}
|
|
214
|
+
textOverflow="ellipsis"
|
|
215
|
+
style={{ width: 240 }}
|
|
216
|
+
/>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Updating or removing these props reapplies the native settings. Removed props
|
|
222
|
+
restore normal wrapping, unlimited lines, and clipping. An explicit multiline
|
|
223
|
+
limit (including zero) releases the renderer's implicit one-line height so
|
|
224
|
+
AppKit can measure the text; an assigned `style.height` still controls the
|
|
225
|
+
frame. With no line-limit prop, the existing default label height remains one
|
|
226
|
+
font-based line unless the application supplies its own height or line height.
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
export function UpdatingTitle(props: { title: string; compact: boolean }) {
|
|
230
|
+
return (
|
|
231
|
+
<label
|
|
232
|
+
text={props.title}
|
|
233
|
+
maxLines={props.compact ? 1 : 2}
|
|
234
|
+
textOverflow={props.compact ? 'ellipsis' : undefined}
|
|
235
|
+
style={{ width: 240 }}
|
|
236
|
+
/>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Container layout channels
|
|
242
|
+
|
|
243
|
+
A container can arrange its content through direct props or an inline style
|
|
244
|
+
object. Both declarations below make the same row with a 12-point gap and
|
|
245
|
+
vertically centered content. The renderer recognizes these inputs on
|
|
246
|
+
`flexboxlayout` and `stack`; other elements retain unsupported-style diagnostics.
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
/** @jsxImportSource @octane-xplat/macos-renderer */
|
|
250
|
+
export function Rows() {
|
|
251
|
+
return <stack>
|
|
252
|
+
<flexboxlayout flexDirection="row" gap={12} alignItems="center" justifyContent="start">
|
|
253
|
+
<label text="Name" /><label text="Value" />
|
|
254
|
+
</flexboxlayout>
|
|
255
|
+
<flexboxlayout style={{ flexDirection: 'row', gap: 12, alignItems: 'center', justifyContent: 'start' }}>
|
|
256
|
+
<label text="Name" /><label text="Value" />
|
|
257
|
+
</flexboxlayout>
|
|
258
|
+
</stack>
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Inline style wins over recognized classes, which win over direct props,
|
|
263
|
+
regardless of prop order. Removing or setting an input to `null` or `undefined`
|
|
264
|
+
restores the next source. With no source, defaults are column direction, zero
|
|
265
|
+
gap, stretch alignment, and start justification. Here, removing `style.gap`
|
|
266
|
+
restores `gap-2` (8 points); removing the class then restores `gap={4}`.
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
/** @jsxImportSource @octane-xplat/macos-renderer */
|
|
270
|
+
export function ConflictingRow({ inlineGap }: { inlineGap?: number }) {
|
|
271
|
+
return <flexboxlayout gap={4} className="flex-row gap-2 items-center justify-end"
|
|
272
|
+
style={{ gap: inlineGap }}>
|
|
273
|
+
<label text="Name" /><label text="Value" />
|
|
274
|
+
</flexboxlayout>
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The supported subset is direction `row`/`column`; nonnegative numeric gaps
|
|
279
|
+
in points; alignment `start`, `end`, `center`, `stretch`, and horizontal
|
|
280
|
+
`baseline`; justification `start`, `end`, `center`, and `space-between`.
|
|
281
|
+
`flex-start`/`flex-end` are accepted aliases. `rowGap` applies to columns and
|
|
282
|
+
`columnGap` to rows; within each channel, the matching axis gap wins over `gap`.
|
|
283
|
+
The class forms are `flex-row`/`flex-col`, `gap-1/2/3/4/6`,
|
|
284
|
+
`items-start/end/center/stretch/baseline`, and `justify-start/end/center/between`.
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
/** @jsxImportSource @octane-xplat/macos-renderer */
|
|
288
|
+
export function AxisGap() {
|
|
289
|
+
return <flexboxlayout style={{ flexDirection: 'row', gap: 4, columnGap: 12 }}>
|
|
290
|
+
<label text="Name" /><label text="Value" />
|
|
291
|
+
</flexboxlayout>
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
This subset does not implement child `flexGrow`, `flexShrink`, `alignSelf`,
|
|
296
|
+
`order`, wrapping, reverse directions, or `space-around`/`space-evenly`.
|
|
297
|
+
Existing `flex-1`/`shrink-0` class approximations are not full child-flex
|
|
298
|
+
semantics. Attached parent-placement metadata remains a direct prop contract:
|
|
299
|
+
container channel normalization does not move `row`, `col`, spans, or absolute
|
|
300
|
+
placement into styles. Keep those values on the child host.
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
/** @jsxImportSource @octane-xplat/macos-renderer */
|
|
304
|
+
export function PositionedRow() {
|
|
305
|
+
return <absolutelayout>
|
|
306
|
+
<flexboxlayout left={20} top={12} style={{ width: 240, height: 40, flexDirection: 'row', gap: 12 }}>
|
|
307
|
+
<label text="Name" /><label text="Value" />
|
|
308
|
+
</flexboxlayout>
|
|
309
|
+
</absolutelayout>
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
175
313
|
## Development bundles
|
|
176
314
|
|
|
177
315
|
Call `xplatMacOS(mode, { packaged: false, hmr: true, entry: 'src/App.macos.tsx' })`
|
|
178
316
|
for the component bundle. It retains renderer and Octane imports so the
|
|
179
317
|
CLI-managed shell can share their live instances across edits. Build the
|
|
180
318
|
shell with `packaged: true`, and populate `__xplatDevModules` with the renderer
|
|
181
|
-
and Octane modules. The [independent development shell](test/fixtures/dev-shell.
|
|
319
|
+
and Octane modules. The [independent development shell](test/fixtures/dev-shell.ts)
|
|
182
320
|
shows the module mapping and retained-root HMR lifecycle.
|
|
183
321
|
|
|
184
322
|
```js
|
|
@@ -221,10 +359,16 @@ From this repository:
|
|
|
221
359
|
|
|
222
360
|
```sh
|
|
223
361
|
pnpm --filter @octane-xplat/macos-renderer test
|
|
362
|
+
pnpm --filter @octane-xplat/macos-renderer test:overflow
|
|
224
363
|
pnpm --filter @octane-xplat/macos-renderer test:packed
|
|
225
364
|
pnpm --filter @xplat/macos test:fonts
|
|
365
|
+
pnpm --filter @xplat/macos test:layout-channels
|
|
226
366
|
```
|
|
227
367
|
|
|
368
|
+
The overflow check runs an AppKit fixture with long and short strings, assigned
|
|
369
|
+
frame resizing, multiline limits, attributed line height, and prop removals. It
|
|
370
|
+
asserts native values and measurements; it does not inspect rendered pixels.
|
|
371
|
+
|
|
228
372
|
The packed check uses the renderer and CLI tarballs in a temporary app outside
|
|
229
373
|
the checkout. It checks declarations and production compilation on every
|
|
230
374
|
host; on Apple Silicon macOS it also packages and launches the app and verifies
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/macos-renderer",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Experimental AppKit renderer for Octane on macOS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,8 +39,9 @@
|
|
|
39
39
|
"octane": ">=0.6.3 <1"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"test": "node --test test/*.test.
|
|
43
|
-
"test:
|
|
42
|
+
"test": "node --experimental-strip-types --test test/*.test.ts",
|
|
43
|
+
"test:overflow": "node ../../scripts/probe.mjs run test/fixtures/text-overflow.macos.tsrx --target macos",
|
|
44
|
+
"test:packed": "node --experimental-strip-types test/packed-consumer.ts",
|
|
44
45
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
45
46
|
}
|
|
46
47
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** App-wide effective appearance. AppKit resolves both system mode and an
|
|
2
|
+
* explicit NSApplication.appearance override; colors remain app-owned. */
|
|
3
|
+
export function installAppearanceBridge(bridge: Record<string, any>): void {
|
|
4
|
+
// Preserve an installed producer across module reloads and host customization.
|
|
5
|
+
if (bridge.getColorScheme && bridge.onAppearanceChange) {
|
|
6
|
+
return
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const listeners = new Set<() => void>()
|
|
10
|
+
let observer: any
|
|
11
|
+
let application: any
|
|
12
|
+
let Observer: any
|
|
13
|
+
|
|
14
|
+
bridge.getColorScheme = () => {
|
|
15
|
+
const appearance = NSApplication.sharedApplication.effectiveAppearance
|
|
16
|
+
return appearance?.bestMatchFromAppearancesWithNames?.([
|
|
17
|
+
'NSAppearanceNameAqua',
|
|
18
|
+
'NSAppearanceNameDarkAqua',
|
|
19
|
+
]) === 'NSAppearanceNameDarkAqua'
|
|
20
|
+
? 'dark'
|
|
21
|
+
: 'light'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
bridge.onAppearanceChange = (listener: () => void) => {
|
|
25
|
+
if (!observer) {
|
|
26
|
+
// extend binds the inherited NSObject KVO selector in node-api.
|
|
27
|
+
Observer ??= NSObject.extend({
|
|
28
|
+
observeValueForKeyPathOfObjectChangeContext(keyPath: string) {
|
|
29
|
+
if (keyPath === 'effectiveAppearance') {
|
|
30
|
+
listeners.forEach((callback) => callback())
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
application = NSApplication.sharedApplication
|
|
36
|
+
observer = Observer.new()
|
|
37
|
+
application.addObserverForKeyPathOptionsContext(observer, 'effectiveAppearance', 0, null)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
listeners.add(listener)
|
|
41
|
+
let active = true
|
|
42
|
+
return () => {
|
|
43
|
+
if (!active) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
active = false
|
|
48
|
+
listeners.delete(listener)
|
|
49
|
+
if (listeners.size === 0 && observer) {
|
|
50
|
+
application.removeObserverForKeyPath(observer, 'effectiveAppearance')
|
|
51
|
+
observer = undefined
|
|
52
|
+
application = undefined
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|