@compilr-dev/cli 0.4.0 → 0.5.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 +30 -12
- package/dist/agent.d.ts +74 -1
- package/dist/agent.js +259 -76
- package/dist/anchors/index.d.ts +9 -0
- package/dist/anchors/index.js +9 -0
- package/dist/anchors/project-anchors.d.ts +79 -0
- package/dist/anchors/project-anchors.js +202 -0
- package/dist/commands/handler-types.d.ts +68 -0
- package/dist/commands/handler-types.js +8 -0
- package/dist/commands/handlers/agent-commands.d.ts +13 -0
- package/dist/commands/handlers/agent-commands.js +305 -0
- package/dist/commands/handlers/design-commands.d.ts +15 -0
- package/dist/commands/handlers/design-commands.js +334 -0
- package/dist/commands/handlers/index.d.ts +20 -0
- package/dist/commands/handlers/index.js +43 -0
- package/dist/commands/handlers/overlay-commands.d.ts +21 -0
- package/dist/commands/handlers/overlay-commands.js +287 -0
- package/dist/commands/handlers/project-commands.d.ts +11 -0
- package/dist/commands/handlers/project-commands.js +167 -0
- package/dist/commands/handlers/simple-commands.d.ts +19 -0
- package/dist/commands/handlers/simple-commands.js +144 -0
- package/dist/commands/index.d.ts +2 -1
- package/dist/commands/registry.d.ts +50 -0
- package/dist/commands/registry.js +75 -0
- package/dist/commands-v2/handlers/context.d.ts +13 -0
- package/dist/commands-v2/handlers/context.js +348 -0
- package/dist/commands-v2/handlers/core.d.ts +13 -0
- package/dist/commands-v2/handlers/core.js +165 -0
- package/dist/commands-v2/handlers/debug.d.ts +11 -0
- package/dist/commands-v2/handlers/debug.js +159 -0
- package/dist/commands-v2/handlers/index.d.ts +12 -0
- package/dist/commands-v2/handlers/index.js +24 -0
- package/dist/commands-v2/handlers/project.d.ts +22 -0
- package/dist/commands-v2/handlers/project.js +814 -0
- package/dist/commands-v2/handlers/settings.d.ts +15 -0
- package/dist/commands-v2/handlers/settings.js +235 -0
- package/dist/commands-v2/index.d.ts +13 -0
- package/dist/commands-v2/index.js +15 -0
- package/dist/commands-v2/registry.d.ts +37 -0
- package/dist/commands-v2/registry.js +80 -0
- package/dist/commands-v2/types.d.ts +75 -0
- package/dist/commands-v2/types.js +7 -0
- package/dist/commands.js +110 -7
- package/dist/index.js +288 -29
- package/dist/input-handlers/index.d.ts +7 -0
- package/dist/input-handlers/index.js +7 -0
- package/dist/input-handlers/memory-handler.d.ts +26 -0
- package/dist/input-handlers/memory-handler.js +68 -0
- package/dist/repl-helpers.d.ts +63 -0
- package/dist/repl-helpers.js +318 -0
- package/dist/repl-v2.d.ts +155 -0
- package/dist/repl-v2.js +774 -0
- package/dist/repl.d.ts +32 -4
- package/dist/repl.js +250 -977
- package/dist/settings/index.d.ts +23 -0
- package/dist/settings/index.js +48 -0
- package/dist/settings/paths.d.ts +110 -0
- package/dist/settings/paths.js +264 -0
- package/dist/templates/compilr-md.js +7 -4
- package/dist/templates/index.js +3 -4
- package/dist/themes/colors.js +3 -1
- package/dist/themes/registry.d.ts +5 -36
- package/dist/themes/registry.js +11 -95
- package/dist/themes/types.d.ts +3 -38
- package/dist/themes/types.js +2 -2
- package/dist/tools/anchor-tools.d.ts +31 -0
- package/dist/tools/anchor-tools.js +255 -0
- package/dist/tools/backlog-wrappers.d.ts +54 -0
- package/dist/tools/backlog-wrappers.js +338 -0
- package/dist/tools/backlog.js +1 -1
- package/dist/tools/db-tools.d.ts +65 -0
- package/dist/tools/db-tools.js +19 -0
- package/dist/tools/document-db.d.ts +43 -0
- package/dist/tools/document-db.js +220 -0
- package/dist/tools/project-db.d.ts +102 -0
- package/dist/tools/project-db.js +370 -0
- package/dist/tools/workitem-db.d.ts +103 -0
- package/dist/tools/workitem-db.js +549 -0
- package/dist/tools.js +13 -3
- package/dist/ui/agents-overlay-v2.d.ts +43 -0
- package/dist/ui/agents-overlay-v2.js +809 -0
- package/dist/ui/agents-overlay.d.ts +5 -5
- package/dist/ui/agents-overlay.js +782 -420
- package/dist/ui/anchors-overlay.d.ts +12 -0
- package/dist/ui/anchors-overlay.js +775 -0
- package/dist/ui/arch-type-overlay.d.ts +1 -6
- package/dist/ui/arch-type-overlay.js +175 -203
- package/dist/ui/ask-user-overlay-v2.d.ts +26 -0
- package/dist/ui/ask-user-overlay-v2.js +555 -0
- package/dist/ui/ask-user-overlay.d.ts +2 -2
- package/dist/ui/ask-user-overlay.js +443 -535
- package/dist/ui/ask-user-simple-overlay-v2.d.ts +25 -0
- package/dist/ui/ask-user-simple-overlay-v2.js +215 -0
- package/dist/ui/ask-user-simple-overlay.d.ts +2 -2
- package/dist/ui/ask-user-simple-overlay.js +182 -209
- package/dist/ui/backlog-overlay.d.ts +16 -1
- package/dist/ui/backlog-overlay.js +525 -659
- package/dist/ui/base/index.d.ts +26 -0
- package/dist/ui/base/index.js +33 -0
- package/dist/ui/base/inline-overlay-utils.d.ts +217 -0
- package/dist/ui/base/inline-overlay-utils.js +320 -0
- package/dist/ui/base/inline-overlay.d.ts +159 -0
- package/dist/ui/base/inline-overlay.js +257 -0
- package/dist/ui/base/key-utils.d.ts +15 -0
- package/dist/ui/base/key-utils.js +30 -0
- package/dist/ui/base/overlay-base-v2.d.ts +193 -0
- package/dist/ui/base/overlay-base-v2.js +246 -0
- package/dist/ui/base/overlay-base.d.ts +156 -0
- package/dist/ui/base/overlay-base.js +238 -0
- package/dist/ui/base/overlay-lifecycle.d.ts +65 -0
- package/dist/ui/base/overlay-lifecycle.js +159 -0
- package/dist/ui/base/overlay-types.d.ts +185 -0
- package/dist/ui/base/overlay-types.js +7 -0
- package/dist/ui/base/render-utils.d.ts +8 -0
- package/dist/ui/base/render-utils.js +11 -0
- package/dist/ui/base/screen-stack.d.ts +148 -0
- package/dist/ui/base/screen-stack.js +184 -0
- package/dist/ui/base/tabbed-list-overlay-v2.d.ts +103 -0
- package/dist/ui/base/tabbed-list-overlay-v2.js +317 -0
- package/dist/ui/base/tabbed-list-overlay.d.ts +153 -0
- package/dist/ui/base/tabbed-list-overlay.js +369 -0
- package/dist/ui/commands-overlay-v2.d.ts +33 -0
- package/dist/ui/commands-overlay-v2.js +441 -0
- package/dist/ui/commands-overlay.d.ts +7 -2
- package/dist/ui/commands-overlay.js +384 -355
- package/dist/ui/config-overlay.d.ts +5 -4
- package/dist/ui/config-overlay.js +243 -513
- package/dist/ui/conversation.d.ts +75 -4
- package/dist/ui/conversation.js +374 -161
- package/dist/ui/docs-overlay.d.ts +17 -0
- package/dist/ui/docs-overlay.js +303 -0
- package/dist/ui/ephemeral.d.ts +1 -1
- package/dist/ui/ephemeral.js +1 -1
- package/dist/ui/features/index.d.ts +34 -0
- package/dist/ui/features/index.js +34 -0
- package/dist/ui/features/input-feature.d.ts +85 -0
- package/dist/ui/features/input-feature.js +238 -0
- package/dist/ui/features/list-feature.d.ts +155 -0
- package/dist/ui/features/list-feature.js +244 -0
- package/dist/ui/features/pagination-feature.d.ts +154 -0
- package/dist/ui/features/pagination-feature.js +238 -0
- package/dist/ui/features/search-feature.d.ts +148 -0
- package/dist/ui/features/search-feature.js +185 -0
- package/dist/ui/features/tab-feature.d.ts +194 -0
- package/dist/ui/features/tab-feature.js +307 -0
- package/dist/ui/footer-v2.d.ts +222 -0
- package/dist/ui/footer-v2.js +1349 -0
- package/dist/ui/footer.d.ts +107 -0
- package/dist/ui/footer.js +359 -67
- package/dist/ui/guardrail-overlay.d.ts +29 -0
- package/dist/ui/guardrail-overlay.js +145 -0
- package/dist/ui/help-overlay-v2.d.ts +34 -0
- package/dist/ui/help-overlay-v2.js +309 -0
- package/dist/ui/help-overlay.d.ts +16 -0
- package/dist/ui/help-overlay.js +316 -0
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -3
- package/dist/ui/init-overlay-v2.d.ts +34 -0
- package/dist/ui/init-overlay-v2.js +600 -0
- package/dist/ui/init-overlay.d.ts +12 -2
- package/dist/ui/init-overlay.js +349 -270
- package/dist/ui/input-prompt-v2.d.ts +1 -0
- package/dist/ui/input-prompt-v2.js +14 -6
- package/dist/ui/input-prompt.d.ts +116 -33
- package/dist/ui/input-prompt.js +536 -337
- package/dist/ui/iteration-limit-overlay-v2.d.ts +21 -0
- package/dist/ui/iteration-limit-overlay-v2.js +114 -0
- package/dist/ui/iteration-limit-overlay.d.ts +2 -2
- package/dist/ui/iteration-limit-overlay.js +92 -128
- package/dist/ui/keys-overlay-v2.d.ts +41 -0
- package/dist/ui/keys-overlay-v2.js +248 -0
- package/dist/ui/keys-overlay.d.ts +1 -0
- package/dist/ui/keys-overlay.js +203 -141
- package/dist/ui/line-utils.d.ts +88 -0
- package/dist/ui/line-utils.js +150 -0
- package/dist/ui/live-region.d.ts +161 -0
- package/dist/ui/live-region.js +387 -0
- package/dist/ui/mascot/expressions.d.ts +32 -0
- package/dist/ui/mascot/expressions.js +213 -0
- package/dist/ui/mascot/index.d.ts +8 -0
- package/dist/ui/mascot/index.js +8 -0
- package/dist/ui/mascot/renderer.d.ts +19 -0
- package/dist/ui/mascot/renderer.js +97 -0
- package/dist/ui/mascot-overlay-v2.d.ts +41 -0
- package/dist/ui/mascot-overlay-v2.js +138 -0
- package/dist/ui/mascot-overlay.d.ts +21 -0
- package/dist/ui/mascot-overlay.js +146 -0
- package/dist/ui/model-overlay-v2.d.ts +49 -0
- package/dist/ui/model-overlay-v2.js +118 -0
- package/dist/ui/model-overlay.d.ts +27 -0
- package/dist/ui/model-overlay.js +221 -0
- package/dist/ui/model-warning-overlay.js +3 -5
- package/dist/ui/new-overlay.d.ts +34 -0
- package/dist/ui/new-overlay.js +604 -0
- package/dist/ui/overlay/impl/agents-overlay-v2.d.ts +45 -0
- package/dist/ui/overlay/impl/agents-overlay-v2.js +825 -0
- package/dist/ui/overlay/impl/anchors-overlay-v2.d.ts +47 -0
- package/dist/ui/overlay/impl/anchors-overlay-v2.js +783 -0
- package/dist/ui/overlay/impl/arch-type-overlay-v2.d.ts +37 -0
- package/dist/ui/overlay/impl/arch-type-overlay-v2.js +240 -0
- package/dist/ui/overlay/impl/ask-user-overlay-v2.d.ts +72 -0
- package/dist/ui/overlay/impl/ask-user-overlay-v2.js +584 -0
- package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.d.ts +46 -0
- package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.js +204 -0
- package/dist/ui/overlay/impl/backlog-overlay-v2.d.ts +49 -0
- package/dist/ui/overlay/impl/backlog-overlay-v2.js +642 -0
- package/dist/ui/overlay/impl/commands-overlay-v2.d.ts +33 -0
- package/dist/ui/overlay/impl/commands-overlay-v2.js +441 -0
- package/dist/ui/overlay/impl/config-overlay-v2.d.ts +100 -0
- package/dist/ui/overlay/impl/config-overlay-v2.js +654 -0
- package/dist/ui/overlay/impl/dashboard-overlay-v2.d.ts +55 -0
- package/dist/ui/overlay/impl/dashboard-overlay-v2.js +359 -0
- package/dist/ui/overlay/impl/docs-overlay-v2.d.ts +45 -0
- package/dist/ui/overlay/impl/docs-overlay-v2.js +114 -0
- package/dist/ui/overlay/impl/document-detail-overlay-v2.d.ts +77 -0
- package/dist/ui/overlay/impl/document-detail-overlay-v2.js +1071 -0
- package/dist/ui/overlay/impl/guardrail-overlay-v2.d.ts +43 -0
- package/dist/ui/overlay/impl/guardrail-overlay-v2.js +114 -0
- package/dist/ui/overlay/impl/help-overlay-v2.d.ts +34 -0
- package/dist/ui/overlay/impl/help-overlay-v2.js +309 -0
- package/dist/ui/overlay/impl/init-overlay-v2.d.ts +77 -0
- package/dist/ui/overlay/impl/init-overlay-v2.js +593 -0
- package/dist/ui/overlay/impl/init-setup-overlay-v2.d.ts +25 -0
- package/dist/ui/overlay/impl/init-setup-overlay-v2.js +97 -0
- package/dist/ui/overlay/impl/iteration-limit-overlay-v2.d.ts +35 -0
- package/dist/ui/overlay/impl/iteration-limit-overlay-v2.js +105 -0
- package/dist/ui/overlay/impl/keys-overlay-v2.d.ts +41 -0
- package/dist/ui/overlay/impl/keys-overlay-v2.js +248 -0
- package/dist/ui/overlay/impl/mascot-overlay-v2.d.ts +41 -0
- package/dist/ui/overlay/impl/mascot-overlay-v2.js +138 -0
- package/dist/ui/overlay/impl/model-overlay-v2.d.ts +49 -0
- package/dist/ui/overlay/impl/model-overlay-v2.js +118 -0
- package/dist/ui/overlay/impl/model-warning-overlay-v2.d.ts +46 -0
- package/dist/ui/overlay/impl/model-warning-overlay-v2.js +132 -0
- package/dist/ui/overlay/impl/new-overlay-v2.d.ts +77 -0
- package/dist/ui/overlay/impl/new-overlay-v2.js +593 -0
- package/dist/ui/overlay/impl/permission-overlay-v2.d.ts +36 -0
- package/dist/ui/overlay/impl/permission-overlay-v2.js +380 -0
- package/dist/ui/overlay/impl/projects-overlay-v2.d.ts +36 -0
- package/dist/ui/overlay/impl/projects-overlay-v2.js +499 -0
- package/dist/ui/overlay/impl/theme-overlay-v2.d.ts +42 -0
- package/dist/ui/overlay/impl/theme-overlay-v2.js +135 -0
- package/dist/ui/overlay/impl/tools-overlay-v2.d.ts +47 -0
- package/dist/ui/overlay/impl/tools-overlay-v2.js +218 -0
- package/dist/ui/overlay/impl/tutorial-overlay-v2.d.ts +31 -0
- package/dist/ui/overlay/impl/tutorial-overlay-v2.js +1035 -0
- package/dist/ui/overlay/impl/workflow-overlay-v2.d.ts +80 -0
- package/dist/ui/overlay/impl/workflow-overlay-v2.js +637 -0
- package/dist/ui/overlay/index.d.ts +33 -0
- package/dist/ui/overlay/index.js +35 -0
- package/dist/ui/overlay/key-utils.d.ts +6 -0
- package/dist/ui/overlay/key-utils.js +6 -0
- package/dist/ui/overlay/overlay-types.d.ts +128 -0
- package/dist/ui/overlay/overlay-types.js +22 -0
- package/dist/ui/overlay/types.d.ts +135 -0
- package/dist/ui/overlay/types.js +22 -0
- package/dist/ui/overlays/help-overlay-v2.d.ts +28 -0
- package/dist/ui/overlays/help-overlay-v2.js +198 -0
- package/dist/ui/overlays/index.d.ts +11 -0
- package/dist/ui/overlays/index.js +11 -0
- package/dist/ui/overlays.d.ts +0 -4
- package/dist/ui/overlays.js +0 -444
- package/dist/ui/permission-overlay-v2.d.ts +36 -0
- package/dist/ui/permission-overlay-v2.js +380 -0
- package/dist/ui/permission-overlay.d.ts +1 -1
- package/dist/ui/permission-overlay.js +186 -298
- package/dist/ui/projects-overlay.d.ts +19 -0
- package/dist/ui/projects-overlay.js +484 -0
- package/dist/ui/providers/types.d.ts +178 -0
- package/dist/ui/providers/types.js +9 -0
- package/dist/ui/render-modes.d.ts +36 -0
- package/dist/ui/render-modes.js +44 -0
- package/dist/ui/startup-menu.d.ts +36 -0
- package/dist/ui/startup-menu.js +236 -0
- package/dist/ui/subagent-renderer.d.ts +117 -0
- package/dist/ui/subagent-renderer.js +334 -0
- package/dist/ui/terminal-codes.d.ts +94 -0
- package/dist/ui/terminal-codes.js +124 -0
- package/dist/ui/terminal-renderer.d.ts +221 -0
- package/dist/ui/terminal-renderer.js +751 -0
- package/dist/ui/terminal-ui.d.ts +463 -0
- package/dist/ui/terminal-ui.js +2296 -0
- package/dist/ui/terminal.d.ts +20 -0
- package/dist/ui/terminal.js +72 -0
- package/dist/ui/theme-overlay-v2.d.ts +42 -0
- package/dist/ui/theme-overlay-v2.js +135 -0
- package/dist/ui/theme-overlay.d.ts +24 -0
- package/dist/ui/theme-overlay.js +127 -0
- package/dist/ui/todo-zone.js +53 -25
- package/dist/ui/tool-formatters.d.ts +16 -0
- package/dist/ui/tool-formatters.js +516 -0
- package/dist/ui/tools-overlay-v2.d.ts +47 -0
- package/dist/ui/tools-overlay-v2.js +218 -0
- package/dist/ui/tools-overlay.d.ts +10 -2
- package/dist/ui/tools-overlay.js +172 -220
- package/dist/ui/tutorial-overlay-v2.d.ts +31 -0
- package/dist/ui/tutorial-overlay-v2.js +1035 -0
- package/dist/ui/tutorial-overlay.d.ts +1 -0
- package/dist/ui/tutorial-overlay.js +400 -302
- package/dist/ui/workflow-overlay.d.ts +22 -0
- package/dist/ui/workflow-overlay.js +636 -0
- package/dist/utils/debug-log.d.ts +28 -0
- package/dist/utils/debug-log.js +57 -0
- package/dist/utils/model-tiers.js +1 -1
- package/dist/utils/path-safety.d.ts +56 -0
- package/dist/utils/path-safety.js +239 -0
- package/dist/workflow/guided-mode-injector.d.ts +42 -0
- package/dist/workflow/guided-mode-injector.js +191 -0
- package/dist/workflow/index.d.ts +8 -0
- package/dist/workflow/index.js +8 -0
- package/dist/workflow/step-criteria.d.ts +62 -0
- package/dist/workflow/step-criteria.js +150 -0
- package/dist/workflow/step-tracker.d.ts +92 -0
- package/dist/workflow/step-tracker.js +141 -0
- package/package.json +12 -5
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Feature
|
|
3
|
+
*
|
|
4
|
+
* Composable feature for search/filter text input in overlays.
|
|
5
|
+
* Handles typing, backspace, and escape to clear.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* interface MyState extends OverlayState, SearchState {
|
|
10
|
+
* items: string[];
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* class MyScreen extends BaseScreen {
|
|
14
|
+
* private search = new SearchFeature({
|
|
15
|
+
* onSearch: (query) => this.filterItems(query),
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* handleKey(data: Buffer): ScreenResult {
|
|
19
|
+
* const result = this.search.handleKey(data, this.state);
|
|
20
|
+
* if (result?.handled) {
|
|
21
|
+
* return result.action === 'render' ? stay() : stay(false);
|
|
22
|
+
* }
|
|
23
|
+
* // ... other key handling
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* render(): string[] {
|
|
27
|
+
* return [
|
|
28
|
+
* this.search.renderSearchBox(this.state, styles, { label: 'Search' }),
|
|
29
|
+
* ...filteredItemLines,
|
|
30
|
+
* ];
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
import { isBackspace, isEscape, isPrintable, getPrintableChar, } from '../base/key-utils.js';
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// SearchFeature Class
|
|
38
|
+
// =============================================================================
|
|
39
|
+
/**
|
|
40
|
+
* Feature for search/filter text input.
|
|
41
|
+
*
|
|
42
|
+
* Handles:
|
|
43
|
+
* - Printable character input
|
|
44
|
+
* - Backspace to delete
|
|
45
|
+
* - Escape to clear (optional)
|
|
46
|
+
*/
|
|
47
|
+
export class SearchFeature {
|
|
48
|
+
config;
|
|
49
|
+
constructor(config = {}) {
|
|
50
|
+
this.config = {
|
|
51
|
+
escapeClearsSearch: true,
|
|
52
|
+
placeholder: '',
|
|
53
|
+
...config,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ===========================================================================
|
|
57
|
+
// Key Handling
|
|
58
|
+
// ===========================================================================
|
|
59
|
+
/**
|
|
60
|
+
* Handle a key press.
|
|
61
|
+
*
|
|
62
|
+
* @param data - Raw key buffer
|
|
63
|
+
* @param state - State containing searchQuery
|
|
64
|
+
* @returns Result if handled, null otherwise
|
|
65
|
+
*/
|
|
66
|
+
handleKey(data, state) {
|
|
67
|
+
// Escape clears search
|
|
68
|
+
if (this.config.escapeClearsSearch && isEscape(data)) {
|
|
69
|
+
if (state.searchQuery.length > 0) {
|
|
70
|
+
state.searchQuery = '';
|
|
71
|
+
this.config.onSearch?.(state.searchQuery);
|
|
72
|
+
return { handled: true, action: 'render', cleared: true };
|
|
73
|
+
}
|
|
74
|
+
// Don't handle Escape if search is already empty
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
// Backspace deletes last character
|
|
78
|
+
if (isBackspace(data)) {
|
|
79
|
+
if (state.searchQuery.length > 0) {
|
|
80
|
+
state.searchQuery = state.searchQuery.slice(0, -1);
|
|
81
|
+
this.config.onSearch?.(state.searchQuery);
|
|
82
|
+
return { handled: true, action: 'render' };
|
|
83
|
+
}
|
|
84
|
+
return { handled: true, action: 'none' };
|
|
85
|
+
}
|
|
86
|
+
// Printable characters add to search
|
|
87
|
+
if (isPrintable(data)) {
|
|
88
|
+
const char = getPrintableChar(data);
|
|
89
|
+
if (char) {
|
|
90
|
+
state.searchQuery += char;
|
|
91
|
+
this.config.onSearch?.(state.searchQuery);
|
|
92
|
+
return { handled: true, action: 'render' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
// ===========================================================================
|
|
98
|
+
// Rendering
|
|
99
|
+
// ===========================================================================
|
|
100
|
+
/**
|
|
101
|
+
* Render a search box with optional cursor.
|
|
102
|
+
*
|
|
103
|
+
* @param state - State containing searchQuery
|
|
104
|
+
* @param styles - Theme styles
|
|
105
|
+
* @param options - Rendering options
|
|
106
|
+
* @returns Formatted search box string
|
|
107
|
+
*/
|
|
108
|
+
renderSearchBox(state, styles, options = {}) {
|
|
109
|
+
const label = options.label ?? 'Search';
|
|
110
|
+
const showCursor = options.showCursor ?? true;
|
|
111
|
+
const prefix = options.prefix ?? ' ';
|
|
112
|
+
const cursor = showCursor ? '█' : '';
|
|
113
|
+
const query = state.searchQuery;
|
|
114
|
+
const placeholder = !query && this.config.placeholder
|
|
115
|
+
? styles.muted(this.config.placeholder)
|
|
116
|
+
: '';
|
|
117
|
+
if (query) {
|
|
118
|
+
const styledQuery = styles.primary
|
|
119
|
+
? styles.primary(query)
|
|
120
|
+
: query;
|
|
121
|
+
return `${prefix}${label}: ${styledQuery}${cursor}`;
|
|
122
|
+
}
|
|
123
|
+
return `${prefix}${label}: ${placeholder}${cursor}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Render search hints for the footer.
|
|
127
|
+
*
|
|
128
|
+
* @param hasQuery - Whether there's an active search query
|
|
129
|
+
* @returns Hint string
|
|
130
|
+
*/
|
|
131
|
+
renderHints(hasQuery = false) {
|
|
132
|
+
if (hasQuery && this.config.escapeClearsSearch) {
|
|
133
|
+
return 'Type to search · Esc Clear';
|
|
134
|
+
}
|
|
135
|
+
return 'Type to search';
|
|
136
|
+
}
|
|
137
|
+
// ===========================================================================
|
|
138
|
+
// Utilities
|
|
139
|
+
// ===========================================================================
|
|
140
|
+
/**
|
|
141
|
+
* Check if there's an active search query.
|
|
142
|
+
*/
|
|
143
|
+
hasQuery(state) {
|
|
144
|
+
return state.searchQuery.length > 0;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get the current search query.
|
|
148
|
+
*/
|
|
149
|
+
getQuery(state) {
|
|
150
|
+
return state.searchQuery;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Clear the search query.
|
|
154
|
+
*/
|
|
155
|
+
clear(state) {
|
|
156
|
+
state.searchQuery = '';
|
|
157
|
+
this.config.onSearch?.(state.searchQuery);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Set the search query programmatically.
|
|
161
|
+
*/
|
|
162
|
+
setQuery(state, query) {
|
|
163
|
+
state.searchQuery = query;
|
|
164
|
+
this.config.onSearch?.(state.searchQuery);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Filter items based on search query.
|
|
168
|
+
* Generic helper that works with any item type.
|
|
169
|
+
*
|
|
170
|
+
* @param items - Items to filter
|
|
171
|
+
* @param state - State containing searchQuery
|
|
172
|
+
* @param getSearchableText - Function to extract searchable text from item
|
|
173
|
+
* @returns Filtered items
|
|
174
|
+
*/
|
|
175
|
+
filterItems(items, state, getSearchableText) {
|
|
176
|
+
const query = state.searchQuery.toLowerCase().trim();
|
|
177
|
+
if (!query) {
|
|
178
|
+
return items;
|
|
179
|
+
}
|
|
180
|
+
return items.filter((item) => {
|
|
181
|
+
const text = getSearchableText(item).toLowerCase();
|
|
182
|
+
return text.includes(query);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab Feature
|
|
3
|
+
*
|
|
4
|
+
* Composable feature for tab/filter navigation.
|
|
5
|
+
* Handles Tab/Shift+Tab and number keys for tab switching.
|
|
6
|
+
*
|
|
7
|
+
* Note: Arrow keys (←→/hl) are NOT used for tabs - they are reserved
|
|
8
|
+
* for page navigation in paginated overlays. This ensures consistent
|
|
9
|
+
* keyboard behavior across all overlays.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const TABS = [
|
|
14
|
+
* { id: 'all', label: 'All' },
|
|
15
|
+
* { id: 'active', label: 'Active' },
|
|
16
|
+
* { id: 'completed', label: 'Completed' },
|
|
17
|
+
* ];
|
|
18
|
+
*
|
|
19
|
+
* interface MyState extends OverlayState, TabState {
|
|
20
|
+
* // ...
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* class MyOverlay extends BaseOverlay<MyState, void> {
|
|
24
|
+
* private tabFeature = new TabFeature(TABS, (tabId) => {
|
|
25
|
+
* this.onTabChange(tabId);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* handleKey(data: Buffer): void {
|
|
29
|
+
* const result = this.tabFeature.handleKey(data, this.state);
|
|
30
|
+
* if (result?.handled) {
|
|
31
|
+
* if (result.action === 'render') this.update();
|
|
32
|
+
* return;
|
|
33
|
+
* }
|
|
34
|
+
* // ... other key handling
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* render(): string[] {
|
|
38
|
+
* return [
|
|
39
|
+
* ...this.renderHeader('My Overlay'),
|
|
40
|
+
* ' ' + this.tabFeature.renderTabs(this.state, this.styles),
|
|
41
|
+
* '',
|
|
42
|
+
* // ... content
|
|
43
|
+
* ...this.renderFooter(this.tabFeature.renderHints() + ' · ↑↓/jk Navigate')
|
|
44
|
+
* ];
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
import type { KeyResult, TabState, TabDefinition } from '../base/overlay-types.js';
|
|
50
|
+
/**
|
|
51
|
+
* Configuration for TabFeature.
|
|
52
|
+
*/
|
|
53
|
+
export interface TabFeatureConfig {
|
|
54
|
+
/**
|
|
55
|
+
* Tab definitions.
|
|
56
|
+
*/
|
|
57
|
+
tabs: TabDefinition[];
|
|
58
|
+
/**
|
|
59
|
+
* Callback when tab changes.
|
|
60
|
+
* @param tabId - ID of the new tab
|
|
61
|
+
* @param index - Index of the new tab
|
|
62
|
+
*/
|
|
63
|
+
onChange?: (tabId: string, index: number) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Whether to use number keys (1-9) for direct tab selection.
|
|
66
|
+
* Default: true (if tabs.length <= 9)
|
|
67
|
+
*/
|
|
68
|
+
useNumberKeys?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Whether navigation wraps at boundaries.
|
|
71
|
+
* Default: true
|
|
72
|
+
*/
|
|
73
|
+
wrapNavigation?: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Feature for tab/filter navigation.
|
|
77
|
+
*
|
|
78
|
+
* Handles:
|
|
79
|
+
* - Tab/Shift+Tab for cycling tabs
|
|
80
|
+
* - Number keys 1-9 for direct selection (optional)
|
|
81
|
+
*
|
|
82
|
+
* Note: Arrow keys are NOT handled - they are reserved for pagination.
|
|
83
|
+
*/
|
|
84
|
+
export declare class TabFeature {
|
|
85
|
+
private readonly config;
|
|
86
|
+
/**
|
|
87
|
+
* Create a new TabFeature.
|
|
88
|
+
*
|
|
89
|
+
* @param tabs - Tab definitions
|
|
90
|
+
* @param onChange - Optional callback when tab changes
|
|
91
|
+
*/
|
|
92
|
+
constructor(tabs: TabDefinition[], onChange?: (tabId: string, index: number) => void);
|
|
93
|
+
/**
|
|
94
|
+
* Create from config object (alternative constructor).
|
|
95
|
+
*/
|
|
96
|
+
static fromConfig(config: TabFeatureConfig): TabFeature;
|
|
97
|
+
/**
|
|
98
|
+
* Handle a key press.
|
|
99
|
+
*
|
|
100
|
+
* @param data - Raw key buffer
|
|
101
|
+
* @param state - State containing currentTab
|
|
102
|
+
* @returns KeyResult if handled, null otherwise
|
|
103
|
+
*/
|
|
104
|
+
handleKey(data: Buffer, state: TabState): KeyResult | null;
|
|
105
|
+
/**
|
|
106
|
+
* Render tabs in a row.
|
|
107
|
+
*
|
|
108
|
+
* @param state - State containing currentTab
|
|
109
|
+
* @param styles - Theme styles
|
|
110
|
+
* @returns Rendered tab bar string
|
|
111
|
+
*/
|
|
112
|
+
renderTabs(state: TabState, styles: {
|
|
113
|
+
selected: (s: string) => string;
|
|
114
|
+
muted: (s: string) => string;
|
|
115
|
+
}): string;
|
|
116
|
+
/**
|
|
117
|
+
* Render tabs with number key hints.
|
|
118
|
+
*
|
|
119
|
+
* @param state - State containing currentTab
|
|
120
|
+
* @param styles - Theme styles
|
|
121
|
+
* @returns Rendered tab bar string with numbers
|
|
122
|
+
*/
|
|
123
|
+
renderTabsWithNumbers(state: TabState, styles: {
|
|
124
|
+
selected: (s: string) => string;
|
|
125
|
+
muted: (s: string) => string;
|
|
126
|
+
}): string;
|
|
127
|
+
/**
|
|
128
|
+
* Render tabs with custom separator.
|
|
129
|
+
*
|
|
130
|
+
* @param state - State containing currentTab
|
|
131
|
+
* @param styles - Theme styles
|
|
132
|
+
* @param separator - Separator between tabs (default: '')
|
|
133
|
+
* @returns Rendered tab bar string
|
|
134
|
+
*/
|
|
135
|
+
renderTabsWithSeparator(state: TabState, styles: {
|
|
136
|
+
selected: (s: string) => string;
|
|
137
|
+
muted: (s: string) => string;
|
|
138
|
+
}, separator?: string): string;
|
|
139
|
+
/**
|
|
140
|
+
* Render keyboard hints for tabs.
|
|
141
|
+
*
|
|
142
|
+
* @returns Hint string like "Tab Switch · 1-5 Jump"
|
|
143
|
+
*/
|
|
144
|
+
renderHints(): string;
|
|
145
|
+
/**
|
|
146
|
+
* Get the current tab definition.
|
|
147
|
+
*
|
|
148
|
+
* @param state - State containing currentTab
|
|
149
|
+
* @returns Current tab or undefined
|
|
150
|
+
*/
|
|
151
|
+
getCurrentTab(state: TabState): TabDefinition | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* Get the current tab ID.
|
|
154
|
+
*
|
|
155
|
+
* @param state - State containing currentTab
|
|
156
|
+
* @returns Current tab ID or empty string
|
|
157
|
+
*/
|
|
158
|
+
getCurrentTabId(state: TabState): string;
|
|
159
|
+
/**
|
|
160
|
+
* Get the number of tabs.
|
|
161
|
+
*/
|
|
162
|
+
getTabCount(): number;
|
|
163
|
+
/**
|
|
164
|
+
* Find tab index by ID.
|
|
165
|
+
*
|
|
166
|
+
* @param tabId - Tab ID to find
|
|
167
|
+
* @returns Index or -1 if not found
|
|
168
|
+
*/
|
|
169
|
+
findTabIndex(tabId: string): number;
|
|
170
|
+
/**
|
|
171
|
+
* Set current tab by ID.
|
|
172
|
+
*
|
|
173
|
+
* @param state - State to modify
|
|
174
|
+
* @param tabId - Tab ID to select
|
|
175
|
+
* @returns true if tab was found and selected
|
|
176
|
+
*/
|
|
177
|
+
setTabById(state: TabState, tabId: string): boolean;
|
|
178
|
+
private notifyChange;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Create tab definitions from an array of strings.
|
|
182
|
+
* IDs are lowercase versions of labels.
|
|
183
|
+
*
|
|
184
|
+
* @param labels - Tab labels
|
|
185
|
+
* @returns Tab definitions
|
|
186
|
+
*/
|
|
187
|
+
export declare function createTabsFromLabels(labels: string[]): TabDefinition[];
|
|
188
|
+
/**
|
|
189
|
+
* Create filter tabs (common pattern).
|
|
190
|
+
*
|
|
191
|
+
* @param filters - Array of filter names
|
|
192
|
+
* @returns Tab definitions with 'all' as first tab
|
|
193
|
+
*/
|
|
194
|
+
export declare function createFilterTabs(filters: string[]): TabDefinition[];
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab Feature
|
|
3
|
+
*
|
|
4
|
+
* Composable feature for tab/filter navigation.
|
|
5
|
+
* Handles Tab/Shift+Tab and number keys for tab switching.
|
|
6
|
+
*
|
|
7
|
+
* Note: Arrow keys (←→/hl) are NOT used for tabs - they are reserved
|
|
8
|
+
* for page navigation in paginated overlays. This ensures consistent
|
|
9
|
+
* keyboard behavior across all overlays.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const TABS = [
|
|
14
|
+
* { id: 'all', label: 'All' },
|
|
15
|
+
* { id: 'active', label: 'Active' },
|
|
16
|
+
* { id: 'completed', label: 'Completed' },
|
|
17
|
+
* ];
|
|
18
|
+
*
|
|
19
|
+
* interface MyState extends OverlayState, TabState {
|
|
20
|
+
* // ...
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* class MyOverlay extends BaseOverlay<MyState, void> {
|
|
24
|
+
* private tabFeature = new TabFeature(TABS, (tabId) => {
|
|
25
|
+
* this.onTabChange(tabId);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* handleKey(data: Buffer): void {
|
|
29
|
+
* const result = this.tabFeature.handleKey(data, this.state);
|
|
30
|
+
* if (result?.handled) {
|
|
31
|
+
* if (result.action === 'render') this.update();
|
|
32
|
+
* return;
|
|
33
|
+
* }
|
|
34
|
+
* // ... other key handling
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* render(): string[] {
|
|
38
|
+
* return [
|
|
39
|
+
* ...this.renderHeader('My Overlay'),
|
|
40
|
+
* ' ' + this.tabFeature.renderTabs(this.state, this.styles),
|
|
41
|
+
* '',
|
|
42
|
+
* // ... content
|
|
43
|
+
* ...this.renderFooter(this.tabFeature.renderHints() + ' · ↑↓/jk Navigate')
|
|
44
|
+
* ];
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
import { isTab, isShiftTab, getNumberKey, } from '../base/key-utils.js';
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// TabFeature Class
|
|
52
|
+
// =============================================================================
|
|
53
|
+
/**
|
|
54
|
+
* Feature for tab/filter navigation.
|
|
55
|
+
*
|
|
56
|
+
* Handles:
|
|
57
|
+
* - Tab/Shift+Tab for cycling tabs
|
|
58
|
+
* - Number keys 1-9 for direct selection (optional)
|
|
59
|
+
*
|
|
60
|
+
* Note: Arrow keys are NOT handled - they are reserved for pagination.
|
|
61
|
+
*/
|
|
62
|
+
export class TabFeature {
|
|
63
|
+
config;
|
|
64
|
+
/**
|
|
65
|
+
* Create a new TabFeature.
|
|
66
|
+
*
|
|
67
|
+
* @param tabs - Tab definitions
|
|
68
|
+
* @param onChange - Optional callback when tab changes
|
|
69
|
+
*/
|
|
70
|
+
constructor(tabs, onChange) {
|
|
71
|
+
this.config = {
|
|
72
|
+
tabs,
|
|
73
|
+
onChange,
|
|
74
|
+
useNumberKeys: tabs.length <= 9,
|
|
75
|
+
wrapNavigation: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create from config object (alternative constructor).
|
|
80
|
+
*/
|
|
81
|
+
static fromConfig(config) {
|
|
82
|
+
const feature = new TabFeature(config.tabs, config.onChange);
|
|
83
|
+
if (config.useNumberKeys !== undefined) {
|
|
84
|
+
feature.config.useNumberKeys = config.useNumberKeys;
|
|
85
|
+
}
|
|
86
|
+
if (config.wrapNavigation !== undefined) {
|
|
87
|
+
feature.config.wrapNavigation = config.wrapNavigation;
|
|
88
|
+
}
|
|
89
|
+
return feature;
|
|
90
|
+
}
|
|
91
|
+
// ===========================================================================
|
|
92
|
+
// Key Handling
|
|
93
|
+
// ===========================================================================
|
|
94
|
+
/**
|
|
95
|
+
* Handle a key press.
|
|
96
|
+
*
|
|
97
|
+
* @param data - Raw key buffer
|
|
98
|
+
* @param state - State containing currentTab
|
|
99
|
+
* @returns KeyResult if handled, null otherwise
|
|
100
|
+
*/
|
|
101
|
+
handleKey(data, state) {
|
|
102
|
+
const tabCount = this.config.tabs.length;
|
|
103
|
+
if (tabCount === 0) {
|
|
104
|
+
return null; // No tabs
|
|
105
|
+
}
|
|
106
|
+
// Tab - next tab
|
|
107
|
+
if (isTab(data)) {
|
|
108
|
+
const newIndex = this.config.wrapNavigation
|
|
109
|
+
? (state.currentTab + 1) % tabCount
|
|
110
|
+
: Math.min(state.currentTab + 1, tabCount - 1);
|
|
111
|
+
if (newIndex !== state.currentTab) {
|
|
112
|
+
state.currentTab = newIndex;
|
|
113
|
+
this.notifyChange(state);
|
|
114
|
+
return { handled: true, action: 'render' };
|
|
115
|
+
}
|
|
116
|
+
return { handled: true }; // At end, consume but no change
|
|
117
|
+
}
|
|
118
|
+
// Shift+Tab - previous tab
|
|
119
|
+
if (isShiftTab(data)) {
|
|
120
|
+
const newIndex = this.config.wrapNavigation
|
|
121
|
+
? (state.currentTab - 1 + tabCount) % tabCount
|
|
122
|
+
: Math.max(state.currentTab - 1, 0);
|
|
123
|
+
if (newIndex !== state.currentTab) {
|
|
124
|
+
state.currentTab = newIndex;
|
|
125
|
+
this.notifyChange(state);
|
|
126
|
+
return { handled: true, action: 'render' };
|
|
127
|
+
}
|
|
128
|
+
return { handled: true }; // At start, consume but no change
|
|
129
|
+
}
|
|
130
|
+
// Number keys 1-9 for direct selection
|
|
131
|
+
if (this.config.useNumberKeys) {
|
|
132
|
+
const num = getNumberKey(data);
|
|
133
|
+
if (num !== null && num >= 1 && num <= tabCount) {
|
|
134
|
+
const newIndex = num - 1;
|
|
135
|
+
if (newIndex !== state.currentTab) {
|
|
136
|
+
state.currentTab = newIndex;
|
|
137
|
+
this.notifyChange(state);
|
|
138
|
+
return { handled: true, action: 'render' };
|
|
139
|
+
}
|
|
140
|
+
return { handled: true }; // Already on this tab
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null; // Not handled
|
|
144
|
+
}
|
|
145
|
+
// ===========================================================================
|
|
146
|
+
// Rendering
|
|
147
|
+
// ===========================================================================
|
|
148
|
+
/**
|
|
149
|
+
* Render tabs in a row.
|
|
150
|
+
*
|
|
151
|
+
* @param state - State containing currentTab
|
|
152
|
+
* @param styles - Theme styles
|
|
153
|
+
* @returns Rendered tab bar string
|
|
154
|
+
*/
|
|
155
|
+
renderTabs(state, styles) {
|
|
156
|
+
return this.config.tabs
|
|
157
|
+
.map((tab, index) => {
|
|
158
|
+
const label = ` ${tab.label} `;
|
|
159
|
+
return index === state.currentTab
|
|
160
|
+
? styles.selected(label)
|
|
161
|
+
: styles.muted(label);
|
|
162
|
+
})
|
|
163
|
+
.join('');
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Render tabs with number key hints.
|
|
167
|
+
*
|
|
168
|
+
* @param state - State containing currentTab
|
|
169
|
+
* @param styles - Theme styles
|
|
170
|
+
* @returns Rendered tab bar string with numbers
|
|
171
|
+
*/
|
|
172
|
+
renderTabsWithNumbers(state, styles) {
|
|
173
|
+
return this.config.tabs
|
|
174
|
+
.map((tab, index) => {
|
|
175
|
+
const num = String(index + 1);
|
|
176
|
+
const label = ` ${num}:${tab.label} `;
|
|
177
|
+
return index === state.currentTab
|
|
178
|
+
? styles.selected(label)
|
|
179
|
+
: styles.muted(label);
|
|
180
|
+
})
|
|
181
|
+
.join('');
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Render tabs with custom separator.
|
|
185
|
+
*
|
|
186
|
+
* @param state - State containing currentTab
|
|
187
|
+
* @param styles - Theme styles
|
|
188
|
+
* @param separator - Separator between tabs (default: '')
|
|
189
|
+
* @returns Rendered tab bar string
|
|
190
|
+
*/
|
|
191
|
+
renderTabsWithSeparator(state, styles, separator = '') {
|
|
192
|
+
return this.config.tabs
|
|
193
|
+
.map((tab, index) => {
|
|
194
|
+
const label = ` ${tab.label} `;
|
|
195
|
+
return index === state.currentTab
|
|
196
|
+
? styles.selected(label)
|
|
197
|
+
: styles.muted(label);
|
|
198
|
+
})
|
|
199
|
+
.join(separator);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Render keyboard hints for tabs.
|
|
203
|
+
*
|
|
204
|
+
* @returns Hint string like "Tab Switch · 1-5 Jump"
|
|
205
|
+
*/
|
|
206
|
+
renderHints() {
|
|
207
|
+
const tabCount = this.config.tabs.length;
|
|
208
|
+
if (tabCount === 0) {
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
if (this.config.useNumberKeys && tabCount <= 9) {
|
|
212
|
+
return `Tab Switch · 1-${String(tabCount)} Jump`;
|
|
213
|
+
}
|
|
214
|
+
return 'Tab Switch';
|
|
215
|
+
}
|
|
216
|
+
// ===========================================================================
|
|
217
|
+
// Utilities
|
|
218
|
+
// ===========================================================================
|
|
219
|
+
/**
|
|
220
|
+
* Get the current tab definition.
|
|
221
|
+
*
|
|
222
|
+
* @param state - State containing currentTab
|
|
223
|
+
* @returns Current tab or undefined
|
|
224
|
+
*/
|
|
225
|
+
getCurrentTab(state) {
|
|
226
|
+
return this.config.tabs[state.currentTab];
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get the current tab ID.
|
|
230
|
+
*
|
|
231
|
+
* @param state - State containing currentTab
|
|
232
|
+
* @returns Current tab ID or empty string
|
|
233
|
+
*/
|
|
234
|
+
getCurrentTabId(state) {
|
|
235
|
+
return this.config.tabs[state.currentTab]?.id ?? '';
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get the number of tabs.
|
|
239
|
+
*/
|
|
240
|
+
getTabCount() {
|
|
241
|
+
return this.config.tabs.length;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Find tab index by ID.
|
|
245
|
+
*
|
|
246
|
+
* @param tabId - Tab ID to find
|
|
247
|
+
* @returns Index or -1 if not found
|
|
248
|
+
*/
|
|
249
|
+
findTabIndex(tabId) {
|
|
250
|
+
return this.config.tabs.findIndex((t) => t.id === tabId);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Set current tab by ID.
|
|
254
|
+
*
|
|
255
|
+
* @param state - State to modify
|
|
256
|
+
* @param tabId - Tab ID to select
|
|
257
|
+
* @returns true if tab was found and selected
|
|
258
|
+
*/
|
|
259
|
+
setTabById(state, tabId) {
|
|
260
|
+
const index = this.findTabIndex(tabId);
|
|
261
|
+
if (index >= 0) {
|
|
262
|
+
state.currentTab = index;
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
// ===========================================================================
|
|
268
|
+
// Private Methods
|
|
269
|
+
// ===========================================================================
|
|
270
|
+
notifyChange(state) {
|
|
271
|
+
if (this.config.onChange) {
|
|
272
|
+
const tab = this.config.tabs[state.currentTab];
|
|
273
|
+
this.config.onChange(tab.id, state.currentTab);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// =============================================================================
|
|
278
|
+
// Helper Functions
|
|
279
|
+
// =============================================================================
|
|
280
|
+
/**
|
|
281
|
+
* Create tab definitions from an array of strings.
|
|
282
|
+
* IDs are lowercase versions of labels.
|
|
283
|
+
*
|
|
284
|
+
* @param labels - Tab labels
|
|
285
|
+
* @returns Tab definitions
|
|
286
|
+
*/
|
|
287
|
+
export function createTabsFromLabels(labels) {
|
|
288
|
+
return labels.map((label) => ({
|
|
289
|
+
id: label.toLowerCase().replace(/\s+/g, '-'),
|
|
290
|
+
label,
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Create filter tabs (common pattern).
|
|
295
|
+
*
|
|
296
|
+
* @param filters - Array of filter names
|
|
297
|
+
* @returns Tab definitions with 'all' as first tab
|
|
298
|
+
*/
|
|
299
|
+
export function createFilterTabs(filters) {
|
|
300
|
+
return [
|
|
301
|
+
{ id: 'all', label: 'All' },
|
|
302
|
+
...filters.map((f) => ({
|
|
303
|
+
id: f.toLowerCase().replace(/\s+/g, '-'),
|
|
304
|
+
label: f,
|
|
305
|
+
})),
|
|
306
|
+
];
|
|
307
|
+
}
|