@octane-xplat/macos-renderer 0.11.0 → 0.12.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/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. 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.
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.mjs' })
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.mjs) demonstrates a complete host
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.mjs)
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.0",
3
+ "version": "0.12.0",
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.mjs",
43
- "test:packed": "node test/packed-consumer.mjs",
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
+ }