react-x11 2.6.1 → 2.8.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.
Files changed (59) hide show
  1. package/README.md +5 -3
  2. package/package.json +10 -3
  3. package/src/activate.js +12 -0
  4. package/src/anchor.js +6 -0
  5. package/src/appearance.js +351 -28
  6. package/src/appearancehooks.js +5 -2
  7. package/src/application.js +41 -0
  8. package/src/cocoa/app.js +367 -3
  9. package/src/cocoa/bezels.js +51 -1
  10. package/src/cocoa/dnd.js +358 -0
  11. package/src/cocoa/dock.js +39 -0
  12. package/src/cocoa/filepanels.js +155 -0
  13. package/src/cocoa/fonts.js +93 -2
  14. package/src/cocoa/globalmenu.js +41 -33
  15. package/src/cocoa/notifications.js +244 -0
  16. package/src/cocoa/permissions.js +74 -0
  17. package/src/cocoa/presenter.js +274 -33
  18. package/src/cocoa/promotion.js +708 -0
  19. package/src/cocoa/statusitem.js +112 -0
  20. package/src/cocoa/window.js +113 -4
  21. package/src/components/Button.js +20 -1
  22. package/src/components/Checkbox.js +17 -2
  23. package/src/components/Menu.js +108 -38
  24. package/src/components/Radio.js +17 -2
  25. package/src/components/Select.js +159 -27
  26. package/src/components/Switch.js +8 -1
  27. package/src/components/native.js +99 -0
  28. package/src/components/theme.js +37 -20
  29. package/src/desktopsettings.js +34 -2
  30. package/src/dnd.js +137 -11
  31. package/src/errors.js +6 -3
  32. package/src/filedialog.js +81 -16
  33. package/src/index.d.ts +29 -2
  34. package/src/index.js +17 -0
  35. package/src/launcher.js +170 -0
  36. package/src/launcherhooks.js +81 -0
  37. package/src/nodes.js +604 -37
  38. package/src/notificationhooks.js +56 -0
  39. package/src/notifications.js +558 -0
  40. package/src/palette.js +144 -8
  41. package/src/permissionhooks.js +89 -0
  42. package/src/permissions.js +196 -0
  43. package/src/style.d.ts +10 -4
  44. package/src/style.js +1 -0
  45. package/src/styles.js +161 -15
  46. package/src/textselection.js +1 -4
  47. package/src/trayhooks.js +90 -0
  48. package/src/types/appearance.d.ts +24 -0
  49. package/src/types/components.d.ts +10 -0
  50. package/src/types/elements.d.ts +14 -0
  51. package/src/types/events.d.ts +14 -0
  52. package/src/types/filedialog.d.ts +18 -7
  53. package/src/types/launcher.d.ts +43 -0
  54. package/src/types/notifications.d.ts +113 -0
  55. package/src/types/permissions.d.ts +100 -0
  56. package/src/types/style.d.ts +30 -2
  57. package/src/types/system.d.ts +5 -3
  58. package/src/types/tray.d.ts +54 -0
  59. package/src/windowid.js +23 -0
package/src/styles.js CHANGED
@@ -449,6 +449,12 @@ const STYLE_PROPS = new Set([
449
449
  // layout nor paint — the one thing it must never do is grow the visuals,
450
450
  // since the whole point is a 24px target under a 16px control.
451
451
  'hitSlop',
452
+ // Declares this node a container that the `'@container …'` blocks below
453
+ // it ask about — CSS's `container-type` and `container-name` in one
454
+ // property: `true` answers the unnamed queries, a name answers those and
455
+ // the ones that say it. Neither layout nor paint: it changes nothing about
456
+ // this node, only what the blocks under it resolve against.
457
+ 'container',
452
458
  ]);
453
459
 
454
460
  export const isStyleProp = (name) => STYLE_PROPS.has(name);
@@ -478,6 +484,25 @@ const isState = (key) => key.charCodeAt(0) === 58; /* ':' */
478
484
  */
479
485
  const SIZE_QUERY = /^@(width|height)\s*(>=|<=|>|<)\s*(\d+(?:\.\d+)?)$/;
480
486
 
487
+ /**
488
+ * Container queries: `'@container width >= 400'`, or
489
+ * `'@container sidebar width >= 400'` naming the container. Where a size
490
+ * query asks about the window, this asks about the box the node is inside
491
+ * — the nearest ancestor whose style declares `container` (any container
492
+ * for the unnamed form; for the named form, the one carrying that name,
493
+ * however many nearer containers it has to reach past).
494
+ *
495
+ * The same operators and the same logical pixels as a size query, and the
496
+ * same licence: a container block may set layout properties. It differs in
497
+ * *when* it is answered — a container's size is what a layout pass
498
+ * produces, not what one starts from, so the blocks are resolved after the
499
+ * pass and the tree laid out once more if an answer moved
500
+ * (nodes.js, `_resolveContainerQueries`).
501
+ */
502
+ const CONTAINER_QUERY =
503
+ /^@container(?:\s+([A-Za-z_][\w-]*))?\s+(width|height)\s*(>=|<=|>|<)\s*(\d+(?:\.\d+)?)$/;
504
+ const CONTAINER_NAME = /^[A-Za-z_][\w-]*$/;
505
+
481
506
  /**
482
507
  * Capability queries: `'@supports transparency'`. Where a size query asks
483
508
  * about the window, this asks about the *server* — what will actually be
@@ -499,12 +524,23 @@ function parseQuery(key) {
499
524
  let q = parsedQueries.get(key);
500
525
  if (q === undefined) {
501
526
  const size = SIZE_QUERY.exec(key);
502
- const supports = size ? null : SUPPORTS_QUERY.exec(key);
527
+ const container = size ? null : CONTAINER_QUERY.exec(key);
528
+ const supports = size || container ? null : SUPPORTS_QUERY.exec(key);
503
529
  q = size
504
530
  ? { kind: 'size', axis: size[1], op: size[2], value: Number(size[3]) }
505
- : supports
506
- ? { kind: 'supports', feature: supports[1] }
507
- : null;
531
+ : container
532
+ ? {
533
+ kind: 'container',
534
+ // `''` is the unnamed query, and the key the nearest container
535
+ // of any name answers under
536
+ name: container[1] ?? '',
537
+ axis: container[2],
538
+ op: container[3],
539
+ value: Number(container[4]),
540
+ }
541
+ : supports
542
+ ? { kind: 'supports', feature: supports[1] }
543
+ : null;
508
544
  parsedQueries.set(key, q);
509
545
  }
510
546
  return q;
@@ -539,6 +575,69 @@ export const styleHasSizeQueries = (style) => hasQueryOfKind(style, 'size');
539
575
  export const styleHasSupportsQueries = (style) =>
540
576
  hasQueryOfKind(style, 'supports');
541
577
 
578
+ /** Re-resolved after a layout pass moved a container the style asks about. */
579
+ export const styleHasContainerQueries = (style) =>
580
+ hasQueryOfKind(style, 'container');
581
+
582
+ /**
583
+ * Which kinds of `@` block a style carries, in **one** pass over its keys —
584
+ * a mask of the three below, or 0. What `_syncStyle` asks of every node on
585
+ * every restyle, so it is asked once rather than once per kind.
586
+ */
587
+ export const QUERY_SIZE = 1;
588
+ export const QUERY_SUPPORTS = 2;
589
+ export const QUERY_CONTAINER = 4;
590
+ const QUERY_KIND_BITS = {
591
+ size: QUERY_SIZE,
592
+ supports: QUERY_SUPPORTS,
593
+ container: QUERY_CONTAINER,
594
+ };
595
+ export function queryKinds(style) {
596
+ let kinds = 0;
597
+ for (const key of Object.keys(style)) {
598
+ if (!isQuery(key)) continue;
599
+ const q = parseQuery(key);
600
+ if (q) kinds |= QUERY_KIND_BITS[q.kind];
601
+ }
602
+ return kinds;
603
+ }
604
+
605
+ /** The container names a style asks about — `''` for the unnamed blocks —
606
+ * or null when it asks about none. */
607
+ export function containerQueryNames(style) {
608
+ let names = null;
609
+ for (const key of Object.keys(style)) {
610
+ if (!isQuery(key)) continue;
611
+ const q = parseQuery(key);
612
+ if (q?.kind === 'container') (names ??= new Set()).add(q.name);
613
+ }
614
+ return names;
615
+ }
616
+
617
+ /**
618
+ * Which of a style's container blocks match, as one string with a character
619
+ * per block in declaration order. What a node remembers between layout
620
+ * passes so a re-resolution runs only when an answer actually moved, and
621
+ * what the oscillation check compares (nodes.js `_resolveContainerQueries`).
622
+ */
623
+ export function containerAnswers(style, containers) {
624
+ let out = '';
625
+ for (const key of Object.keys(style)) {
626
+ if (!isQuery(key)) continue;
627
+ const q = parseQuery(key);
628
+ if (q?.kind !== 'container') continue;
629
+ out += containers && sizeMatches(q, containers[q.name]) ? '1' : '0';
630
+ }
631
+ return out;
632
+ }
633
+
634
+ /** A style value is a container declaration: `true`, a name, or `false` to
635
+ * take one back from a style earlier in the array. */
636
+ export const isContainerDeclaration = (value) =>
637
+ value === true ||
638
+ value === false ||
639
+ (typeof value === 'string' && CONTAINER_NAME.test(value));
640
+
542
641
  /**
543
642
  * Merge the query blocks that match, in declaration order, over the base.
544
643
  * Size and capability blocks resolve in one pass so that ordering between
@@ -558,23 +657,39 @@ export function resolveSizeQueries(style, size) {
558
657
  return size ? resolveQueries(style, { size }) : style;
559
658
  }
560
659
 
561
- export function resolveQueries(style, { size = null, supports = null } = {}) {
660
+ export function resolveQueries(
661
+ style,
662
+ { size = null, supports = null, containers = null } = {},
663
+ ) {
562
664
  let out = style;
563
665
  for (const key of Object.keys(style)) {
564
666
  if (!isQuery(key)) continue;
565
667
  const q = parseQuery(key);
566
- if (!q) continue;
567
- const hit =
568
- q.kind === 'size'
569
- ? size && sizeMatches(q, size)
570
- : Boolean(supports?.[q.feature]);
571
- if (!hit) continue;
668
+ if (!q || !queryMatches(q, size, supports, containers)) continue;
572
669
  if (out === style) out = { ...style };
573
670
  Object.assign(out, style[key]);
574
671
  }
575
672
  return out;
576
673
  }
577
674
 
675
+ /**
676
+ * `containers` maps a container name (`''` for the unnamed query) to that
677
+ * container's size in the node's logical pixels; a name it does not hold is
678
+ * a container the node has not got — above it, or laid out yet — and the
679
+ * block does not apply. That is the rule that lets one component render
680
+ * inside and outside a `sidebar` container with the same style.
681
+ */
682
+ function queryMatches(q, size, supports, containers) {
683
+ switch (q.kind) {
684
+ case 'size':
685
+ return Boolean(size) && sizeMatches(q, size);
686
+ case 'container':
687
+ return Boolean(containers) && sizeMatches(q, containers[q.name]);
688
+ default:
689
+ return Boolean(supports?.[q.feature]);
690
+ }
691
+ }
692
+
578
693
  /**
579
694
  * The two style values that are a small language rather than a number, and
580
695
  * therefore the two that can be *wrong* rather than merely absent. Parsed in
@@ -605,7 +720,9 @@ function validateStyle(style, where) {
605
720
  if (!parseQuery(key)) {
606
721
  throw new Error(
607
722
  `react-x11: bad query "${key}" in ${where} (expected a size query ` +
608
- 'like "@width >= 600", or a capability query like ' +
723
+ 'like "@width >= 600", a container query like ' +
724
+ '"@container width >= 400" or "@container sidebar width >= 400", ' +
725
+ 'or a capability query like ' +
609
726
  `"@supports ${SUPPORTS_FEATURES.join('" / "@supports ')}")`,
610
727
  );
611
728
  }
@@ -641,6 +758,13 @@ function validateStyle(style, where) {
641
758
  // anything wrong, it simply never moves.
642
759
  if (key === 'animation') animationsOf(style, where);
643
760
  validateValue(key, style[key], where);
761
+ if (key === 'container' && !isContainerDeclaration(style[key])) {
762
+ throw new Error(
763
+ `react-x11: invalid container ${JSON.stringify(style[key])} in ` +
764
+ `${where} (expected true, or a name like 'sidebar' — letters, ` +
765
+ "digits, '_' and '-', not starting with a digit)",
766
+ );
767
+ }
644
768
  if (key === 'flex' && !isFlexShorthand(style[key])) {
645
769
  throw new Error(
646
770
  `react-x11: invalid flex ${JSON.stringify(style[key])} in ${where} ` +
@@ -671,10 +795,13 @@ export function flattenStyle(style, into) {
671
795
  // hoisted style across renders
672
796
  if (!into) return style;
673
797
  for (const key of Object.keys(style)) {
674
- // a state block merges with one already collected rather than replacing
675
- // it, so [{':hover': {color}}, {':hover': {backgroundColor}}] keeps both
798
+ // a state or query block merges with one already collected rather than
799
+ // replacing it, so [{':hover': {color}}, {':hover': {backgroundColor}}]
800
+ // keeps both — and so does the same '@width >= 600' key written twice
676
801
  into[key] =
677
- isState(key) && into[key] ? { ...into[key], ...style[key] } : style[key];
802
+ (isState(key) || isQuery(key)) && into[key]
803
+ ? { ...into[key], ...style[key] }
804
+ : style[key];
678
805
  }
679
806
  return into;
680
807
  }
@@ -788,6 +915,23 @@ const EASINGS = {
788
915
 
789
916
  export const EASING_NAMES = Object.freeze(Object.keys(EASINGS));
790
917
 
918
+ /**
919
+ * The same curves as cubic-bezier control points, for a presenter whose
920
+ * render server evaluates them itself (src/cocoa/presenter.js). The two
921
+ * evaluators have to agree on one declaration, and they do: each polynomial
922
+ * above and its bezier twin differ by at most 0.01 over the unit interval
923
+ * (test/style.test.js pins it). Core Animation's *named* curves are not
924
+ * these — its `easeOut` is (0, 0, 0.58, 1), 0.22 away from `ease` at
925
+ * t = 0.35 — which is why the presenter sends points rather than names. A
926
+ * new easing lands in both tables or in neither.
927
+ */
928
+ export const EASING_CONTROL_POINTS = Object.freeze({
929
+ linear: Object.freeze([0, 0, 1, 1]),
930
+ 'ease-in': Object.freeze([0.32, 0, 0.67, 0]),
931
+ 'ease-out': Object.freeze([0.33, 1, 0.68, 1]),
932
+ 'ease-in-out': Object.freeze([0.65, 0, 0.35, 1]),
933
+ });
934
+
791
935
  /** A value that is still a `$token` reference, or mentions one. */
792
936
  const unresolvedValue = (v) => isToken(v) || mentionsToken(v);
793
937
 
@@ -1119,6 +1263,8 @@ export function tint(color, alpha) {
1119
1263
  // ease-out cubic: fast to start, settles gently — the shape almost every UI
1120
1264
  // toolkit defaults to for state changes
1121
1265
  export const ease = (t) => 1 - (1 - t) ** 3;
1266
+ /** …and the same curve as control points, for the presenter (above). */
1267
+ export const TRANSITION_CONTROL_POINTS = EASING_CONTROL_POINTS['ease-out'];
1122
1268
 
1123
1269
  /**
1124
1270
  * Theme tokens. A style value of `'$name'` resolves against the nearest
@@ -32,8 +32,6 @@ import { callHandler } from './errors.js';
32
32
  import { lastInputTime } from './inputtime.js';
33
33
  import { ctrlChordLetter } from './keysyms.js';
34
34
  import { codePoints, wordRangeAt } from './textrange.js';
35
- import { tint } from './styles.js';
36
-
37
35
  // --- who is showing a selection ------------------------------------------
38
36
  //
39
37
  // One node per app: a `<textinput>`, or the surface below. The registry is
@@ -316,8 +314,7 @@ export class TextSelection {
316
314
  * the drag, the keys, and the programmatic `setSelection`.
317
315
  */
318
316
  apply() {
319
- const color =
320
- this.node.props.selectionColor ?? tint(this.node.theme.accent, 0.35);
317
+ const color = this.node.props.selectionColor ?? this.node.theme.selection;
321
318
  const next = this.rangesOf(this.ordered());
322
319
  let changed = false;
323
320
  for (const [node, range] of next) {
@@ -0,0 +1,90 @@
1
+ // `useTray()` — an icon in the system tray for as long as a component is
2
+ // mounted, on the backends that have one.
3
+ //
4
+ // Today that is the cocoa backend's menu-bar extra (`NSStatusItem`,
5
+ // src/cocoa/statusitem.js). The freedesktop counterpart, StatusNotifierItem
6
+ // over D-Bus, is react-x11#353's open question; until it lands the hook is
7
+ // inert on X11 and says so — `available: false`, and a one-time development
8
+ // note — rather than pretending, so an app can keep the feature behind the
9
+ // answer. The inert-props policy of docs/macos.md, for a hook.
10
+
11
+ import { useEffect, useRef, useState } from 'react';
12
+
13
+ import { useAppOrNull } from './appcontext.js';
14
+
15
+ let warnedInert = false;
16
+
17
+ /**
18
+ * An icon in the system tray while this component is mounted.
19
+ *
20
+ * ```jsx
21
+ * const { available } = useTray({
22
+ * icon: 'bell.badge', // an SF Symbol name, or PNG bytes
23
+ * tooltip: 'Notifications',
24
+ * menu: [{ label: 'Open', onSelect: open }, { label: 'Quit', onSelect: quit }],
25
+ * });
26
+ * ```
27
+ *
28
+ * With `menu`, a click opens it — the same `items` vocabulary `MenuBar` and
29
+ * `useDockMenu` take, an item's `onSelect` firing when picked. Without one,
30
+ * `onClick` is called with the button and the item's screen rect, which is
31
+ * where to anchor a popup of your own. `null` means no item. Every field
32
+ * follows its value while mounted; the item is removed on unmount.
33
+ *
34
+ * `available` is whether this backend has a tray at all: false on X11
35
+ * today (#353), and the honest answer to branch on.
36
+ */
37
+ export function useTray(options) {
38
+ const app = useAppOrNull();
39
+ const available = typeof app?.createStatusItem === 'function';
40
+ const itemRef = useRef(null);
41
+ const [rect] = useState(null);
42
+
43
+ // the options a click or a pick reads are the current render's, not the
44
+ // ones the item was created with three minutes ago
45
+ const live = useRef(options);
46
+ live.current = options;
47
+
48
+ useEffect(() => {
49
+ if (!available) {
50
+ if (process.env.NODE_ENV !== 'production' && !warnedInert && app) {
51
+ warnedInert = true;
52
+ console.warn(
53
+ 'react-x11: useTray() is inert on this backend — the freedesktop ' +
54
+ 'tray (StatusNotifierItem) is not implemented yet (#353). Read ' +
55
+ '`available` to keep the feature behind the answer.',
56
+ );
57
+ }
58
+ return undefined;
59
+ }
60
+ if (!options) return undefined;
61
+ const item = app.createStatusItem({
62
+ ...options,
63
+ onClick: (ev) => live.current?.onClick?.(ev),
64
+ });
65
+ itemRef.current = item;
66
+ return () => {
67
+ itemRef.current = null;
68
+ item.remove();
69
+ };
70
+ // Recreated only when the item comes or goes: the fields patch in
71
+ // place below, and an `options` object rebuilt every render must not
72
+ // rebuild the item every render.
73
+ }, [app, available, options == null]);
74
+
75
+ useEffect(() => {
76
+ const item = itemRef.current;
77
+ if (!item || !options) return;
78
+ item.update({ ...options, onClick: (ev) => live.current?.onClick?.(ev) });
79
+ }, [
80
+ options?.icon,
81
+ options?.title,
82
+ options?.tooltip,
83
+ options?.visible,
84
+ options?.template,
85
+ options?.length,
86
+ options?.menu,
87
+ ]);
88
+
89
+ return { available, rect };
90
+ }
@@ -3,6 +3,7 @@
3
3
  * and reduced motion. See docs/appearance.md.
4
4
  */
5
5
 
6
+ import type { Theme } from './components.js';
6
7
  import type { NtkApp } from './nodes.js';
7
8
 
8
9
  /**
@@ -32,6 +33,29 @@ export interface SystemAppearance {
32
33
  * own brand colour rather than to grey.
33
34
  */
34
35
  readonly accent: string | null;
36
+ /**
37
+ * The ink the desktop writes on its accent — `'#ffffff'` on macOS, whatever
38
+ * the accent — or **null** where the source names a fill and nothing about
39
+ * what goes on it (the portal). The built-in palette uses it as
40
+ * `accentText`, and picks the legible ink by contrast where it is null.
41
+ */
42
+ readonly accentText: string | null;
43
+ /**
44
+ * The fill the desktop puts under a selected menu or list row — on macOS
45
+ * `selectedContentBackgroundColor`, a darker cut of the accent — or
46
+ * **null** where nothing names one. The built-in palette uses it as
47
+ * `hoverBackground`, and the accent itself where it is null.
48
+ */
49
+ readonly selection: string | null;
50
+ /**
51
+ * The desktop's whole palette as react-x11 tokens, where the desktop can
52
+ * name one: on macOS the semantic colours AppKit paints its own windows
53
+ * and controls with, flattened over the window ground. The built-in
54
+ * palette merges it over the scheme's own, so an app that says nothing
55
+ * about colour comes up in the desktop's greys, inks and status colours.
56
+ * Null from the portal and XSETTINGS, which name no such thing.
57
+ */
58
+ readonly palette: Readonly<Partial<Theme>> | null;
35
59
  readonly contrast: 'normal' | 'high';
36
60
  readonly reducedMotion: boolean;
37
61
  /**
@@ -81,6 +81,16 @@ export interface Theme {
81
81
  /** The keyboard focus ring every focusable node under this palette draws
82
82
  * on `:focus-visible` — read by the renderer, not by the widgets. */
83
83
  focusRing: string;
84
+ /** The highlight behind selected text: a tint of `accent` unless a
85
+ * palette names it. A desktop that names one — macOS's Highlight colour —
86
+ * gives an opaque fill here. */
87
+ selection: string;
88
+ /** The insertion caret, or `null` for the text's own colour, which is what
89
+ * a caret is unless the desktop says otherwise. */
90
+ caret: string | null;
91
+ /** Ink for a link. Not the accent: a link is blue everywhere, as a note
92
+ * is. */
93
+ link: string;
84
94
  focusRingWidth: number;
85
95
  focusRingOffset: number;
86
96
  radius: number;
@@ -23,6 +23,7 @@ import type {
23
23
  SubmitEvent,
24
24
  SyntheticEvent,
25
25
  ViewportEvent,
26
+ LayoutEvent,
26
27
  WheelEvent,
27
28
  WindowResizeEvent,
28
29
  } from './events.js';
@@ -188,6 +189,15 @@ export interface DrawnProps<T = DrawnNode>
188
189
  SelectionProps<T>,
189
190
  EventHandlers<T> {
190
191
  ref?: Ref<T>;
192
+ /**
193
+ * After a layout pass moved or resized this element: `{x, y, width,
194
+ * height}` in logical pixels, the position within the parent as laid out
195
+ * — so scrolling does not fire it. Once after the first layout, then only
196
+ * on change; deferred past the pass, so `setState` in it is safe. The
197
+ * seam for a decision that is not a style; where it is one, a
198
+ * `'@container'` block answers in the same frame (docs/styling.md).
199
+ */
200
+ onLayout?: (ev: LayoutEvent) => void;
191
201
  /**
192
202
  * Zoom this subtree, CSS `zoom` rather than a transform: every length
193
203
  * under here — and this element's *own* style — is multiplied by it, so
@@ -348,6 +358,10 @@ export interface WindowProps
348
358
  *
349
359
  * Applied before the window is mapped, which is the only way to open
350
360
  * already fullscreen rather than flashing at the normal size first.
361
+ *
362
+ * On the cocoa backend `demands_attention` bounces the Dock icon until
363
+ * the app is activated and is cancelled when the state is removed; the
364
+ * other names have no verb in the bridge yet and resolve to nothing.
351
365
  */
352
366
  states?: WindowStateName[];
353
367
  /** Sugar for `states={['fullscreen']}`; they union. */
@@ -335,6 +335,20 @@ export interface ScrollEvent {
335
335
  viewportHeight: number;
336
336
  }
337
337
 
338
+ /**
339
+ * `onLayout` on any drawn element: the rect a layout pass gave it, in its
340
+ * own logical pixels — `x`/`y` the position **within the parent as laid
341
+ * out**, which scrolling does not move. Reported after the first layout and
342
+ * then only when it changes, deferred past the pass so state set from it is
343
+ * safe. See docs/react-features.md#measuring-a-node.
344
+ */
345
+ export interface LayoutEvent {
346
+ x: number;
347
+ y: number;
348
+ width: number;
349
+ height: number;
350
+ }
351
+
338
352
  /** `<box onViewport>` — fired from layout, not from scrolling. */
339
353
  export interface ViewportEvent {
340
354
  width: number;
@@ -25,20 +25,26 @@ export interface AbortSignalLike {
25
25
  removeEventListener(type: 'abort', listener: () => void): void;
26
26
  }
27
27
 
28
- /** Which rung of the ladder answered — or would. */
29
- export type FileDialogBackend = 'portal' | 'osascript' | 'builtin';
28
+ /**
29
+ * Which rung of the ladder answered — or would. `'cocoa'` is the native
30
+ * `NSOpenPanel`/`NSSavePanel` on the cocoa backend; `'osascript'` is macOS on
31
+ * the X11 backend.
32
+ */
33
+ export type FileDialogBackend = 'cocoa' | 'portal' | 'osascript' | 'builtin';
30
34
 
31
35
  /**
32
36
  * One entry in a dialog's type filter.
33
37
  *
34
38
  * Give `extensions` where you can: they translate to every backend. MIME types
35
- * reach the portal exactly and macOS not at all see docs/filedialog.md.
39
+ * reach the portal and the native macOS panel exactly, and `osascript` not at
40
+ * all — see docs/filedialog.md.
36
41
  */
37
42
  export interface FileFilter {
38
43
  name: string;
39
44
  /** `['png', 'jpg']` — with or without the leading dot. */
40
45
  extensions?: string[];
41
- /** `['image/png']`. Portal only. */
46
+ /** `['image/png']`. The portal and the native macOS panel; dropped by
47
+ * `osascript`. */
42
48
  mimeTypes?: string[];
43
49
  }
44
50
 
@@ -65,11 +71,16 @@ export interface FileDialogOptions {
65
71
  acceptLabel?: string;
66
72
  /**
67
73
  * The window the dialog belongs to. `transientFor` for the built-in dialog,
68
- * `parent_window` for the portal; macOS has no cross-process equivalent and
69
- * ignores it. Worth the one line without it the dialog floats.
74
+ * `parent_window` for the portal, and the window the native macOS panel is
75
+ * a **sheet** on; `osascript` has no cross-process equivalent and ignores
76
+ * it. Worth the one line — without it the dialog floats, and on the cocoa
77
+ * backend a panel with no window is app-modal and blocks the process until
78
+ * it is dismissed.
70
79
  */
71
80
  parentWindow?: WindowTarget;
72
- /** Abort the dialog. Closes the portal request or kills `osascript`. */
81
+ /** Abort the dialog. Closes the portal request, dismisses the native
82
+ * panel, or kills `osascript`; the promise rejects with the signal's
83
+ * reason. */
73
84
  signal?: AbortSignalLike;
74
85
  /**
75
86
  * Force a rung instead of taking the best available one. The seam for a
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The launcher's view of the app: a badge on its icon, and the Dock menu.
3
+ * See docs/desktop.md "The launcher".
4
+ */
5
+
6
+ import type { MenuItem } from './components.js';
7
+ import type { NtkApp } from './nodes.js';
8
+
9
+ /**
10
+ * What a badge shows. A number shows on both backends; a string shows on
11
+ * macOS and is a visible count of nothing on Linux, whose protocol carries
12
+ * only a count. `0`, `null`, `''`, `false` and `undefined` all clear it.
13
+ */
14
+ export type BadgeValue = number | string | null | false | undefined;
15
+
16
+ export interface SetBadgeOptions {
17
+ /** The connection whose icon to badge, when there are several. */
18
+ app?: NtkApp;
19
+ }
20
+
21
+ /**
22
+ * Show `value` on the app's icon, or clear it.
23
+ *
24
+ * `NSDockTile.badgeLabel` on the cocoa backend; the
25
+ * `com.canonical.Unity.LauncherEntry` signal on Linux, which needs the
26
+ * identity `registerApplication({ appId })` establishes. Resolves to whether
27
+ * a launcher was told, and never rejects for anything about the machine.
28
+ */
29
+ export declare function setBadge(
30
+ value: BadgeValue,
31
+ options?: SetBadgeOptions,
32
+ ): Promise<boolean>;
33
+
34
+ /** {@link setBadge} while mounted; cleared on unmount. */
35
+ export declare function useBadge(value: BadgeValue): void;
36
+
37
+ /**
38
+ * The menu behind a right-click on the Dock icon, from the same item
39
+ * vocabulary `MenuBar` takes; an item's `onSelect` fires when picked.
40
+ * Installed while mounted, replaced when `items` changes, taken down on
41
+ * unmount. Inert off the cocoa backend.
42
+ */
43
+ export declare function useDockMenu(items: MenuItem[] | null): void;