react-x11 2.51.1 → 2.53.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 +94 -61
- package/docs/README.md +256 -0
- package/docs/accessibility.md +575 -0
- package/docs/appearance.md +317 -0
- package/docs/click-to-component.md +92 -0
- package/docs/clipboard.md +262 -0
- package/docs/components.md +1231 -0
- package/docs/dbus.md +205 -0
- package/docs/debugging.md +371 -0
- package/docs/desktop-calendar.md +247 -0
- package/docs/desktop.md +523 -0
- package/docs/devtools.md +133 -0
- package/docs/drag-and-drop.md +585 -0
- package/docs/ecosystem/animation.md +360 -0
- package/docs/ecosystem/data-fetching.md +186 -0
- package/docs/ecosystem/dev-tooling.md +393 -0
- package/docs/ecosystem/forms.md +277 -0
- package/docs/ecosystem/headless.md +194 -0
- package/docs/ecosystem/i18n.md +86 -0
- package/docs/ecosystem/icons.md +377 -0
- package/docs/ecosystem/layout.md +206 -0
- package/docs/ecosystem/routing.md +135 -0
- package/docs/ecosystem/state.md +344 -0
- package/docs/ecosystem/testing.md +474 -0
- package/docs/ecosystem/theming.md +494 -0
- package/docs/ecosystem.md +439 -0
- package/docs/elements.md +2079 -0
- package/docs/embedding.md +239 -0
- package/docs/events.md +742 -0
- package/docs/extending.md +1700 -0
- package/docs/eyedropper.md +164 -0
- package/docs/filedialog.md +222 -0
- package/docs/frame.md +436 -0
- package/docs/gl.md +173 -0
- package/docs/globalmenu.md +195 -0
- package/docs/macos.md +592 -0
- package/docs/notifications.md +150 -0
- package/docs/packaging.md +468 -0
- package/docs/permissions.md +143 -0
- package/docs/react-features.md +405 -0
- package/docs/remote.md +179 -0
- package/docs/scale.md +225 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1710 -0
- package/docs/system.md +519 -0
- package/docs/testing.md +494 -0
- package/docs/typescript.md +75 -0
- package/docs/uri-schemes.md +340 -0
- package/docs/wayland-backend.md +332 -0
- package/docs/windows-integrations.md +177 -0
- package/docs/windows.md +380 -0
- package/package.json +11 -3
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/dnd.js +2 -2
- package/src/filedialog.js +3 -3
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/keyboardstate.js +28 -0
- package/src/layouts.js +1 -1
- package/src/node.d.ts +10 -10
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/portal.js +31 -0
- package/src/refresh/index.js +2 -1
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +86 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/video.js +376 -0
- package/src/wayland/xkb.js +137 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
package/docs/desktop.md
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
# Desktop integration
|
|
2
|
+
|
|
3
|
+
What an app tells the desktop about itself, beyond drawing: startup
|
|
4
|
+
notification (on by default, nothing to call); the launcher's view of the app
|
|
5
|
+
— a badge, a progress bar, a bounce for attention, the Dock's menu; the tray;
|
|
6
|
+
what a password field needs to be reachable by password managers; and the one
|
|
7
|
+
switch that turns off everything here that talks over D-Bus.
|
|
8
|
+
|
|
9
|
+
The menu bar has its own page, [globalmenu.md](globalmenu.md). The other
|
|
10
|
+
direction — the desktop handing the app a `myapp://…` link, and single
|
|
11
|
+
instance — is [uri-schemes.md](uri-schemes.md); its `ctx.timestamp` is the
|
|
12
|
+
same `_TIME` suffix [`launchTimestamp()`](#launchtimestamp) parses here.
|
|
13
|
+
|
|
14
|
+
## Startup notification
|
|
15
|
+
|
|
16
|
+
A launcher that spawns an app opens a **startup sequence** — the busy cursor —
|
|
17
|
+
and closes it when the app says it is up. An app that never says so leaves the
|
|
18
|
+
cursor spinning until the desktop gives up (15 seconds on mutter), long after
|
|
19
|
+
its window is on screen, and users take that for a failed launch. The same
|
|
20
|
+
handshake carries **focus** — `_NET_WM_USER_TIME`, without which a strict
|
|
21
|
+
desktop opens the new window behind whatever the user was doing — and
|
|
22
|
+
**placement**, the workspace the launch happened on. react-x11 does all of it
|
|
23
|
+
with no configuration:
|
|
24
|
+
|
|
25
|
+
```jsx
|
|
26
|
+
const root = await createRoot(); // that is the whole of it
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`DESKTOP_STARTUP_ID` is read from the environment and **removed from it**: the
|
|
30
|
+
variable names one launch, and a child process that inherited it would claim
|
|
31
|
+
the parent's sequence and end it early. With no id in the environment — a
|
|
32
|
+
terminal, CI, XQuartz — nothing is set and nothing is sent.
|
|
33
|
+
|
|
34
|
+
### When is an app "started"?
|
|
35
|
+
|
|
36
|
+
The default is **the first frame that actually painted**. A window maps a
|
|
37
|
+
frame before it draws, so ending the sequence on map (what GTK does) would say
|
|
38
|
+
"ready" over a blank window. A suspense fallback counts as painted: the app is
|
|
39
|
+
up and telling the user what it is doing, and there is no signal for "finished
|
|
40
|
+
loading".
|
|
41
|
+
|
|
42
|
+
`completeOn` is there for apps the default does not suit:
|
|
43
|
+
|
|
44
|
+
```jsx
|
|
45
|
+
await createRoot({ startupNotification: { completeOn: 'map' } });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| `completeOn` | |
|
|
49
|
+
| ------------ | ------------------------------------------------------- |
|
|
50
|
+
| `'paint'` | the first frame that drew (default) |
|
|
51
|
+
| `'map'` | the first toplevel mapping — earlier, and what GTK does |
|
|
52
|
+
| `'manual'` | nothing automatic; call `notifyStartupComplete()` |
|
|
53
|
+
|
|
54
|
+
`'map'` suits an app whose first frame is expensive enough that it would
|
|
55
|
+
rather the cursor stopped before it. `'manual'` suits one that is not up
|
|
56
|
+
until it says so — restoring a session behind a splash, say:
|
|
57
|
+
|
|
58
|
+
```jsx
|
|
59
|
+
import { notifyStartupComplete } from 'react-x11';
|
|
60
|
+
|
|
61
|
+
await createRoot({ startupNotification: { completeOn: 'manual' } });
|
|
62
|
+
await restoreSession();
|
|
63
|
+
notifyStartupComplete(); // idempotent, and a no-op if there is no sequence
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**A backstop ends the sequence regardless**, ten seconds after the window
|
|
67
|
+
maps, whichever mode is in force, so an app that never paints or forgets to
|
|
68
|
+
call cannot leave the cursor spinning.
|
|
69
|
+
|
|
70
|
+
### Turning it off, and supplying an id
|
|
71
|
+
|
|
72
|
+
```jsx
|
|
73
|
+
await createRoot({ startupNotification: false });
|
|
74
|
+
await createRoot({ startupNotification: 'launcher/app/1-0_TIME9876' });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`false` is for an app that runs its own sequence, or an embedder that owns
|
|
78
|
+
the toplevel. Opting out leaves `DESKTOP_STARTUP_ID` in the environment — if
|
|
79
|
+
you manage the sequence yourself, the id is yours to read and to clear.
|
|
80
|
+
|
|
81
|
+
A string supplies the id for a launch where it did not arrive in the
|
|
82
|
+
environment. A D-Bus-activated app gets it in `platform_data` instead.
|
|
83
|
+
|
|
84
|
+
### `launchTimestamp()`
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
import { launchTimestamp } from 'react-x11';
|
|
88
|
+
|
|
89
|
+
const when = launchTimestamp(); // number | null
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The X server timestamp of the user action that launched the app, parsed from
|
|
93
|
+
the id's `_TIME` suffix — the "when" any later request to come forward is
|
|
94
|
+
weighed against. **`null` is a real answer**, not a failure: an app started
|
|
95
|
+
from a shell has no launch timestamp. Do not substitute `0` for a missing one;
|
|
96
|
+
EWMH reads zero as "do not focus this window when it maps".
|
|
97
|
+
|
|
98
|
+
### On macOS
|
|
99
|
+
|
|
100
|
+
XQuartz's quartz-wm implements none of this: the messages go to a root window
|
|
101
|
+
nobody listens at and `_NET_WM_USER_TIME` is ignored. Harmless, and not a sign
|
|
102
|
+
the feature is broken.
|
|
103
|
+
|
|
104
|
+
### What is deliberately not here
|
|
105
|
+
|
|
106
|
+
- **The launcher half** — an app that spawns _another_ app manages that
|
|
107
|
+
child's sequence itself.
|
|
108
|
+
- **Ongoing `_NET_WM_USER_TIME` maintenance** — it is set once, at launch, not
|
|
109
|
+
kept current on every keypress.
|
|
110
|
+
|
|
111
|
+
### Security
|
|
112
|
+
|
|
113
|
+
The id is broadcast to the root window, so every client on the display sees
|
|
114
|
+
it — X11's usual lack of isolation ([security.md](security.md)). The messages
|
|
115
|
+
carry only the id the launcher gave, and its value is never logged.
|
|
116
|
+
|
|
117
|
+
## The launcher: a badge, attention, and the Dock menu
|
|
118
|
+
|
|
119
|
+
The marks the user reads on the app's icon without opening it. Each exists on
|
|
120
|
+
a backend only where that backend has a mechanism for it:
|
|
121
|
+
|
|
122
|
+
| | Linux (`launcherentry`) | macOS (`cocoa`) | Windows (`taskbar`) |
|
|
123
|
+
| --------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------- |
|
|
124
|
+
| a **badge** | `com.canonical.Unity.LauncherEntry` | `NSDockTile.badgeLabel` | an overlay icon with the label drawn into it |
|
|
125
|
+
| badge **text** | — the protocol carries a count and nothing else | any label | up to three glyphs; longer clamps to `99+`, ~16px on screen |
|
|
126
|
+
| **progress** | `useProgress(0…1)` — a bar across the tile | — `NSDockTile` has none | `ITaskbarList3::SetProgressValue`, across the taskbar button |
|
|
127
|
+
| **attention** | `states={['demands_attention']}`, or `setUrgent()` for the launcher entry itself | the same prop: the Dock icon bounces until activated | `FlashWindowEx` |
|
|
128
|
+
| the **launcher menu** | `useLauncherMenu(items)` — the launcher protocol's quicklist | `useLauncherMenu(items)` | — the jump list instead (below) |
|
|
129
|
+
| **tasks** | — `Actions=` in the `.desktop` file, not driven from here | — | `useJumpList(tasks)` — the jump list's Tasks category |
|
|
130
|
+
| a **hover toolbar** | — | — | `useThumbnailToolbar(buttons)` — up to seven, under the preview |
|
|
131
|
+
| **recent documents** | — | — the platform has one; this backend does not use it | `useRecentDocument(path)` — the shell's Recent lists |
|
|
132
|
+
|
|
133
|
+
On Linux, KDE, elementary and Cairo-Dock show the badge; GNOME needs an
|
|
134
|
+
extension. On Windows the taskbar button's menu is the jump list, whose entries
|
|
135
|
+
start a _new_ process, so it is `features.tasks` and `useJumpList` rather than
|
|
136
|
+
`useLauncherMenu` ([windows-integrations.md](windows-integrations.md)).
|
|
137
|
+
|
|
138
|
+
### The badge
|
|
139
|
+
|
|
140
|
+
```jsx
|
|
141
|
+
import { setBadge, useBadge } from 'react-x11';
|
|
142
|
+
|
|
143
|
+
useBadge(unread); // a count: 3 shows "3", 0 clears
|
|
144
|
+
await setBadge(null); // the imperative twin, for code with no component
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
A badge is a **count**, the one shape both desktops show. A string (`'•'`,
|
|
148
|
+
`'!'`) shows on macOS, where the tile takes any label, and as a count of
|
|
149
|
+
nothing on Linux, whose protocol has no text — pass one only where the Mac is
|
|
150
|
+
the audience. `0`, `null` and `''` all clear it. `useBadge` clears on unmount;
|
|
151
|
+
`setBadge` resolves to whether a launcher was told and never rejects for
|
|
152
|
+
anything about the machine.
|
|
153
|
+
|
|
154
|
+
On Linux the launcher pins the count to the app's `.desktop` file, so it needs
|
|
155
|
+
the identity [`registerApplication({ appId })`](uri-schemes.md) establishes
|
|
156
|
+
and a `.desktop` file of that name; without them `setBadge` resolves `false`.
|
|
157
|
+
A launcher that starts after the badge was set does not see it until the next
|
|
158
|
+
`setBadge`.
|
|
159
|
+
|
|
160
|
+
### Progress
|
|
161
|
+
|
|
162
|
+
```jsx
|
|
163
|
+
import { useProgress, setProgress } from 'react-x11';
|
|
164
|
+
|
|
165
|
+
useProgress(downloaded / total); // 0…1; null or undefined clears it
|
|
166
|
+
await setProgress(null); // the imperative twin
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
A bar drawn across the app's icon, for a download, an export, a long build.
|
|
170
|
+
Out-of-range values are clamped rather than refused, so a total that briefly
|
|
171
|
+
went to zero does not throw.
|
|
172
|
+
|
|
173
|
+
**Not on macOS:** `NSDockTile` has no progress bar; an app that wants one
|
|
174
|
+
there draws it into a custom tile view itself. Branch on
|
|
175
|
+
`useDesktopCapability('launcher').features.progress`.
|
|
176
|
+
|
|
177
|
+
### Attention
|
|
178
|
+
|
|
179
|
+
Nothing new to call: `<window states={['demands_attention']}>`. On the cocoa
|
|
180
|
+
backend it is `NSApp.requestUserAttention` — the Dock icon bounces until the
|
|
181
|
+
user activates the app, and stops when the prop drops the state or the window
|
|
182
|
+
goes away. AppKit ignores the request while the app is already active, as a
|
|
183
|
+
window manager ignores a focused window's urgency hint. There, `fullscreen`,
|
|
184
|
+
`maximized` and `hidden` are AppKit's full-screen Space, zoom and miniaturize,
|
|
185
|
+
and the other `states` names are inert.
|
|
186
|
+
|
|
187
|
+
### The Dock menu
|
|
188
|
+
|
|
189
|
+
```jsx
|
|
190
|
+
useLauncherMenu([
|
|
191
|
+
{ label: 'New Window', onSelect: openWindow },
|
|
192
|
+
{ type: 'separator' },
|
|
193
|
+
{
|
|
194
|
+
label: 'Recent',
|
|
195
|
+
items: recent.map((r) => ({ label: r.name, onSelect: () => open(r) })),
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The same `items` vocabulary as `MenuBar` and `ContextMenu` ([menuitem
|
|
201
|
+
names](globalmenu.md)), so the window's, the panel's, the tray's and the
|
|
202
|
+
Dock's menus are one authoring model. Installed while the component is
|
|
203
|
+
mounted, replaced when `items` changes, taken down on unmount.
|
|
204
|
+
|
|
205
|
+
On the cocoa backend it is `NSDockTile`'s menu; on Linux, the launcher
|
|
206
|
+
protocol's **quicklist**, which ubuntu-dock/Dash-to-Dock, Plank and the
|
|
207
|
+
Unity-heritage launchers render. Entries that must work **while the app is not
|
|
208
|
+
running** belong in `Actions=` in the `.desktop` file instead; the quicklist
|
|
209
|
+
is for a menu that follows the app's state. Like the badge, it needs
|
|
210
|
+
[`registerApplication({ appId })`](uri-schemes.md) and a `.desktop` file of
|
|
211
|
+
that name.
|
|
212
|
+
|
|
213
|
+
### The app's presence
|
|
214
|
+
|
|
215
|
+
Two things the Dock and the ⌘-Tab switcher show that a bare `node` process
|
|
216
|
+
gets wrong, as root options on the cocoa backend:
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
await createRoot({
|
|
220
|
+
cocoa: {
|
|
221
|
+
appName: 'Notes', // what the Dock, ⌘-Tab and the app menu print (was "node")
|
|
222
|
+
activationPolicy: 'regular', // 'accessory' for a menu-bar app: no tile, no ⌘-Tab entry
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`activationPolicy` is a root option rather than a hook because it is fixed
|
|
228
|
+
before the app finishes launching — an agent app that switched afterwards
|
|
229
|
+
would already have flashed its Dock icon. `appName` renames LaunchServices'
|
|
230
|
+
record of an unbundled process; a bundle's `Info.plist` wins.
|
|
231
|
+
|
|
232
|
+
## The tray
|
|
233
|
+
|
|
234
|
+
```jsx
|
|
235
|
+
import { useTray } from 'react-x11';
|
|
236
|
+
|
|
237
|
+
const { available, backend, features, error } = useTray({
|
|
238
|
+
icon: 'mail-unread', // a themed icon name (an SF Symbol on macOS), or PNG bytes
|
|
239
|
+
tooltip: 'Notifications',
|
|
240
|
+
menu: [
|
|
241
|
+
{ label: 'Open', onSelect: open },
|
|
242
|
+
{ type: 'separator' },
|
|
243
|
+
{ label: 'Quit', onSelect: quit },
|
|
244
|
+
],
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
An icon in the system tray for as long as the component is mounted. With
|
|
249
|
+
`menu` a click opens it — the same `items` vocabulary as `MenuBar` and the
|
|
250
|
+
Dock menu. Without one, `onClick` gets the button, where the click was and,
|
|
251
|
+
where the backend knows it, the item's screen rect — all in logical screen
|
|
252
|
+
pixels, the unit a `<popup>`'s `x`/`y` are in, and what a popover under the
|
|
253
|
+
item anchors to ([elements.md](elements.md#a-popover-under-a-tray-item)).
|
|
254
|
+
`title` shows text beside the icon or alone; `visible`, `tooltip`, `attention`
|
|
255
|
+
and the rest follow their values while mounted, and the item is removed on
|
|
256
|
+
unmount. `null` means no item. Several trays coexist: each is its own item.
|
|
257
|
+
|
|
258
|
+
| | Linux (`statusnotifier`) | macOS (`cocoa`) | Windows (`shellnotifyicon`) |
|
|
259
|
+
| --------------- | --------------------------------------- | ---------------------------- | --------------------------- |
|
|
260
|
+
| mechanism | `org.kde.StatusNotifierItem` over D-Bus | `NSStatusItem` | `Shell_NotifyIcon` |
|
|
261
|
+
| menu | `com.canonical.dbusmenu` | `NSMenu` | a tracked popup menu |
|
|
262
|
+
| icon by name | the desktop's icon theme | SF Symbols | — no icon theme to name |
|
|
263
|
+
| icon by bytes | ARGB32 pixmap | a template image | an `HICON` from RGBA |
|
|
264
|
+
| title | text beside the icon | text beside the icon | — an icon and a tooltip |
|
|
265
|
+
| attention | `Status = NeedsAttention` | — | — |
|
|
266
|
+
| click rect | — | the item's frame | — |
|
|
267
|
+
| click modifiers | — | shift/control/option/command | — |
|
|
268
|
+
| scroll | `Scroll(delta, orientation)` | — | — |
|
|
269
|
+
|
|
270
|
+
**On Linux** it is D-Bus only, so it works identically under X11, XWayland and
|
|
271
|
+
Wayland. It needs something _hosting_ a tray: KDE Plasma and most panels do
|
|
272
|
+
out of the box; GNOME needs an AppIndicator extension. A stock GNOME session
|
|
273
|
+
with no extension has no tray, `available` is `false`, and nothing is logged —
|
|
274
|
+
that is a configuration, not a fault.
|
|
275
|
+
|
|
276
|
+
**A click's position on Linux** is in a unit the protocol never names: Plasma
|
|
277
|
+
and GNOME's physical layout send device pixels; GNOME's logical layout (the
|
|
278
|
+
default from GNOME 50), xfce4-panel, LXQt and Cinnamon send logical ones. The
|
|
279
|
+
point is read both ways: a reading on no monitor is dropped, then the one the
|
|
280
|
+
pointer is on wins, else the numbers are taken as sent. One case is misread —
|
|
281
|
+
a device-pixel host under XWayland whose tray, read as logical pixels, still
|
|
282
|
+
lands on a monitor (the top-left quarter of the screen, or with another
|
|
283
|
+
monitor beyond it): there the point is `scale` times as far from the screen's
|
|
284
|
+
origin as the click was.
|
|
285
|
+
|
|
286
|
+
**On the cocoa backend** it is `NSStatusItem`, the menu-bar extra. A menu-bar
|
|
287
|
+
app that wants no Dock tile pairs it with
|
|
288
|
+
`createRoot({ cocoa: { activationPolicy: 'accessory' } })` above. A PNG icon
|
|
289
|
+
is drawn as a **template** — its shape in the bar's ink, following light and
|
|
290
|
+
dark like a symbol; `template: false` keeps its colours. The menu is rebuilt
|
|
291
|
+
only when its items change, so a title that ticks every second leaves it
|
|
292
|
+
alone, whether `menu` is memoized or written inline.
|
|
293
|
+
|
|
294
|
+
### What the hook hands back
|
|
295
|
+
|
|
296
|
+
`available` **settles**: finding a freedesktop tray takes a bus round trip, so
|
|
297
|
+
it is `false` on the first frame and `true` a tick later if a host is there —
|
|
298
|
+
render the fallback first and upgrade. It follows the host for the life of the
|
|
299
|
+
item, so a panel that exits flips it back.
|
|
300
|
+
|
|
301
|
+
**`settled` says whether `available` is an answer yet.** For an app whose
|
|
302
|
+
whole UI is its tray item, the fallback _is_ a window, and rendering it first
|
|
303
|
+
flashes a window up and away on every start on a desktop that has a tray. So
|
|
304
|
+
wait for the answer instead:
|
|
305
|
+
|
|
306
|
+
```jsx
|
|
307
|
+
const tray = useTray({ icon: 'waveform', onClick: open });
|
|
308
|
+
if (!tray.settled) return null; // a bus round trip, not a timeout
|
|
309
|
+
if (!tray.available) return <window title="Hush">{controls}</window>;
|
|
310
|
+
return popover;
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
It is `true` on the first frame on macOS, where the status item is made there
|
|
314
|
+
and then, and for `useTray(null)`; on the freedesktop tray it turns `true` once
|
|
315
|
+
the host has taken or refused the item, and stays so.
|
|
316
|
+
|
|
317
|
+
`features` is the same vocabulary [feature discovery](#feature-discovery)
|
|
318
|
+
uses, for the rung that answered. Read it instead of testing the platform:
|
|
319
|
+
|
|
320
|
+
```jsx
|
|
321
|
+
// Right: ask about the thing you need.
|
|
322
|
+
{
|
|
323
|
+
features.clickModifiers && <ShiftClickHint />;
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`error` separates **"this desktop has no tray"** (`error` null) from **"there
|
|
328
|
+
is a tray and it refused me"** (the reason is there) — only the second has a
|
|
329
|
+
fix.
|
|
330
|
+
|
|
331
|
+
## Feature discovery
|
|
332
|
+
|
|
333
|
+
Everything on this page is a _ladder_, and which rung a machine lands on
|
|
334
|
+
changes what the app may do. `available` alone is not enough: a notification
|
|
335
|
+
daemon with `actions` gives a banner with buttons that call back, and
|
|
336
|
+
`notify-send` gives one-way text — an app that posts reply buttons on the
|
|
337
|
+
second gets a banner with no buttons, silently. So the unit of discovery is a
|
|
338
|
+
**feature set**, not a flag:
|
|
339
|
+
|
|
340
|
+
```jsx
|
|
341
|
+
import { useDesktopCapability } from 'react-x11';
|
|
342
|
+
|
|
343
|
+
const notifications = useDesktopCapability('notifications');
|
|
344
|
+
|
|
345
|
+
if (notifications.features.actions) return <ReplyFromBanner />;
|
|
346
|
+
if (notifications.available) return <BannerThenOpenApp />;
|
|
347
|
+
return <InAppToastOnly />;
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`{ available, backend, features, settled }`. `backend` names the
|
|
351
|
+
**mechanism** — `'dbus'`, `'cocoa'`, `'statusnotifier'`, `'launcherentry'`,
|
|
352
|
+
`'notify-send'`, `'osascript'`, `'win32'`, `'shellnotifyicon'`, `'taskbar'` —
|
|
353
|
+
never the platform. Branch on `features`; `backend` is for logs and bug
|
|
354
|
+
reports.
|
|
355
|
+
|
|
356
|
+
`settled` is `false` until the first probe answers and `true` from then on, so
|
|
357
|
+
an app can hold its fallback back rather than flash it; it is `true` on the
|
|
358
|
+
first frame for the answers that need nothing asked, the tray and the Dock tile
|
|
359
|
+
on macOS.
|
|
360
|
+
|
|
361
|
+
Capability names are `'notifications'`, `'tray'` and `'launcher'`.
|
|
362
|
+
`desktopCapability(name)` is the imperative twin for code with no component.
|
|
363
|
+
|
|
364
|
+
### Which one to ask
|
|
365
|
+
|
|
366
|
+
A hook that _does_ the thing already knows whether it worked, and that
|
|
367
|
+
measurement beats any prediction:
|
|
368
|
+
|
|
369
|
+
- **`useTray(options)`** — "did my icon get taken?" It tried; it knows.
|
|
370
|
+
- **`useDesktopCapability('tray')`** — "would one be taken if I asked?" This
|
|
371
|
+
is what a settings screen needs: a "Show tray icon" checkbox has to render
|
|
372
|
+
correctly _without_ putting an icon in the tray to find out.
|
|
373
|
+
|
|
374
|
+
Prefer the feature hook wherever the feature is actually mounted. Both read
|
|
375
|
+
the same probe, so they cannot disagree.
|
|
376
|
+
|
|
377
|
+
### Degrading well
|
|
378
|
+
|
|
379
|
+
1. **Render the fallback first.** Every desktop answer takes a round trip, so
|
|
380
|
+
the first frame has none. Upgrading a fallback is invisible; downgrading a
|
|
381
|
+
feature the user already reached for is not.
|
|
382
|
+
2. **Never disable a feature permanently on a `false`.** A panel restarts, an
|
|
383
|
+
extension is enabled, a daemon is installed. The hooks re-probe when a
|
|
384
|
+
service appears on the bus and nothing is cached — an app that latched the
|
|
385
|
+
first answer would outlive every one of those fixes.
|
|
386
|
+
3. **A missing desktop service is not an error.** No bus, no tray host, no
|
|
387
|
+
notification daemon and no launcher are all ordinary states of a healthy
|
|
388
|
+
machine. Nothing is logged and nothing throws. `error` is for a service
|
|
389
|
+
that answered and then refused.
|
|
390
|
+
|
|
391
|
+
### All of it, in one app
|
|
392
|
+
|
|
393
|
+
`examples/desktop.jsx` (`npm run examples:desktop`) is a backup agent that
|
|
394
|
+
uses every hook on this page at once — the tray icon with its attention state
|
|
395
|
+
and overlay, the badge, the progress bar, the quicklist, the urgency flag, and
|
|
396
|
+
a banner whose buttons are offered only where the daemon has `actions` — and
|
|
397
|
+
whose second pane renders `useDesktopCapability()` for each of `CAPABILITIES`
|
|
398
|
+
beside the live answers. Read it to see what an app will get on a machine you
|
|
399
|
+
do not have. The feature-sized examples are `badge.jsx` (the launcher),
|
|
400
|
+
`tray.jsx` (the tray) and `notify.jsx` (the notification).
|
|
401
|
+
|
|
402
|
+
## Turning the desktop off
|
|
403
|
+
|
|
404
|
+
Three things react-x11 turns on for you reach the **session bus**:
|
|
405
|
+
|
|
406
|
+
| | | |
|
|
407
|
+
| ------------ | ----------------------------------------------------------------------- | ------------------------------------ |
|
|
408
|
+
| `appearance` | following the desktop's light/dark, accent, contrast and reduced motion | [appearance.md](appearance.md) |
|
|
409
|
+
| `a11y` | the AT-SPI bridge — whether a screen reader can see the app | [accessibility.md](accessibility.md) |
|
|
410
|
+
| `globalMenu` | a `MenuBar` handing its menu to the panel instead of drawing it | [globalmenu.md](globalmenu.md) |
|
|
411
|
+
|
|
412
|
+
Each is the right default, but an embedder, a kiosk or a test may own the
|
|
413
|
+
thing they assume is theirs:
|
|
414
|
+
|
|
415
|
+
```jsx
|
|
416
|
+
await createRoot({ desktop: false }); // none of the three
|
|
417
|
+
await createRoot({ desktop: { appearance: false } }); // just that one
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
With all three off, **nothing dials the session bus at startup** — no
|
|
421
|
+
connection, no subprocess, no portal probe.
|
|
422
|
+
|
|
423
|
+
Prefer this to the environment variables that do the same — `NO_AT_BRIDGE=1`,
|
|
424
|
+
`REACT_X11_NO_GLOBAL_MENU=1`, or unsetting `DBUS_SESSION_BUS_ADDRESS`: the
|
|
425
|
+
environment is inherited, so they reach every child process too, and the
|
|
426
|
+
D-Bus one also takes down the portals and the app's own exported services.
|
|
427
|
+
|
|
428
|
+
Turning `appearance` off means the app draws in its own palette:
|
|
429
|
+
`useSystemAppearance()` reports `'no-preference'` with `source: null`, and the
|
|
430
|
+
answer remembered on disk is not read either.
|
|
431
|
+
|
|
432
|
+
**Off is process-wide, and it latches.** There is one D-Bus identity and one
|
|
433
|
+
AT-SPI bridge per process, so the policy is not per-root: nothing turns an
|
|
434
|
+
integration back on, and a feature already started stays started — pass this
|
|
435
|
+
on the first root.
|
|
436
|
+
|
|
437
|
+
## Not blocking the first frame on the bus
|
|
438
|
+
|
|
439
|
+
On macOS the session bus address comes from launchd rather than the
|
|
440
|
+
environment; react-x11 looks it up once, asynchronously, so it costs the event
|
|
441
|
+
loop and the first frame nothing. There is nothing to configure. On Linux
|
|
442
|
+
`$DBUS_SESSION_BUS_ADDRESS` or `$XDG_RUNTIME_DIR/bus` answers directly.
|
|
443
|
+
Design notes: [design/desktop.md](../design/desktop.md#not-blocking-the-first-frame-on-the-bus).
|
|
444
|
+
|
|
445
|
+
## Password fields and password managers
|
|
446
|
+
|
|
447
|
+
There is **no password-field protocol on the Linux desktop**: no toolkit
|
|
448
|
+
publishes "this is a password field, fill it", and no manager asks. There are
|
|
449
|
+
four seams instead, which an application either supports or does not.
|
|
450
|
+
`PasswordInput` supports the two that reach a field.
|
|
451
|
+
How the managers do it: [design/desktop.md](../design/desktop.md#password-fields-and-password-managers).
|
|
452
|
+
|
|
453
|
+
### 1. Typing — XTEST auto-type, and the keymap race
|
|
454
|
+
|
|
455
|
+
Most desktop managers **auto-type**: KeePassXC matches the focused window's
|
|
456
|
+
**title** against its entries and synthesises the keystrokes over XTEST.
|
|
457
|
+
|
|
458
|
+
- **A faked key is an ordinary key.** `PasswordInput` — and every other
|
|
459
|
+
focusable node — cannot tell auto-type from a person, and nothing has to be
|
|
460
|
+
done to support it. A sequence of `{USERNAME}{TAB}{PASSWORD}{ENTER}` walks a
|
|
461
|
+
react-x11 form because Tab moves focus and Enter reaches `onSubmit`.
|
|
462
|
+
- **The window needs a title worth matching.** `<window title="…">` is the
|
|
463
|
+
integration surface: a window titled after the document with nothing
|
|
464
|
+
identifying the app is one a user cannot write an auto-type rule for. Set
|
|
465
|
+
`appId` too, for managers that match on it.
|
|
466
|
+
- **Characters outside the user's layout can race.** A manager types them by
|
|
467
|
+
remapping a spare keycode first; one that types immediately after remapping
|
|
468
|
+
can land the key before react-x11 has refetched the keymap. ASCII passwords
|
|
469
|
+
on an ASCII layout are not affected, and nothing on this side can close it.
|
|
470
|
+
|
|
471
|
+
### 2. Pasting — the clipboard, and the hint that keeps it out of history
|
|
472
|
+
|
|
473
|
+
Copy-to-clipboard is ordinary `CLIPBOARD` interop ([clipboard.md](clipboard.md)):
|
|
474
|
+
`PasswordInput` takes Ctrl+V and Shift+Insert, and strips control characters
|
|
475
|
+
so a manager's trailing newline does not end up inside the secret.
|
|
476
|
+
|
|
477
|
+
If your app ever puts a secret **on** the clipboard — this widget never does
|
|
478
|
+
— offer `x-kde-passwordManagerHint` with the value `secret` beside the text.
|
|
479
|
+
Klipper drops such an offer from its history, KDE Connect refuses to forward
|
|
480
|
+
it, and `wl-clipboard` marks it sensitive. It is a convention rather than a
|
|
481
|
+
specification, and the only thing between a copied password and a clipboard
|
|
482
|
+
manager's on-disk history:
|
|
483
|
+
|
|
484
|
+
```js
|
|
485
|
+
clipboard.write({ UTF8_STRING: secret, 'x-kde-passwordManagerHint': 'secret' });
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
`PasswordInput` never takes the **PRIMARY** selection, which a `<textinput>`
|
|
489
|
+
does on every selection: PRIMARY is pasted by a middle click in any window on
|
|
490
|
+
the display.
|
|
491
|
+
|
|
492
|
+
### 3. Fetching it yourself — the Secret Service
|
|
493
|
+
|
|
494
|
+
An app with an account of its own to unlock needs no field at all: the
|
|
495
|
+
**Secret Service** D-Bus API, `org.freedesktop.secrets` (gnome-keyring,
|
|
496
|
+
KWallet, KeePassXC), is reachable through `useSessionBus()`
|
|
497
|
+
([dbus.md](dbus.md)) — search by attributes, unlock the collection if locked,
|
|
498
|
+
read the secret back. react-x11 has no wrapper for it. Inside a Flatpak
|
|
499
|
+
sandbox that name is absent; `org.freedesktop.portal.Secret`'s
|
|
500
|
+
`RetrieveSecret` hands the app a per-application master secret instead, which
|
|
501
|
+
libsecret uses automatically to encrypt a local store.
|
|
502
|
+
|
|
503
|
+
### 4. Reading the field — AT-SPI, which does not mark it yet
|
|
504
|
+
|
|
505
|
+
AT-SPI2 gives a password entry the role `password text`, which is how a screen
|
|
506
|
+
reader knows to announce "bullet" and how some automation finds the field.
|
|
507
|
+
react-x11 publishes an accessibility tree ([accessibility.md](accessibility.md)),
|
|
508
|
+
but `PasswordInput` is a plain `role="textbox"` in it, so this seam is still
|
|
509
|
+
closed.
|
|
510
|
+
|
|
511
|
+
### What a field can still do wrong
|
|
512
|
+
|
|
513
|
+
None of the above stops the field itself from leaking. What `PasswordInput`
|
|
514
|
+
does about that, and what it cannot:
|
|
515
|
+
|
|
516
|
+
- the masked value is **never laid out or drawn** — no glyphs of the secret
|
|
517
|
+
reach ntk's shaping cache or the X server, and the mask's width is measured
|
|
518
|
+
from one reference character;
|
|
519
|
+
- there is **no copy, no selection, no undo history**;
|
|
520
|
+
- the value is still a JavaScript string, and strings are immutable. It
|
|
521
|
+
cannot be zeroed, it lives until the garbage collector takes it, and every
|
|
522
|
+
intermediate value typed on the way lives alongside it. A design that needs
|
|
523
|
+
more than that needs the secret to never enter this process.
|
package/docs/devtools.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# React DevTools
|
|
2
|
+
|
|
3
|
+
react-x11 speaks to the standalone React DevTools app over its WebSocket
|
|
4
|
+
bridge.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm i -D react-devtools-core # already a dev dep of this repo
|
|
10
|
+
|
|
11
|
+
# terminal 1 — the standalone UI (listens on :8097)
|
|
12
|
+
npx react-devtools
|
|
13
|
+
|
|
14
|
+
# terminal 2 — any app/example with the bridge enabled
|
|
15
|
+
REACT_X11_DEVTOOLS=1 npm run examples:dashboard
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Either order works: the backend retries every two seconds, so an app
|
|
19
|
+
started first attaches a moment after the UI comes up. Under
|
|
20
|
+
`npx react-x11 dev` the bridge is on by default wherever
|
|
21
|
+
`react-devtools-core` is installed — run `npx react-devtools` and the app
|
|
22
|
+
shows up, `REACT_X11_DEVTOOLS=0` turns it off — and where it is not
|
|
23
|
+
installed while a UI is listening, `dev` says what to install.
|
|
24
|
+
|
|
25
|
+
What you get:
|
|
26
|
+
|
|
27
|
+
- the **component tree**, live as the app updates;
|
|
28
|
+
- **props, hooks and state** when selecting a component, and editing any of
|
|
29
|
+
them — the app really re-renders;
|
|
30
|
+
- **highlight-on-hover** — hovering an element in the tree tints its rect
|
|
31
|
+
in the window;
|
|
32
|
+
- **"highlight updates when components render"** — the rects that just
|
|
33
|
+
re-rendered are outlined in DevTools' own colour ramp (teal for a first
|
|
34
|
+
render through amber for one that keeps re-rendering), fading out;
|
|
35
|
+
- the **element picker** — the crosshair in the DevTools toolbar. While it
|
|
36
|
+
is on, the pointer belongs to DevTools: motion tints whatever is under
|
|
37
|
+
it, a click selects that element in the tree, `Escape` gives up. The app
|
|
38
|
+
sees none of those events;
|
|
39
|
+
- the **style editor** — the box model and the flattened `style` of a
|
|
40
|
+
selected element, editable live. Host elements are hidden by the default
|
|
41
|
+
component filter, so turn that filter off (⚙ → Components → uncheck
|
|
42
|
+
"host components") to have something with a style to select;
|
|
43
|
+
- the Profiler's commit list and flamegraph, including **"reload and start
|
|
44
|
+
profiling"**, which re-execs the app with profiling already on so the
|
|
45
|
+
mount itself is recorded. **Not** its Timeline view, which this reconciler
|
|
46
|
+
build does not support;
|
|
47
|
+
- **DevTools' own settings** — component filters, console patching — kept
|
|
48
|
+
between runs, in `$XDG_CACHE_HOME/react-x11/devtools.json`
|
|
49
|
+
(`~/Library/Caches/react-x11/devtools.json` on macOS).
|
|
50
|
+
|
|
51
|
+
With `REACT_X11_CLICK_TO_COMPONENT=1` as well, an Alt+Click also selects
|
|
52
|
+
the clicked element in the DevTools tree on its way to opening the source
|
|
53
|
+
([click-to-component.md](click-to-component.md)).
|
|
54
|
+
|
|
55
|
+
`REACT_X11_DEVTOOLS_HOST` / `REACT_X11_DEVTOOLS_PORT` point the backend at
|
|
56
|
+
a non-default standalone (e.g. devtools on another machine — handy when the
|
|
57
|
+
X app runs on a headless box). `REACT_X11_DEVTOOLS_STATE` moves the
|
|
58
|
+
settings file.
|
|
59
|
+
|
|
60
|
+
DevTools has no network panel — nothing in its protocol carries network
|
|
61
|
+
data. For that, `node --inspect` and `chrome://inspect` still work alongside
|
|
62
|
+
all of this.
|
|
63
|
+
|
|
64
|
+
## Opening the source from the DevTools panel
|
|
65
|
+
|
|
66
|
+
The inspected-element panel already shows where a component was written —
|
|
67
|
+
`file:///…/tasks.jsx:42:7`, absolute and source-mapped. Making that line
|
|
68
|
+
_clickable_ is a standalone-side setting, with two independent routes:
|
|
69
|
+
|
|
70
|
+
- **Spawn an editor.** The standalone launches one itself, choosing it from
|
|
71
|
+
`REACT_EDITOR` (a full command line, shell-quoted), else — on macOS — an
|
|
72
|
+
editor already running, else `VISUAL`, else `EDITOR`. It knows each
|
|
73
|
+
editor's argument style (vim's `+42`, sublime's and JetBrains' `file:42`),
|
|
74
|
+
so this is the route to reach for:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
REACT_EDITOR=cursor npx react-devtools
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- **Open a URL.** ⚙ → General → "Open local files directly in your code
|
|
81
|
+
editor", with the VS Code preset or a custom template using `{path}`,
|
|
82
|
+
`{line}` and `{column}`. The setting lives in the standalone's own
|
|
83
|
+
storage, not the app's.
|
|
84
|
+
|
|
85
|
+
Both are configured where the standalone runs, which may be a different
|
|
86
|
+
machine from the app; the app cannot inject either.
|
|
87
|
+
[Click-to-component](click-to-component.md) is the app-side answer to the same
|
|
88
|
+
question, and the one that works when DevTools runs somewhere without your
|
|
89
|
+
editor on it.
|
|
90
|
+
|
|
91
|
+
The same backend, minus the socket, powers `react-x11/test`'s component
|
|
92
|
+
inspection: `inspect()`/`setHook` in
|
|
93
|
+
[testing.md](testing.md#inspectnode-and-sethook) run entirely in-process — no
|
|
94
|
+
standalone app, no `ws`, no env variable. A test run that also sets
|
|
95
|
+
`REACT_X11_DEVTOOLS=1` shares one renderer registration with them.
|
|
96
|
+
|
|
97
|
+
## How it works (and its sharp edges)
|
|
98
|
+
|
|
99
|
+
The bridge is installed by `createRoot()` when `REACT_X11_DEVTOOLS` is set (by
|
|
100
|
+
`react-x11 dev`, earlier, before React Refresh loads). Its socket is `ws` from
|
|
101
|
+
`react-devtools-core`'s own dependencies, so `ws` is not a separate install.
|
|
102
|
+
What an app can notice while it is on:
|
|
103
|
+
|
|
104
|
+
- **It does not keep the process alive.** The backend's retry timer and socket
|
|
105
|
+
are unref'd, so the process still exits when the app's last window closes;
|
|
106
|
+
the app's own `setTimeout` and `WebSocket` are untouched.
|
|
107
|
+
- **Browser-ish globals appear.** `self` and `window` are set to the global
|
|
108
|
+
where absent, `requestAnimationFrame`/`cancelAnimationFrame` are shimmed
|
|
109
|
+
where absent, and `localStorage`/`sessionStorage` are defined (the store
|
|
110
|
+
behind DevTools' settings file).
|
|
111
|
+
- **`Error.stackTraceLimit` is raised to 50** (never lowered), because React
|
|
112
|
+
reads hook names and sources off a stack captured at each hook call and a
|
|
113
|
+
loader such as `tsx` eats the default 10 frames. Without it, hooks show with
|
|
114
|
+
no names and no source lines.
|
|
115
|
+
- **Selecting a component with hooks can blank the DevTools window.** That is
|
|
116
|
+
the standalone app's own renderer process aborting, upstream of anything the
|
|
117
|
+
app sends. Turning ⚙ → Components → "Always parse hook names for the
|
|
118
|
+
selected element" off makes it intermittent rather than reliable;
|
|
119
|
+
restarting `npx react-devtools` reconnects to the app, which is unaffected.
|
|
120
|
+
|
|
121
|
+
Design notes and maintainer notes:
|
|
122
|
+
[design/devtools.md](../design/devtools.md#how-it-works-and-its-sharp-edges).
|
|
123
|
+
|
|
124
|
+
## Other debugging aids
|
|
125
|
+
|
|
126
|
+
- `REACT_X11_DEBUG_LAYOUT=1` — outline every laid-out node, color-coded by
|
|
127
|
+
tree depth.
|
|
128
|
+
- Refs: drawn elements expose the retained node (`abs` rect, `scrollTo`,
|
|
129
|
+
…); `<window>`/`<popup>` expose the live ntk window (`getContext('2d')`,
|
|
130
|
+
`queryTree`, `setCursor`, …).
|
|
131
|
+
- The in-process X server (node-x11's `lib/xserver`) renders headlessly and
|
|
132
|
+
reads pixels back; `react-x11/test` is built on it
|
|
133
|
+
([testing.md](testing.md#pixels)).
|