@flyingrobots/bijou-tui 0.5.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -7
- package/dist/browsable-list.d.ts +87 -0
- package/dist/browsable-list.d.ts.map +1 -0
- package/dist/browsable-list.js +143 -0
- package/dist/browsable-list.js.map +1 -0
- package/dist/command-palette.d.ts +101 -0
- package/dist/command-palette.d.ts.map +1 -0
- package/dist/command-palette.js +212 -0
- package/dist/command-palette.js.map +1 -0
- package/dist/file-picker.d.ts +91 -0
- package/dist/file-picker.d.ts.map +1 -0
- package/dist/file-picker.js +210 -0
- package/dist/file-picker.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +14 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +71 -0
- package/dist/layout.js.map +1 -1
- package/dist/navigable-table.d.ts +79 -0
- package/dist/navigable-table.d.ts.map +1 -0
- package/dist/navigable-table.js +145 -0
- package/dist/navigable-table.js.map +1 -0
- package/dist/overlay.d.ts +28 -0
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +89 -1
- package/dist/overlay.js.map +1 -1
- package/dist/status-bar.d.ts +17 -0
- package/dist/status-bar.d.ts.map +1 -0
- package/dist/status-bar.js +106 -0
- package/dist/status-bar.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,14 +4,11 @@ TEA runtime for terminal UIs — model/update/view with physics-based animation,
|
|
|
4
4
|
|
|
5
5
|
Inspired by [Bubble Tea](https://github.com/charmbracelet/bubbletea) (Go) and [GSAP](https://gsap.com/) animation.
|
|
6
6
|
|
|
7
|
-
## What's New in 0.
|
|
7
|
+
## What's New in 0.6.0?
|
|
8
8
|
|
|
9
|
-
- **`
|
|
10
|
-
- **`
|
|
11
|
-
- **`
|
|
12
|
-
- **`pager()`** — scrollable content viewer with status line and convenience keymap
|
|
13
|
-
- **`interactiveAccordion()`** — navigable accordion with focus tracking and expand/collapse
|
|
14
|
-
- **`createPanelGroup()`** — multi-pane focus management with hotkey switching
|
|
9
|
+
- **`navigableTable()`** — keyboard-navigable table with focus management, vertical scrolling, and vim-style keybindings
|
|
10
|
+
- **`browsableList()`** — navigable list with focus tracking, scroll viewport, descriptions, and convenience keymap
|
|
11
|
+
- **`filePicker()`** — directory browser with focus navigation, extension filtering, and `IOPort` integration
|
|
15
12
|
|
|
16
13
|
See the [CHANGELOG](https://github.com/flyingrobots/bijou/blob/main/docs/CHANGELOG.md) for the full release history.
|
|
17
14
|
|
|
@@ -286,6 +283,51 @@ const output = composite(backgroundView, [dialog, notification], { dim: true });
|
|
|
286
283
|
|
|
287
284
|
Each overlay is a `{ content, row, col }` object. `composite()` splices them onto the background using painter's algorithm (last overlay wins on overlap). The `dim` option fades the background with ANSI dim.
|
|
288
285
|
|
|
286
|
+
## Building Blocks
|
|
287
|
+
|
|
288
|
+
Reusable stateful components that follow the TEA state + pure transformers + sync render + convenience keymap pattern:
|
|
289
|
+
|
|
290
|
+
### Navigable Table
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import {
|
|
294
|
+
createNavigableTableState, navigableTable, navTableFocusNext,
|
|
295
|
+
navTableKeyMap, helpShort,
|
|
296
|
+
} from '@flyingrobots/bijou-tui';
|
|
297
|
+
|
|
298
|
+
const state = createNavigableTableState({ columns, rows, height: 10 });
|
|
299
|
+
const output = navigableTable(state, { ctx });
|
|
300
|
+
const next = navTableFocusNext(state);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Browsable List
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
import {
|
|
307
|
+
createBrowsableListState, browsableList, listFocusNext,
|
|
308
|
+
browsableListKeyMap,
|
|
309
|
+
} from '@flyingrobots/bijou-tui';
|
|
310
|
+
|
|
311
|
+
const state = createBrowsableListState({ items, height: 10 });
|
|
312
|
+
const output = browsableList(state);
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### File Picker
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import {
|
|
319
|
+
createFilePickerState, filePicker, fpFocusNext, fpEnter, fpBack,
|
|
320
|
+
filePickerKeyMap,
|
|
321
|
+
} from '@flyingrobots/bijou-tui';
|
|
322
|
+
import { nodeIO } from '@flyingrobots/bijou-node';
|
|
323
|
+
|
|
324
|
+
const io = nodeIO();
|
|
325
|
+
const state = createFilePickerState({ cwd: process.cwd(), io, height: 15 });
|
|
326
|
+
const output = filePicker(state);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
All building blocks include `*KeyMap()` factories for preconfigured vim-style keybindings.
|
|
330
|
+
|
|
289
331
|
## Related Packages
|
|
290
332
|
|
|
291
333
|
- [`@flyingrobots/bijou`](https://www.npmjs.com/package/@flyingrobots/bijou) — Zero-dependency core with all components and theme engine
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browsable list building block — a scrollable, navigable list with focus tracking.
|
|
3
|
+
*
|
|
4
|
+
* Provides state transformers for focus and page navigation, a pure render
|
|
5
|
+
* function with viewport clipping, and a convenience keymap for vim-style
|
|
6
|
+
* navigation.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // In TEA init:
|
|
10
|
+
* const listState = createBrowsableListState({ items, height: 10 });
|
|
11
|
+
*
|
|
12
|
+
* // In TEA view:
|
|
13
|
+
* const output = browsableList(model.listState);
|
|
14
|
+
*
|
|
15
|
+
* // In TEA update:
|
|
16
|
+
* case 'focus-next':
|
|
17
|
+
* return [{ ...model, listState: listFocusNext(model.listState) }, []];
|
|
18
|
+
* case 'select':
|
|
19
|
+
* const selected = model.listState.items[model.listState.focusIndex];
|
|
20
|
+
* // handle selection...
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import type { BijouContext } from '@flyingrobots/bijou';
|
|
24
|
+
import { type KeyMap } from './keybindings.js';
|
|
25
|
+
export interface BrowsableListItem<T = string> {
|
|
26
|
+
label: string;
|
|
27
|
+
value: T;
|
|
28
|
+
description?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface BrowsableListState<T = string> {
|
|
31
|
+
readonly items: readonly BrowsableListItem<T>[];
|
|
32
|
+
readonly focusIndex: number;
|
|
33
|
+
readonly scrollY: number;
|
|
34
|
+
readonly height: number;
|
|
35
|
+
}
|
|
36
|
+
export interface BrowsableListOptions<T = string> {
|
|
37
|
+
readonly items: readonly BrowsableListItem<T>[];
|
|
38
|
+
readonly height?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface BrowsableListRenderOptions {
|
|
41
|
+
readonly focusIndicator?: string;
|
|
42
|
+
readonly ctx?: BijouContext;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create initial browsable list state from items and optional height.
|
|
46
|
+
* Focus starts at index 0 with scroll at the top.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createBrowsableListState<T = string>(options: BrowsableListOptions<T>): BrowsableListState<T>;
|
|
49
|
+
/** Move focus to the next item (wraps around). */
|
|
50
|
+
export declare function listFocusNext<T>(state: BrowsableListState<T>): BrowsableListState<T>;
|
|
51
|
+
/** Move focus to the previous item (wraps around). */
|
|
52
|
+
export declare function listFocusPrev<T>(state: BrowsableListState<T>): BrowsableListState<T>;
|
|
53
|
+
/** Move focus down by one page (clamps to last item). */
|
|
54
|
+
export declare function listPageDown<T>(state: BrowsableListState<T>): BrowsableListState<T>;
|
|
55
|
+
/** Move focus up by one page (clamps to first item). */
|
|
56
|
+
export declare function listPageUp<T>(state: BrowsableListState<T>): BrowsableListState<T>;
|
|
57
|
+
/**
|
|
58
|
+
* Render the browsable list — visible items within the viewport with a
|
|
59
|
+
* focus indicator on the currently focused item.
|
|
60
|
+
*
|
|
61
|
+
* Items with a `description` field render as `label — description`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function browsableList<T>(state: BrowsableListState<T>, options?: BrowsableListRenderOptions): string;
|
|
64
|
+
/**
|
|
65
|
+
* Create a preconfigured KeyMap for browsable list navigation.
|
|
66
|
+
*
|
|
67
|
+
* The caller provides their own message types for each action:
|
|
68
|
+
* ```ts
|
|
69
|
+
* const keys = browsableListKeyMap({
|
|
70
|
+
* focusNext: { type: 'next' },
|
|
71
|
+
* focusPrev: { type: 'prev' },
|
|
72
|
+
* pageDown: { type: 'page-down' },
|
|
73
|
+
* pageUp: { type: 'page-up' },
|
|
74
|
+
* select: { type: 'select' },
|
|
75
|
+
* quit: { type: 'quit' },
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function browsableListKeyMap<Msg>(actions: {
|
|
80
|
+
focusNext: Msg;
|
|
81
|
+
focusPrev: Msg;
|
|
82
|
+
pageDown: Msg;
|
|
83
|
+
pageUp: Msg;
|
|
84
|
+
select: Msg;
|
|
85
|
+
quit: Msg;
|
|
86
|
+
}): KeyMap<Msg>;
|
|
87
|
+
//# sourceMappingURL=browsable-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browsable-list.d.ts","sourceRoot":"","sources":["../src/browsable-list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAM7D,MAAM,WAAW,iBAAiB,CAAC,CAAC,GAAG,MAAM;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,CAAC,CAAC;IACT,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,MAAM;IAC5C,QAAQ,CAAC,KAAK,EAAE,SAAS,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC;IAChD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC,GAAG,MAAM;IAC9C,QAAQ,CAAC,KAAK,EAAE,SAAS,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC;IAChD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,YAAY,CAAC;CAC7B;AAMD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,MAAM,EACjD,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC/B,kBAAkB,CAAC,CAAC,CAAC,CAQvB;AAMD,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAIpF;AAED,sDAAsD;AACtD,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAIpF;AAED,yDAAyD;AACzD,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAInF;AAED,wDAAwD;AACxD,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAIjF;AAqBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAC5B,OAAO,CAAC,EAAE,0BAA0B,GACnC,MAAM,CAkBR;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE;IAChD,SAAS,EAAE,GAAG,CAAC;IACf,SAAS,EAAE,GAAG,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,EAAE,GAAG,CAAC;IACZ,MAAM,EAAE,GAAG,CAAC;IACZ,IAAI,EAAE,GAAG,CAAC;CACX,GAAG,MAAM,CAAC,GAAG,CAAC,CAed"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browsable list building block — a scrollable, navigable list with focus tracking.
|
|
3
|
+
*
|
|
4
|
+
* Provides state transformers for focus and page navigation, a pure render
|
|
5
|
+
* function with viewport clipping, and a convenience keymap for vim-style
|
|
6
|
+
* navigation.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // In TEA init:
|
|
10
|
+
* const listState = createBrowsableListState({ items, height: 10 });
|
|
11
|
+
*
|
|
12
|
+
* // In TEA view:
|
|
13
|
+
* const output = browsableList(model.listState);
|
|
14
|
+
*
|
|
15
|
+
* // In TEA update:
|
|
16
|
+
* case 'focus-next':
|
|
17
|
+
* return [{ ...model, listState: listFocusNext(model.listState) }, []];
|
|
18
|
+
* case 'select':
|
|
19
|
+
* const selected = model.listState.items[model.listState.focusIndex];
|
|
20
|
+
* // handle selection...
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { createKeyMap } from './keybindings.js';
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// State creation
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Create initial browsable list state from items and optional height.
|
|
29
|
+
* Focus starts at index 0 with scroll at the top.
|
|
30
|
+
*/
|
|
31
|
+
export function createBrowsableListState(options) {
|
|
32
|
+
const height = Math.max(1, options.height ?? 10);
|
|
33
|
+
return {
|
|
34
|
+
items: [...options.items],
|
|
35
|
+
focusIndex: 0,
|
|
36
|
+
scrollY: 0,
|
|
37
|
+
height,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// State transformers
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
/** Move focus to the next item (wraps around). */
|
|
44
|
+
export function listFocusNext(state) {
|
|
45
|
+
if (state.items.length === 0)
|
|
46
|
+
return state;
|
|
47
|
+
const focusIndex = (state.focusIndex + 1) % state.items.length;
|
|
48
|
+
return { ...state, focusIndex, scrollY: adjustScroll(focusIndex, state.scrollY, state.height, state.items.length) };
|
|
49
|
+
}
|
|
50
|
+
/** Move focus to the previous item (wraps around). */
|
|
51
|
+
export function listFocusPrev(state) {
|
|
52
|
+
if (state.items.length === 0)
|
|
53
|
+
return state;
|
|
54
|
+
const focusIndex = (state.focusIndex - 1 + state.items.length) % state.items.length;
|
|
55
|
+
return { ...state, focusIndex, scrollY: adjustScroll(focusIndex, state.scrollY, state.height, state.items.length) };
|
|
56
|
+
}
|
|
57
|
+
/** Move focus down by one page (clamps to last item). */
|
|
58
|
+
export function listPageDown(state) {
|
|
59
|
+
if (state.items.length === 0)
|
|
60
|
+
return state;
|
|
61
|
+
const focusIndex = Math.min(state.focusIndex + state.height, state.items.length - 1);
|
|
62
|
+
return { ...state, focusIndex, scrollY: adjustScroll(focusIndex, state.scrollY, state.height, state.items.length) };
|
|
63
|
+
}
|
|
64
|
+
/** Move focus up by one page (clamps to first item). */
|
|
65
|
+
export function listPageUp(state) {
|
|
66
|
+
if (state.items.length === 0)
|
|
67
|
+
return state;
|
|
68
|
+
const focusIndex = Math.max(state.focusIndex - state.height, 0);
|
|
69
|
+
return { ...state, focusIndex, scrollY: adjustScroll(focusIndex, state.scrollY, state.height, state.items.length) };
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Scroll helper
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
function adjustScroll(focusIndex, scrollY, height, totalItems) {
|
|
75
|
+
let newScrollY = scrollY;
|
|
76
|
+
if (focusIndex < newScrollY) {
|
|
77
|
+
newScrollY = focusIndex;
|
|
78
|
+
}
|
|
79
|
+
else if (focusIndex >= newScrollY + height) {
|
|
80
|
+
newScrollY = focusIndex - height + 1;
|
|
81
|
+
}
|
|
82
|
+
const maxScroll = Math.max(0, totalItems - height);
|
|
83
|
+
return Math.min(newScrollY, maxScroll);
|
|
84
|
+
}
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Render
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
/**
|
|
89
|
+
* Render the browsable list — visible items within the viewport with a
|
|
90
|
+
* focus indicator on the currently focused item.
|
|
91
|
+
*
|
|
92
|
+
* Items with a `description` field render as `label — description`.
|
|
93
|
+
*/
|
|
94
|
+
export function browsableList(state, options) {
|
|
95
|
+
if (state.items.length === 0)
|
|
96
|
+
return '';
|
|
97
|
+
const indicator = options?.focusIndicator ?? '\u25b8';
|
|
98
|
+
const pad = ' '.repeat(indicator.length);
|
|
99
|
+
const visibleItems = state.items.slice(state.scrollY, state.scrollY + state.height);
|
|
100
|
+
const lines = [];
|
|
101
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
102
|
+
const item = visibleItems[i];
|
|
103
|
+
const globalIndex = state.scrollY + i;
|
|
104
|
+
const prefix = globalIndex === state.focusIndex ? indicator : pad;
|
|
105
|
+
const desc = item.description ? ` \u2014 ${item.description}` : '';
|
|
106
|
+
lines.push(`${prefix} ${item.label}${desc}`);
|
|
107
|
+
}
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Convenience keymap
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
/**
|
|
114
|
+
* Create a preconfigured KeyMap for browsable list navigation.
|
|
115
|
+
*
|
|
116
|
+
* The caller provides their own message types for each action:
|
|
117
|
+
* ```ts
|
|
118
|
+
* const keys = browsableListKeyMap({
|
|
119
|
+
* focusNext: { type: 'next' },
|
|
120
|
+
* focusPrev: { type: 'prev' },
|
|
121
|
+
* pageDown: { type: 'page-down' },
|
|
122
|
+
* pageUp: { type: 'page-up' },
|
|
123
|
+
* select: { type: 'select' },
|
|
124
|
+
* quit: { type: 'quit' },
|
|
125
|
+
* });
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function browsableListKeyMap(actions) {
|
|
129
|
+
return createKeyMap()
|
|
130
|
+
.group('Navigation', (g) => g
|
|
131
|
+
.bind('j', 'Next item', actions.focusNext)
|
|
132
|
+
.bind('down', 'Next item', actions.focusNext)
|
|
133
|
+
.bind('k', 'Previous item', actions.focusPrev)
|
|
134
|
+
.bind('up', 'Previous item', actions.focusPrev)
|
|
135
|
+
.bind('d', 'Page down', actions.pageDown)
|
|
136
|
+
.bind('pagedown', 'Page down', actions.pageDown)
|
|
137
|
+
.bind('u', 'Page up', actions.pageUp)
|
|
138
|
+
.bind('pageup', 'Page up', actions.pageUp))
|
|
139
|
+
.bind('enter', 'Select', actions.select)
|
|
140
|
+
.bind('q', 'Quit', actions.quit)
|
|
141
|
+
.bind('ctrl+c', 'Quit', actions.quit);
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=browsable-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browsable-list.js","sourceRoot":"","sources":["../src/browsable-list.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command palette building block — a filterable, navigable action list.
|
|
3
|
+
*
|
|
4
|
+
* Provides state transformers for filtering, focus, and page navigation,
|
|
5
|
+
* a pure render function with viewport clipping, and a convenience keymap.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* // In TEA init:
|
|
9
|
+
* const cpState = createCommandPaletteState(items);
|
|
10
|
+
*
|
|
11
|
+
* // In TEA view:
|
|
12
|
+
* const output = commandPalette(model.cpState, { width: 60 });
|
|
13
|
+
*
|
|
14
|
+
* // In TEA update:
|
|
15
|
+
* case 'filter':
|
|
16
|
+
* return [{ ...model, cpState: cpFilter(model.cpState, msg.query) }, []];
|
|
17
|
+
* case 'select':
|
|
18
|
+
* const selected = cpSelectedItem(model.cpState);
|
|
19
|
+
* // handle selection...
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import type { BijouContext } from '@flyingrobots/bijou';
|
|
23
|
+
import { type KeyMap } from './keybindings.js';
|
|
24
|
+
export interface CommandPaletteItem {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly label: string;
|
|
27
|
+
readonly description?: string;
|
|
28
|
+
readonly category?: string;
|
|
29
|
+
readonly shortcut?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface CommandPaletteState {
|
|
32
|
+
readonly items: readonly CommandPaletteItem[];
|
|
33
|
+
readonly filteredItems: readonly CommandPaletteItem[];
|
|
34
|
+
readonly query: string;
|
|
35
|
+
readonly focusIndex: number;
|
|
36
|
+
readonly scrollY: number;
|
|
37
|
+
readonly height: number;
|
|
38
|
+
}
|
|
39
|
+
export interface CommandPaletteOptions {
|
|
40
|
+
readonly width: number;
|
|
41
|
+
readonly showCategory?: boolean;
|
|
42
|
+
readonly showShortcut?: boolean;
|
|
43
|
+
readonly ctx?: BijouContext;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create initial command palette state from items and optional height.
|
|
47
|
+
* Focus starts at index 0 with empty query showing all items.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createCommandPaletteState(items: readonly CommandPaletteItem[], height?: number): CommandPaletteState;
|
|
50
|
+
/** Filter items by case-insensitive substring match. Resets focus to 0. */
|
|
51
|
+
export declare function cpFilter(state: CommandPaletteState, query: string): CommandPaletteState;
|
|
52
|
+
/** Move focus to the next item (wraps around on filteredItems). */
|
|
53
|
+
export declare function cpFocusNext(state: CommandPaletteState): CommandPaletteState;
|
|
54
|
+
/** Move focus to the previous item (wraps around on filteredItems). */
|
|
55
|
+
export declare function cpFocusPrev(state: CommandPaletteState): CommandPaletteState;
|
|
56
|
+
/** Move focus down by half a page (vim Ctrl+D convention, clamps to last item). */
|
|
57
|
+
export declare function cpPageDown(state: CommandPaletteState): CommandPaletteState;
|
|
58
|
+
/** Move focus up by half a page (vim Ctrl+U convention, clamps to first item). */
|
|
59
|
+
export declare function cpPageUp(state: CommandPaletteState): CommandPaletteState;
|
|
60
|
+
/** Get the currently focused item, or undefined if no items match. */
|
|
61
|
+
export declare function cpSelectedItem(state: CommandPaletteState): CommandPaletteItem | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Render the command palette — a search line followed by filtered items
|
|
64
|
+
* in the viewport with a focus indicator.
|
|
65
|
+
*
|
|
66
|
+
* Layout:
|
|
67
|
+
* - Line 1: `> {query}` search input
|
|
68
|
+
* - Lines 2+: filtered items in viewport
|
|
69
|
+
* - Each item: `[category] label description shortcut`
|
|
70
|
+
* - Focus indicator: `▸` on focused item
|
|
71
|
+
*/
|
|
72
|
+
export declare function commandPalette(state: CommandPaletteState, options: CommandPaletteOptions): string;
|
|
73
|
+
/**
|
|
74
|
+
* Create a preconfigured KeyMap for command palette navigation.
|
|
75
|
+
*
|
|
76
|
+
* Bindings: Ctrl+N/Down (next), Ctrl+P/Up (prev), Ctrl+D/PageDown (page down),
|
|
77
|
+
* Ctrl+U/PageUp (page up), Enter (select), Escape (close).
|
|
78
|
+
*
|
|
79
|
+
* Ctrl+D and Ctrl+U follow vim half-page scroll conventions. In raw-mode TUIs
|
|
80
|
+
* these do not conflict with shell behavior (EOF / line-clear).
|
|
81
|
+
*
|
|
82
|
+
* ```ts
|
|
83
|
+
* const keys = commandPaletteKeyMap({
|
|
84
|
+
* focusNext: { type: 'cp-next' },
|
|
85
|
+
* focusPrev: { type: 'cp-prev' },
|
|
86
|
+
* pageDown: { type: 'cp-page-down' },
|
|
87
|
+
* pageUp: { type: 'cp-page-up' },
|
|
88
|
+
* select: { type: 'cp-select' },
|
|
89
|
+
* close: { type: 'cp-close' },
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export declare function commandPaletteKeyMap<Msg>(actions: {
|
|
94
|
+
focusNext: Msg;
|
|
95
|
+
focusPrev: Msg;
|
|
96
|
+
pageDown: Msg;
|
|
97
|
+
pageUp: Msg;
|
|
98
|
+
select: Msg;
|
|
99
|
+
close: Msg;
|
|
100
|
+
}): KeyMap<Msg>;
|
|
101
|
+
//# sourceMappingURL=command-palette.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../src/command-palette.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAO7D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC9C,QAAQ,CAAC,aAAa,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,GAAG,CAAC,EAAE,YAAY,CAAC;CAC7B;AAMD;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,SAAS,kBAAkB,EAAE,EACpC,MAAM,SAAK,GACV,mBAAmB,CAWrB;AAgBD,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,CAkBvF;AAaD,mEAAmE;AACnE,wBAAgB,WAAW,CAAC,KAAK,EAAE,mBAAmB,GAAG,mBAAmB,CAI3E;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,KAAK,EAAE,mBAAmB,GAAG,mBAAmB,CAI3E;AAED,mFAAmF;AACnF,wBAAgB,UAAU,CAAC,KAAK,EAAE,mBAAmB,GAAG,mBAAmB,CAK1E;AAED,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,mBAAmB,CAKxE;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,kBAAkB,GAAG,SAAS,CAEzF;AAMD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,qBAAqB,GAC7B,MAAM,CAiDR;AAMD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE;IACjD,SAAS,EAAE,GAAG,CAAC;IACf,SAAS,EAAE,GAAG,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,EAAE,GAAG,CAAC;IACZ,MAAM,EAAE,GAAG,CAAC;IACZ,KAAK,EAAE,GAAG,CAAC;CACZ,GAAG,MAAM,CAAC,GAAG,CAAC,CAcd"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command palette building block — a filterable, navigable action list.
|
|
3
|
+
*
|
|
4
|
+
* Provides state transformers for filtering, focus, and page navigation,
|
|
5
|
+
* a pure render function with viewport clipping, and a convenience keymap.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* // In TEA init:
|
|
9
|
+
* const cpState = createCommandPaletteState(items);
|
|
10
|
+
*
|
|
11
|
+
* // In TEA view:
|
|
12
|
+
* const output = commandPalette(model.cpState, { width: 60 });
|
|
13
|
+
*
|
|
14
|
+
* // In TEA update:
|
|
15
|
+
* case 'filter':
|
|
16
|
+
* return [{ ...model, cpState: cpFilter(model.cpState, msg.query) }, []];
|
|
17
|
+
* case 'select':
|
|
18
|
+
* const selected = cpSelectedItem(model.cpState);
|
|
19
|
+
* // handle selection...
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { createKeyMap } from './keybindings.js';
|
|
23
|
+
import { clipToWidth } from './viewport.js';
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// State creation
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Create initial command palette state from items and optional height.
|
|
29
|
+
* Focus starts at index 0 with empty query showing all items.
|
|
30
|
+
*/
|
|
31
|
+
export function createCommandPaletteState(items, height = 10) {
|
|
32
|
+
const h = Math.max(1, height);
|
|
33
|
+
const copied = [...items];
|
|
34
|
+
return {
|
|
35
|
+
items: copied,
|
|
36
|
+
filteredItems: copied,
|
|
37
|
+
query: '',
|
|
38
|
+
focusIndex: 0,
|
|
39
|
+
scrollY: 0,
|
|
40
|
+
height: h,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Filtering
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
function matchesQuery(item, query) {
|
|
47
|
+
const q = query.toLowerCase();
|
|
48
|
+
if (item.label.toLowerCase().includes(q))
|
|
49
|
+
return true;
|
|
50
|
+
if (item.description?.toLowerCase().includes(q))
|
|
51
|
+
return true;
|
|
52
|
+
if (item.category?.toLowerCase().includes(q))
|
|
53
|
+
return true;
|
|
54
|
+
if (item.id.toLowerCase().includes(q))
|
|
55
|
+
return true;
|
|
56
|
+
if (item.shortcut?.toLowerCase().includes(q))
|
|
57
|
+
return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/** Filter items by case-insensitive substring match. Resets focus to 0. */
|
|
61
|
+
export function cpFilter(state, query) {
|
|
62
|
+
if (query === '') {
|
|
63
|
+
return {
|
|
64
|
+
...state,
|
|
65
|
+
query: '',
|
|
66
|
+
filteredItems: [...state.items],
|
|
67
|
+
focusIndex: 0,
|
|
68
|
+
scrollY: 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const filtered = state.items.filter(item => matchesQuery(item, query));
|
|
72
|
+
return {
|
|
73
|
+
...state,
|
|
74
|
+
query,
|
|
75
|
+
filteredItems: filtered,
|
|
76
|
+
focusIndex: 0,
|
|
77
|
+
scrollY: 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Focus navigation
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
function adjustScroll(focusIndex, scrollY, height, total) {
|
|
84
|
+
let s = scrollY;
|
|
85
|
+
if (focusIndex < s)
|
|
86
|
+
s = focusIndex;
|
|
87
|
+
else if (focusIndex >= s + height)
|
|
88
|
+
s = focusIndex - height + 1;
|
|
89
|
+
return Math.min(s, Math.max(0, total - height));
|
|
90
|
+
}
|
|
91
|
+
/** Move focus to the next item (wraps around on filteredItems). */
|
|
92
|
+
export function cpFocusNext(state) {
|
|
93
|
+
if (state.filteredItems.length === 0)
|
|
94
|
+
return state;
|
|
95
|
+
const focusIndex = (state.focusIndex + 1) % state.filteredItems.length;
|
|
96
|
+
return { ...state, focusIndex, scrollY: adjustScroll(focusIndex, state.scrollY, state.height, state.filteredItems.length) };
|
|
97
|
+
}
|
|
98
|
+
/** Move focus to the previous item (wraps around on filteredItems). */
|
|
99
|
+
export function cpFocusPrev(state) {
|
|
100
|
+
if (state.filteredItems.length === 0)
|
|
101
|
+
return state;
|
|
102
|
+
const focusIndex = (state.focusIndex - 1 + state.filteredItems.length) % state.filteredItems.length;
|
|
103
|
+
return { ...state, focusIndex, scrollY: adjustScroll(focusIndex, state.scrollY, state.height, state.filteredItems.length) };
|
|
104
|
+
}
|
|
105
|
+
/** Move focus down by half a page (vim Ctrl+D convention, clamps to last item). */
|
|
106
|
+
export function cpPageDown(state) {
|
|
107
|
+
if (state.filteredItems.length === 0)
|
|
108
|
+
return state;
|
|
109
|
+
const half = Math.max(1, Math.floor(state.height / 2));
|
|
110
|
+
const focusIndex = Math.min(state.focusIndex + half, state.filteredItems.length - 1);
|
|
111
|
+
return { ...state, focusIndex, scrollY: adjustScroll(focusIndex, state.scrollY, state.height, state.filteredItems.length) };
|
|
112
|
+
}
|
|
113
|
+
/** Move focus up by half a page (vim Ctrl+U convention, clamps to first item). */
|
|
114
|
+
export function cpPageUp(state) {
|
|
115
|
+
if (state.filteredItems.length === 0)
|
|
116
|
+
return state;
|
|
117
|
+
const half = Math.max(1, Math.floor(state.height / 2));
|
|
118
|
+
const focusIndex = Math.max(state.focusIndex - half, 0);
|
|
119
|
+
return { ...state, focusIndex, scrollY: adjustScroll(focusIndex, state.scrollY, state.height, state.filteredItems.length) };
|
|
120
|
+
}
|
|
121
|
+
/** Get the currently focused item, or undefined if no items match. */
|
|
122
|
+
export function cpSelectedItem(state) {
|
|
123
|
+
return state.filteredItems[state.focusIndex];
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Render
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
/**
|
|
129
|
+
* Render the command palette — a search line followed by filtered items
|
|
130
|
+
* in the viewport with a focus indicator.
|
|
131
|
+
*
|
|
132
|
+
* Layout:
|
|
133
|
+
* - Line 1: `> {query}` search input
|
|
134
|
+
* - Lines 2+: filtered items in viewport
|
|
135
|
+
* - Each item: `[category] label description shortcut`
|
|
136
|
+
* - Focus indicator: `▸` on focused item
|
|
137
|
+
*/
|
|
138
|
+
export function commandPalette(state, options) {
|
|
139
|
+
const { width, showCategory = true, showShortcut = true, ctx } = options;
|
|
140
|
+
const lines = [];
|
|
141
|
+
// Search line
|
|
142
|
+
const searchPrefix = '> ';
|
|
143
|
+
const queryLine = searchPrefix + state.query;
|
|
144
|
+
lines.push(clipToWidth(queryLine, width));
|
|
145
|
+
if (state.filteredItems.length === 0) {
|
|
146
|
+
lines.push(clipToWidth(' No matches', width));
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
// Visible items in viewport
|
|
150
|
+
const visible = state.filteredItems.slice(state.scrollY, state.scrollY + state.height);
|
|
151
|
+
const indicator = '\u25b8'; // ▸
|
|
152
|
+
const pad = ' ';
|
|
153
|
+
const muted = (text) => ctx ? ctx.style.styled(ctx.theme.theme.semantic.muted, text) : text;
|
|
154
|
+
for (let i = 0; i < visible.length; i++) {
|
|
155
|
+
const item = visible[i];
|
|
156
|
+
const globalIndex = state.scrollY + i;
|
|
157
|
+
const prefix = globalIndex === state.focusIndex ? indicator : pad;
|
|
158
|
+
const parts = [];
|
|
159
|
+
if (showCategory && item.category) {
|
|
160
|
+
parts.push(muted(`[${item.category}]`));
|
|
161
|
+
}
|
|
162
|
+
parts.push(item.label);
|
|
163
|
+
if (item.description) {
|
|
164
|
+
parts.push(muted(item.description));
|
|
165
|
+
}
|
|
166
|
+
if (showShortcut && item.shortcut) {
|
|
167
|
+
parts.push(muted(item.shortcut));
|
|
168
|
+
}
|
|
169
|
+
const content = parts.join(' ');
|
|
170
|
+
const line = `${prefix} ${content}`;
|
|
171
|
+
lines.push(clipToWidth(line, width));
|
|
172
|
+
}
|
|
173
|
+
return lines.join('\n');
|
|
174
|
+
}
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Convenience keymap
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
/**
|
|
179
|
+
* Create a preconfigured KeyMap for command palette navigation.
|
|
180
|
+
*
|
|
181
|
+
* Bindings: Ctrl+N/Down (next), Ctrl+P/Up (prev), Ctrl+D/PageDown (page down),
|
|
182
|
+
* Ctrl+U/PageUp (page up), Enter (select), Escape (close).
|
|
183
|
+
*
|
|
184
|
+
* Ctrl+D and Ctrl+U follow vim half-page scroll conventions. In raw-mode TUIs
|
|
185
|
+
* these do not conflict with shell behavior (EOF / line-clear).
|
|
186
|
+
*
|
|
187
|
+
* ```ts
|
|
188
|
+
* const keys = commandPaletteKeyMap({
|
|
189
|
+
* focusNext: { type: 'cp-next' },
|
|
190
|
+
* focusPrev: { type: 'cp-prev' },
|
|
191
|
+
* pageDown: { type: 'cp-page-down' },
|
|
192
|
+
* pageUp: { type: 'cp-page-up' },
|
|
193
|
+
* select: { type: 'cp-select' },
|
|
194
|
+
* close: { type: 'cp-close' },
|
|
195
|
+
* });
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export function commandPaletteKeyMap(actions) {
|
|
199
|
+
return createKeyMap()
|
|
200
|
+
.group('Navigation', (g) => g
|
|
201
|
+
.bind('ctrl+n', 'Next item', actions.focusNext)
|
|
202
|
+
.bind('down', 'Next item', actions.focusNext)
|
|
203
|
+
.bind('ctrl+p', 'Previous item', actions.focusPrev)
|
|
204
|
+
.bind('up', 'Previous item', actions.focusPrev)
|
|
205
|
+
.bind('ctrl+d', 'Half page down', actions.pageDown)
|
|
206
|
+
.bind('pagedown', 'Half page down', actions.pageDown)
|
|
207
|
+
.bind('ctrl+u', 'Half page up', actions.pageUp)
|
|
208
|
+
.bind('pageup', 'Half page up', actions.pageUp))
|
|
209
|
+
.bind('enter', 'Select', actions.select)
|
|
210
|
+
.bind('escape', 'Close', actions.close);
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=command-palette.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-palette.js","sourceRoot":"","sources":["../src/command-palette.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,YAAY,EAAe,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AA8B5C,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CACvC,KAAoC,EACpC,MAAM,GAAG,EAAE;IAEX,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAC1B,OAAO;QACL,KAAK,EAAE,MAAM;QACb,aAAa,EAAE,MAAM;QACrB,KAAK,EAAE,EAAE;QACT,UAAU,EAAE,CAAC;QACb,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,CAAC;KACV,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,SAAS,YAAY,CAAC,IAAwB,EAAE,KAAa;IAC3D,MAAM,CAAC,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAC9B,IAAI,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,IAAI,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,IAAI,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1D,IAAI,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACnD,IAAI,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1D,OAAO,KAAK,CAAC;AACf,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,QAAQ,CAAC,KAA0B,EAAE,KAAa;IAChE,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACjB,OAAO;YACL,GAAG,KAAK;YACR,KAAK,EAAE,EAAE;YACT,aAAa,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;YAC/B,UAAU,EAAE,CAAC;YACb,OAAO,EAAE,CAAC;SACX,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;IACvE,OAAO;QACL,GAAG,KAAK;QACR,KAAK;QACL,aAAa,EAAE,QAAQ;QACvB,UAAU,EAAE,CAAC;QACb,OAAO,EAAE,CAAC;KACX,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,SAAS,YAAY,CAAC,UAAkB,EAAE,OAAe,EAAE,MAAc,EAAE,KAAa;IACtF,IAAI,CAAC,GAAG,OAAO,CAAC;IAChB,IAAI,UAAU,GAAG,CAAC;QAAE,CAAC,GAAG,UAAU,CAAC;SAC9B,IAAI,UAAU,IAAI,CAAC,GAAG,MAAM;QAAE,CAAC,GAAG,UAAU,GAAG,MAAM,GAAG,CAAC,CAAC;IAC/D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC;AAClD,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,WAAW,CAAC,KAA0B;IACpD,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnD,MAAM,UAAU,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC;IACvE,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,CAAC,UAAU,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;AAC9H,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,WAAW,CAAC,KAA0B;IACpD,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnD,MAAM,UAAU,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,GAAG,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC;IACpG,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,CAAC,UAAU,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;AAC9H,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,UAAU,CAAC,KAA0B;IACnD,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IACvD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,EAAE,KAAK,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACrF,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,CAAC,UAAU,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;AAC9H,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,QAAQ,CAAC,KAA0B;IACjD,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IACvD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;IACxD,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,CAAC,UAAU,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;AAC9H,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,cAAc,CAAC,KAA0B;IACvD,OAAO,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;AAC/C,CAAC;AAED,8EAA8E;AAC9E,SAAS;AACT,8EAA8E;AAE9E;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC5B,KAA0B,EAC1B,OAA8B;IAE9B,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,EAAE,YAAY,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IACzE,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,cAAc;IACd,MAAM,YAAY,GAAG,IAAI,CAAC;IAC1B,MAAM,SAAS,GAAG,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC;IAC7C,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;IAE1C,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC,CAAC;QAC/C,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,4BAA4B;IAC5B,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAEvF,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,IAAI;IAChC,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAC7B,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEtE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAE,CAAC;QACzB,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC;QACtC,MAAM,MAAM,GAAG,WAAW,KAAK,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC;QAElE,MAAM,KAAK,GAAa,EAAE,CAAC;QAE3B,IAAI,YAAY,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;QAC1C,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAEvB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;QACtC,CAAC;QAED,IAAI,YAAY,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;QACnC,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,IAAI,GAAG,GAAG,MAAM,IAAI,OAAO,EAAE,CAAC;QACpC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;IACvC,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,oBAAoB,CAAM,OAOzC;IACC,OAAO,YAAY,EAAO;SACvB,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;SAC1B,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,OAAO,CAAC,SAAS,CAAC;SAC9C,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,SAAS,CAAC;SAC5C,IAAI,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,CAAC,SAAS,CAAC;SAClD,IAAI,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,CAAC,SAAS,CAAC;SAC9C,IAAI,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,CAAC,QAAQ,CAAC;SAClD,IAAI,CAAC,UAAU,EAAE,gBAAgB,EAAE,OAAO,CAAC,QAAQ,CAAC;SACpD,IAAI,CAAC,QAAQ,EAAE,cAAc,EAAE,OAAO,CAAC,MAAM,CAAC;SAC9C,IAAI,CAAC,QAAQ,EAAE,cAAc,EAAE,OAAO,CAAC,MAAM,CAAC,CAChD;SACA,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC;SACvC,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AAC5C,CAAC"}
|