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.
@@ -0,0 +1,11 @@
1
+ // src/server.ts
2
+ var FolderPluginFailure = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "FolderPluginFailure";
6
+ }
7
+ };
8
+
9
+ export {
10
+ FolderPluginFailure
11
+ };
@@ -0,0 +1,9 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ export {
8
+ __export
9
+ };
@@ -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
+ };
@@ -0,0 +1,8 @@
1
+ // src/types.ts
2
+ function isPluginPayloadError(data) {
3
+ return typeof data === "object" && data !== null && "status" in data && data.status === "error";
4
+ }
5
+
6
+ export {
7
+ isPluginPayloadError
8
+ };
@@ -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,11 @@
1
+ import {
2
+ folderPathId
3
+ } from "./chunk-STR4VH4C.js";
4
+ import {
5
+ isPluginPayloadError
6
+ } from "./chunk-YLSYYX2L.js";
7
+ import "./chunk-MLKGABMK.js";
8
+ export {
9
+ folderPathId,
10
+ isPluginPayloadError
11
+ };
@@ -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 };
@@ -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
+ };
@@ -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
+ };