kennel-plugins 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-454D7WI6.js +16332 -0
- package/dist/chunk-AX4A4GYF.js +11 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-STR4VH4C.js +11 -0
- package/dist/chunk-TCV5SJIW.js +171 -0
- package/dist/chunk-YLSYYX2L.js +8 -0
- package/dist/client.d.ts +65 -0
- package/dist/client.js +11 -0
- package/dist/endpoint.d.ts +61 -0
- package/dist/endpoint.js +21 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +51 -0
- package/dist/page.d.ts +204 -0
- package/dist/page.js +23 -0
- package/dist/server.d.ts +67 -0
- package/dist/server.js +7 -0
- package/dist/subpath.d.ts +21 -0
- package/dist/subpath.js +16 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +7 -0
- package/dist/validation.d.ts +30 -0
- package/dist/validation.js +29 -0
- package/package.json +102 -4
- package/README.md +0 -3
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
function folderPathId(path) {
|
|
3
|
+
const bytes = new TextEncoder().encode(path);
|
|
4
|
+
let binary = "";
|
|
5
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
6
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
folderPathId
|
|
11
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// src/endpoint.ts
|
|
2
|
+
var statusByCode = {
|
|
3
|
+
aborted: 400,
|
|
4
|
+
"bad-request": 400,
|
|
5
|
+
conflict: 409,
|
|
6
|
+
failed: 500,
|
|
7
|
+
forbidden: 403,
|
|
8
|
+
internal: 500,
|
|
9
|
+
"method-not-allowed": 405,
|
|
10
|
+
"not-found": 404,
|
|
11
|
+
"payload-too-large": 413,
|
|
12
|
+
timeout: 504
|
|
13
|
+
};
|
|
14
|
+
var ApiError = class extends Error {
|
|
15
|
+
code;
|
|
16
|
+
status;
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "ApiError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.status = statusByCode[code];
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
function apiError(code, message) {
|
|
25
|
+
return new ApiError(code, message);
|
|
26
|
+
}
|
|
27
|
+
function jsonResponse(response, status, body, extraHeaders = {}) {
|
|
28
|
+
if (response.writableEnded || response.headersSent || response.destroyed) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
response.writeHead(status, {
|
|
32
|
+
"Cache-Control": "no-store",
|
|
33
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
34
|
+
...extraHeaders
|
|
35
|
+
});
|
|
36
|
+
response.end(JSON.stringify(body));
|
|
37
|
+
}
|
|
38
|
+
function jsonError(response, status, code, message, extraHeaders = {}) {
|
|
39
|
+
jsonResponse(response, status, { error: { code, message } }, extraHeaders);
|
|
40
|
+
}
|
|
41
|
+
function sameOrigin(request) {
|
|
42
|
+
const host = request.headers.host;
|
|
43
|
+
if (!host) return false;
|
|
44
|
+
const origin = request.headers.origin;
|
|
45
|
+
if (origin) {
|
|
46
|
+
try {
|
|
47
|
+
return new URL(origin).host === host;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const referer = request.headers.referer;
|
|
53
|
+
if (referer) {
|
|
54
|
+
try {
|
|
55
|
+
return new URL(referer).host === host;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
async function readBoundedBody(request, maxBytes, signal) {
|
|
63
|
+
const iterable = request;
|
|
64
|
+
if (typeof iterable[Symbol.asyncIterator] !== "function") return "";
|
|
65
|
+
let total = 0;
|
|
66
|
+
const chunks = [];
|
|
67
|
+
for await (const chunk of iterable) {
|
|
68
|
+
if (signal.aborted) throw apiError("aborted", "Request aborted");
|
|
69
|
+
total += chunk.length;
|
|
70
|
+
if (total > maxBytes) {
|
|
71
|
+
throw apiError("payload-too-large", "Request body too large");
|
|
72
|
+
}
|
|
73
|
+
chunks.push(chunk);
|
|
74
|
+
}
|
|
75
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
76
|
+
}
|
|
77
|
+
function parseJsonBody(body) {
|
|
78
|
+
if (!body.trim()) throw apiError("bad-request", "Request body required");
|
|
79
|
+
let value;
|
|
80
|
+
try {
|
|
81
|
+
value = JSON.parse(body);
|
|
82
|
+
} catch {
|
|
83
|
+
throw apiError("bad-request", "Request body is not valid JSON");
|
|
84
|
+
}
|
|
85
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
86
|
+
throw apiError("bad-request", "Request body must be a JSON object");
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
var methodAllowHeader = (method) => ({ Allow: method });
|
|
91
|
+
async function runApiRoute(request, response, url, spec, context) {
|
|
92
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
93
|
+
if (method !== spec.method) {
|
|
94
|
+
jsonError(
|
|
95
|
+
response,
|
|
96
|
+
405,
|
|
97
|
+
"method-not-allowed",
|
|
98
|
+
`Method ${method} not allowed`,
|
|
99
|
+
methodAllowHeader(spec.method)
|
|
100
|
+
);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const disconnect = () => controller.abort();
|
|
105
|
+
request.on?.("aborted", disconnect);
|
|
106
|
+
response.on?.("close", disconnect);
|
|
107
|
+
let timer;
|
|
108
|
+
const timeoutPromise = spec.timeoutMs ? new Promise((_resolve, reject) => {
|
|
109
|
+
timer = setTimeout(() => {
|
|
110
|
+
controller.abort();
|
|
111
|
+
reject(apiError("timeout", "Request timed out"));
|
|
112
|
+
}, spec.timeoutMs);
|
|
113
|
+
}) : null;
|
|
114
|
+
try {
|
|
115
|
+
if (method !== "GET" && !sameOrigin(request)) {
|
|
116
|
+
throw apiError("forbidden", "Cross-origin mutations are not allowed");
|
|
117
|
+
}
|
|
118
|
+
const maxBody = spec.maxBodyBytes ?? (method === "GET" ? 0 : 1024 * 1024);
|
|
119
|
+
const body = method === "GET" ? "" : await readBoundedBody(request, maxBody, controller.signal);
|
|
120
|
+
const ctx = {
|
|
121
|
+
body,
|
|
122
|
+
context,
|
|
123
|
+
query: url.searchParams,
|
|
124
|
+
request,
|
|
125
|
+
response,
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
url
|
|
128
|
+
};
|
|
129
|
+
let handlerError;
|
|
130
|
+
const handlerPromise = (async () => {
|
|
131
|
+
await spec.handle(ctx);
|
|
132
|
+
})().catch((error) => {
|
|
133
|
+
handlerError = error;
|
|
134
|
+
});
|
|
135
|
+
if (timeoutPromise) await Promise.race([handlerPromise, timeoutPromise]);
|
|
136
|
+
else await handlerPromise;
|
|
137
|
+
if (handlerError !== void 0) throw handlerError;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error instanceof ApiError && error.code === "timeout") {
|
|
140
|
+
jsonError(response, 504, "timeout", error.message);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (controller.signal.aborted) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (error instanceof ApiError) {
|
|
147
|
+
jsonError(response, error.status, error.code, error.message);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
151
|
+
jsonError(response, 400, "aborted", "Request aborted");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
jsonError(response, 500, "internal", "Internal server error");
|
|
155
|
+
} finally {
|
|
156
|
+
if (timer) clearTimeout(timer);
|
|
157
|
+
request.removeListener?.("aborted", disconnect);
|
|
158
|
+
response.removeListener?.("close", disconnect);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export {
|
|
163
|
+
ApiError,
|
|
164
|
+
apiError,
|
|
165
|
+
jsonResponse,
|
|
166
|
+
jsonError,
|
|
167
|
+
sameOrigin,
|
|
168
|
+
readBoundedBody,
|
|
169
|
+
parseJsonBody,
|
|
170
|
+
runApiRoute
|
|
171
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
import { FolderPluginManifest } from './types.js';
|
|
3
|
+
export { isPluginPayloadError } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Host actions a plugin view may call while rendering. These are injected by
|
|
7
|
+
* the host (never imported by the plugin) and are absent when the server does
|
|
8
|
+
* not provide them — e.g. a phone on `--lan` must never see server-side
|
|
9
|
+
* "open in default app" actions. Plugins gate UI on presence, not on path
|
|
10
|
+
* heuristics.
|
|
11
|
+
*/
|
|
12
|
+
type FolderPluginHostActions = {
|
|
13
|
+
/** Open a physical file in the host's default app (local hosts only). */
|
|
14
|
+
openPath: (path: string) => Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Create an empty AGENTS.md in a folder and open it in the default
|
|
17
|
+
* Markdown app (local hosts only). Resolves with the created file path.
|
|
18
|
+
*/
|
|
19
|
+
createAgentFile: (folder: string) => Promise<{
|
|
20
|
+
path: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Context the host injects into a plugin's client view. The host owns folder
|
|
25
|
+
* resolution, navigation/link helpers, and a scoped request function. Plugins
|
|
26
|
+
* never import `apps/web` helpers or alter the shell directly.
|
|
27
|
+
*/
|
|
28
|
+
type FolderPluginViewProps = {
|
|
29
|
+
/** Absolute folder path shown by the host. */
|
|
30
|
+
folder: string;
|
|
31
|
+
/** Folder route id used as the shared query-key prefix for cache invalidation. */
|
|
32
|
+
folderId: string;
|
|
33
|
+
/** Plugin-relative subpath: `""` for the list view, an id for a detail view. */
|
|
34
|
+
subpath: string;
|
|
35
|
+
/** Absolute href to this plugin's list view, for back navigation. */
|
|
36
|
+
listHref: string;
|
|
37
|
+
/** Absolute href to folder home (Notes), for shell back navigation. */
|
|
38
|
+
homeHref: string;
|
|
39
|
+
/** Builds an absolute href for any plugin-relative subpath. */
|
|
40
|
+
href: (subpath: string) => string;
|
|
41
|
+
/**
|
|
42
|
+
* Builds an absolute href for a document inside a folder, for hosts that
|
|
43
|
+
* serve documents. Undefined on hosts without a document route; content
|
|
44
|
+
* that references documents then renders as plain labels instead of links.
|
|
45
|
+
*/
|
|
46
|
+
documentHref?: (folder: string, relativePath: string) => string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Fetches plugin data for an authorized folder and plugin-relative subpath.
|
|
49
|
+
* Resolves to the decoded JSON payload (throwing on an error response).
|
|
50
|
+
*/
|
|
51
|
+
request: (subpath: string, init?: RequestInit) => Promise<unknown>;
|
|
52
|
+
/**
|
|
53
|
+
* Host actions available to this view, when the server provides them (see
|
|
54
|
+
* `FolderPluginHostActions`). Undefined on remote/unavailable hosts.
|
|
55
|
+
*/
|
|
56
|
+
hostActions?: FolderPluginHostActions;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Stable UTF-8 folder id shared by host routes and plugin query keys. */
|
|
60
|
+
declare function folderPathId(path: string): string;
|
|
61
|
+
type FolderPluginClient = FolderPluginManifest & {
|
|
62
|
+
component: ComponentType<FolderPluginViewProps>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export { type FolderPluginClient, type FolderPluginHostActions, type FolderPluginViewProps, folderPathId };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ServerResponse, IncomingMessage } from 'node:http';
|
|
2
|
+
|
|
3
|
+
type ApiErrorCode = "aborted" | "bad-request" | "conflict" | "failed" | "forbidden" | "internal" | "method-not-allowed" | "not-found" | "payload-too-large" | "timeout";
|
|
4
|
+
declare class ApiError extends Error {
|
|
5
|
+
readonly code: ApiErrorCode;
|
|
6
|
+
readonly status: number;
|
|
7
|
+
constructor(code: ApiErrorCode, message: string);
|
|
8
|
+
}
|
|
9
|
+
declare function apiError(code: ApiErrorCode, message: string): ApiError;
|
|
10
|
+
declare function jsonResponse(response: ServerResponse, status: number, body: unknown, extraHeaders?: Record<string, string>): void;
|
|
11
|
+
declare function jsonError(response: ServerResponse, status: number, code: ApiErrorCode, message: string, extraHeaders?: Record<string, string>): void;
|
|
12
|
+
/**
|
|
13
|
+
* Same-origin guard for mutating requests (POST/DELETE). A browser sends
|
|
14
|
+
* `Origin` (or `Referer`) on cross-site requests; require the header, when
|
|
15
|
+
* present, to match the requested host and reject mutations without any
|
|
16
|
+
* origin signal. GET requests never mutate and are exempt.
|
|
17
|
+
*/
|
|
18
|
+
declare function sameOrigin(request: IncomingMessage): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Read a request body bounded to `maxBytes` bytes. Real Node requests are
|
|
21
|
+
* async-iterable; the caller must set `Content-Length` expectations too. An
|
|
22
|
+
* oversized body or a cancellation aborts with a structured ApiError.
|
|
23
|
+
*/
|
|
24
|
+
declare function readBoundedBody(request: IncomingMessage, maxBytes: number, signal: AbortSignal): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* JSON body helper for mutations. Returns the parsed object or throws a
|
|
27
|
+
* structured bad-request error; non-Object payloads are rejected.
|
|
28
|
+
*/
|
|
29
|
+
declare function parseJsonBody(body: string): Record<string, unknown>;
|
|
30
|
+
type ApiRouteHandle<Context> = (ctx: {
|
|
31
|
+
body: string;
|
|
32
|
+
context: Context;
|
|
33
|
+
query: URLSearchParams;
|
|
34
|
+
request: IncomingMessage;
|
|
35
|
+
response: ServerResponse;
|
|
36
|
+
signal: AbortSignal;
|
|
37
|
+
url: URL;
|
|
38
|
+
}) => Promise<void> | void;
|
|
39
|
+
type ApiRouteSpec<Context> = {
|
|
40
|
+
/** Only this HTTP method is accepted; anything else gets a 405. */
|
|
41
|
+
method: "GET" | "POST" | "DELETE";
|
|
42
|
+
/** Bounded request body in bytes; mutations without a body may set 0. */
|
|
43
|
+
maxBodyBytes?: number;
|
|
44
|
+
/** Hard cap on handler execution; the child/request is cancelled after. */
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Height of `ctx.body` before validation. Sizes over `maxBodyBytes` are
|
|
48
|
+
* rejected as 413, non-mutating methods never read a body.
|
|
49
|
+
*/
|
|
50
|
+
handle: ApiRouteHandle<Context>;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Run one declarative API route. Handles explicit method restriction,
|
|
54
|
+
* same-origin protection for mutations, bounded bodies, abort on client
|
|
55
|
+
* disconnect, timeouts, and structured JSON errors. Handlers never see raw
|
|
56
|
+
* internals; unexpected errors become a generic 500 that cannot leak
|
|
57
|
+
* command output or credentials.
|
|
58
|
+
*/
|
|
59
|
+
declare function runApiRoute<Context>(request: IncomingMessage, response: ServerResponse, url: URL, spec: ApiRouteSpec<Context>, context: Context): Promise<void>;
|
|
60
|
+
|
|
61
|
+
export { ApiError, type ApiErrorCode, type ApiRouteHandle, type ApiRouteSpec, apiError, jsonError, jsonResponse, parseJsonBody, readBoundedBody, runApiRoute, sameOrigin };
|
package/dist/endpoint.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiError,
|
|
3
|
+
apiError,
|
|
4
|
+
jsonError,
|
|
5
|
+
jsonResponse,
|
|
6
|
+
parseJsonBody,
|
|
7
|
+
readBoundedBody,
|
|
8
|
+
runApiRoute,
|
|
9
|
+
sameOrigin
|
|
10
|
+
} from "./chunk-TCV5SJIW.js";
|
|
11
|
+
import "./chunk-MLKGABMK.js";
|
|
12
|
+
export {
|
|
13
|
+
ApiError,
|
|
14
|
+
apiError,
|
|
15
|
+
jsonError,
|
|
16
|
+
jsonResponse,
|
|
17
|
+
parseJsonBody,
|
|
18
|
+
readBoundedBody,
|
|
19
|
+
runApiRoute,
|
|
20
|
+
sameOrigin
|
|
21
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { CopyContextButton, FilePreview, FilePreviewList, FolderFilterBar, FolderFilterChoice, FolderFilterMode, FolderSelection, PluginPage, PluginPageError, PluginPageStatus, useFolderFilter } from './page.js';
|
|
2
|
+
export { FolderPluginClient, FolderPluginHostActions, FolderPluginViewProps, folderPathId } from './client.js';
|
|
3
|
+
export { ApiError, ApiErrorCode, ApiRouteHandle, ApiRouteSpec, apiError, jsonError, jsonResponse, parseJsonBody, readBoundedBody, runApiRoute, sameOrigin } from './endpoint.js';
|
|
4
|
+
export { FolderPluginContext, FolderPluginData, FolderPluginFailure, FolderPluginInitFile, FolderPluginInitResult, FolderPluginServer } from './server.js';
|
|
5
|
+
export { FolderPluginDataset, FolderPluginDetection, FolderPluginId, FolderPluginManifest, FolderPluginPayload, FolderPluginPayloadError, isPluginPayloadError } from './types.js';
|
|
6
|
+
import 'react';
|
|
7
|
+
import 'class-variance-authority/types';
|
|
8
|
+
import 'class-variance-authority';
|
|
9
|
+
import 'node:http';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiError,
|
|
3
|
+
apiError,
|
|
4
|
+
jsonError,
|
|
5
|
+
jsonResponse,
|
|
6
|
+
parseJsonBody,
|
|
7
|
+
readBoundedBody,
|
|
8
|
+
runApiRoute,
|
|
9
|
+
sameOrigin
|
|
10
|
+
} from "./chunk-TCV5SJIW.js";
|
|
11
|
+
import {
|
|
12
|
+
CopyContextButton,
|
|
13
|
+
EntityError,
|
|
14
|
+
EntityPage,
|
|
15
|
+
EntityStatus,
|
|
16
|
+
FilePreview,
|
|
17
|
+
FilePreviewList,
|
|
18
|
+
FolderFilterBar,
|
|
19
|
+
useFolderFilter
|
|
20
|
+
} from "./chunk-454D7WI6.js";
|
|
21
|
+
import {
|
|
22
|
+
folderPathId
|
|
23
|
+
} from "./chunk-STR4VH4C.js";
|
|
24
|
+
import {
|
|
25
|
+
FolderPluginFailure
|
|
26
|
+
} from "./chunk-AX4A4GYF.js";
|
|
27
|
+
import {
|
|
28
|
+
isPluginPayloadError
|
|
29
|
+
} from "./chunk-YLSYYX2L.js";
|
|
30
|
+
import "./chunk-MLKGABMK.js";
|
|
31
|
+
export {
|
|
32
|
+
ApiError,
|
|
33
|
+
CopyContextButton,
|
|
34
|
+
FilePreview,
|
|
35
|
+
FilePreviewList,
|
|
36
|
+
FolderFilterBar,
|
|
37
|
+
FolderPluginFailure,
|
|
38
|
+
EntityPage as PluginPage,
|
|
39
|
+
EntityError as PluginPageError,
|
|
40
|
+
EntityStatus as PluginPageStatus,
|
|
41
|
+
apiError,
|
|
42
|
+
folderPathId,
|
|
43
|
+
isPluginPayloadError,
|
|
44
|
+
jsonError,
|
|
45
|
+
jsonResponse,
|
|
46
|
+
parseJsonBody,
|
|
47
|
+
readBoundedBody,
|
|
48
|
+
runApiRoute,
|
|
49
|
+
sameOrigin,
|
|
50
|
+
useFolderFilter
|
|
51
|
+
};
|
package/dist/page.d.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, ComponentProps } from 'react';
|
|
3
|
+
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
4
|
+
import { VariantProps } from 'class-variance-authority';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Page shell for entity list and detail pages: a centered single-column
|
|
8
|
+
* grid with a header slot (back link, linked-source status), an extra slot
|
|
9
|
+
* (filters, warnings), and the page body. Keeps every plugin page on the
|
|
10
|
+
* same width and rhythm.
|
|
11
|
+
*/
|
|
12
|
+
declare function EntityPage({ header, extra, children, }: {
|
|
13
|
+
header?: ReactNode;
|
|
14
|
+
extra?: ReactNode;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
}): react.JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* A status line: plain strings render in the shared muted style, richer
|
|
19
|
+
* nodes (issue panels, error details) pass through untouched.
|
|
20
|
+
*/
|
|
21
|
+
declare function EntityStatus({ children }: {
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}): number | bigint | boolean | react.JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* An error block: plain strings render as a destructive line with an
|
|
26
|
+
* optional retry button; richer nodes pass through untouched.
|
|
27
|
+
*/
|
|
28
|
+
declare function EntityError({ error, onRetry, }: {
|
|
29
|
+
error: ReactNode;
|
|
30
|
+
onRetry?: () => void;
|
|
31
|
+
}): number | bigint | boolean | react.JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
|
|
32
|
+
|
|
33
|
+
declare const buttonVariants: (props?: ({
|
|
34
|
+
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null | undefined;
|
|
35
|
+
size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
|
|
36
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
37
|
+
declare function Button({ className, variant, size, asChild, ...props }: react.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
|
|
38
|
+
asChild?: boolean;
|
|
39
|
+
}): react.JSX.Element;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The context to copy: either a ready string or a builder function. Builders
|
|
43
|
+
* may be async, so a plugin can assemble its prompt inside a wait function
|
|
44
|
+
* (fetch a detail, append a body read from disk, …) before the clipboard
|
|
45
|
+
* write happens.
|
|
46
|
+
*/
|
|
47
|
+
type CopyContextSource = string | (() => string | Promise<string>);
|
|
48
|
+
/**
|
|
49
|
+
* Ready-made icon button for copying an item's context (tasks rows, mail
|
|
50
|
+
* detail headers, …): Copy icon, swaps to a green check on success. Callers
|
|
51
|
+
* pass the item's context — or a builder that resolves it on click.
|
|
52
|
+
*/
|
|
53
|
+
type CopyContextButtonProps = {
|
|
54
|
+
context: CopyContextSource;
|
|
55
|
+
className?: string;
|
|
56
|
+
/** Accessible name and tooltip; defaults to "Copy context". */
|
|
57
|
+
label?: string;
|
|
58
|
+
variant?: ComponentProps<typeof Button>["variant"];
|
|
59
|
+
size?: ComponentProps<typeof Button>["size"];
|
|
60
|
+
/** Toast confirmation after the copy attempt (default: true). */
|
|
61
|
+
notify?: boolean;
|
|
62
|
+
onCopied?: () => void;
|
|
63
|
+
onFailed?: () => void;
|
|
64
|
+
};
|
|
65
|
+
declare function CopyContextButton({ className, context, label, notify, onCopied, onFailed, size, variant, }: CopyContextButtonProps): react.JSX.Element;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Card preview for one file attachment. Shows an inline thumbnail for
|
|
69
|
+
* image and PDF files (the icon of the well-known type otherwise) plus the
|
|
70
|
+
* file name. The thumbnail links to the raw file in a new browser tab and
|
|
71
|
+
* the Open button opens it in the host's default application without
|
|
72
|
+
* navigating the viewer, so the sender can check exactly what will go out
|
|
73
|
+
* with the draft.
|
|
74
|
+
*/
|
|
75
|
+
declare function FilePreview({ path }: {
|
|
76
|
+
path: string;
|
|
77
|
+
}): react.JSX.Element;
|
|
78
|
+
/** Row of attachment preview cards, one card per absolute file path. */
|
|
79
|
+
declare function FilePreviewList({ paths }: {
|
|
80
|
+
paths: string[];
|
|
81
|
+
}): react.JSX.Element;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generalized folder filtering for lists of items that carry an owning
|
|
85
|
+
* folder path.
|
|
86
|
+
*
|
|
87
|
+
* Pure string/array logic — no React, no DOM, no `node:` builtins — so the
|
|
88
|
+
* exact same helpers power web chips, folder-plugin views, and CLI
|
|
89
|
+
* filtering. The model mirrors the mail plugin's draft selector but is
|
|
90
|
+
* accessor-driven: you hand over your items, a function that reads each
|
|
91
|
+
* item's owning folder, and the viewed root folder; you get back the
|
|
92
|
+
* distinct folder choices (root first, then descendants by relative path,
|
|
93
|
+
* each with a count) and a predicate that decides which items a selection
|
|
94
|
+
* keeps.
|
|
95
|
+
*
|
|
96
|
+
* CLI example (no React):
|
|
97
|
+
*
|
|
98
|
+
* ```ts
|
|
99
|
+
* import {
|
|
100
|
+
* folderFilterChoices,
|
|
101
|
+
* folderFilterMatches,
|
|
102
|
+
* } from "@raggle-ai/ui/lib/folder-filter"
|
|
103
|
+
*
|
|
104
|
+
* const choices = folderFilterChoices(items, root, (item) => item.folder)
|
|
105
|
+
* const visible = items.filter((item) =>
|
|
106
|
+
* folderFilterMatches(item.folder, flags.folder, "subtree"),
|
|
107
|
+
* )
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
/** How a selected folder applies to an item's owning folder. */
|
|
111
|
+
type FolderFilterMode = "exact" | "subtree";
|
|
112
|
+
/**
|
|
113
|
+
* One clickable chip: a distinct owning folder in the item list. `folder`
|
|
114
|
+
* is the full owning path and the exact filter key (duplicate leaf names
|
|
115
|
+
* stay distinct); `label` is the short display text (basename for the
|
|
116
|
+
* root, relative path for descendants).
|
|
117
|
+
*/
|
|
118
|
+
type FolderFilterChoice = {
|
|
119
|
+
/** Exact owning folder path — the filter key. */
|
|
120
|
+
folder: string;
|
|
121
|
+
/** Number of items owned by this folder. */
|
|
122
|
+
count: number;
|
|
123
|
+
/** Short display label: basename for the root, relative path for descendants. */
|
|
124
|
+
label: string;
|
|
125
|
+
/** Path relative to the viewed root; "" for the root itself. */
|
|
126
|
+
relativePath: string;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* A selection for folder filtering: a single owning folder, several
|
|
130
|
+
* (multi-select — an item passes when any of them matches), or null/
|
|
131
|
+
* empty for "all items". Single-string callers keep working unchanged.
|
|
132
|
+
*/
|
|
133
|
+
type FolderSelection = string | readonly string[] | null;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Multi-select folder filter as a single "Folder (count)" button. The
|
|
137
|
+
* count badge shows how many folders are selected, or the total when
|
|
138
|
+
* nothing is filtered. The popover holds a searchable command list with
|
|
139
|
+
* an "All folders" row that clears back to everything; every folder row
|
|
140
|
+
* toggles into the selection in real time.
|
|
141
|
+
*
|
|
142
|
+
* Rows are portal-mounted on open (like this repo's Select), so closed
|
|
143
|
+
* dropdown content is not in the SSR/tree — the trigger carries the
|
|
144
|
+
* accessible count instead.
|
|
145
|
+
*/
|
|
146
|
+
declare function FolderFilterBar({ allLabel, ariaLabel, choices, onSelect, selected, total, }: {
|
|
147
|
+
/** Label of the row that clears the selection. */
|
|
148
|
+
allLabel?: string;
|
|
149
|
+
/** Accessible name for the filter trigger. */
|
|
150
|
+
ariaLabel?: string;
|
|
151
|
+
choices: FolderFilterChoice[];
|
|
152
|
+
/** All currently selected owning folders; pass [] to show everything. */
|
|
153
|
+
onSelect: (folders: readonly string[]) => void;
|
|
154
|
+
/** Currently selected owning folders; empty means "all". */
|
|
155
|
+
selected: readonly string[];
|
|
156
|
+
/** Count shown on the trigger while nothing is selected. */
|
|
157
|
+
total: number;
|
|
158
|
+
}): react.JSX.Element;
|
|
159
|
+
|
|
160
|
+
type UseFolderFilterOptions<T> = {
|
|
161
|
+
/** Owning folder of each item; null when the item has none. */
|
|
162
|
+
getFolder: (item: T) => string | null;
|
|
163
|
+
/** "exact" keeps the selected folder's items only; "subtree" also keeps descendant items. */
|
|
164
|
+
mode?: FolderFilterMode;
|
|
165
|
+
/** Include ancestor/outside folders as chips (see `folderFilterChoices`). */
|
|
166
|
+
allowOutside?: boolean;
|
|
167
|
+
/** Folder everything is shown relative to (the viewed root). */
|
|
168
|
+
rootFolder: string;
|
|
169
|
+
/** Initial selection (owning folder paths); a selection with no items falls back to all. */
|
|
170
|
+
initialFolder?: FolderSelection;
|
|
171
|
+
};
|
|
172
|
+
type UseFolderFilterResult<T> = {
|
|
173
|
+
/** Distinct owning-folder choices with counts, root first. */
|
|
174
|
+
choices: FolderFilterChoice[];
|
|
175
|
+
/**
|
|
176
|
+
* Selection entries that still match items. Empty means "all items" —
|
|
177
|
+
* either nothing is selected or every selected folder lost its items.
|
|
178
|
+
*/
|
|
179
|
+
effectiveSelected: readonly string[];
|
|
180
|
+
/** Raw selection state; entries whose folder has no items left may be stale. */
|
|
181
|
+
selected: readonly string[];
|
|
182
|
+
/**
|
|
183
|
+
* Replace the selection. Pass a single folder, an array of folders (an
|
|
184
|
+
* item passes when any of them matches), or null/[] to show everything.
|
|
185
|
+
*/
|
|
186
|
+
setSelected: (folder: FolderSelection) => void;
|
|
187
|
+
/** Toggle one owning folder in/out of the selection. */
|
|
188
|
+
toggle: (folder: string) => void;
|
|
189
|
+
/** Clear the selection back to "all items". */
|
|
190
|
+
reset: () => void;
|
|
191
|
+
/** Number of items before filtering — shown on the "All" chip. */
|
|
192
|
+
total: number;
|
|
193
|
+
/** Items after applying the effective selection. */
|
|
194
|
+
visibleItems: T[];
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Folder-filter state for a plugin view: pass the items and a folder
|
|
198
|
+
* accessor, get the choices, the reconciled (multi-)selection, and the
|
|
199
|
+
* filtered `visibleItems` in one place. Recomputes only when the items,
|
|
200
|
+
* accessor, root, mode, or selection change.
|
|
201
|
+
*/
|
|
202
|
+
declare function useFolderFilter<T>(items: readonly T[], options: UseFolderFilterOptions<T>): UseFolderFilterResult<T>;
|
|
203
|
+
|
|
204
|
+
export { CopyContextButton, FilePreview, FilePreviewList, FolderFilterBar, type FolderFilterChoice, type FolderFilterMode, type FolderSelection, EntityPage as PluginPage, EntityError as PluginPageError, EntityStatus as PluginPageStatus, useFolderFilter };
|
package/dist/page.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CopyContextButton,
|
|
3
|
+
EntityError,
|
|
4
|
+
EntityPage,
|
|
5
|
+
EntityStatus,
|
|
6
|
+
FilePreview,
|
|
7
|
+
FilePreviewList,
|
|
8
|
+
FolderFilterBar,
|
|
9
|
+
useFolderFilter
|
|
10
|
+
} from "./chunk-454D7WI6.js";
|
|
11
|
+
import "./chunk-STR4VH4C.js";
|
|
12
|
+
import "./chunk-YLSYYX2L.js";
|
|
13
|
+
import "./chunk-MLKGABMK.js";
|
|
14
|
+
export {
|
|
15
|
+
CopyContextButton,
|
|
16
|
+
FilePreview,
|
|
17
|
+
FilePreviewList,
|
|
18
|
+
FolderFilterBar,
|
|
19
|
+
EntityPage as PluginPage,
|
|
20
|
+
EntityError as PluginPageError,
|
|
21
|
+
EntityStatus as PluginPageStatus,
|
|
22
|
+
useFolderFilter
|
|
23
|
+
};
|