react-x11 2.6.1 → 2.7.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 +5 -3
- package/package.json +10 -3
- package/src/activate.js +12 -0
- package/src/anchor.js +6 -0
- package/src/appearance.js +351 -28
- package/src/appearancehooks.js +5 -2
- package/src/application.js +41 -0
- package/src/cocoa/app.js +317 -1
- package/src/cocoa/bezels.js +51 -1
- package/src/cocoa/dnd.js +347 -0
- package/src/cocoa/dock.js +39 -0
- package/src/cocoa/filepanels.js +155 -0
- package/src/cocoa/fonts.js +93 -2
- package/src/cocoa/globalmenu.js +41 -33
- package/src/cocoa/notifications.js +244 -0
- package/src/cocoa/permissions.js +74 -0
- package/src/cocoa/presenter.js +190 -2
- package/src/cocoa/statusitem.js +112 -0
- package/src/cocoa/window.js +85 -4
- package/src/components/Button.js +20 -1
- package/src/components/Checkbox.js +17 -2
- package/src/components/Menu.js +108 -38
- package/src/components/Radio.js +17 -2
- package/src/components/Select.js +159 -27
- package/src/components/Switch.js +8 -1
- package/src/components/native.js +99 -0
- package/src/components/theme.js +37 -20
- package/src/desktopsettings.js +34 -2
- package/src/dnd.js +92 -3
- package/src/errors.js +6 -3
- package/src/filedialog.js +81 -16
- package/src/index.d.ts +17 -1
- package/src/index.js +17 -0
- package/src/launcher.js +170 -0
- package/src/launcherhooks.js +81 -0
- package/src/nodes.js +553 -35
- package/src/notificationhooks.js +56 -0
- package/src/notifications.js +558 -0
- package/src/palette.js +144 -8
- package/src/permissionhooks.js +89 -0
- package/src/permissions.js +196 -0
- package/src/style.d.ts +10 -4
- package/src/style.js +1 -0
- package/src/styles.js +161 -15
- package/src/textselection.js +1 -4
- package/src/trayhooks.js +90 -0
- package/src/types/appearance.d.ts +24 -0
- package/src/types/components.d.ts +10 -0
- package/src/types/elements.d.ts +14 -0
- package/src/types/events.d.ts +14 -0
- package/src/types/filedialog.d.ts +18 -7
- package/src/types/launcher.d.ts +43 -0
- package/src/types/notifications.d.ts +113 -0
- package/src/types/permissions.d.ts +100 -0
- package/src/types/style.d.ts +30 -2
- package/src/types/system.d.ts +5 -3
- package/src/types/tray.d.ts +54 -0
- 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
|
|
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
|
-
:
|
|
506
|
-
? {
|
|
507
|
-
|
|
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(
|
|
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",
|
|
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
|
|
675
|
-
// it, so [{':hover': {color}}, {':hover': {backgroundColor}}]
|
|
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)
|
|
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
|
package/src/textselection.js
CHANGED
|
@@ -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) {
|
package/src/trayhooks.js
ADDED
|
@@ -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;
|
package/src/types/elements.d.ts
CHANGED
|
@@ -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. */
|
package/src/types/events.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
29
|
-
|
|
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
|
|
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']`.
|
|
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
|
|
69
|
-
*
|
|
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
|
|
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;
|