@dropsh/plugin-tui 0.5.7 → 0.5.8
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/dist/plugins/tui/src/browse-app.d.ts +7 -0
- package/dist/plugins/tui/src/browse-app.js +18 -1
- package/dist/plugins/tui/src/entity-list.d.ts +12 -0
- package/dist/plugins/tui/src/entity-view.d.ts +14 -0
- package/dist/plugins/tui/src/entity-view.js +16 -3
- package/dist/plugins/tui/src/index.d.ts +14 -0
- package/dist/plugins/tui/src/index.js +1 -0
- package/dist/plugins/tui/src/jsonapi-type.d.ts +6 -0
- package/dist/plugins/tui/src/link.d.ts +20 -0
- package/dist/plugins/tui/src/registry.d.ts +7 -0
- package/dist/plugins/tui/src/registry.js +1 -1
- package/dist/plugins/tui/src/router.d.ts +15 -0
- package/dist/plugins/tui/src/router.js +11 -2
- package/dist/plugins/tui/src/types.d.ts +49 -0
- package/package.json +3 -3
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { FocusRegistryProvider, useFocusRegistry } from "./link.js";
|
|
5
5
|
// Inner component: has access to the focus registry so Enter can read the
|
|
6
6
|
// focused link target and ask the router to navigate to it.
|
|
7
7
|
function Keys({ router, onExit, focusedIndex, setFocusedIndex, bump, setError, setNavSeq, }) {
|
|
8
8
|
const reg = useFocusRegistry();
|
|
9
|
+
// Guards against a second Enter landing while a navigation is still pending,
|
|
10
|
+
// which would push a duplicate screen onto the router stack.
|
|
11
|
+
const navigating = useRef(false);
|
|
9
12
|
useInput((input, key) => {
|
|
10
13
|
if (input === "q" || key.escape) {
|
|
11
14
|
const hasMore = router.back();
|
|
@@ -24,8 +27,11 @@ function Keys({ router, onExit, focusedIndex, setFocusedIndex, bump, setError, s
|
|
|
24
27
|
if (key.downArrow)
|
|
25
28
|
setFocusedIndex((i) => Math.min(Math.max(0, reg.count() - 1), i + 1));
|
|
26
29
|
if (key.return) {
|
|
30
|
+
if (navigating.current)
|
|
31
|
+
return;
|
|
27
32
|
const target = reg.targetAt(focusedIndex);
|
|
28
33
|
if (target) {
|
|
34
|
+
navigating.current = true;
|
|
29
35
|
void router
|
|
30
36
|
.navigate(target.route, target.params)
|
|
31
37
|
.then(() => {
|
|
@@ -37,6 +43,9 @@ function Keys({ router, onExit, focusedIndex, setFocusedIndex, bump, setError, s
|
|
|
37
43
|
.catch((err) => {
|
|
38
44
|
setError(err instanceof Error ? err.message : String(err));
|
|
39
45
|
bump();
|
|
46
|
+
})
|
|
47
|
+
.finally(() => {
|
|
48
|
+
navigating.current = false;
|
|
40
49
|
});
|
|
41
50
|
}
|
|
42
51
|
}
|
|
@@ -49,6 +58,14 @@ export function Browse({ router, onExit }) {
|
|
|
49
58
|
const [error, setError] = useState(null);
|
|
50
59
|
const [navSeq, setNavSeq] = useState(0);
|
|
51
60
|
const bump = () => setNonce((n) => n + 1);
|
|
61
|
+
// Surface controller-initiated (fire-and-forget) navigation errors on the
|
|
62
|
+
// same red error line as host-level ones.
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
router.setErrorHandler((msg) => {
|
|
65
|
+
setError(msg);
|
|
66
|
+
setNonce((n) => n + 1);
|
|
67
|
+
});
|
|
68
|
+
}, [router]);
|
|
52
69
|
const element = router.current();
|
|
53
70
|
return (_jsxs(FocusRegistryProvider, { focusedIndex: focusedIndex, children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { flexDirection: "column", children: element }, navSeq), error !== null ? _jsxs(Text, { color: "red", children: ["Error: ", error] }) : null, _jsx(Text, { dimColor: true, children: "(\u2191/\u2193: move, enter: open, q/esc: back)" })] }), _jsx(Keys, { router: router, onExit: onExit, focusedIndex: focusedIndex, setFocusedIndex: setFocusedIndex, bump: bump, setError: setError, setNavSeq: setNavSeq })] }));
|
|
54
71
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { JsonApiResource } from "dropsh/plugin";
|
|
2
|
+
import { type ReactElement } from "react";
|
|
3
|
+
import type { BuildContext } from "./types.js";
|
|
4
|
+
export declare abstract class TuiEntityList {
|
|
5
|
+
static entityType: string;
|
|
6
|
+
static bundle?: string;
|
|
7
|
+
abstract build(resources: JsonApiResource[], ctx: BuildContext): ReactElement;
|
|
8
|
+
}
|
|
9
|
+
export declare class GenericEntityList extends TuiEntityList {
|
|
10
|
+
static entityType: string;
|
|
11
|
+
build(resources: JsonApiResource[], ctx: BuildContext): ReactElement;
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JsonApiResource } from "dropsh/plugin";
|
|
2
|
+
import { type ReactElement } from "react";
|
|
3
|
+
import type { BuildContext, ViewModeMap } from "./types.js";
|
|
4
|
+
export declare abstract class TuiEntityView {
|
|
5
|
+
static entityType: string;
|
|
6
|
+
static bundle?: string;
|
|
7
|
+
static viewModes: ViewModeMap;
|
|
8
|
+
abstract build(entity: JsonApiResource, ctx: BuildContext): ReactElement;
|
|
9
|
+
}
|
|
10
|
+
export declare class GenericEntityView extends TuiEntityView {
|
|
11
|
+
static entityType: string;
|
|
12
|
+
static viewModes: ViewModeMap;
|
|
13
|
+
build(entity: JsonApiResource, ctx: BuildContext): ReactElement;
|
|
14
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { renderMarkdown } from "@dropsh/plugin-markdown/render";
|
|
2
3
|
import { Box, Text } from "ink";
|
|
3
4
|
import { splitType } from "./jsonapi-type.js";
|
|
4
5
|
import { TuiLink } from "./link.js";
|
|
@@ -11,9 +12,21 @@ export class GenericEntityView extends TuiEntityView {
|
|
|
11
12
|
static entityType = "*";
|
|
12
13
|
static viewModes = { default: {} };
|
|
13
14
|
build(entity, ctx) {
|
|
14
|
-
const attrs = entity.attributes ?? {};
|
|
15
15
|
const rels = entity.relationships ?? {};
|
|
16
|
-
|
|
16
|
+
// Reuse the markdown renderer for the attribute pane (AC: "the detail pane
|
|
17
|
+
// reuses the markdown renderer"). Relationships are stripped from the copy
|
|
18
|
+
// fed to markdown so they stay focusable `TuiLink`s below, keeping in-pane
|
|
19
|
+
// navigation working instead of the markdown renderer's inert `[type/id]`.
|
|
20
|
+
const attrDoc = {
|
|
21
|
+
data: {
|
|
22
|
+
type: entity.type,
|
|
23
|
+
id: entity.id,
|
|
24
|
+
...(entity.attributes ? { attributes: entity.attributes } : {}),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const renderCtx = { command: "read", viewMode: ctx.viewMode };
|
|
28
|
+
const markdown = renderMarkdown(attrDoc, renderCtx);
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: markdown }), Object.entries(rels).map(([key, rel]) => {
|
|
17
30
|
const data = rel.data;
|
|
18
31
|
if (!data || Array.isArray(data))
|
|
19
32
|
return _jsxs(Text, { children: [key, ": \u2014"] }, `r:${key}`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DropSHPlugin, JsonApiDocument, RenderContext } from "dropsh/plugin";
|
|
2
|
+
import type { TuiSubPlugin } from "./types.js";
|
|
3
|
+
export { GenericEntityList, TuiEntityList } from "./entity-list.js";
|
|
4
|
+
export { GenericEntityView, TuiEntityView } from "./entity-view.js";
|
|
5
|
+
export { TuiLink } from "./link.js";
|
|
6
|
+
export type { BuildContext, LinkDescriptor, TuiController, TuiRoute, TuiSubPlugin, ViewModeMap, } from "./types.js";
|
|
7
|
+
export interface TuiOptions {
|
|
8
|
+
plugins?: TuiSubPlugin[];
|
|
9
|
+
}
|
|
10
|
+
export declare function initialTarget(doc: JsonApiDocument, ctx: RenderContext): {
|
|
11
|
+
route: string;
|
|
12
|
+
params: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
export declare function tuiPlugin(tuiOpts?: TuiOptions): DropSHPlugin;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { LinkDescriptor } from "./types.js";
|
|
3
|
+
interface FocusRegistry {
|
|
4
|
+
register(id: symbol, target: LinkDescriptor): void;
|
|
5
|
+
unregister(id: symbol): void;
|
|
6
|
+
indexOf(id: symbol): number;
|
|
7
|
+
count(): number;
|
|
8
|
+
focusedIndex: number;
|
|
9
|
+
targetAt(i: number): LinkDescriptor | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function useFocusRegistry(): FocusRegistry;
|
|
12
|
+
export declare function FocusRegistryProvider({ focusedIndex, children, }: {
|
|
13
|
+
focusedIndex?: number;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}): React.ReactElement;
|
|
16
|
+
export declare function TuiLink({ target, children, }: {
|
|
17
|
+
target: LinkDescriptor;
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}): React.ReactElement;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EntityListClass, EntityViewClass, TuiRoute, TuiSubPlugin } from "./types.js";
|
|
2
|
+
export interface Registry {
|
|
3
|
+
resolveView(entityType: string, bundle?: string): EntityViewClass;
|
|
4
|
+
resolveList(entityType: string, bundle?: string): EntityListClass;
|
|
5
|
+
routes: Map<string, TuiRoute>;
|
|
6
|
+
}
|
|
7
|
+
export declare function buildRegistry(plugins: TuiSubPlugin[]): Registry;
|
|
@@ -23,7 +23,7 @@ function coreRoutes() {
|
|
|
23
23
|
path: "/{type}/{bundle}/{id}",
|
|
24
24
|
controller: async (params, ctx) => {
|
|
25
25
|
const ViewClass = ctx.resolveView(params.type, params.bundle);
|
|
26
|
-
const include = ViewClass
|
|
26
|
+
const include = ViewClass.viewModes[ctx.viewMode]?.include;
|
|
27
27
|
const path = params.bundle
|
|
28
28
|
? `${params.type}/${params.bundle}/${params.id}`
|
|
29
29
|
: `${params.type}/${params.id}`;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { JsonApiClient } from "dropsh/plugin";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
import type { Registry } from "./registry.js";
|
|
4
|
+
export interface Router {
|
|
5
|
+
stackDepth(): number;
|
|
6
|
+
current(): ReactElement | null;
|
|
7
|
+
navigate(route: string, params: Record<string, string>, seededDoc?: import("dropsh/plugin").JsonApiDocument): Promise<void>;
|
|
8
|
+
back(): boolean;
|
|
9
|
+
setErrorHandler(fn: (message: string) => void): void;
|
|
10
|
+
}
|
|
11
|
+
export declare function createRouter(opts: {
|
|
12
|
+
registry: Registry;
|
|
13
|
+
client: JsonApiClient;
|
|
14
|
+
viewMode: string;
|
|
15
|
+
}): Router;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export function createRouter(opts) {
|
|
2
2
|
const stack = [];
|
|
3
|
+
let errorHandler = (message) => {
|
|
4
|
+
console.error(`tui navigation error: ${message}`);
|
|
5
|
+
};
|
|
3
6
|
async function navigate(routeName, params, seededDoc) {
|
|
4
7
|
const route = opts.registry.routes.get(routeName);
|
|
5
8
|
if (!route)
|
|
@@ -10,8 +13,11 @@ export function createRouter(opts) {
|
|
|
10
13
|
...(seededDoc !== undefined ? { seededDoc } : {}),
|
|
11
14
|
navigate: (r, p) => {
|
|
12
15
|
// Host-level navigation surfaces errors in the UI; a controller-initiated
|
|
13
|
-
// navigation failure must not become an unhandled rejection / crash
|
|
14
|
-
|
|
16
|
+
// navigation failure must not become an unhandled rejection / crash, but
|
|
17
|
+
// it is reported through the registered error handler, not swallowed.
|
|
18
|
+
void navigate(r, p).catch((err) => {
|
|
19
|
+
errorHandler(err instanceof Error ? err.message : String(err));
|
|
20
|
+
});
|
|
15
21
|
},
|
|
16
22
|
resolveView: opts.registry.resolveView,
|
|
17
23
|
resolveList: opts.registry.resolveList,
|
|
@@ -27,5 +33,8 @@ export function createRouter(opts) {
|
|
|
27
33
|
stack.pop();
|
|
28
34
|
return stack.length > 0;
|
|
29
35
|
},
|
|
36
|
+
setErrorHandler(fn) {
|
|
37
|
+
errorHandler = fn;
|
|
38
|
+
},
|
|
30
39
|
};
|
|
31
40
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { JsonApiClient, JsonApiDocument } from "dropsh/plugin";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
export interface LinkDescriptor {
|
|
4
|
+
route: string;
|
|
5
|
+
params: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export interface BuildContext {
|
|
8
|
+
viewMode: string;
|
|
9
|
+
doc: JsonApiDocument;
|
|
10
|
+
link(route: string, params: Record<string, string>): LinkDescriptor;
|
|
11
|
+
}
|
|
12
|
+
export type ViewModeMap = Record<string, {
|
|
13
|
+
include?: string[];
|
|
14
|
+
}>;
|
|
15
|
+
export interface ControllerContext {
|
|
16
|
+
client: JsonApiClient;
|
|
17
|
+
viewMode: string;
|
|
18
|
+
seededDoc?: JsonApiDocument;
|
|
19
|
+
navigate(route: string, params: Record<string, string>): void;
|
|
20
|
+
resolveView(entityType: string, bundle?: string): EntityViewClass;
|
|
21
|
+
resolveList(entityType: string, bundle?: string): EntityListClass;
|
|
22
|
+
}
|
|
23
|
+
export type TuiController = (params: Record<string, string>, ctx: ControllerContext) => Promise<ReactElement>;
|
|
24
|
+
export interface TuiRoute {
|
|
25
|
+
name: string;
|
|
26
|
+
path: string;
|
|
27
|
+
controller: TuiController;
|
|
28
|
+
}
|
|
29
|
+
export interface EntityViewClass {
|
|
30
|
+
entityType: string;
|
|
31
|
+
bundle?: string;
|
|
32
|
+
viewModes: ViewModeMap;
|
|
33
|
+
new (): {
|
|
34
|
+
build(entity: import("dropsh/plugin").JsonApiResource, ctx: BuildContext): ReactElement;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export interface EntityListClass {
|
|
38
|
+
entityType: string;
|
|
39
|
+
bundle?: string;
|
|
40
|
+
new (): {
|
|
41
|
+
build(resources: import("dropsh/plugin").JsonApiResource[], ctx: BuildContext): ReactElement;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export type EntityHandlerClass = EntityViewClass | EntityListClass;
|
|
45
|
+
export interface TuiSubPlugin {
|
|
46
|
+
id: string;
|
|
47
|
+
entities?: EntityHandlerClass[];
|
|
48
|
+
routes?: TuiRoute[];
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dropsh/plugin-tui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/plugins/tui/src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"ink": "^5.0.0",
|
|
20
20
|
"react": "^18.3.0",
|
|
21
|
-
"@dropsh/plugin-markdown": "0.5.
|
|
21
|
+
"@dropsh/plugin-markdown": "0.5.8"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/react": "^18.3.0",
|
|
25
25
|
"ink-testing-library": "^4.0.0",
|
|
26
26
|
"typescript": "^5.6.0",
|
|
27
27
|
"vitest": "^2.1.0",
|
|
28
|
-
"dropsh": "0.5.
|
|
28
|
+
"dropsh": "0.5.8"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsc",
|