@textui/core 0.1.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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +11 -0
- package/dist/app/app.d.ts +200 -0
- package/dist/app/app.d.ts.map +1 -0
- package/dist/app/app.js +980 -0
- package/dist/core/animation.d.ts +34 -0
- package/dist/core/animation.d.ts.map +1 -0
- package/dist/core/animation.js +125 -0
- package/dist/core/clipboard.d.ts +29 -0
- package/dist/core/clipboard.d.ts.map +1 -0
- package/dist/core/clipboard.js +30 -0
- package/dist/core/commands.d.ts +42 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/commands.js +146 -0
- package/dist/core/components.d.ts +27 -0
- package/dist/core/components.d.ts.map +1 -0
- package/dist/core/components.js +78 -0
- package/dist/core/events.d.ts +23 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +77 -0
- package/dist/core/focus.d.ts +69 -0
- package/dist/core/focus.d.ts.map +1 -0
- package/dist/core/focus.js +336 -0
- package/dist/core/i18n.d.ts +27 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/i18n.js +88 -0
- package/dist/core/keybindings.d.ts +53 -0
- package/dist/core/keybindings.d.ts.map +1 -0
- package/dist/core/keybindings.js +163 -0
- package/dist/core/layers.d.ts +28 -0
- package/dist/core/layers.d.ts.map +1 -0
- package/dist/core/layers.js +84 -0
- package/dist/core/manifest.d.ts +22 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +85 -0
- package/dist/core/navigation.d.ts +47 -0
- package/dist/core/navigation.d.ts.map +1 -0
- package/dist/core/navigation.js +110 -0
- package/dist/core/resources.d.ts +115 -0
- package/dist/core/resources.d.ts.map +1 -0
- package/dist/core/resources.js +321 -0
- package/dist/core/services.d.ts +21 -0
- package/dist/core/services.d.ts.map +1 -0
- package/dist/core/services.js +63 -0
- package/dist/core/store.d.ts +60 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +593 -0
- package/dist/core/surfaces.d.ts +81 -0
- package/dist/core/surfaces.d.ts.map +1 -0
- package/dist/core/surfaces.js +237 -0
- package/dist/core/syntax.d.ts +49 -0
- package/dist/core/syntax.d.ts.map +1 -0
- package/dist/core/syntax.js +172 -0
- package/dist/core/when.d.ts +15 -0
- package/dist/core/when.d.ts.map +1 -0
- package/dist/core/when.js +224 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/jsx/factory.d.ts +27 -0
- package/dist/jsx/factory.d.ts.map +1 -0
- package/dist/jsx/factory.js +102 -0
- package/dist/jsx/intrinsics.d.ts +96 -0
- package/dist/jsx/intrinsics.d.ts.map +1 -0
- package/dist/jsx/intrinsics.js +1 -0
- package/dist/jsx/jsx-dev-runtime.d.ts +13 -0
- package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-dev-runtime.js +12 -0
- package/dist/jsx/jsx-runtime.d.ts +42 -0
- package/dist/jsx/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-runtime.js +16 -0
- package/dist/render/buffer.d.ts +77 -0
- package/dist/render/buffer.d.ts.map +1 -0
- package/dist/render/buffer.js +275 -0
- package/dist/render/color.d.ts +35 -0
- package/dist/render/color.d.ts.map +1 -0
- package/dist/render/color.js +160 -0
- package/dist/render/diff.d.ts +37 -0
- package/dist/render/diff.d.ts.map +1 -0
- package/dist/render/diff.js +66 -0
- package/dist/render/layout.d.ts +72 -0
- package/dist/render/layout.d.ts.map +1 -0
- package/dist/render/layout.js +633 -0
- package/dist/render/static.d.ts +63 -0
- package/dist/render/static.d.ts.map +1 -0
- package/dist/render/static.js +209 -0
- package/dist/runtime/bindings.d.ts +46 -0
- package/dist/runtime/bindings.d.ts.map +1 -0
- package/dist/runtime/bindings.js +116 -0
- package/dist/runtime/hooks.d.ts +284 -0
- package/dist/runtime/hooks.d.ts.map +1 -0
- package/dist/runtime/hooks.js +846 -0
- package/dist/runtime/instance.d.ts +106 -0
- package/dist/runtime/instance.d.ts.map +1 -0
- package/dist/runtime/instance.js +181 -0
- package/dist/runtime/paint.d.ts +27 -0
- package/dist/runtime/paint.d.ts.map +1 -0
- package/dist/runtime/paint.js +567 -0
- package/dist/runtime/reconcile.d.ts +23 -0
- package/dist/runtime/reconcile.d.ts.map +1 -0
- package/dist/runtime/reconcile.js +260 -0
- package/dist/runtime/runtime.d.ts +42 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/runtime.js +1 -0
- package/dist/runtime/style.d.ts +55 -0
- package/dist/runtime/style.d.ts.map +1 -0
- package/dist/runtime/style.js +143 -0
- package/dist/themes/borders.d.ts +10 -0
- package/dist/themes/borders.d.ts.map +1 -0
- package/dist/themes/borders.js +88 -0
- package/dist/themes/builtin.d.ts +31 -0
- package/dist/themes/builtin.d.ts.map +1 -0
- package/dist/themes/builtin.js +291 -0
- package/dist/themes/glyphs.d.ts +15 -0
- package/dist/themes/glyphs.d.ts.map +1 -0
- package/dist/themes/glyphs.js +97 -0
- package/dist/themes/index.d.ts +5 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +4 -0
- package/dist/themes/registry.d.ts +20 -0
- package/dist/themes/registry.d.ts.map +1 -0
- package/dist/themes/registry.js +204 -0
- package/dist/types/adapter.d.ts +38 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +1 -0
- package/dist/types/animation.d.ts +31 -0
- package/dist/types/animation.d.ts.map +1 -0
- package/dist/types/animation.js +1 -0
- package/dist/types/app.d.ts +148 -0
- package/dist/types/app.d.ts.map +1 -0
- package/dist/types/app.js +1 -0
- package/dist/types/async.d.ts +30 -0
- package/dist/types/async.d.ts.map +1 -0
- package/dist/types/async.js +1 -0
- package/dist/types/capabilities.d.ts +44 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +33 -0
- package/dist/types/cells.d.ts +72 -0
- package/dist/types/cells.d.ts.map +1 -0
- package/dist/types/cells.js +10 -0
- package/dist/types/command.d.ts +127 -0
- package/dist/types/command.d.ts.map +1 -0
- package/dist/types/command.js +1 -0
- package/dist/types/component-registry.d.ts +77 -0
- package/dist/types/component-registry.d.ts.map +1 -0
- package/dist/types/component-registry.js +1 -0
- package/dist/types/disposable.d.ts +8 -0
- package/dist/types/disposable.d.ts.map +1 -0
- package/dist/types/disposable.js +1 -0
- package/dist/types/focus.d.ts +72 -0
- package/dist/types/focus.d.ts.map +1 -0
- package/dist/types/focus.js +1 -0
- package/dist/types/geometry.d.ts +28 -0
- package/dist/types/geometry.d.ts.map +1 -0
- package/dist/types/geometry.js +14 -0
- package/dist/types/graph.d.ts +129 -0
- package/dist/types/graph.d.ts.map +1 -0
- package/dist/types/graph.js +31 -0
- package/dist/types/i18n.d.ts +23 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/i18n.js +1 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +31 -0
- package/dist/types/input.d.ts +65 -0
- package/dist/types/input.d.ts.map +1 -0
- package/dist/types/input.js +1 -0
- package/dist/types/keybinding.d.ts +49 -0
- package/dist/types/keybinding.d.ts.map +1 -0
- package/dist/types/keybinding.js +1 -0
- package/dist/types/layer.d.ts +54 -0
- package/dist/types/layer.d.ts.map +1 -0
- package/dist/types/layer.js +1 -0
- package/dist/types/manifest.d.ts +114 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/manifest.js +1 -0
- package/dist/types/markdown.d.ts +58 -0
- package/dist/types/markdown.d.ts.map +1 -0
- package/dist/types/markdown.js +1 -0
- package/dist/types/navigation.d.ts +40 -0
- package/dist/types/navigation.d.ts.map +1 -0
- package/dist/types/navigation.js +1 -0
- package/dist/types/render.d.ts +72 -0
- package/dist/types/render.d.ts.map +1 -0
- package/dist/types/render.js +1 -0
- package/dist/types/resource.d.ts +170 -0
- package/dist/types/resource.d.ts.map +1 -0
- package/dist/types/resource.js +1 -0
- package/dist/types/services.d.ts +24 -0
- package/dist/types/services.d.ts.map +1 -0
- package/dist/types/services.js +3 -0
- package/dist/types/shell.d.ts +32 -0
- package/dist/types/shell.d.ts.map +1 -0
- package/dist/types/shell.js +1 -0
- package/dist/types/store.d.ts +124 -0
- package/dist/types/store.d.ts.map +1 -0
- package/dist/types/store.js +1 -0
- package/dist/types/stream.d.ts +18 -0
- package/dist/types/stream.d.ts.map +1 -0
- package/dist/types/stream.js +1 -0
- package/dist/types/style.d.ts +166 -0
- package/dist/types/style.d.ts.map +1 -0
- package/dist/types/style.js +1 -0
- package/dist/types/surface.d.ts +97 -0
- package/dist/types/surface.d.ts.map +1 -0
- package/dist/types/surface.js +1 -0
- package/dist/types/syntax.d.ts +68 -0
- package/dist/types/syntax.d.ts.map +1 -0
- package/dist/types/syntax.js +5 -0
- package/dist/types/terminal.d.ts +52 -0
- package/dist/types/terminal.d.ts.map +1 -0
- package/dist/types/terminal.js +1 -0
- package/dist/types/theme.d.ts +117 -0
- package/dist/types/theme.d.ts.map +1 -0
- package/dist/types/theme.js +1 -0
- package/dist/types/when.d.ts +19 -0
- package/dist/types/when.d.ts.map +1 -0
- package/dist/types/when.js +1 -0
- package/dist/ui/primitives.d.ts +56 -0
- package/dist/ui/primitives.d.ts.map +1 -0
- package/dist/ui/primitives.js +108 -0
- package/dist/ui/screen.d.ts +23 -0
- package/dist/ui/screen.d.ts.map +1 -0
- package/dist/ui/screen.js +18 -0
- package/dist/util/disposable.d.ts +6 -0
- package/dist/util/disposable.d.ts.map +1 -0
- package/dist/util/disposable.js +49 -0
- package/dist/util/markdown.d.ts +24 -0
- package/dist/util/markdown.d.ts.map +1 -0
- package/dist/util/markdown.js +220 -0
- package/dist/util/paths.d.ts +47 -0
- package/dist/util/paths.d.ts.map +1 -0
- package/dist/util/paths.js +134 -0
- package/dist/util/stream.d.ts +32 -0
- package/dist/util/stream.d.ts.map +1 -0
- package/dist/util/stream.js +206 -0
- package/dist/util/text.d.ts +65 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +419 -0
- package/package.json +70 -0
- package/src/adapters/index.ts +11 -0
- package/src/app/app.ts +1096 -0
- package/src/core/animation.ts +144 -0
- package/src/core/clipboard.ts +40 -0
- package/src/core/commands.ts +169 -0
- package/src/core/components.ts +91 -0
- package/src/core/events.ts +95 -0
- package/src/core/focus.ts +358 -0
- package/src/core/i18n.ts +107 -0
- package/src/core/keybindings.ts +184 -0
- package/src/core/layers.ts +94 -0
- package/src/core/manifest.ts +84 -0
- package/src/core/navigation.ts +135 -0
- package/src/core/resources.ts +362 -0
- package/src/core/services.ts +69 -0
- package/src/core/store.ts +640 -0
- package/src/core/surfaces.ts +292 -0
- package/src/core/syntax.ts +200 -0
- package/src/core/when.ts +238 -0
- package/src/index.ts +76 -0
- package/src/jsx/factory.ts +124 -0
- package/src/jsx/intrinsics.ts +99 -0
- package/src/jsx/jsx-dev-runtime.ts +23 -0
- package/src/jsx/jsx-runtime.ts +76 -0
- package/src/render/buffer.ts +318 -0
- package/src/render/color.ts +180 -0
- package/src/render/diff.ts +99 -0
- package/src/render/layout.ts +764 -0
- package/src/render/static.ts +290 -0
- package/src/runtime/bindings.ts +150 -0
- package/src/runtime/hooks.ts +1057 -0
- package/src/runtime/instance.ts +284 -0
- package/src/runtime/paint.ts +737 -0
- package/src/runtime/reconcile.ts +335 -0
- package/src/runtime/runtime.ts +47 -0
- package/src/runtime/style.ts +205 -0
- package/src/themes/borders.ts +95 -0
- package/src/themes/builtin.ts +301 -0
- package/src/themes/glyphs.ts +101 -0
- package/src/themes/index.ts +4 -0
- package/src/themes/registry.ts +224 -0
- package/src/types/adapter.ts +41 -0
- package/src/types/animation.ts +34 -0
- package/src/types/app.ts +149 -0
- package/src/types/async.ts +35 -0
- package/src/types/capabilities.ts +79 -0
- package/src/types/cells.ts +79 -0
- package/src/types/command.ts +139 -0
- package/src/types/component-registry.ts +82 -0
- package/src/types/disposable.ts +8 -0
- package/src/types/focus.ts +77 -0
- package/src/types/geometry.ts +50 -0
- package/src/types/graph.ts +174 -0
- package/src/types/i18n.ts +25 -0
- package/src/types/index.ts +31 -0
- package/src/types/input.ts +81 -0
- package/src/types/keybinding.ts +51 -0
- package/src/types/layer.ts +46 -0
- package/src/types/manifest.ts +101 -0
- package/src/types/markdown.ts +47 -0
- package/src/types/navigation.ts +42 -0
- package/src/types/render.ts +86 -0
- package/src/types/resource.ts +185 -0
- package/src/types/services.ts +28 -0
- package/src/types/shell.ts +30 -0
- package/src/types/store.ts +148 -0
- package/src/types/stream.ts +24 -0
- package/src/types/style.ts +205 -0
- package/src/types/surface.ts +119 -0
- package/src/types/syntax.ts +93 -0
- package/src/types/terminal.ts +58 -0
- package/src/types/theme.ts +121 -0
- package/src/types/when.ts +21 -0
- package/src/ui/primitives.ts +118 -0
- package/src/ui/screen.ts +41 -0
- package/src/util/disposable.ts +49 -0
- package/src/util/markdown.ts +225 -0
- package/src/util/paths.ts +138 -0
- package/src/util/stream.ts +213 -0
- package/src/util/text.ts +428 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { FocusableOptions, FocusDirection, FocusManager, FocusScopeOptions } from '../types/focus.js';
|
|
2
|
+
import type { KeyEvent } from '../types/input.js';
|
|
3
|
+
import type { Rect } from '../types/geometry.js';
|
|
4
|
+
import type { Disposable } from '../types/disposable.js';
|
|
5
|
+
/** The scope every global handler lives in, always active and always last. */
|
|
6
|
+
export declare const GLOBAL_SCOPE = "__global__";
|
|
7
|
+
/**
|
|
8
|
+
* Focus.
|
|
9
|
+
*
|
|
10
|
+
* Tab order is registration order within a scope unless a node states one.
|
|
11
|
+
* Directional navigation is geometric, because in a terminal "the thing to the
|
|
12
|
+
* right" is a real question a table, a menu and a dashboard all ask, and none
|
|
13
|
+
* of them can answer it from document order.
|
|
14
|
+
*/
|
|
15
|
+
export declare class Focus implements FocusManager {
|
|
16
|
+
private onChange;
|
|
17
|
+
private scopes;
|
|
18
|
+
private nodes;
|
|
19
|
+
/** Registration order, which is tab order when nothing states otherwise. */
|
|
20
|
+
private order_;
|
|
21
|
+
private stack;
|
|
22
|
+
private current;
|
|
23
|
+
constructor(onChange?: () => void);
|
|
24
|
+
registerScope(options: FocusScopeOptions): Disposable;
|
|
25
|
+
activateScope(id: string): void;
|
|
26
|
+
deactivateScope(id: string): void;
|
|
27
|
+
activeScope(): string | null;
|
|
28
|
+
/** The innermost active scope that traps, if any. */
|
|
29
|
+
private trappingScope;
|
|
30
|
+
/** Which scope a focusable belongs to. `GLOBAL_SCOPE` when it names none. */
|
|
31
|
+
scopeOf(nodeId: string): string;
|
|
32
|
+
register(options: FocusableOptions): Disposable;
|
|
33
|
+
update(id: string, patch: Partial<FocusableOptions>): void;
|
|
34
|
+
has(id: string): boolean;
|
|
35
|
+
focus(id: string): boolean;
|
|
36
|
+
blur(): void;
|
|
37
|
+
focused(): string | null;
|
|
38
|
+
chain(): string[];
|
|
39
|
+
/** Tab order: explicit `order` first, then registration order. */
|
|
40
|
+
order(scopeId?: string): string[];
|
|
41
|
+
move(direction: FocusDirection): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Geometric navigation. Candidates must lie in the requested direction; the
|
|
44
|
+
* winner is the nearest, with distance along the axis weighted more heavily
|
|
45
|
+
* than drift across it, so `right` from a table cell finds the next column
|
|
46
|
+
* rather than a distant button that happens to be closer in a straight line.
|
|
47
|
+
*/
|
|
48
|
+
private moveDirectional;
|
|
49
|
+
at(x: number, y: number): string | null;
|
|
50
|
+
/**
|
|
51
|
+
* Focused node first, then outward through active scopes, then global -
|
|
52
|
+
* except that a trap stops the walk at itself.
|
|
53
|
+
*
|
|
54
|
+
* `order` and `move` already honoured the trap, so tab could not leave a
|
|
55
|
+
* modal; keys could, and did. A menu bar whose labels live in the global
|
|
56
|
+
* scope kept receiving every arrow while its own dropdown was open, so down
|
|
57
|
+
* re-opened the menu instead of moving inside it, and a palette opened over
|
|
58
|
+
* the top inherited the same problem - two layers reading one keystroke, and
|
|
59
|
+
* two escapes needed to get out.
|
|
60
|
+
*
|
|
61
|
+
* A trap owns the keyboard. Nothing outside it is a target, including a
|
|
62
|
+
* focused node left over from before it opened.
|
|
63
|
+
*/
|
|
64
|
+
dispatch(event: KeyEvent): boolean;
|
|
65
|
+
/** Called by the renderer after layout, so hit-testing has real rects. */
|
|
66
|
+
setRect(id: string, rect: Rect): void;
|
|
67
|
+
}
|
|
68
|
+
export declare function createFocus(onChange?: () => void): Focus;
|
|
69
|
+
//# sourceMappingURL=focus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../../src/core/focus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAAE,cAAc,EAAE,YAAY,EAAE,iBAAiB,EAClE,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AASzD,8EAA8E;AAC9E,eAAO,MAAM,YAAY,eAAe,CAAC;AAEzC;;;;;;;GAOG;AACH,qBAAa,KAAM,YAAW,YAAY;IAQ5B,OAAO,CAAC,QAAQ;IAP5B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,KAAK,CAAuC;IACpD,4EAA4E;IAC5E,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,OAAO,CAAuB;gBAElB,QAAQ,GAAE,MAAM,IAAe;IAOnD,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU;IAQrD,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAe/B,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAgCjC,WAAW,IAAI,MAAM,GAAG,IAAI;IAK5B,qDAAqD;IACrD,OAAO,CAAC,aAAa;IAQrB,6EAA6E;IAC7E,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAM/B,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,UAAU;IAiC/C,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI;IAgB1D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAa1B,IAAI,IAAI,IAAI;IAQZ,OAAO,IAAI,MAAM,GAAG,IAAI;IAIxB,KAAK,IAAI,MAAM,EAAE;IAOjB,kEAAkE;IAClE,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE;IAsBjC,IAAI,CAAC,SAAS,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI;IAe9C;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAwCvB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAgBvC;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO;IAiClC,0EAA0E;IAC1E,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI;CAItC;AAED,wBAAgB,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,KAAK,CAExD"}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { toDisposable } from '../util/disposable.js';
|
|
2
|
+
import { rectContains } from '../types/geometry.js';
|
|
3
|
+
/** The scope every global handler lives in, always active and always last. */
|
|
4
|
+
export const GLOBAL_SCOPE = '__global__';
|
|
5
|
+
/**
|
|
6
|
+
* Focus.
|
|
7
|
+
*
|
|
8
|
+
* Tab order is registration order within a scope unless a node states one.
|
|
9
|
+
* Directional navigation is geometric, because in a terminal "the thing to the
|
|
10
|
+
* right" is a real question a table, a menu and a dashboard all ask, and none
|
|
11
|
+
* of them can answer it from document order.
|
|
12
|
+
*/
|
|
13
|
+
export class Focus {
|
|
14
|
+
onChange;
|
|
15
|
+
scopes = new Map();
|
|
16
|
+
nodes = new Map();
|
|
17
|
+
/** Registration order, which is tab order when nothing states otherwise. */
|
|
18
|
+
order_ = [];
|
|
19
|
+
stack = [];
|
|
20
|
+
current = null;
|
|
21
|
+
constructor(onChange = () => { }) {
|
|
22
|
+
this.onChange = onChange;
|
|
23
|
+
this.scopes.set(GLOBAL_SCOPE, { id: GLOBAL_SCOPE, priority: -1, restoreTo: null });
|
|
24
|
+
this.stack.push(GLOBAL_SCOPE);
|
|
25
|
+
}
|
|
26
|
+
// ------------------------------------------------------------- scopes
|
|
27
|
+
registerScope(options) {
|
|
28
|
+
this.scopes.set(options.id, { ...options, restoreTo: null });
|
|
29
|
+
return toDisposable(() => {
|
|
30
|
+
this.deactivateScope(options.id);
|
|
31
|
+
this.scopes.delete(options.id);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
activateScope(id) {
|
|
35
|
+
const scope = this.scopes.get(id);
|
|
36
|
+
if (!scope)
|
|
37
|
+
return;
|
|
38
|
+
if (this.stack.includes(id))
|
|
39
|
+
return;
|
|
40
|
+
scope.restoreTo = this.current;
|
|
41
|
+
this.stack.push(id);
|
|
42
|
+
if (scope.autoFocus) {
|
|
43
|
+
const first = this.order(id)[0];
|
|
44
|
+
if (first)
|
|
45
|
+
this.focus(first);
|
|
46
|
+
}
|
|
47
|
+
this.onChange();
|
|
48
|
+
}
|
|
49
|
+
deactivateScope(id) {
|
|
50
|
+
const index = this.stack.indexOf(id);
|
|
51
|
+
if (index <= 0)
|
|
52
|
+
return;
|
|
53
|
+
const scope = this.scopes.get(id);
|
|
54
|
+
this.stack.splice(index, 1);
|
|
55
|
+
// A scope closing may only move focus it was holding. Blurring regardless
|
|
56
|
+
// took focus off whatever was driving the change - a sidebar that swaps
|
|
57
|
+
// the screen beside it lost focus on every arrow press, and the incoming
|
|
58
|
+
// screen then claimed it, so the sidebar could be moved exactly once.
|
|
59
|
+
// "Nothing holds focus" counts as held: a scope closing takes its own
|
|
60
|
+
// contents with it, so by the time this runs the thing it had focused has
|
|
61
|
+
// usually already unmounted. What must not count is focus sitting on
|
|
62
|
+
// something *outside* - that belongs to whoever put it there.
|
|
63
|
+
const held = this.current === null || this.scopeOf(this.current) === id;
|
|
64
|
+
if (scope?.restore && held) {
|
|
65
|
+
const target = scope.restoreTo;
|
|
66
|
+
scope.restoreTo = null;
|
|
67
|
+
if (target && this.nodes.has(target)) {
|
|
68
|
+
this.focus(target);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.blur();
|
|
72
|
+
}
|
|
73
|
+
else if (scope?.restore) {
|
|
74
|
+
scope.restoreTo = null;
|
|
75
|
+
}
|
|
76
|
+
else if (held) {
|
|
77
|
+
this.blur();
|
|
78
|
+
}
|
|
79
|
+
this.onChange();
|
|
80
|
+
}
|
|
81
|
+
activeScope() {
|
|
82
|
+
const top = this.stack[this.stack.length - 1];
|
|
83
|
+
return top === GLOBAL_SCOPE ? null : top ?? null;
|
|
84
|
+
}
|
|
85
|
+
/** The innermost active scope that traps, if any. */
|
|
86
|
+
trappingScope() {
|
|
87
|
+
for (let i = this.stack.length - 1; i >= 0; i--) {
|
|
88
|
+
const id = this.stack[i];
|
|
89
|
+
if (this.scopes.get(id)?.trap)
|
|
90
|
+
return id;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/** Which scope a focusable belongs to. `GLOBAL_SCOPE` when it names none. */
|
|
95
|
+
scopeOf(nodeId) {
|
|
96
|
+
return this.nodes.get(nodeId)?.scopeId ?? GLOBAL_SCOPE;
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------- focusables
|
|
99
|
+
register(options) {
|
|
100
|
+
const existing = this.nodes.has(options.id);
|
|
101
|
+
this.nodes.set(options.id, options);
|
|
102
|
+
if (!existing)
|
|
103
|
+
this.order_.push(options.id);
|
|
104
|
+
// A scope that asked to take focus takes it here, as its first control
|
|
105
|
+
// arrives - not when it was activated, because a scope is empty then. Its
|
|
106
|
+
// contents register on the way up, one effect at a time, and effects run
|
|
107
|
+
// parent-first, so `autoFocus` at activation could only ever have found
|
|
108
|
+
// something in a scope that already had something in it.
|
|
109
|
+
//
|
|
110
|
+
// Only when *nothing at all* holds focus. That is the difference between
|
|
111
|
+
// the screen this is for - pushed over whatever had focus, which the push
|
|
112
|
+
// unmounted, leaving none - and a dialog, which opens while its opener
|
|
113
|
+
// still holds focus and whose own controls say which of them wants it.
|
|
114
|
+
if (!existing && options.disabled !== true && this.current === null) {
|
|
115
|
+
const scopeId = options.scopeId ?? GLOBAL_SCOPE;
|
|
116
|
+
if (this.scopes.get(scopeId)?.autoFocus === true && this.stack.includes(scopeId)) {
|
|
117
|
+
this.focus(options.id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return toDisposable(() => {
|
|
121
|
+
this.nodes.delete(options.id);
|
|
122
|
+
const i = this.order_.indexOf(options.id);
|
|
123
|
+
if (i >= 0)
|
|
124
|
+
this.order_.splice(i, 1);
|
|
125
|
+
if (this.current === options.id) {
|
|
126
|
+
this.current = null;
|
|
127
|
+
this.onChange();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
update(id, patch) {
|
|
132
|
+
const node = this.nodes.get(id);
|
|
133
|
+
if (!node)
|
|
134
|
+
return;
|
|
135
|
+
this.nodes.set(id, { ...node, ...patch });
|
|
136
|
+
// Focus cannot sit on something that has just been disabled. Nothing else
|
|
137
|
+
// would move it - the keys it would have handled are now nobody's - so the
|
|
138
|
+
// tab order is the only way out and this is what puts the reader back in
|
|
139
|
+
// it.
|
|
140
|
+
if (patch.disabled === true && this.current === id) {
|
|
141
|
+
this.current = null;
|
|
142
|
+
node.onBlur?.();
|
|
143
|
+
this.onChange();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
has(id) {
|
|
147
|
+
return this.nodes.has(id);
|
|
148
|
+
}
|
|
149
|
+
focus(id) {
|
|
150
|
+
const node = this.nodes.get(id);
|
|
151
|
+
if (!node || node.disabled)
|
|
152
|
+
return false;
|
|
153
|
+
if (this.current === id)
|
|
154
|
+
return true;
|
|
155
|
+
const previous = this.current ? this.nodes.get(this.current) : undefined;
|
|
156
|
+
this.current = id;
|
|
157
|
+
previous?.onBlur?.();
|
|
158
|
+
node.onFocus?.();
|
|
159
|
+
this.onChange();
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
blur() {
|
|
163
|
+
if (!this.current)
|
|
164
|
+
return;
|
|
165
|
+
const node = this.nodes.get(this.current);
|
|
166
|
+
this.current = null;
|
|
167
|
+
node?.onBlur?.();
|
|
168
|
+
this.onChange();
|
|
169
|
+
}
|
|
170
|
+
focused() {
|
|
171
|
+
return this.current;
|
|
172
|
+
}
|
|
173
|
+
chain() {
|
|
174
|
+
const out = [];
|
|
175
|
+
if (this.current)
|
|
176
|
+
out.push(this.current);
|
|
177
|
+
for (let i = this.stack.length - 1; i >= 0; i--)
|
|
178
|
+
out.push(this.stack[i]);
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/** Tab order: explicit `order` first, then registration order. */
|
|
182
|
+
order(scopeId) {
|
|
183
|
+
const trap = this.trappingScope();
|
|
184
|
+
const scope = scopeId ?? trap ?? undefined;
|
|
185
|
+
const candidates = this.order_
|
|
186
|
+
.map((id) => this.nodes.get(id))
|
|
187
|
+
.filter((n) => {
|
|
188
|
+
if (!n || n.disabled || n.skipTab)
|
|
189
|
+
return false;
|
|
190
|
+
if (scope === undefined)
|
|
191
|
+
return true;
|
|
192
|
+
return (n.scopeId ?? GLOBAL_SCOPE) === scope;
|
|
193
|
+
});
|
|
194
|
+
return candidates
|
|
195
|
+
.map((n, i) => ({ n, i }))
|
|
196
|
+
.sort((a, b) => {
|
|
197
|
+
const ao = a.n.order ?? Number.MAX_SAFE_INTEGER;
|
|
198
|
+
const bo = b.n.order ?? Number.MAX_SAFE_INTEGER;
|
|
199
|
+
return ao === bo ? a.i - b.i : ao - bo;
|
|
200
|
+
})
|
|
201
|
+
.map((e) => e.n.id);
|
|
202
|
+
}
|
|
203
|
+
move(direction) {
|
|
204
|
+
if (direction === 'next' || direction === 'previous') {
|
|
205
|
+
const list = this.order();
|
|
206
|
+
if (list.length === 0)
|
|
207
|
+
return null;
|
|
208
|
+
const index = this.current ? list.indexOf(this.current) : -1;
|
|
209
|
+
const step = direction === 'next' ? 1 : -1;
|
|
210
|
+
const nextIndex = index === -1
|
|
211
|
+
? (direction === 'next' ? 0 : list.length - 1)
|
|
212
|
+
: (index + step + list.length) % list.length;
|
|
213
|
+
const id = list[nextIndex];
|
|
214
|
+
return this.focus(id) ? id : null;
|
|
215
|
+
}
|
|
216
|
+
return this.moveDirectional(direction);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Geometric navigation. Candidates must lie in the requested direction; the
|
|
220
|
+
* winner is the nearest, with distance along the axis weighted more heavily
|
|
221
|
+
* than drift across it, so `right` from a table cell finds the next column
|
|
222
|
+
* rather than a distant button that happens to be closer in a straight line.
|
|
223
|
+
*/
|
|
224
|
+
moveDirectional(direction) {
|
|
225
|
+
const from = this.current ? this.nodes.get(this.current) : undefined;
|
|
226
|
+
const origin = from?.rect;
|
|
227
|
+
if (!origin) {
|
|
228
|
+
const first = this.order()[0];
|
|
229
|
+
return first && this.focus(first) ? first : null;
|
|
230
|
+
}
|
|
231
|
+
const trap = this.trappingScope();
|
|
232
|
+
const cx = origin.x + origin.width / 2;
|
|
233
|
+
const cy = origin.y + origin.height / 2;
|
|
234
|
+
let best = null;
|
|
235
|
+
for (const node of this.nodes.values()) {
|
|
236
|
+
if (node.id === from.id || node.disabled || !node.rect)
|
|
237
|
+
continue;
|
|
238
|
+
if (trap && (node.scopeId ?? GLOBAL_SCOPE) !== trap)
|
|
239
|
+
continue;
|
|
240
|
+
const r = node.rect;
|
|
241
|
+
const nx = r.x + r.width / 2;
|
|
242
|
+
const ny = r.y + r.height / 2;
|
|
243
|
+
const dx = nx - cx;
|
|
244
|
+
const dy = ny - cy;
|
|
245
|
+
const along = direction === 'left' ? -dx : direction === 'right' ? dx
|
|
246
|
+
: direction === 'up' ? -dy : dy;
|
|
247
|
+
if (along <= 0)
|
|
248
|
+
continue;
|
|
249
|
+
const across = direction === 'left' || direction === 'right'
|
|
250
|
+
? Math.abs(dy)
|
|
251
|
+
: Math.abs(dx);
|
|
252
|
+
const score = along + across * 3;
|
|
253
|
+
if (!best || score < best.score)
|
|
254
|
+
best = { id: node.id, score };
|
|
255
|
+
}
|
|
256
|
+
if (!best)
|
|
257
|
+
return null;
|
|
258
|
+
return this.focus(best.id) ? best.id : null;
|
|
259
|
+
}
|
|
260
|
+
at(x, y) {
|
|
261
|
+
let found = null;
|
|
262
|
+
let smallest = Infinity;
|
|
263
|
+
for (const node of this.nodes.values()) {
|
|
264
|
+
if (node.disabled || !node.rect)
|
|
265
|
+
continue;
|
|
266
|
+
if (!rectContains(node.rect, x, y))
|
|
267
|
+
continue;
|
|
268
|
+
// The innermost hit wins, and "innermost" is the smallest area.
|
|
269
|
+
const area = node.rect.width * node.rect.height;
|
|
270
|
+
if (area < smallest) {
|
|
271
|
+
smallest = area;
|
|
272
|
+
found = node.id;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Focused node first, then outward through active scopes, then global -
|
|
279
|
+
* except that a trap stops the walk at itself.
|
|
280
|
+
*
|
|
281
|
+
* `order` and `move` already honoured the trap, so tab could not leave a
|
|
282
|
+
* modal; keys could, and did. A menu bar whose labels live in the global
|
|
283
|
+
* scope kept receiving every arrow while its own dropdown was open, so down
|
|
284
|
+
* re-opened the menu instead of moving inside it, and a palette opened over
|
|
285
|
+
* the top inherited the same problem - two layers reading one keystroke, and
|
|
286
|
+
* two escapes needed to get out.
|
|
287
|
+
*
|
|
288
|
+
* A trap owns the keyboard. Nothing outside it is a target, including a
|
|
289
|
+
* focused node left over from before it opened.
|
|
290
|
+
*/
|
|
291
|
+
dispatch(event) {
|
|
292
|
+
const trap = this.trappingScope();
|
|
293
|
+
const inTrap = (node) => trap === null || (node.scopeId ?? GLOBAL_SCOPE) === trap;
|
|
294
|
+
if (this.current) {
|
|
295
|
+
const node = this.nodes.get(this.current);
|
|
296
|
+
if (node && inTrap(node) && (node.onKey?.(event) === true || event.handled))
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
// Then the handlers that are not about one control.
|
|
300
|
+
//
|
|
301
|
+
// Only those. This loop used to offer the event to every registered
|
|
302
|
+
// handler, focused or not, which quietly undid the line above it: a list
|
|
303
|
+
// moved on an arrow it never received, because the arrow reached it here.
|
|
304
|
+
// With nothing focused it went to whichever focusable registered first -
|
|
305
|
+
// so a page with two lists moved the earlier one, and a tab strip and a
|
|
306
|
+
// list beneath it split the arrow keys between them by accident. That
|
|
307
|
+
// reads as working until the second list exists.
|
|
308
|
+
for (let i = this.stack.length - 1; i >= 0; i--) {
|
|
309
|
+
const scopeId = this.stack[i];
|
|
310
|
+
if (trap !== null && scopeId !== trap)
|
|
311
|
+
continue;
|
|
312
|
+
for (const node of this.nodes.values()) {
|
|
313
|
+
if (node.id === this.current)
|
|
314
|
+
continue;
|
|
315
|
+
if (node.global !== true)
|
|
316
|
+
continue;
|
|
317
|
+
if ((node.scopeId ?? GLOBAL_SCOPE) !== scopeId)
|
|
318
|
+
continue;
|
|
319
|
+
if (!node.onKey)
|
|
320
|
+
continue;
|
|
321
|
+
if (node.onKey(event) === true || event.handled)
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
/** Called by the renderer after layout, so hit-testing has real rects. */
|
|
328
|
+
setRect(id, rect) {
|
|
329
|
+
const node = this.nodes.get(id);
|
|
330
|
+
if (node)
|
|
331
|
+
node.rect = rect;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
export function createFocus(onChange) {
|
|
335
|
+
return new Focus(onChange);
|
|
336
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { I18n, LocaleId, TranslationBundle } from '../types/i18n.js';
|
|
2
|
+
import type { Disposable } from '../types/disposable.js';
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight i18n. Formatting is `Intl`'s job - recreating number, date and
|
|
5
|
+
* list formatting would be a second, worse implementation of something the
|
|
6
|
+
* runtime already ships.
|
|
7
|
+
*/
|
|
8
|
+
export declare class I18nRegistry implements I18n {
|
|
9
|
+
private fallbackLocale;
|
|
10
|
+
locale: LocaleId;
|
|
11
|
+
private bundles;
|
|
12
|
+
private listeners;
|
|
13
|
+
constructor(locale?: LocaleId, fallbackLocale?: LocaleId);
|
|
14
|
+
setLocale(locale: LocaleId): void;
|
|
15
|
+
register(bundle: TranslationBundle): Disposable;
|
|
16
|
+
locales(): LocaleId[];
|
|
17
|
+
/** Missing keys fall through to the fallback locale, then to the key. */
|
|
18
|
+
t(key: string, values?: Record<string, unknown>): string;
|
|
19
|
+
number(value: number, options?: Intl.NumberFormatOptions): string;
|
|
20
|
+
date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
|
|
21
|
+
relative(value: number, unit: Intl.RelativeTimeFormatUnit): string;
|
|
22
|
+
list(items: string[], options?: Intl.ListFormatOptions): string;
|
|
23
|
+
plural(count: number, forms: Record<string, string>): string;
|
|
24
|
+
onChange(fn: (locale: LocaleId) => void): Disposable;
|
|
25
|
+
}
|
|
26
|
+
export declare function createI18n(locale?: string, fallback?: string): I18nRegistry;
|
|
27
|
+
//# sourceMappingURL=i18n.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/core/i18n.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAsBzD;;;;GAIG;AACH,qBAAa,YAAa,YAAW,IAAI;IAOrC,OAAO,CAAC,cAAc;IANxB,MAAM,EAAE,QAAQ,CAAC;IACjB,OAAO,CAAC,OAAO,CAAgD;IAC/D,OAAO,CAAC,SAAS,CAAyC;gBAGxD,MAAM,GAAE,QAAe,EACf,cAAc,GAAE,QAAe;IAKzC,SAAS,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI;IAMjC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,UAAU;IAS/C,OAAO,IAAI,QAAQ,EAAE;IAIrB,yEAAyE;IACzE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;IAgBxD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,mBAAmB,GAAG,MAAM;IAIjE,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,GAAG,MAAM;IAIxE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,sBAAsB,GAAG,MAAM;IAIlE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,iBAAiB,GAAG,MAAM;IAI/D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM;IAM5D,QAAQ,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,IAAI,GAAG,UAAU;CAIrD;AAED,wBAAgB,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAE3E"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { toDisposable } from '../util/disposable.js';
|
|
2
|
+
function lookup(messages, key) {
|
|
3
|
+
if (key in messages) {
|
|
4
|
+
const direct = messages[key];
|
|
5
|
+
return typeof direct === 'string' ? direct : undefined;
|
|
6
|
+
}
|
|
7
|
+
let cursor = messages;
|
|
8
|
+
for (const part of key.split('.')) {
|
|
9
|
+
if (cursor === null || typeof cursor !== 'object')
|
|
10
|
+
return undefined;
|
|
11
|
+
cursor = cursor[part];
|
|
12
|
+
}
|
|
13
|
+
return typeof cursor === 'string' ? cursor : undefined;
|
|
14
|
+
}
|
|
15
|
+
function interpolate(template, values) {
|
|
16
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => name in values ? String(values[name]) : match);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Lightweight i18n. Formatting is `Intl`'s job - recreating number, date and
|
|
20
|
+
* list formatting would be a second, worse implementation of something the
|
|
21
|
+
* runtime already ships.
|
|
22
|
+
*/
|
|
23
|
+
export class I18nRegistry {
|
|
24
|
+
fallbackLocale;
|
|
25
|
+
locale;
|
|
26
|
+
bundles = new Map();
|
|
27
|
+
listeners = new Set();
|
|
28
|
+
constructor(locale = 'en', fallbackLocale = 'en') {
|
|
29
|
+
this.fallbackLocale = fallbackLocale;
|
|
30
|
+
this.locale = locale;
|
|
31
|
+
}
|
|
32
|
+
setLocale(locale) {
|
|
33
|
+
if (this.locale === locale)
|
|
34
|
+
return;
|
|
35
|
+
this.locale = locale;
|
|
36
|
+
for (const fn of [...this.listeners])
|
|
37
|
+
fn(locale);
|
|
38
|
+
}
|
|
39
|
+
register(bundle) {
|
|
40
|
+
const existing = this.bundles.get(bundle.locale) ?? {};
|
|
41
|
+
this.bundles.set(bundle.locale, { ...existing, ...bundle.messages });
|
|
42
|
+
if (this.locale === bundle.locale) {
|
|
43
|
+
for (const fn of [...this.listeners])
|
|
44
|
+
fn(this.locale);
|
|
45
|
+
}
|
|
46
|
+
return toDisposable(() => this.bundles.delete(bundle.locale));
|
|
47
|
+
}
|
|
48
|
+
locales() {
|
|
49
|
+
return [...this.bundles.keys()];
|
|
50
|
+
}
|
|
51
|
+
/** Missing keys fall through to the fallback locale, then to the key. */
|
|
52
|
+
t(key, values) {
|
|
53
|
+
const primary = this.bundles.get(this.locale);
|
|
54
|
+
const base = this.locale.includes('-')
|
|
55
|
+
? this.bundles.get(this.locale.split('-')[0])
|
|
56
|
+
: undefined;
|
|
57
|
+
const fallback = this.bundles.get(this.fallbackLocale);
|
|
58
|
+
const template = (primary && lookup(primary, key)) ??
|
|
59
|
+
(base && lookup(base, key)) ??
|
|
60
|
+
(fallback && lookup(fallback, key)) ??
|
|
61
|
+
key;
|
|
62
|
+
return values ? interpolate(template, values) : template;
|
|
63
|
+
}
|
|
64
|
+
number(value, options) {
|
|
65
|
+
return new Intl.NumberFormat(this.locale, options).format(value);
|
|
66
|
+
}
|
|
67
|
+
date(value, options) {
|
|
68
|
+
return new Intl.DateTimeFormat(this.locale, options).format(value);
|
|
69
|
+
}
|
|
70
|
+
relative(value, unit) {
|
|
71
|
+
return new Intl.RelativeTimeFormat(this.locale, { numeric: 'auto' }).format(value, unit);
|
|
72
|
+
}
|
|
73
|
+
list(items, options) {
|
|
74
|
+
return new Intl.ListFormat(this.locale, options).format(items);
|
|
75
|
+
}
|
|
76
|
+
plural(count, forms) {
|
|
77
|
+
const rule = new Intl.PluralRules(this.locale).select(count);
|
|
78
|
+
const template = forms[rule] ?? forms.other ?? '';
|
|
79
|
+
return interpolate(template, { count });
|
|
80
|
+
}
|
|
81
|
+
onChange(fn) {
|
|
82
|
+
this.listeners.add(fn);
|
|
83
|
+
return toDisposable(() => this.listeners.delete(fn));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function createI18n(locale, fallback) {
|
|
87
|
+
return new I18nRegistry(locale, fallback);
|
|
88
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Chord, KeybindingDefinition, KeybindingRegistry } from '../types/keybinding.js';
|
|
2
|
+
import type { KeyEvent } from '../types/input.js';
|
|
3
|
+
import type { Disposable } from '../types/disposable.js';
|
|
4
|
+
import type { WhenEngine } from '../types/when.js';
|
|
5
|
+
import type { CommandRegistry } from '../types/command.js';
|
|
6
|
+
/**
|
|
7
|
+
* Split one stroke into its modifiers and its key.
|
|
8
|
+
*
|
|
9
|
+
* Modifiers are consumed off the front by name and whatever survives is the
|
|
10
|
+
* key, verbatim. Splitting on `+` cannot work here: `+` is the separator and
|
|
11
|
+
* a key, so `'+'.split('+')` loses the only thing it was asked about. Parsing
|
|
12
|
+
* forwards means `+` is `+`, and `ctrl++` is ctrl and `+`.
|
|
13
|
+
*
|
|
14
|
+
* One parser, exported, because a chord that the test harness presses and a
|
|
15
|
+
* chord that the registry stored have to agree - and two implementations of
|
|
16
|
+
* one grammar disagree eventually.
|
|
17
|
+
*/
|
|
18
|
+
export declare function splitStroke(stroke: string): {
|
|
19
|
+
mods: string[];
|
|
20
|
+
key: string;
|
|
21
|
+
};
|
|
22
|
+
/** The canonical stroke a chord is filed under. `strokeOf` must agree with it. */
|
|
23
|
+
export declare function normalizeStroke(stroke: string): string;
|
|
24
|
+
export declare function strokeOf(event: KeyEvent): string;
|
|
25
|
+
/**
|
|
26
|
+
* Keybindings resolve to commands, never to handlers, so the same action is
|
|
27
|
+
* reachable from a chord, a menu row and the palette without three copies.
|
|
28
|
+
* A partially typed chord is remembered, which is what makes `ctrl+k ctrl+s`
|
|
29
|
+
* possible without blocking `ctrl+k` on its own.
|
|
30
|
+
*/
|
|
31
|
+
export declare class Keybindings implements KeybindingRegistry {
|
|
32
|
+
private deps;
|
|
33
|
+
private bindings;
|
|
34
|
+
private pending;
|
|
35
|
+
private pendingTimer;
|
|
36
|
+
constructor(deps: {
|
|
37
|
+
when: WhenEngine;
|
|
38
|
+
commands: CommandRegistry;
|
|
39
|
+
activeScopes(): string[];
|
|
40
|
+
onError(err: unknown, context: string): void;
|
|
41
|
+
/** How long a half-typed chord waits for its next stroke. */
|
|
42
|
+
chordTimeoutMs?: number;
|
|
43
|
+
});
|
|
44
|
+
register(def: KeybindingDefinition): Disposable;
|
|
45
|
+
unregister(keys: Chord, commandId?: string): void;
|
|
46
|
+
list(): KeybindingDefinition[];
|
|
47
|
+
forCommand(commandId: string): Chord[];
|
|
48
|
+
private applicable;
|
|
49
|
+
handle(event: KeyEvent): 'handled' | 'pending' | 'unhandled';
|
|
50
|
+
reset(): void;
|
|
51
|
+
}
|
|
52
|
+
export declare function createKeybindings(deps: ConstructorParameters<typeof Keybindings>[0]): Keybindings;
|
|
53
|
+
//# sourceMappingURL=keybindings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keybindings.d.ts","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC9F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAU3D;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAS3E;AAED,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAuBtD;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAYhD;AAED;;;;;GAKG;AACH,qBAAa,WAAY,YAAW,kBAAkB;IAMlD,OAAO,CAAC,IAAI;IALd,OAAO,CAAC,QAAQ,CAAwD;IACxE,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAA8C;gBAGxD,IAAI,EAAE;QACZ,IAAI,EAAE,UAAU,CAAC;QACjB,QAAQ,EAAE,eAAe,CAAC;QAC1B,YAAY,IAAI,MAAM,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7C,6DAA6D;QAC7D,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB;IAGH,QAAQ,CAAC,GAAG,EAAE,oBAAoB,GAAG,UAAU;IAU/C,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAOjD,IAAI,IAAI,oBAAoB,EAAE;IAI9B,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,EAAE;IAUtC,OAAO,CAAC,UAAU;IAMlB,MAAM,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW;IAoC5D,KAAK,IAAI,IAAI;CAOd;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,CAEjG"}
|