mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-30 06:11:48 +02:00
Merge main into proxy branch (formatting and docs)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
export function debounce(fn: (...args: any[]) => void, delay = 500) {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
const result = (...args: any[]) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), delay);
|
||||
|
||||
@@ -4,16 +4,16 @@ export function formatSize(bytes: number): string {
|
||||
|
||||
if (bytes > 1000 * 1000 * 1000) {
|
||||
num = bytes / 1000 / 1000 / 1000;
|
||||
unit = 'GB';
|
||||
unit = "GB";
|
||||
} else if (bytes > 1000 * 1000) {
|
||||
num = bytes / 1000 / 1000;
|
||||
unit = 'MB';
|
||||
unit = "MB";
|
||||
} else if (bytes > 1000) {
|
||||
num = bytes / 1000;
|
||||
unit = 'KB';
|
||||
unit = "KB";
|
||||
} else {
|
||||
num = bytes;
|
||||
unit = 'B';
|
||||
unit = "B";
|
||||
}
|
||||
|
||||
return `${Math.round(num * 10) / 10} ${unit}`;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './debounce';
|
||||
export * from './formatSize';
|
||||
export * from './templateFunction';
|
||||
export * from "./debounce";
|
||||
export * from "./formatSize";
|
||||
export * from "./templateFunction";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/lib",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "index.ts"
|
||||
}
|
||||
|
||||
@@ -2,20 +2,20 @@ import type {
|
||||
CallTemplateFunctionArgs,
|
||||
JsonPrimitive,
|
||||
TemplateFunctionArg,
|
||||
} from '@yaakapp-internal/plugins';
|
||||
} from "@yaakapp-internal/plugins";
|
||||
|
||||
export function validateTemplateFunctionArgs(
|
||||
fnName: string,
|
||||
args: TemplateFunctionArg[],
|
||||
values: CallTemplateFunctionArgs['values'],
|
||||
values: CallTemplateFunctionArgs["values"],
|
||||
): string | null {
|
||||
for (const arg of args) {
|
||||
if ('inputs' in arg && arg.inputs) {
|
||||
if ("inputs" in arg && arg.inputs) {
|
||||
// Recurse down
|
||||
const err = validateTemplateFunctionArgs(fnName, arg.inputs, values);
|
||||
if (err) return err;
|
||||
}
|
||||
if (!('name' in arg)) continue;
|
||||
if (!("name" in arg)) continue;
|
||||
if (arg.optional) continue;
|
||||
if (arg.defaultValue != null) continue;
|
||||
if (arg.hidden) continue;
|
||||
@@ -34,14 +34,14 @@ export function applyFormInputDefaults(
|
||||
) {
|
||||
let newValues: { [p: string]: JsonPrimitive | undefined } = { ...values };
|
||||
for (const input of inputs) {
|
||||
if ('defaultValue' in input && values[input.name] === undefined) {
|
||||
if ("defaultValue" in input && values[input.name] === undefined) {
|
||||
newValues[input.name] = input.defaultValue;
|
||||
}
|
||||
if (input.type === 'checkbox' && values[input.name] === undefined) {
|
||||
if (input.type === "checkbox" && values[input.name] === undefined) {
|
||||
newValues[input.name] = false;
|
||||
}
|
||||
// Recurse down to all child inputs
|
||||
if ('inputs' in input) {
|
||||
if ("inputs" in input) {
|
||||
newValues = applyFormInputDefaults(input.inputs ?? [], newValues);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/model-store",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -19,9 +19,7 @@ function emptyStore<M extends ModelMap>(keys: (keyof M)[]): StoreData<M> {
|
||||
return data;
|
||||
}
|
||||
|
||||
export function createModelStore<M extends ModelMap>(
|
||||
modelTypes: (keyof M & string)[],
|
||||
) {
|
||||
export function createModelStore<M extends ModelMap>(modelTypes: (keyof M & string)[]) {
|
||||
const dataAtom = atom<StoreData<M>>(emptyStore<M>(modelTypes));
|
||||
|
||||
/** Apply a single upsert or delete to the store. */
|
||||
|
||||
@@ -3,23 +3,23 @@
|
||||
"version": "0.8.0",
|
||||
"keywords": [
|
||||
"api-client",
|
||||
"insomnia-alternative",
|
||||
"bruno-alternative",
|
||||
"insomnia-alternative",
|
||||
"postman-alternative"
|
||||
],
|
||||
"homepage": "https://yaak.app",
|
||||
"bugs": {
|
||||
"url": "https://feedback.yaak.app"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://feedback.yaak.app"
|
||||
},
|
||||
"homepage": "https://yaak.app",
|
||||
"main": "lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"scripts": {
|
||||
"bootstrap": "npm run build",
|
||||
"build": "run-s build:copy-types build:tsc",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export type * from './plugins';
|
||||
export type * from './themes';
|
||||
export type * from "./plugins";
|
||||
export type * from "./themes";
|
||||
|
||||
export * from './bindings/gen_models';
|
||||
export * from './bindings/gen_events';
|
||||
export * from "./bindings/gen_models";
|
||||
export * from "./bindings/gen_events";
|
||||
|
||||
// Some extras for utility
|
||||
|
||||
export type { PartialImportResources } from './plugins/ImporterPlugin';
|
||||
export type { PartialImportResources } from "./plugins/ImporterPlugin";
|
||||
|
||||
@@ -5,9 +5,9 @@ import type {
|
||||
FormInput,
|
||||
GetHttpAuthenticationSummaryResponse,
|
||||
HttpAuthenticationAction,
|
||||
} from '../bindings/gen_events';
|
||||
import type { MaybePromise } from '../helpers';
|
||||
import type { Context } from './Context';
|
||||
} from "../bindings/gen_events";
|
||||
import type { MaybePromise } from "../helpers";
|
||||
import type { Context } from "./Context";
|
||||
|
||||
type AddDynamicMethod<T> = {
|
||||
dynamic?: (
|
||||
@@ -16,16 +16,16 @@ type AddDynamicMethod<T> = {
|
||||
) => MaybePromise<Partial<T> | null | undefined>;
|
||||
};
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern
|
||||
// oxlint-disable-next-line no-explicit-any -- distributive conditional type pattern
|
||||
type AddDynamic<T> = T extends any
|
||||
? T extends { inputs?: FormInput[] }
|
||||
? Omit<T, 'inputs'> & {
|
||||
? Omit<T, "inputs"> & {
|
||||
inputs: Array<AddDynamic<FormInput>>;
|
||||
dynamic?: (
|
||||
ctx: Context,
|
||||
args: CallHttpAuthenticationActionArgs,
|
||||
) => MaybePromise<
|
||||
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
||||
Partial<Omit<T, "inputs"> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
||||
>;
|
||||
}
|
||||
: T & AddDynamicMethod<T>
|
||||
|
||||
@@ -26,10 +26,10 @@ import type {
|
||||
ShowToastRequest,
|
||||
TemplateRenderRequest,
|
||||
WorkspaceInfo,
|
||||
} from '../bindings/gen_events.ts';
|
||||
import type { Folder, HttpRequest } from '../bindings/gen_models.ts';
|
||||
import type { JsonValue } from '../bindings/serde_json/JsonValue';
|
||||
import type { MaybePromise } from '../helpers';
|
||||
} from "../bindings/gen_events.ts";
|
||||
import type { Folder, HttpRequest } from "../bindings/gen_models.ts";
|
||||
import type { JsonValue } from "../bindings/serde_json/JsonValue";
|
||||
import type { MaybePromise } from "../helpers";
|
||||
|
||||
export type CallPromptFormDynamicArgs = {
|
||||
values: { [key in string]?: JsonPrimitive };
|
||||
@@ -42,16 +42,16 @@ type AddDynamicMethod<T> = {
|
||||
) => MaybePromise<Partial<T> | null | undefined>;
|
||||
};
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern
|
||||
// oxlint-disable-next-line no-explicit-any -- distributive conditional type pattern
|
||||
type AddDynamic<T> = T extends any
|
||||
? T extends { inputs?: FormInput[] }
|
||||
? Omit<T, 'inputs'> & {
|
||||
? Omit<T, "inputs"> & {
|
||||
inputs: Array<AddDynamic<FormInput>>;
|
||||
dynamic?: (
|
||||
ctx: Context,
|
||||
args: CallPromptFormDynamicArgs,
|
||||
) => MaybePromise<
|
||||
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
||||
Partial<Omit<T, "inputs"> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
||||
>;
|
||||
}
|
||||
: T & AddDynamicMethod<T>
|
||||
@@ -59,11 +59,11 @@ type AddDynamic<T> = T extends any
|
||||
|
||||
export type DynamicPromptFormArg = AddDynamic<FormInput>;
|
||||
|
||||
type DynamicPromptFormRequest = Omit<PromptFormRequest, 'inputs'> & {
|
||||
type DynamicPromptFormRequest = Omit<PromptFormRequest, "inputs"> & {
|
||||
inputs: DynamicPromptFormArg[];
|
||||
};
|
||||
|
||||
export type WorkspaceHandle = Pick<WorkspaceInfo, 'id' | 'name'>;
|
||||
export type WorkspaceHandle = Pick<WorkspaceInfo, "id" | "name">;
|
||||
|
||||
export interface Context {
|
||||
clipboard: {
|
||||
@@ -73,8 +73,8 @@ export interface Context {
|
||||
show(args: ShowToastRequest): Promise<void>;
|
||||
};
|
||||
prompt: {
|
||||
text(args: PromptTextRequest): Promise<PromptTextResponse['value']>;
|
||||
form(args: DynamicPromptFormRequest): Promise<PromptFormResponse['values']>;
|
||||
text(args: PromptTextRequest): Promise<PromptTextResponse["value"]>;
|
||||
form(args: DynamicPromptFormRequest): Promise<PromptFormResponse["values"]>;
|
||||
};
|
||||
store: {
|
||||
set<T>(key: string, value: T): Promise<void>;
|
||||
@@ -94,41 +94,41 @@ export interface Context {
|
||||
openExternalUrl(url: string): Promise<void>;
|
||||
};
|
||||
cookies: {
|
||||
listNames(): Promise<ListCookieNamesResponse['names']>;
|
||||
getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse['value']>;
|
||||
listNames(): Promise<ListCookieNamesResponse["names"]>;
|
||||
getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse["value"]>;
|
||||
};
|
||||
grpcRequest: {
|
||||
render(args: RenderGrpcRequestRequest): Promise<RenderGrpcRequestResponse['grpcRequest']>;
|
||||
render(args: RenderGrpcRequestRequest): Promise<RenderGrpcRequestResponse["grpcRequest"]>;
|
||||
};
|
||||
httpRequest: {
|
||||
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
|
||||
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
|
||||
render(args: RenderHttpRequestRequest): Promise<RenderHttpRequestResponse['httpRequest']>;
|
||||
list(args?: ListHttpRequestsRequest): Promise<ListHttpRequestsResponse['httpRequests']>;
|
||||
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse["httpResponse"]>;
|
||||
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse["httpRequest"]>;
|
||||
render(args: RenderHttpRequestRequest): Promise<RenderHttpRequestResponse["httpRequest"]>;
|
||||
list(args?: ListHttpRequestsRequest): Promise<ListHttpRequestsResponse["httpRequests"]>;
|
||||
create(
|
||||
args: Omit<Partial<HttpRequest>, 'id' | 'model' | 'createdAt' | 'updatedAt'> &
|
||||
Pick<HttpRequest, 'workspaceId' | 'url'>,
|
||||
args: Omit<Partial<HttpRequest>, "id" | "model" | "createdAt" | "updatedAt"> &
|
||||
Pick<HttpRequest, "workspaceId" | "url">,
|
||||
): Promise<HttpRequest>;
|
||||
update(
|
||||
args: Omit<Partial<HttpRequest>, 'model' | 'createdAt' | 'updatedAt'> &
|
||||
Pick<HttpRequest, 'id'>,
|
||||
args: Omit<Partial<HttpRequest>, "model" | "createdAt" | "updatedAt"> &
|
||||
Pick<HttpRequest, "id">,
|
||||
): Promise<HttpRequest>;
|
||||
delete(args: { id: string }): Promise<HttpRequest>;
|
||||
};
|
||||
folder: {
|
||||
list(args?: ListFoldersRequest): Promise<ListFoldersResponse['folders']>;
|
||||
list(args?: ListFoldersRequest): Promise<ListFoldersResponse["folders"]>;
|
||||
getById(args: { id: string }): Promise<Folder | null>;
|
||||
create(
|
||||
args: Omit<Partial<Folder>, 'id' | 'model' | 'createdAt' | 'updatedAt'> &
|
||||
Pick<Folder, 'workspaceId' | 'name'>,
|
||||
args: Omit<Partial<Folder>, "id" | "model" | "createdAt" | "updatedAt"> &
|
||||
Pick<Folder, "workspaceId" | "name">,
|
||||
): Promise<Folder>;
|
||||
update(
|
||||
args: Omit<Partial<Folder>, 'model' | 'createdAt' | 'updatedAt'> & Pick<Folder, 'id'>,
|
||||
args: Omit<Partial<Folder>, "model" | "createdAt" | "updatedAt"> & Pick<Folder, "id">,
|
||||
): Promise<Folder>;
|
||||
delete(args: { id: string }): Promise<Folder>;
|
||||
};
|
||||
httpResponse: {
|
||||
find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse['httpResponses']>;
|
||||
find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse["httpResponses"]>;
|
||||
};
|
||||
templates: {
|
||||
render<T extends JsonValue>(args: TemplateRenderRequest & { data: T }): Promise<T>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FilterResponse } from '../bindings/gen_events';
|
||||
import type { Context } from './Context';
|
||||
import type { FilterResponse } from "../bindings/gen_events";
|
||||
import type { Context } from "./Context";
|
||||
|
||||
export type FilterPlugin = {
|
||||
name: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CallFolderActionArgs, FolderAction } from '../bindings/gen_events';
|
||||
import type { Context } from './Context';
|
||||
import type { CallFolderActionArgs, FolderAction } from "../bindings/gen_events";
|
||||
import type { Context } from "./Context";
|
||||
|
||||
export type FolderActionPlugin = FolderAction & {
|
||||
onSelect(ctx: Context, args: CallFolderActionArgs): Promise<void> | void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CallGrpcRequestActionArgs, GrpcRequestAction } from '../bindings/gen_events';
|
||||
import type { Context } from './Context';
|
||||
import type { CallGrpcRequestActionArgs, GrpcRequestAction } from "../bindings/gen_events";
|
||||
import type { Context } from "./Context";
|
||||
|
||||
export type GrpcRequestActionPlugin = GrpcRequestAction & {
|
||||
onSelect(ctx: Context, args: CallGrpcRequestActionArgs): Promise<void> | void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CallHttpRequestActionArgs, HttpRequestAction } from '../bindings/gen_events';
|
||||
import type { Context } from './Context';
|
||||
import type { CallHttpRequestActionArgs, HttpRequestAction } from "../bindings/gen_events";
|
||||
import type { Context } from "./Context";
|
||||
|
||||
export type HttpRequestActionPlugin = HttpRequestAction & {
|
||||
onSelect(ctx: Context, args: CallHttpRequestActionArgs): Promise<void> | void;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { ImportResources } from '../bindings/gen_events';
|
||||
import type { AtLeast, MaybePromise } from '../helpers';
|
||||
import type { Context } from './Context';
|
||||
import type { ImportResources } from "../bindings/gen_events";
|
||||
import type { AtLeast, MaybePromise } from "../helpers";
|
||||
import type { Context } from "./Context";
|
||||
|
||||
type RootFields = 'name' | 'id' | 'model';
|
||||
type CommonFields = RootFields | 'workspaceId';
|
||||
type RootFields = "name" | "id" | "model";
|
||||
type CommonFields = RootFields | "workspaceId";
|
||||
|
||||
export type PartialImportResources = {
|
||||
workspaces: Array<AtLeast<ImportResources['workspaces'][0], RootFields>>;
|
||||
environments: Array<AtLeast<ImportResources['environments'][0], CommonFields>>;
|
||||
folders: Array<AtLeast<ImportResources['folders'][0], CommonFields>>;
|
||||
httpRequests: Array<AtLeast<ImportResources['httpRequests'][0], CommonFields>>;
|
||||
grpcRequests: Array<AtLeast<ImportResources['grpcRequests'][0], CommonFields>>;
|
||||
websocketRequests: Array<AtLeast<ImportResources['websocketRequests'][0], CommonFields>>;
|
||||
workspaces: Array<AtLeast<ImportResources["workspaces"][0], RootFields>>;
|
||||
environments: Array<AtLeast<ImportResources["environments"][0], CommonFields>>;
|
||||
folders: Array<AtLeast<ImportResources["folders"][0], CommonFields>>;
|
||||
httpRequests: Array<AtLeast<ImportResources["httpRequests"][0], CommonFields>>;
|
||||
grpcRequests: Array<AtLeast<ImportResources["grpcRequests"][0], CommonFields>>;
|
||||
websocketRequests: Array<AtLeast<ImportResources["websocketRequests"][0], CommonFields>>;
|
||||
};
|
||||
|
||||
export type ImportPluginResponse = null | {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events';
|
||||
import type { MaybePromise } from '../helpers';
|
||||
import type { Context } from './Context';
|
||||
import type { CallTemplateFunctionArgs, FormInput, TemplateFunction } from "../bindings/gen_events";
|
||||
import type { MaybePromise } from "../helpers";
|
||||
import type { Context } from "./Context";
|
||||
|
||||
type AddDynamicMethod<T> = {
|
||||
dynamic?: (
|
||||
@@ -9,16 +9,16 @@ type AddDynamicMethod<T> = {
|
||||
) => MaybePromise<Partial<T> | null | undefined>;
|
||||
};
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern
|
||||
// oxlint-disable-next-line no-explicit-any -- distributive conditional type pattern
|
||||
type AddDynamic<T> = T extends any
|
||||
? T extends { inputs?: FormInput[] }
|
||||
? Omit<T, 'inputs'> & {
|
||||
? Omit<T, "inputs"> & {
|
||||
inputs: Array<AddDynamic<FormInput>>;
|
||||
dynamic?: (
|
||||
ctx: Context,
|
||||
args: CallTemplateFunctionArgs,
|
||||
) => MaybePromise<
|
||||
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
||||
Partial<Omit<T, "inputs"> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
||||
>;
|
||||
}
|
||||
: T & AddDynamicMethod<T>
|
||||
@@ -26,7 +26,7 @@ type AddDynamic<T> = T extends any
|
||||
|
||||
export type DynamicTemplateFunctionArg = AddDynamic<FormInput>;
|
||||
|
||||
export type TemplateFunctionPlugin = Omit<TemplateFunction, 'args'> & {
|
||||
export type TemplateFunctionPlugin = Omit<TemplateFunction, "args"> & {
|
||||
args: DynamicTemplateFunctionArg[];
|
||||
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import type { Theme } from '../bindings/gen_events';
|
||||
import type { Theme } from "../bindings/gen_events";
|
||||
|
||||
export type ThemePlugin = Theme;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type {
|
||||
CallWebsocketRequestActionArgs,
|
||||
WebsocketRequestAction,
|
||||
} from '../bindings/gen_events';
|
||||
import type { Context } from './Context';
|
||||
} from "../bindings/gen_events";
|
||||
import type { Context } from "./Context";
|
||||
|
||||
export type WebsocketRequestActionPlugin = WebsocketRequestAction & {
|
||||
onSelect(ctx: Context, args: CallWebsocketRequestActionArgs): Promise<void> | void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CallWorkspaceActionArgs, WorkspaceAction } from '../bindings/gen_events';
|
||||
import type { Context } from './Context';
|
||||
import type { CallWorkspaceActionArgs, WorkspaceAction } from "../bindings/gen_events";
|
||||
import type { Context } from "./Context";
|
||||
|
||||
export type WorkspaceActionPlugin = WorkspaceAction & {
|
||||
onSelect(ctx: Context, args: CallWorkspaceActionArgs): Promise<void> | void;
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { AuthenticationPlugin } from './AuthenticationPlugin';
|
||||
import type { AuthenticationPlugin } from "./AuthenticationPlugin";
|
||||
|
||||
import type { Context } from './Context';
|
||||
import type { FilterPlugin } from './FilterPlugin';
|
||||
import type { FolderActionPlugin } from './FolderActionPlugin';
|
||||
import type { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
|
||||
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
|
||||
import type { ImporterPlugin } from './ImporterPlugin';
|
||||
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
|
||||
import type { ThemePlugin } from './ThemePlugin';
|
||||
import type { WebsocketRequestActionPlugin } from './WebsocketRequestActionPlugin';
|
||||
import type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
|
||||
import type { Context } from "./Context";
|
||||
import type { FilterPlugin } from "./FilterPlugin";
|
||||
import type { FolderActionPlugin } from "./FolderActionPlugin";
|
||||
import type { GrpcRequestActionPlugin } from "./GrpcRequestActionPlugin";
|
||||
import type { HttpRequestActionPlugin } from "./HttpRequestActionPlugin";
|
||||
import type { ImporterPlugin } from "./ImporterPlugin";
|
||||
import type { TemplateFunctionPlugin } from "./TemplateFunctionPlugin";
|
||||
import type { ThemePlugin } from "./ThemePlugin";
|
||||
import type { WebsocketRequestActionPlugin } from "./WebsocketRequestActionPlugin";
|
||||
import type { WorkspaceActionPlugin } from "./WorkspaceActionPlugin";
|
||||
|
||||
export type { Context };
|
||||
export type { DynamicAuthenticationArg } from './AuthenticationPlugin';
|
||||
export type { CallPromptFormDynamicArgs, DynamicPromptFormArg } from './Context';
|
||||
export type { DynamicTemplateFunctionArg } from './TemplateFunctionPlugin';
|
||||
export type { DynamicAuthenticationArg } from "./AuthenticationPlugin";
|
||||
export type { CallPromptFormDynamicArgs, DynamicPromptFormArg } from "./Context";
|
||||
export type { DynamicTemplateFunctionArg } from "./TemplateFunctionPlugin";
|
||||
export type { TemplateFunctionPlugin };
|
||||
export type { FolderActionPlugin } from './FolderActionPlugin';
|
||||
export type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
|
||||
export type { FolderActionPlugin } from "./FolderActionPlugin";
|
||||
export type { WorkspaceActionPlugin } from "./WorkspaceActionPlugin";
|
||||
|
||||
/**
|
||||
* The global structure of a Yaak plugin
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { InternalEvent } from '@yaakapp/api';
|
||||
import type { InternalEvent } from "@yaakapp/api";
|
||||
|
||||
export class EventChannel {
|
||||
#listeners = new Set<(event: InternalEvent) => void>();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BootRequest, InternalEvent } from '@yaakapp/api';
|
||||
import type { PluginContext } from '@yaakapp-internal/plugins';
|
||||
import type { EventChannel } from './EventChannel';
|
||||
import { PluginInstance, type PluginWorkerData } from './PluginInstance';
|
||||
import type { BootRequest, InternalEvent } from "@yaakapp/api";
|
||||
import type { PluginContext } from "@yaakapp-internal/plugins";
|
||||
import type { EventChannel } from "./EventChannel";
|
||||
import { PluginInstance, type PluginWorkerData } from "./PluginInstance";
|
||||
|
||||
export class PluginHandle {
|
||||
#instance: PluginInstance;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import console from 'node:console';
|
||||
import { type Stats, statSync, watch } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import console from "node:console";
|
||||
import { type Stats, statSync, watch } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type {
|
||||
CallPromptFormDynamicArgs,
|
||||
Context,
|
||||
DynamicPromptFormArg,
|
||||
PluginDefinition,
|
||||
} from '@yaakapp/api';
|
||||
} from "@yaakapp/api";
|
||||
import {
|
||||
applyFormInputDefaults,
|
||||
validateTemplateFunctionArgs,
|
||||
} from '@yaakapp-internal/lib/templateFunction';
|
||||
} from "@yaakapp-internal/lib/templateFunction";
|
||||
import type {
|
||||
BootRequest,
|
||||
DeleteKeyValueResponse,
|
||||
@@ -45,10 +45,10 @@ import type {
|
||||
TemplateRenderResponse,
|
||||
UpsertModelResponse,
|
||||
WindowInfoResponse,
|
||||
} from '@yaakapp-internal/plugins';
|
||||
import { applyDynamicFormInput } from './common';
|
||||
import { EventChannel } from './EventChannel';
|
||||
import { migrateTemplateFunctionSelectOptions } from './migrations';
|
||||
} from "@yaakapp-internal/plugins";
|
||||
import { applyDynamicFormInput } from "./common";
|
||||
import { EventChannel } from "./EventChannel";
|
||||
import { migrateTemplateFunctionSelectOptions } from "./migrations";
|
||||
|
||||
export interface PluginWorkerData {
|
||||
bootRequest: BootRequest;
|
||||
@@ -84,16 +84,16 @@ export class PluginInstance {
|
||||
this.#sendPayload(
|
||||
workerData.context,
|
||||
{
|
||||
type: 'reload_response',
|
||||
type: "reload_response",
|
||||
silent: false,
|
||||
},
|
||||
null,
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
await ctx.toast.show({
|
||||
message: `Failed to initialize plugin ${this.#workerData.bootRequest.dir.split('/').pop()}: ${err}`,
|
||||
color: 'notice',
|
||||
icon: 'alert_triangle',
|
||||
message: `Failed to initialize plugin ${this.#workerData.bootRequest.dir.split("/").pop()}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
color: "notice",
|
||||
icon: "alert_triangle",
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
@@ -123,15 +123,15 @@ export class PluginInstance {
|
||||
const { context, payload, id: replyId } = event;
|
||||
|
||||
try {
|
||||
if (payload.type === 'boot_request') {
|
||||
if (payload.type === "boot_request") {
|
||||
await this.#mod?.init?.(ctx);
|
||||
this.#sendPayload(context, { type: 'boot_response' }, replyId);
|
||||
this.#sendPayload(context, { type: "boot_response" }, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'terminate_request') {
|
||||
if (payload.type === "terminate_request") {
|
||||
const payload: InternalEventPayload = {
|
||||
type: 'terminate_response',
|
||||
type: "terminate_response",
|
||||
};
|
||||
await this.terminate();
|
||||
this.#sendPayload(context, payload, replyId);
|
||||
@@ -139,15 +139,15 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'import_request' &&
|
||||
typeof this.#mod?.importer?.onImport === 'function'
|
||||
payload.type === "import_request" &&
|
||||
typeof this.#mod?.importer?.onImport === "function"
|
||||
) {
|
||||
const reply = await this.#mod.importer.onImport(ctx, {
|
||||
text: payload.content,
|
||||
});
|
||||
if (reply != null) {
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'import_response',
|
||||
type: "import_response",
|
||||
resources: reply.resources as ImportResources,
|
||||
};
|
||||
this.#sendPayload(context, replyPayload, replyId);
|
||||
@@ -157,18 +157,18 @@ export class PluginInstance {
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'filter_request' && typeof this.#mod?.filter?.onFilter === 'function') {
|
||||
if (payload.type === "filter_request" && typeof this.#mod?.filter?.onFilter === "function") {
|
||||
const reply = await this.#mod.filter.onFilter(ctx, {
|
||||
filter: payload.filter,
|
||||
payload: payload.content,
|
||||
mimeType: payload.type,
|
||||
});
|
||||
this.#sendPayload(context, { type: 'filter_response', ...reply }, replyId);
|
||||
this.#sendPayload(context, { type: "filter_response", ...reply }, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_grpc_request_actions_request' &&
|
||||
payload.type === "get_grpc_request_actions_request" &&
|
||||
Array.isArray(this.#mod?.grpcRequestActions)
|
||||
) {
|
||||
const reply: GrpcRequestAction[] = this.#mod.grpcRequestActions.map((a) => ({
|
||||
@@ -177,7 +177,7 @@ export class PluginInstance {
|
||||
onSelect: undefined,
|
||||
}));
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_grpc_request_actions_response',
|
||||
type: "get_grpc_request_actions_response",
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
actions: reply,
|
||||
};
|
||||
@@ -186,7 +186,7 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_http_request_actions_request' &&
|
||||
payload.type === "get_http_request_actions_request" &&
|
||||
Array.isArray(this.#mod?.httpRequestActions)
|
||||
) {
|
||||
const reply: HttpRequestAction[] = this.#mod.httpRequestActions.map((a) => ({
|
||||
@@ -195,7 +195,7 @@ export class PluginInstance {
|
||||
onSelect: undefined,
|
||||
}));
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_http_request_actions_response',
|
||||
type: "get_http_request_actions_response",
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
actions: reply,
|
||||
};
|
||||
@@ -204,7 +204,7 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_websocket_request_actions_request' &&
|
||||
payload.type === "get_websocket_request_actions_request" &&
|
||||
Array.isArray(this.#mod?.websocketRequestActions)
|
||||
) {
|
||||
const reply = this.#mod.websocketRequestActions.map((a) => ({
|
||||
@@ -212,7 +212,7 @@ export class PluginInstance {
|
||||
onSelect: undefined,
|
||||
}));
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_websocket_request_actions_response',
|
||||
type: "get_websocket_request_actions_response",
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
actions: reply,
|
||||
};
|
||||
@@ -221,7 +221,7 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_workspace_actions_request' &&
|
||||
payload.type === "get_workspace_actions_request" &&
|
||||
Array.isArray(this.#mod?.workspaceActions)
|
||||
) {
|
||||
const reply = this.#mod.workspaceActions.map((a) => ({
|
||||
@@ -229,7 +229,7 @@ export class PluginInstance {
|
||||
onSelect: undefined,
|
||||
}));
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_workspace_actions_response',
|
||||
type: "get_workspace_actions_response",
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
actions: reply,
|
||||
};
|
||||
@@ -238,7 +238,7 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_folder_actions_request' &&
|
||||
payload.type === "get_folder_actions_request" &&
|
||||
Array.isArray(this.#mod?.folderActions)
|
||||
) {
|
||||
const reply = this.#mod.folderActions.map((a) => ({
|
||||
@@ -246,7 +246,7 @@ export class PluginInstance {
|
||||
onSelect: undefined,
|
||||
}));
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_folder_actions_response',
|
||||
type: "get_folder_actions_response",
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
actions: reply,
|
||||
};
|
||||
@@ -254,9 +254,9 @@ export class PluginInstance {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'get_themes_request' && Array.isArray(this.#mod?.themes)) {
|
||||
if (payload.type === "get_themes_request" && Array.isArray(this.#mod?.themes)) {
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_themes_response',
|
||||
type: "get_themes_response",
|
||||
themes: this.#mod.themes,
|
||||
};
|
||||
this.#sendPayload(context, replyPayload, replyId);
|
||||
@@ -264,7 +264,7 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_template_function_summary_request' &&
|
||||
payload.type === "get_template_function_summary_request" &&
|
||||
Array.isArray(this.#mod?.templateFunctions)
|
||||
) {
|
||||
const functions: TemplateFunction[] = this.#mod.templateFunctions.map(
|
||||
@@ -277,7 +277,7 @@ export class PluginInstance {
|
||||
},
|
||||
);
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_template_function_summary_response',
|
||||
type: "get_template_function_summary_response",
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
functions,
|
||||
};
|
||||
@@ -286,7 +286,7 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_template_function_config_request' &&
|
||||
payload.type === "get_template_function_config_request" &&
|
||||
Array.isArray(this.#mod?.templateFunctions)
|
||||
) {
|
||||
const templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name);
|
||||
@@ -301,11 +301,11 @@ export class PluginInstance {
|
||||
};
|
||||
|
||||
payload.values = applyFormInputDefaults(fn.args, payload.values);
|
||||
const p = { ...payload, purpose: 'preview' } as const;
|
||||
const p = { ...payload, purpose: "preview" } as const;
|
||||
const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, p);
|
||||
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_template_function_config_response',
|
||||
type: "get_template_function_config_response",
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
function: { ...fn, args: stripDynamicCallbacks(resolvedArgs) },
|
||||
};
|
||||
@@ -313,9 +313,9 @@ export class PluginInstance {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'get_http_authentication_summary_request' && this.#mod?.authentication) {
|
||||
if (payload.type === "get_http_authentication_summary_request" && this.#mod?.authentication) {
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_http_authentication_summary_response',
|
||||
type: "get_http_authentication_summary_response",
|
||||
...this.#mod.authentication,
|
||||
};
|
||||
|
||||
@@ -323,17 +323,18 @@ export class PluginInstance {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
|
||||
if (payload.type === "get_http_authentication_config_request" && this.#mod?.authentication) {
|
||||
const { args, actions } = this.#mod.authentication;
|
||||
payload.values = applyFormInputDefaults(args, payload.values);
|
||||
const resolvedArgs = await applyDynamicFormInput(ctx, args, payload);
|
||||
const resolvedActions: HttpAuthenticationAction[] = [];
|
||||
for (const { onSelect, ...action } of actions ?? []) {
|
||||
// oxlint-disable-next-line unbound-method
|
||||
for (const { onSelect: _onSelect, ...action } of actions ?? []) {
|
||||
resolvedActions.push(action);
|
||||
}
|
||||
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_http_authentication_config_response',
|
||||
type: "get_http_authentication_config_response",
|
||||
args: stripDynamicCallbacks(resolvedArgs),
|
||||
actions: resolvedActions,
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
@@ -343,15 +344,15 @@ export class PluginInstance {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
|
||||
if (payload.type === "call_http_authentication_request" && this.#mod?.authentication) {
|
||||
const auth = this.#mod.authentication;
|
||||
if (typeof auth?.onApply === 'function') {
|
||||
if (typeof auth?.onApply === "function") {
|
||||
const resolvedArgs = await applyDynamicFormInput(ctx, auth.args, payload);
|
||||
payload.values = applyFormInputDefaults(resolvedArgs, payload.values);
|
||||
this.#sendPayload(
|
||||
context,
|
||||
{
|
||||
type: 'call_http_authentication_response',
|
||||
type: "call_http_authentication_response",
|
||||
...(await auth.onApply(ctx, payload)),
|
||||
},
|
||||
replyId,
|
||||
@@ -361,11 +362,11 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_http_authentication_action_request' &&
|
||||
payload.type === "call_http_authentication_action_request" &&
|
||||
this.#mod.authentication != null
|
||||
) {
|
||||
const action = this.#mod.authentication.actions?.[payload.index];
|
||||
if (typeof action?.onSelect === 'function') {
|
||||
if (typeof action?.onSelect === "function") {
|
||||
await action.onSelect(ctx, payload.args);
|
||||
this.#sendEmpty(context, replyId);
|
||||
return;
|
||||
@@ -373,11 +374,11 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_http_request_action_request' &&
|
||||
payload.type === "call_http_request_action_request" &&
|
||||
Array.isArray(this.#mod.httpRequestActions)
|
||||
) {
|
||||
const action = this.#mod.httpRequestActions[payload.index];
|
||||
if (typeof action?.onSelect === 'function') {
|
||||
if (typeof action?.onSelect === "function") {
|
||||
await action.onSelect(ctx, payload.args);
|
||||
this.#sendEmpty(context, replyId);
|
||||
return;
|
||||
@@ -385,11 +386,11 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_websocket_request_action_request' &&
|
||||
payload.type === "call_websocket_request_action_request" &&
|
||||
Array.isArray(this.#mod.websocketRequestActions)
|
||||
) {
|
||||
const action = this.#mod.websocketRequestActions[payload.index];
|
||||
if (typeof action?.onSelect === 'function') {
|
||||
if (typeof action?.onSelect === "function") {
|
||||
await action.onSelect(ctx, payload.args);
|
||||
this.#sendEmpty(context, replyId);
|
||||
return;
|
||||
@@ -397,20 +398,20 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_workspace_action_request' &&
|
||||
payload.type === "call_workspace_action_request" &&
|
||||
Array.isArray(this.#mod.workspaceActions)
|
||||
) {
|
||||
const action = this.#mod.workspaceActions[payload.index];
|
||||
if (typeof action?.onSelect === 'function') {
|
||||
if (typeof action?.onSelect === "function") {
|
||||
await action.onSelect(ctx, payload.args);
|
||||
this.#sendEmpty(context, replyId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'call_folder_action_request' && Array.isArray(this.#mod.folderActions)) {
|
||||
if (payload.type === "call_folder_action_request" && Array.isArray(this.#mod.folderActions)) {
|
||||
const action = this.#mod.folderActions[payload.index];
|
||||
if (typeof action?.onSelect === 'function') {
|
||||
if (typeof action?.onSelect === "function") {
|
||||
await action.onSelect(ctx, payload.args);
|
||||
this.#sendEmpty(context, replyId);
|
||||
return;
|
||||
@@ -418,11 +419,11 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_grpc_request_action_request' &&
|
||||
payload.type === "call_grpc_request_action_request" &&
|
||||
Array.isArray(this.#mod.grpcRequestActions)
|
||||
) {
|
||||
const action = this.#mod.grpcRequestActions[payload.index];
|
||||
if (typeof action?.onSelect === 'function') {
|
||||
if (typeof action?.onSelect === "function") {
|
||||
await action.onSelect(ctx, payload.args);
|
||||
this.#sendEmpty(context, replyId);
|
||||
return;
|
||||
@@ -430,32 +431,32 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_template_function_request' &&
|
||||
payload.type === "call_template_function_request" &&
|
||||
Array.isArray(this.#mod?.templateFunctions)
|
||||
) {
|
||||
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
|
||||
if (
|
||||
payload.args.purpose === 'preview' &&
|
||||
(fn?.previewType === 'click' || fn?.previewType === 'none')
|
||||
payload.args.purpose === "preview" &&
|
||||
(fn?.previewType === "click" || fn?.previewType === "none")
|
||||
) {
|
||||
// Send empty render response
|
||||
this.#sendPayload(
|
||||
context,
|
||||
{
|
||||
type: 'call_template_function_response',
|
||||
type: "call_template_function_response",
|
||||
value: null,
|
||||
error: 'Live preview disabled for this function',
|
||||
error: "Live preview disabled for this function",
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
} else if (typeof fn?.onRender === 'function') {
|
||||
} else if (typeof fn?.onRender === "function") {
|
||||
const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, payload.args);
|
||||
const values = applyFormInputDefaults(resolvedArgs, payload.args.values);
|
||||
const error = validateTemplateFunctionArgs(fn.name, resolvedArgs, values);
|
||||
if (error && payload.args.purpose !== 'preview') {
|
||||
if (error && payload.args.purpose !== "preview") {
|
||||
this.#sendPayload(
|
||||
context,
|
||||
{ type: 'call_template_function_response', value: null, error },
|
||||
{ type: "call_template_function_response", value: null, error },
|
||||
replyId,
|
||||
);
|
||||
return;
|
||||
@@ -465,16 +466,19 @@ export class PluginInstance {
|
||||
const result = await fn.onRender(ctx, { ...payload.args, values });
|
||||
this.#sendPayload(
|
||||
context,
|
||||
{ type: 'call_template_function_response', value: result ?? null },
|
||||
{ type: "call_template_function_response", value: result ?? null },
|
||||
replyId,
|
||||
);
|
||||
} catch (err) {
|
||||
this.#sendPayload(
|
||||
context,
|
||||
{
|
||||
type: 'call_template_function_response',
|
||||
type: "call_template_function_response",
|
||||
value: null,
|
||||
error: `${err}`.replace(/^Error:\s*/g, ''),
|
||||
error: (err instanceof Error ? err.message : String(err)).replace(
|
||||
/^Error:\s*/g,
|
||||
"",
|
||||
),
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
@@ -483,9 +487,9 @@ export class PluginInstance {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const error = `${err}`.replace(/^Error:\s*/g, '');
|
||||
console.log('Plugin call threw exception', payload.type, '→', error);
|
||||
this.#sendPayload(context, { type: 'error_response', error }, replyId);
|
||||
const error = (err instanceof Error ? err.message : String(err)).replace(/^Error:\s*/g, "");
|
||||
console.log("Plugin call threw exception", payload.type, "→", error);
|
||||
this.#sendPayload(context, { type: "error_response", error }, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -494,11 +498,11 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
#pathMod() {
|
||||
return path.posix.join(this.#workerData.bootRequest.dir, 'build', 'index.js');
|
||||
return path.posix.join(this.#workerData.bootRequest.dir, "build", "index.js");
|
||||
}
|
||||
|
||||
#pathPkg() {
|
||||
return path.join(this.#workerData.bootRequest.dir, 'package.json');
|
||||
return path.join(this.#workerData.bootRequest.dir, "package.json");
|
||||
}
|
||||
|
||||
#unimportModule() {
|
||||
@@ -545,10 +549,10 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
#sendEmpty(context: PluginContext, replyId: string | null = null): string {
|
||||
return this.#sendPayload(context, { type: 'empty_response' }, replyId);
|
||||
return this.#sendPayload(context, { type: "empty_response" }, replyId);
|
||||
}
|
||||
|
||||
#sendForReply<T extends Omit<InternalEventPayload, 'type'>>(
|
||||
#sendForReply<T extends Omit<InternalEventPayload, "type">>(
|
||||
context: PluginContext,
|
||||
payload: InternalEventPayload,
|
||||
): Promise<T> {
|
||||
@@ -599,7 +603,7 @@ export class PluginInstance {
|
||||
throw new Error("Can't get window context without an active window");
|
||||
}
|
||||
const payload: InternalEventPayload = {
|
||||
type: 'window_info_request',
|
||||
type: "window_info_request",
|
||||
label: context.label,
|
||||
};
|
||||
|
||||
@@ -610,7 +614,7 @@ export class PluginInstance {
|
||||
clipboard: {
|
||||
copyText: async (text) => {
|
||||
await this.#sendForReply(context, {
|
||||
type: 'copy_text_request',
|
||||
type: "copy_text_request",
|
||||
text,
|
||||
});
|
||||
},
|
||||
@@ -618,7 +622,7 @@ export class PluginInstance {
|
||||
toast: {
|
||||
show: async (args) => {
|
||||
await this.#sendForReply(context, {
|
||||
type: 'show_toast_request',
|
||||
type: "show_toast_request",
|
||||
// Handle default here because null/undefined both convert to None in Rust translation
|
||||
timeout: args.timeout === undefined ? 5000 : args.timeout,
|
||||
...args,
|
||||
@@ -637,11 +641,11 @@ export class PluginInstance {
|
||||
},
|
||||
openUrl: async ({ onNavigate, onClose, ...args }) => {
|
||||
args.label = args.label || `${Math.random()}`;
|
||||
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
|
||||
const payload: InternalEventPayload = { type: "open_window_request", ...args };
|
||||
const onEvent = (event: InternalEventPayload) => {
|
||||
if (event.type === 'window_navigate_event') {
|
||||
if (event.type === "window_navigate_event") {
|
||||
onNavigate?.(event);
|
||||
} else if (event.type === 'window_close_event') {
|
||||
} else if (event.type === "window_close_event") {
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
@@ -649,7 +653,7 @@ export class PluginInstance {
|
||||
return {
|
||||
close: () => {
|
||||
const closePayload: InternalEventPayload = {
|
||||
type: 'close_window_request',
|
||||
type: "close_window_request",
|
||||
label: args.label,
|
||||
};
|
||||
this.#sendPayload(context, closePayload, null);
|
||||
@@ -658,7 +662,7 @@ export class PluginInstance {
|
||||
},
|
||||
openExternalUrl: async (url) => {
|
||||
await this.#sendForReply(context, {
|
||||
type: 'open_external_url_request',
|
||||
type: "open_external_url_request",
|
||||
url,
|
||||
});
|
||||
},
|
||||
@@ -666,7 +670,7 @@ export class PluginInstance {
|
||||
prompt: {
|
||||
text: async (args) => {
|
||||
const reply: PromptTextResponse = await this.#sendForReply(context, {
|
||||
type: 'prompt_text_request',
|
||||
type: "prompt_text_request",
|
||||
...args,
|
||||
});
|
||||
return reply.value;
|
||||
@@ -685,7 +689,7 @@ export class PluginInstance {
|
||||
// Build the event manually so we can get the event ID for keying
|
||||
const eventToSend = this.#buildEventToSend(
|
||||
context,
|
||||
{ type: 'prompt_form_request', ...args, inputs: strippedInputs },
|
||||
{ type: "prompt_form_request", ...args, inputs: strippedInputs },
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -696,7 +700,7 @@ export class PluginInstance {
|
||||
const cb = (event: InternalEvent) => {
|
||||
if (event.replyId !== eventToSend.id) return;
|
||||
|
||||
if (event.payload.type === 'prompt_form_response') {
|
||||
if (event.payload.type === "prompt_form_response") {
|
||||
const { done, values } = event.payload as PromptFormResponse;
|
||||
if (done) {
|
||||
// Final response — resolve the promise and clean up
|
||||
@@ -715,12 +719,12 @@ export class PluginInstance {
|
||||
const stripped = stripDynamicCallbacks(resolvedInputs);
|
||||
this.#sendPayload(
|
||||
context,
|
||||
{ type: 'prompt_form_request', ...args, inputs: stripped },
|
||||
{ type: "prompt_form_request", ...args, inputs: stripped },
|
||||
eventToSend.id,
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to resolve dynamic form inputs', err);
|
||||
console.error("Failed to resolve dynamic form inputs", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -738,7 +742,7 @@ export class PluginInstance {
|
||||
httpResponse: {
|
||||
find: async (args) => {
|
||||
const payload = {
|
||||
type: 'find_http_responses_request',
|
||||
type: "find_http_responses_request",
|
||||
...args,
|
||||
} as const;
|
||||
const { httpResponses } = await this.#sendForReply<FindHttpResponsesResponse>(
|
||||
@@ -751,7 +755,7 @@ export class PluginInstance {
|
||||
grpcRequest: {
|
||||
render: async (args) => {
|
||||
const payload = {
|
||||
type: 'render_grpc_request_request',
|
||||
type: "render_grpc_request_request",
|
||||
...args,
|
||||
} as const;
|
||||
const { grpcRequest } = await this.#sendForReply<RenderGrpcRequestResponse>(
|
||||
@@ -764,7 +768,7 @@ export class PluginInstance {
|
||||
httpRequest: {
|
||||
getById: async (args) => {
|
||||
const payload = {
|
||||
type: 'get_http_request_by_id_request',
|
||||
type: "get_http_request_by_id_request",
|
||||
...args,
|
||||
} as const;
|
||||
const { httpRequest } = await this.#sendForReply<GetHttpRequestByIdResponse>(
|
||||
@@ -775,7 +779,7 @@ export class PluginInstance {
|
||||
},
|
||||
send: async (args) => {
|
||||
const payload = {
|
||||
type: 'send_http_request_request',
|
||||
type: "send_http_request_request",
|
||||
...args,
|
||||
} as const;
|
||||
const { httpResponse } = await this.#sendForReply<SendHttpRequestResponse>(
|
||||
@@ -786,7 +790,7 @@ export class PluginInstance {
|
||||
},
|
||||
render: async (args) => {
|
||||
const payload = {
|
||||
type: 'render_http_request_request',
|
||||
type: "render_http_request_request",
|
||||
...args,
|
||||
} as const;
|
||||
const { httpRequest } = await this.#sendForReply<RenderHttpRequestResponse>(
|
||||
@@ -797,9 +801,9 @@ export class PluginInstance {
|
||||
},
|
||||
list: async (args?: { folderId?: string }) => {
|
||||
const payload: InternalEventPayload = {
|
||||
type: 'list_http_requests_request',
|
||||
type: "list_http_requests_request",
|
||||
folderId: args?.folderId,
|
||||
} satisfies ListHttpRequestsRequest & { type: 'list_http_requests_request' };
|
||||
} satisfies ListHttpRequestsRequest & { type: "list_http_requests_request" };
|
||||
const { httpRequests } = await this.#sendForReply<ListHttpRequestsResponse>(
|
||||
context,
|
||||
payload,
|
||||
@@ -808,13 +812,13 @@ export class PluginInstance {
|
||||
},
|
||||
create: async (args) => {
|
||||
const payload = {
|
||||
type: 'upsert_model_request',
|
||||
type: "upsert_model_request",
|
||||
model: {
|
||||
name: '',
|
||||
method: 'GET',
|
||||
name: "",
|
||||
method: "GET",
|
||||
...args,
|
||||
id: '',
|
||||
model: 'http_request',
|
||||
id: "",
|
||||
model: "http_request",
|
||||
},
|
||||
} as InternalEventPayload;
|
||||
const response = await this.#sendForReply<UpsertModelResponse>(context, payload);
|
||||
@@ -822,9 +826,9 @@ export class PluginInstance {
|
||||
},
|
||||
update: async (args) => {
|
||||
const payload = {
|
||||
type: 'upsert_model_request',
|
||||
type: "upsert_model_request",
|
||||
model: {
|
||||
model: 'http_request',
|
||||
model: "http_request",
|
||||
...args,
|
||||
},
|
||||
} as InternalEventPayload;
|
||||
@@ -833,8 +837,8 @@ export class PluginInstance {
|
||||
},
|
||||
delete: async (args) => {
|
||||
const payload = {
|
||||
type: 'delete_model_request',
|
||||
model: 'http_request',
|
||||
type: "delete_model_request",
|
||||
model: "http_request",
|
||||
id: args.id,
|
||||
} as InternalEventPayload;
|
||||
const response = await this.#sendForReply<DeleteModelResponse>(context, payload);
|
||||
@@ -843,23 +847,23 @@ export class PluginInstance {
|
||||
},
|
||||
folder: {
|
||||
list: async () => {
|
||||
const payload = { type: 'list_folders_request' } as const;
|
||||
const payload = { type: "list_folders_request" } as const;
|
||||
const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload);
|
||||
return folders;
|
||||
},
|
||||
getById: async (args: { id: string }) => {
|
||||
const payload = { type: 'list_folders_request' } as const;
|
||||
const payload = { type: "list_folders_request" } as const;
|
||||
const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload);
|
||||
return folders.find((f) => f.id === args.id) ?? null;
|
||||
},
|
||||
create: async ({ name, ...args }) => {
|
||||
const payload = {
|
||||
type: 'upsert_model_request',
|
||||
type: "upsert_model_request",
|
||||
model: {
|
||||
...args,
|
||||
name: name ?? '',
|
||||
id: '',
|
||||
model: 'folder',
|
||||
name: name ?? "",
|
||||
id: "",
|
||||
model: "folder",
|
||||
},
|
||||
} as InternalEventPayload;
|
||||
const response = await this.#sendForReply<UpsertModelResponse>(context, payload);
|
||||
@@ -867,9 +871,9 @@ export class PluginInstance {
|
||||
},
|
||||
update: async (args) => {
|
||||
const payload = {
|
||||
type: 'upsert_model_request',
|
||||
type: "upsert_model_request",
|
||||
model: {
|
||||
model: 'folder',
|
||||
model: "folder",
|
||||
...args,
|
||||
},
|
||||
} as InternalEventPayload;
|
||||
@@ -878,8 +882,8 @@ export class PluginInstance {
|
||||
},
|
||||
delete: async (args: { id: string }) => {
|
||||
const payload = {
|
||||
type: 'delete_model_request',
|
||||
model: 'folder',
|
||||
type: "delete_model_request",
|
||||
model: "folder",
|
||||
id: args.id,
|
||||
} as InternalEventPayload;
|
||||
const response = await this.#sendForReply<DeleteModelResponse>(context, payload);
|
||||
@@ -889,14 +893,14 @@ export class PluginInstance {
|
||||
cookies: {
|
||||
getValue: async (args: GetCookieValueRequest) => {
|
||||
const payload = {
|
||||
type: 'get_cookie_value_request',
|
||||
type: "get_cookie_value_request",
|
||||
...args,
|
||||
} as const;
|
||||
const { value } = await this.#sendForReply<GetCookieValueResponse>(context, payload);
|
||||
return value;
|
||||
},
|
||||
listNames: async () => {
|
||||
const payload = { type: 'list_cookie_names_request' } as const;
|
||||
const payload = { type: "list_cookie_names_request" } as const;
|
||||
const { names } = await this.#sendForReply<ListCookieNamesResponse>(context, payload);
|
||||
return names;
|
||||
},
|
||||
@@ -907,42 +911,42 @@ export class PluginInstance {
|
||||
* (eg. object), it will be recursively rendered.
|
||||
*/
|
||||
render: async (args: TemplateRenderRequest) => {
|
||||
const payload = { type: 'template_render_request', ...args } as const;
|
||||
const payload = { type: "template_render_request", ...args } as const;
|
||||
const result = await this.#sendForReply<TemplateRenderResponse>(context, payload);
|
||||
// biome-ignore lint/suspicious/noExplicitAny: That's okay
|
||||
// oxlint-disable-next-line no-explicit-any -- That's okay
|
||||
return result.data as any;
|
||||
},
|
||||
},
|
||||
store: {
|
||||
get: async <T>(key: string) => {
|
||||
const payload = { type: 'get_key_value_request', key } as const;
|
||||
const payload = { type: "get_key_value_request", key } as const;
|
||||
const result = await this.#sendForReply<GetKeyValueResponse>(context, payload);
|
||||
return result.value ? (JSON.parse(result.value) as T) : undefined;
|
||||
},
|
||||
set: async <T>(key: string, value: T) => {
|
||||
const valueStr = JSON.stringify(value);
|
||||
const payload: InternalEventPayload = {
|
||||
type: 'set_key_value_request',
|
||||
type: "set_key_value_request",
|
||||
key,
|
||||
value: valueStr,
|
||||
};
|
||||
await this.#sendForReply<GetKeyValueResponse>(context, payload);
|
||||
},
|
||||
delete: async (key: string) => {
|
||||
const payload = { type: 'delete_key_value_request', key } as const;
|
||||
const payload = { type: "delete_key_value_request", key } as const;
|
||||
const result = await this.#sendForReply<DeleteKeyValueResponse>(context, payload);
|
||||
return result.deleted;
|
||||
},
|
||||
},
|
||||
plugin: {
|
||||
reload: () => {
|
||||
this.#sendPayload(context, { type: 'reload_response', silent: true }, null);
|
||||
this.#sendPayload(context, { type: "reload_response", silent: true }, null);
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
list: async () => {
|
||||
const payload = {
|
||||
type: 'list_open_workspaces_request',
|
||||
type: "list_open_workspaces_request",
|
||||
} as InternalEventPayload;
|
||||
const response = await this.#sendForReply<ListOpenWorkspacesResponse>(context, payload);
|
||||
return response.workspaces.map((w) => {
|
||||
@@ -972,9 +976,9 @@ export class PluginInstance {
|
||||
|
||||
function stripDynamicCallbacks(inputs: { dynamic?: unknown }[]): FormInput[] {
|
||||
return inputs.map((input) => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: stripping dynamic from union type
|
||||
const { dynamic, ...rest } = input as any;
|
||||
if ('inputs' in rest && Array.isArray(rest.inputs)) {
|
||||
// oxlint-disable-next-line no-explicit-any -- stripping dynamic from union type
|
||||
const { dynamic: _dynamic, ...rest } = input as any;
|
||||
if ("inputs" in rest && Array.isArray(rest.inputs)) {
|
||||
rest.inputs = stripDynamicCallbacks(rest.inputs);
|
||||
}
|
||||
return rest as FormInput;
|
||||
@@ -982,8 +986,8 @@ function stripDynamicCallbacks(inputs: { dynamic?: unknown }[]): FormInput[] {
|
||||
}
|
||||
|
||||
function genId(len = 5): string {
|
||||
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let id = '';
|
||||
const alphabet = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let id = "";
|
||||
for (let i = 0; i < len; i++) {
|
||||
id += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
@@ -1003,7 +1007,7 @@ function watchFile(filepath: string, cb: () => void) {
|
||||
const stat = statSync(filepath, { throwIfNoEntry: false });
|
||||
if (stat == null || stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
|
||||
watchedFiles[filepath] = stat ?? null;
|
||||
console.log('[plugin-runtime] watchFile triggered', filepath);
|
||||
console.log("[plugin-runtime] watchFile triggered", filepath);
|
||||
cb();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,11 +4,11 @@ import type {
|
||||
DynamicAuthenticationArg,
|
||||
DynamicPromptFormArg,
|
||||
DynamicTemplateFunctionArg,
|
||||
} from '@yaakapp/api';
|
||||
} from "@yaakapp/api";
|
||||
import type {
|
||||
CallHttpAuthenticationActionArgs,
|
||||
CallTemplateFunctionArgs,
|
||||
} from '@yaakapp-internal/plugins';
|
||||
} from "@yaakapp-internal/plugins";
|
||||
|
||||
type AnyDynamicArg = DynamicTemplateFunctionArg | DynamicAuthenticationArg | DynamicPromptFormArg;
|
||||
type AnyCallArgs =
|
||||
@@ -42,7 +42,7 @@ export async function applyDynamicFormInput(
|
||||
const resolvedArgs: AnyDynamicArg[] = [];
|
||||
for (const { dynamic, ...arg } of args) {
|
||||
const dynamicResult =
|
||||
typeof dynamic === 'function'
|
||||
typeof dynamic === "function"
|
||||
? await dynamic(
|
||||
ctx,
|
||||
callArgs as CallTemplateFunctionArgs &
|
||||
@@ -56,7 +56,7 @@ export async function applyDynamicFormInput(
|
||||
...dynamicResult,
|
||||
} as AnyDynamicArg;
|
||||
|
||||
if ('inputs' in newArg && Array.isArray(newArg.inputs)) {
|
||||
if ("inputs" in newArg && Array.isArray(newArg.inputs)) {
|
||||
try {
|
||||
newArg.inputs = await applyDynamicFormInput(
|
||||
ctx,
|
||||
@@ -66,7 +66,7 @@ export async function applyDynamicFormInput(
|
||||
CallPromptFormDynamicArgs,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to apply dynamic form input', e);
|
||||
console.error("Failed to apply dynamic form input", e);
|
||||
}
|
||||
}
|
||||
resolvedArgs.push(newArg);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { InternalEvent } from '@yaakapp/api';
|
||||
import WebSocket from 'ws';
|
||||
import { EventChannel } from './EventChannel';
|
||||
import { PluginHandle } from './PluginHandle';
|
||||
import type { InternalEvent } from "@yaakapp/api";
|
||||
import WebSocket from "ws";
|
||||
import { EventChannel } from "./EventChannel";
|
||||
import { PluginHandle } from "./PluginHandle";
|
||||
|
||||
const port = process.env.PORT;
|
||||
if (!port) {
|
||||
throw new Error('Plugin runtime missing PORT');
|
||||
throw new Error("Plugin runtime missing PORT");
|
||||
}
|
||||
|
||||
const host = process.env.HOST;
|
||||
if (!host) {
|
||||
throw new Error('Plugin runtime missing HOST');
|
||||
throw new Error("Plugin runtime missing HOST");
|
||||
}
|
||||
|
||||
const pluginToAppEvents = new EventChannel();
|
||||
@@ -18,16 +18,16 @@ const plugins: Record<string, PluginHandle> = {};
|
||||
|
||||
const ws = new WebSocket(`ws://${host}:${port}`);
|
||||
|
||||
ws.on('message', async (e: Buffer) => {
|
||||
ws.on("message", async (e: Buffer) => {
|
||||
try {
|
||||
await handleIncoming(e.toString());
|
||||
} catch (err) {
|
||||
console.log('Failed to handle incoming plugin event', err);
|
||||
console.log("Failed to handle incoming plugin event", err);
|
||||
}
|
||||
});
|
||||
ws.on('open', () => console.log('Plugin runtime connected to websocket'));
|
||||
ws.on('error', (err: unknown) => console.error('Plugin runtime websocket error', err));
|
||||
ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code));
|
||||
ws.on("open", () => console.log("Plugin runtime connected to websocket"));
|
||||
ws.on("error", (err: unknown) => console.error("Plugin runtime websocket error", err));
|
||||
ws.on("close", (code: number) => console.log("Plugin runtime websocket closed", code));
|
||||
|
||||
// Listen for incoming events from plugins
|
||||
pluginToAppEvents.listen((e) => {
|
||||
@@ -38,7 +38,7 @@ pluginToAppEvents.listen((e) => {
|
||||
async function handleIncoming(msg: string) {
|
||||
const pluginEvent: InternalEvent = JSON.parse(msg);
|
||||
// Handle special event to bootstrap plugin
|
||||
if (pluginEvent.payload.type === 'boot_request') {
|
||||
if (pluginEvent.payload.type === "boot_request") {
|
||||
const plugin = new PluginHandle(
|
||||
pluginEvent.pluginRefId,
|
||||
pluginEvent.context,
|
||||
@@ -51,23 +51,23 @@ async function handleIncoming(msg: string) {
|
||||
// Once booted, forward all events to the plugin worker
|
||||
const plugin = plugins[pluginEvent.pluginRefId];
|
||||
if (!plugin) {
|
||||
console.warn('Failed to get plugin for event by', pluginEvent.pluginRefId);
|
||||
console.warn("Failed to get plugin for event by", pluginEvent.pluginRefId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pluginEvent.payload.type === 'terminate_request') {
|
||||
if (pluginEvent.payload.type === "terminate_request") {
|
||||
await plugin.terminate();
|
||||
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
|
||||
console.log("Terminated plugin worker", pluginEvent.pluginRefId);
|
||||
delete plugins[pluginEvent.pluginRefId];
|
||||
}
|
||||
|
||||
plugin.sendToWorker(pluginEvent);
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
console.error("Unhandled Rejection at:", promise, "reason:", reason);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught Exception:', error);
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error("Uncaught Exception:", error);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import process from 'node:process';
|
||||
/* oxlint-disable unbound-method */
|
||||
import process from "node:process";
|
||||
|
||||
export function interceptStdout(intercept: (text: string) => string) {
|
||||
const old_stdout_write = process.stdout.write;
|
||||
@@ -24,5 +25,5 @@ export function interceptStdout(intercept: (text: string) => string) {
|
||||
}
|
||||
|
||||
function interceptor(text: string, fn: (text: string) => string) {
|
||||
return fn(text).replace(/\n$/, '') + (fn(text) && /\n$/.test(text) ? '\n' : '');
|
||||
return fn(text).replace(/\n$/, "") + (fn(text) && text.endsWith("\n") ? "\n" : "");
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { TemplateFunctionPlugin } from '@yaakapp/api';
|
||||
import type { TemplateFunctionPlugin } from "@yaakapp/api";
|
||||
|
||||
export function migrateTemplateFunctionSelectOptions(
|
||||
f: TemplateFunctionPlugin,
|
||||
): TemplateFunctionPlugin {
|
||||
const migratedArgs = f.args.map((a) => {
|
||||
if (a.type === 'select') {
|
||||
if (a.type === "select") {
|
||||
// Migrate old options that had 'name' instead of 'label'
|
||||
type LegacyOption = { label?: string; value: string; name?: string };
|
||||
a.options = a.options.map((o) => {
|
||||
const legacy = o as LegacyOption;
|
||||
return {
|
||||
label: legacy.label ?? legacy.name ?? '',
|
||||
label: legacy.label ?? legacy.name ?? "",
|
||||
value: legacy.value,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,106 +1,106 @@
|
||||
import { applyFormInputDefaults } from '@yaakapp-internal/lib/templateFunction';
|
||||
import type { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins';
|
||||
import type { Context, DynamicTemplateFunctionArg } from '@yaakapp/api';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { applyDynamicFormInput } from '../src/common';
|
||||
import { applyFormInputDefaults } from "@yaakapp-internal/lib/templateFunction";
|
||||
import type { CallTemplateFunctionArgs } from "@yaakapp-internal/plugins";
|
||||
import type { Context, DynamicTemplateFunctionArg } from "@yaakapp/api";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { applyDynamicFormInput } from "../src/common";
|
||||
|
||||
describe('applyFormInputDefaults', () => {
|
||||
test('Works with top-level select', () => {
|
||||
describe("applyFormInputDefaults", () => {
|
||||
test("Works with top-level select", () => {
|
||||
const args: DynamicTemplateFunctionArg[] = [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'test',
|
||||
options: [{ label: 'Option 1', value: 'one' }],
|
||||
defaultValue: 'one',
|
||||
type: "select",
|
||||
name: "test",
|
||||
options: [{ label: "Option 1", value: "one" }],
|
||||
defaultValue: "one",
|
||||
},
|
||||
];
|
||||
expect(applyFormInputDefaults(args, {})).toEqual({
|
||||
test: 'one',
|
||||
test: "one",
|
||||
});
|
||||
});
|
||||
|
||||
test('Works with existing value', () => {
|
||||
test("Works with existing value", () => {
|
||||
const args: DynamicTemplateFunctionArg[] = [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'test',
|
||||
options: [{ label: 'Option 1', value: 'one' }],
|
||||
defaultValue: 'one',
|
||||
type: "select",
|
||||
name: "test",
|
||||
options: [{ label: "Option 1", value: "one" }],
|
||||
defaultValue: "one",
|
||||
},
|
||||
];
|
||||
expect(applyFormInputDefaults(args, { test: 'explicit' })).toEqual({
|
||||
test: 'explicit',
|
||||
expect(applyFormInputDefaults(args, { test: "explicit" })).toEqual({
|
||||
test: "explicit",
|
||||
});
|
||||
});
|
||||
|
||||
test('Works with recursive select', () => {
|
||||
test("Works with recursive select", () => {
|
||||
const args: DynamicTemplateFunctionArg[] = [
|
||||
{ type: 'text', name: 'dummy', defaultValue: 'top' },
|
||||
{ type: "text", name: "dummy", defaultValue: "top" },
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Test',
|
||||
type: "accordion",
|
||||
label: "Test",
|
||||
inputs: [
|
||||
{ type: 'text', name: 'name', defaultValue: 'hello' },
|
||||
{ type: "text", name: "name", defaultValue: "hello" },
|
||||
{
|
||||
type: 'select',
|
||||
name: 'test',
|
||||
options: [{ label: 'Option 1', value: 'one' }],
|
||||
defaultValue: 'one',
|
||||
type: "select",
|
||||
name: "test",
|
||||
options: [{ label: "Option 1", value: "one" }],
|
||||
defaultValue: "one",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
expect(applyFormInputDefaults(args, {})).toEqual({
|
||||
dummy: 'top',
|
||||
test: 'one',
|
||||
name: 'hello',
|
||||
dummy: "top",
|
||||
test: "one",
|
||||
name: "hello",
|
||||
});
|
||||
});
|
||||
|
||||
test('Works with dynamic options', () => {
|
||||
test("Works with dynamic options", () => {
|
||||
const args: DynamicTemplateFunctionArg[] = [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'test',
|
||||
defaultValue: 'one',
|
||||
type: "select",
|
||||
name: "test",
|
||||
defaultValue: "one",
|
||||
options: [],
|
||||
dynamic() {
|
||||
return { options: [{ label: 'Option 1', value: 'one' }] };
|
||||
return { options: [{ label: "Option 1", value: "one" }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
expect(applyFormInputDefaults(args, {})).toEqual({
|
||||
test: 'one',
|
||||
test: "one",
|
||||
});
|
||||
expect(applyFormInputDefaults(args, {})).toEqual({
|
||||
test: 'one',
|
||||
test: "one",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyDynamicFormInput', () => {
|
||||
test('Works with plain input', async () => {
|
||||
describe("applyDynamicFormInput", () => {
|
||||
test("Works with plain input", async () => {
|
||||
const ctx = {} as Context;
|
||||
const args: DynamicTemplateFunctionArg[] = [
|
||||
{ type: 'text', name: 'name' },
|
||||
{ type: 'checkbox', name: 'checked' },
|
||||
{ type: "text", name: "name" },
|
||||
{ type: "checkbox", name: "checked" },
|
||||
];
|
||||
const callArgs: CallTemplateFunctionArgs = {
|
||||
values: {},
|
||||
purpose: 'preview',
|
||||
purpose: "preview",
|
||||
};
|
||||
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
|
||||
{ type: 'text', name: 'name' },
|
||||
{ type: 'checkbox', name: 'checked' },
|
||||
{ type: "text", name: "name" },
|
||||
{ type: "checkbox", name: "checked" },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Works with dynamic input', async () => {
|
||||
test("Works with dynamic input", async () => {
|
||||
const ctx = {} as Context;
|
||||
const args: DynamicTemplateFunctionArg[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
type: "text",
|
||||
name: "name",
|
||||
async dynamic(_ctx, _args) {
|
||||
return { hidden: true };
|
||||
},
|
||||
@@ -108,28 +108,28 @@ describe('applyDynamicFormInput', () => {
|
||||
];
|
||||
const callArgs: CallTemplateFunctionArgs = {
|
||||
values: {},
|
||||
purpose: 'preview',
|
||||
purpose: "preview",
|
||||
};
|
||||
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
|
||||
{ type: 'text', name: 'name', hidden: true },
|
||||
{ type: "text", name: "name", hidden: true },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Works with recursive dynamic input', async () => {
|
||||
test("Works with recursive dynamic input", async () => {
|
||||
const ctx = {} as Context;
|
||||
const callArgs: CallTemplateFunctionArgs = {
|
||||
values: { hello: 'world' },
|
||||
purpose: 'preview',
|
||||
values: { hello: "world" },
|
||||
purpose: "preview",
|
||||
};
|
||||
const args: DynamicTemplateFunctionArg[] = [
|
||||
{
|
||||
type: 'banner',
|
||||
type: "banner",
|
||||
inputs: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
type: "text",
|
||||
name: "name",
|
||||
async dynamic(_ctx, args) {
|
||||
return { hidden: args.values.hello === 'world' };
|
||||
return { hidden: args.values.hello === "world" };
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -137,11 +137,11 @@ describe('applyDynamicFormInput', () => {
|
||||
];
|
||||
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
|
||||
{
|
||||
type: 'banner',
|
||||
type: "banner",
|
||||
inputs: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
type: "text",
|
||||
name: "name",
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -10,11 +10,7 @@
|
||||
"moduleResolution": "node16",
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "build",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["node_modules/*", "src/types/*"]
|
||||
}
|
||||
"outDir": "build"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -127,10 +127,7 @@ module.exports = {
|
||||
require("@tailwindcss/container-queries"),
|
||||
plugin(({ addVariant }) => {
|
||||
addVariant("hocus", ["&:hover", "&:focus-visible", "&.focus:focus"]);
|
||||
addVariant("focus-visible-or-class", [
|
||||
"&:focus-visible",
|
||||
"&.focus:focus",
|
||||
]);
|
||||
addVariant("focus-visible-or-class", ["&:focus-visible", "&.focus:focus"]);
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/tailwind-config",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "index.cjs",
|
||||
"types": "index.d.ts",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/theme",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -8,12 +8,7 @@ export {
|
||||
} from "./appearance";
|
||||
export { defaultDarkTheme, defaultLightTheme } from "./defaultThemes";
|
||||
export { YaakColor } from "./yaakColor";
|
||||
export type {
|
||||
DocumentPlatform,
|
||||
YaakColorKey,
|
||||
YaakColors,
|
||||
YaakTheme,
|
||||
} from "./window";
|
||||
export type { DocumentPlatform, YaakColorKey, YaakColors, YaakTheme } from "./window";
|
||||
export {
|
||||
addThemeStylesToDocument,
|
||||
applyThemeToDocument,
|
||||
|
||||
@@ -62,15 +62,10 @@ function themeVariables(
|
||||
const color = (value: string | undefined) => yc(theme, value);
|
||||
const vars: CSSVariables = {
|
||||
surface: cmp.surface,
|
||||
surfaceHighlight:
|
||||
cmp.surfaceHighlight ?? color(cmp.surface)?.lift(0.06).css(),
|
||||
surfaceActive:
|
||||
cmp.surfaceActive ??
|
||||
color(cmp.primary)?.lower(0.2).translucify(0.8).css(),
|
||||
backdrop:
|
||||
cmp.backdrop ?? color(cmp.surface)?.lower(0.2).translucify(0.2).css(),
|
||||
selection:
|
||||
cmp.selection ?? color(cmp.primary)?.lower(0.1).translucify(0.7).css(),
|
||||
surfaceHighlight: cmp.surfaceHighlight ?? color(cmp.surface)?.lift(0.06).css(),
|
||||
surfaceActive: cmp.surfaceActive ?? color(cmp.primary)?.lower(0.2).translucify(0.8).css(),
|
||||
backdrop: cmp.backdrop ?? color(cmp.surface)?.lower(0.2).translucify(0.2).css(),
|
||||
selection: cmp.selection ?? color(cmp.primary)?.lower(0.1).translucify(0.7).css(),
|
||||
border: cmp.border ?? color(cmp.surface)?.lift(0.11)?.css(),
|
||||
borderSubtle: cmp.borderSubtle ?? color(cmp.border)?.lower(0.06)?.css(),
|
||||
borderFocus: color(cmp.info)?.translucify(0.5)?.css(),
|
||||
@@ -100,9 +95,7 @@ function themeVariables(
|
||||
return vars;
|
||||
}
|
||||
|
||||
function templateTagColorVariables(
|
||||
color: YaakColor | null,
|
||||
): Partial<CSSVariables> {
|
||||
function templateTagColorVariables(color: YaakColor | null): Partial<CSSVariables> {
|
||||
if (color == null) return {};
|
||||
|
||||
return {
|
||||
@@ -150,7 +143,6 @@ function _inputCSS(color: YaakColor | null): Partial<CSSVariables> {
|
||||
return theme;
|
||||
}
|
||||
|
||||
|
||||
function buttonSolidColorVariables(
|
||||
color: YaakColor | null,
|
||||
isDefault = false,
|
||||
@@ -212,10 +204,7 @@ function variablesToCSS(
|
||||
|
||||
function componentCSS(theme: Theme, component: ComponentName): string | null {
|
||||
if (theme.components == null) return null;
|
||||
return variablesToCSS(
|
||||
`.x-theme-${component}`,
|
||||
themeVariables(theme, component),
|
||||
);
|
||||
return variablesToCSS(`.x-theme-${component}`, themeVariables(theme, component));
|
||||
}
|
||||
|
||||
function buttonCSS(
|
||||
@@ -227,14 +216,8 @@ function buttonCSS(
|
||||
if (color == null) return null;
|
||||
|
||||
return [
|
||||
variablesToCSS(
|
||||
`.x-theme-button--solid--${colorKey}`,
|
||||
buttonSolidColorVariables(color),
|
||||
),
|
||||
variablesToCSS(
|
||||
`.x-theme-button--border--${colorKey}`,
|
||||
buttonBorderColorVariables(color),
|
||||
),
|
||||
variablesToCSS(`.x-theme-button--solid--${colorKey}`, buttonSolidColorVariables(color)),
|
||||
variablesToCSS(`.x-theme-button--border--${colorKey}`, buttonBorderColorVariables(color)),
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
@@ -246,10 +229,7 @@ function bannerCSS(
|
||||
const color = yc(theme, colors?.[colorKey]);
|
||||
if (color == null) return null;
|
||||
|
||||
return variablesToCSS(
|
||||
`.x-theme-banner--${colorKey}`,
|
||||
bannerColorVariables(color),
|
||||
);
|
||||
return variablesToCSS(`.x-theme-banner--${colorKey}`, bannerColorVariables(color));
|
||||
}
|
||||
|
||||
function toastCSS(
|
||||
@@ -260,10 +240,7 @@ function toastCSS(
|
||||
const color = yc(theme, colors?.[colorKey]);
|
||||
if (color == null) return null;
|
||||
|
||||
return variablesToCSS(
|
||||
`.x-theme-toast--${colorKey}`,
|
||||
toastColorVariables(color),
|
||||
);
|
||||
return variablesToCSS(`.x-theme-toast--${colorKey}`, toastColorVariables(color));
|
||||
}
|
||||
|
||||
function templateTagCSS(
|
||||
@@ -274,16 +251,12 @@ function templateTagCSS(
|
||||
const color = yc(theme, colors?.[colorKey]);
|
||||
if (color == null) return null;
|
||||
|
||||
return variablesToCSS(
|
||||
`.x-theme-templateTag--${colorKey}`,
|
||||
templateTagColorVariables(color),
|
||||
);
|
||||
return variablesToCSS(`.x-theme-templateTag--${colorKey}`, templateTagColorVariables(color));
|
||||
}
|
||||
|
||||
export function getThemeCSS(theme: Theme): string {
|
||||
theme.components = theme.components ?? {};
|
||||
theme.components.toast =
|
||||
theme.components.toast ?? theme.components.menu ?? {};
|
||||
theme.components.toast = theme.components.toast ?? theme.components.menu ?? {};
|
||||
const { components, id, label } = theme;
|
||||
const colors = Object.keys(theme.base).reduce((prev, key) => {
|
||||
return { ...prev, [key]: theme.base[key as YaakColorKey] };
|
||||
@@ -294,9 +267,7 @@ export function getThemeCSS(theme: Theme): string {
|
||||
const baseCss = variablesToCSS(null, themeVariables(theme));
|
||||
themeCSS = [
|
||||
baseCss,
|
||||
...Object.keys(components).map((key) =>
|
||||
componentCSS(theme, key as ComponentName),
|
||||
),
|
||||
...Object.keys(components).map((key) => componentCSS(theme, key as ComponentName)),
|
||||
variablesToCSS(
|
||||
".x-theme-button--solid--default",
|
||||
buttonSolidColorVariables(yc(theme, theme.base.surface), true),
|
||||
@@ -306,44 +277,23 @@ export function getThemeCSS(theme: Theme): string {
|
||||
buttonBorderColorVariables(yc(theme, theme.base.surface), true),
|
||||
),
|
||||
...Object.keys(colors).map((key) =>
|
||||
buttonCSS(
|
||||
theme,
|
||||
key as YaakColorKey,
|
||||
theme.components?.button ?? colors,
|
||||
),
|
||||
buttonCSS(theme, key as YaakColorKey, theme.components?.button ?? colors),
|
||||
),
|
||||
...Object.keys(colors).map((key) =>
|
||||
bannerCSS(
|
||||
theme,
|
||||
key as YaakColorKey,
|
||||
theme.components?.banner ?? colors,
|
||||
),
|
||||
bannerCSS(theme, key as YaakColorKey, theme.components?.banner ?? colors),
|
||||
),
|
||||
...Object.keys(colors).map((key) =>
|
||||
toastCSS(
|
||||
theme,
|
||||
key as YaakColorKey,
|
||||
theme.components?.banner ?? colors,
|
||||
),
|
||||
toastCSS(theme, key as YaakColorKey, theme.components?.banner ?? colors),
|
||||
),
|
||||
...Object.keys(colors).map((key) =>
|
||||
templateTagCSS(
|
||||
theme,
|
||||
key as YaakColorKey,
|
||||
theme.components?.templateTag ?? colors,
|
||||
),
|
||||
templateTagCSS(theme, key as YaakColorKey, theme.components?.templateTag ?? colors),
|
||||
),
|
||||
].join("\n\n");
|
||||
} catch (err) {
|
||||
console.error("Failed to generate CSS", err);
|
||||
}
|
||||
|
||||
return [
|
||||
`/* ${label} */`,
|
||||
`[data-theme="${id}"] {`,
|
||||
indent(themeCSS),
|
||||
"}",
|
||||
].join("\n");
|
||||
return [`/* ${label} */`, `[data-theme="${id}"] {`, indent(themeCSS), "}"].join("\n");
|
||||
}
|
||||
|
||||
export function addThemeStylesToDocument(rawTheme: Theme | null) {
|
||||
@@ -382,17 +332,14 @@ export function platformFromUserAgent(userAgent: string): DocumentPlatform {
|
||||
const normalized = userAgent.toLowerCase();
|
||||
|
||||
if (normalized.includes("linux")) return "linux";
|
||||
if (normalized.includes("mac os") || normalized.includes("macintosh"))
|
||||
return "macos";
|
||||
if (normalized.includes("mac os") || normalized.includes("macintosh")) return "macos";
|
||||
if (normalized.includes("win")) return "windows";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export function setPlatformOnDocument(platform: string | null | undefined) {
|
||||
const normalized =
|
||||
platform === "linux" || platform === "macos" || platform === "windows"
|
||||
? platform
|
||||
: "unknown";
|
||||
platform === "linux" || platform === "macos" || platform === "windows" ? platform : "unknown";
|
||||
document.documentElement.setAttribute("data-platform", normalized);
|
||||
}
|
||||
|
||||
@@ -425,10 +372,7 @@ export function completeTheme(theme: Theme): Theme {
|
||||
|
||||
theme.base.surface ??= fallback.surface;
|
||||
theme.base.surfaceHighlight ??= color(theme.base.surface)?.lift(0.06)?.css();
|
||||
theme.base.surfaceActive ??= color(theme.base.primary)
|
||||
?.lower(0.2)
|
||||
.translucify(0.8)
|
||||
.css();
|
||||
theme.base.surfaceActive ??= color(theme.base.primary)?.lower(0.2).translucify(0.8).css();
|
||||
|
||||
theme.base.border ??= color(theme.base.surface)?.lift(0.12)?.css();
|
||||
theme.base.borderSubtle ??= color(theme.base.border)?.lower(0.08)?.css();
|
||||
|
||||
@@ -96,16 +96,12 @@ export class YaakColor {
|
||||
}
|
||||
|
||||
css(): string {
|
||||
const [r, g, b] = parseColor(
|
||||
`hsl(${this.hue},${this.saturation}%,${this.lightness}%)`,
|
||||
).rgb;
|
||||
const [r, g, b] = parseColor(`hsl(${this.hue},${this.saturation}%,${this.lightness}%)`).rgb;
|
||||
return rgbaToHex(r, g, b, this.alpha);
|
||||
}
|
||||
|
||||
hexNoAlpha(): string {
|
||||
const [r, g, b] = parseColor(
|
||||
`hsl(${this.hue},${this.saturation}%,${this.lightness}%)`,
|
||||
).rgb;
|
||||
const [r, g, b] = parseColor(`hsl(${this.hue},${this.saturation}%,${this.lightness}%)`).rgb;
|
||||
return rgbaToHexNoAlpha(r, g, b);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/ui",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface BannerProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger' | 'info';
|
||||
color?: "primary" | "secondary" | "success" | "notice" | "warning" | "danger" | "info";
|
||||
}
|
||||
|
||||
export function Banner({ children, className, color }: BannerProps) {
|
||||
@@ -13,12 +13,12 @@ export function Banner({ children, className, color }: BannerProps) {
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
color && 'bg-surface',
|
||||
color && "bg-surface",
|
||||
`x-theme-banner--${color}`,
|
||||
'border border-border border-dashed',
|
||||
'px-4 py-2 rounded-lg select-auto cursor-auto',
|
||||
'overflow-auto text-text',
|
||||
'mb-auto', // Don't stretch all the way down if the parent is in grid or flexbox
|
||||
"border border-border border-dashed",
|
||||
"px-4 py-2 rounded-lg select-auto cursor-auto",
|
||||
"overflow-auto text-text",
|
||||
"mb-auto", // Don't stretch all the way down if the parent is in grid or flexbox
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -8,10 +8,7 @@ import { LoadingIcon } from "./LoadingIcon";
|
||||
type ButtonVariant = "border" | "solid";
|
||||
type ButtonSize = "2xs" | "xs" | "sm" | "md" | "auto";
|
||||
|
||||
export type ButtonProps = Omit<
|
||||
HTMLAttributes<HTMLButtonElement>,
|
||||
"color" | "onChange"
|
||||
> & {
|
||||
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, "color" | "onChange"> & {
|
||||
innerClassName?: string;
|
||||
color?: Color | "custom" | "default";
|
||||
tone?: Color | "default";
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { CSSProperties } from "react";
|
||||
import { memo } from "react";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
orientation?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
export const DropMarker = memo(
|
||||
function DropMarker({ className, style, orientation = 'horizontal' }: Props) {
|
||||
function DropMarker({ className, style, orientation = "horizontal" }: Props) {
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
className,
|
||||
'absolute pointer-events-none z-50',
|
||||
orientation === 'horizontal' && 'w-full',
|
||||
orientation === 'vertical' && 'w-0 top-0 bottom-0',
|
||||
"absolute pointer-events-none z-50",
|
||||
orientation === "horizontal" && "w-full",
|
||||
orientation === "vertical" && "w-0 top-0 bottom-0",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute bg-primary rounded-full',
|
||||
orientation === 'horizontal' && 'left-2 right-2 -bottom-[0.1rem] h-[0.2rem]',
|
||||
orientation === 'vertical' && '-left-[0.1rem] top-0 bottom-0 w-[0.2rem]',
|
||||
"absolute bg-primary rounded-full",
|
||||
orientation === "horizontal" && "left-2 right-2 -bottom-[0.1rem] h-[0.2rem]",
|
||||
orientation === "vertical" && "-left-[0.1rem] top-0 bottom-0 w-[0.2rem]",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
@@ -11,10 +11,10 @@ export function FormattedError({ children, className }: Props) {
|
||||
<pre
|
||||
className={classNames(
|
||||
className,
|
||||
'cursor-text select-auto',
|
||||
'[&_*]:cursor-text [&_*]:select-auto',
|
||||
'font-mono text-sm w-full bg-surface-highlight p-3 rounded',
|
||||
'whitespace-pre-wrap border border-danger border-dashed overflow-x-auto',
|
||||
"cursor-text select-auto",
|
||||
"[&_*]:cursor-text [&_*]:select-auto",
|
||||
"font-mono text-sm w-full bg-surface-highlight p-3 rounded",
|
||||
"whitespace-pre-wrap border border-danger border-dashed overflow-x-auto",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useIsFullscreen } from '../hooks/useIsFullscreen';
|
||||
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from '../lib/constants';
|
||||
import { WindowControls } from './WindowControls';
|
||||
import classNames from "classnames";
|
||||
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useIsFullscreen } from "../hooks/useIsFullscreen";
|
||||
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from "../lib/constants";
|
||||
import { WindowControls } from "./WindowControls";
|
||||
|
||||
interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: ReactNode;
|
||||
size: 'md' | 'lg';
|
||||
size: "md" | "lg";
|
||||
ignoreControlsSpacing?: boolean;
|
||||
onlyXWindowControl?: boolean;
|
||||
hideControls?: boolean;
|
||||
@@ -35,12 +35,12 @@ export function HeaderSize({
|
||||
const s = { ...style };
|
||||
|
||||
// Set the height (use min-height because scaling font size may make it larger
|
||||
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
|
||||
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
|
||||
if (size === "md") s.minHeight = HEADER_SIZE_MD;
|
||||
if (size === "lg") s.minHeight = HEADER_SIZE_LG;
|
||||
|
||||
if (useNativeTitlebar) {
|
||||
// No style updates when using native titlebar
|
||||
} else if (osType === 'macos') {
|
||||
} else if (osType === "macos") {
|
||||
if (!isFullscreen) {
|
||||
// Add large padding for window controls
|
||||
s.paddingLeft = 76 / interfaceScale;
|
||||
@@ -67,17 +67,17 @@ export function HeaderSize({
|
||||
style={finalStyle}
|
||||
className={classNames(
|
||||
className,
|
||||
'pt-[1px]', // Make up for bottom border
|
||||
'select-none relative flex items-center',
|
||||
'w-full border-b border-border-subtle min-w-0',
|
||||
"pt-[1px]", // Make up for bottom border
|
||||
"select-none relative flex items-center",
|
||||
"w-full border-b border-border-subtle min-w-0",
|
||||
)}
|
||||
>
|
||||
{/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */}
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={classNames(
|
||||
'pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid',
|
||||
'px-1', // Give it some space on either end for focus outlines
|
||||
"pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid",
|
||||
"px-1", // Give it some space on either end for focus outlines
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLHeadingElement> {
|
||||
level?: 1 | 2 | 3;
|
||||
}
|
||||
|
||||
export function Heading({ className, level = 1, ...props }: Props) {
|
||||
const Component = level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3';
|
||||
const Component = level === 1 ? "h1" : level === 2 ? "h2" : "h3";
|
||||
return (
|
||||
<Component
|
||||
className={classNames(
|
||||
className,
|
||||
'font-semibold text-text',
|
||||
level === 1 && 'text-2xl',
|
||||
level === 2 && 'text-xl',
|
||||
level === 3 && 'text-lg',
|
||||
"font-semibold text-text",
|
||||
level === 1 && "text-2xl",
|
||||
level === 2 && "text-xl",
|
||||
level === 3 && "text-lg",
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import type { Color } from "@yaakapp-internal/plugins";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
AlarmClockIcon,
|
||||
AlertTriangleIcon,
|
||||
@@ -137,9 +137,9 @@ import {
|
||||
WifiIcon,
|
||||
WrenchIcon,
|
||||
XIcon,
|
||||
} from 'lucide-react';
|
||||
import type { CSSProperties, HTMLAttributes } from 'react';
|
||||
import { memo } from 'react';
|
||||
} from "lucide-react";
|
||||
import type { CSSProperties, HTMLAttributes } from "react";
|
||||
import { memo } from "react";
|
||||
|
||||
const icons = {
|
||||
alarm_clock: AlarmClockIcon,
|
||||
@@ -287,17 +287,17 @@ export interface IconProps {
|
||||
icon: keyof typeof icons;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
size?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl";
|
||||
spin?: boolean;
|
||||
title?: string;
|
||||
color?: Color | 'custom' | 'default';
|
||||
color?: Color | "custom" | "default";
|
||||
}
|
||||
|
||||
export const Icon = memo(function Icon({
|
||||
icon,
|
||||
color = 'default',
|
||||
color = "default",
|
||||
spin,
|
||||
size = 'md',
|
||||
size = "md",
|
||||
style,
|
||||
className,
|
||||
title,
|
||||
@@ -309,23 +309,23 @@ export const Icon = memo(function Icon({
|
||||
title={title}
|
||||
className={classNames(
|
||||
className,
|
||||
!spin && 'transform-gpu',
|
||||
spin && 'animate-spin',
|
||||
'flex-shrink-0',
|
||||
size === 'xl' && 'h-6 w-6',
|
||||
size === 'lg' && 'h-5 w-5',
|
||||
size === 'md' && 'h-4 w-4',
|
||||
size === 'sm' && 'h-3.5 w-3.5',
|
||||
size === 'xs' && 'h-3 w-3',
|
||||
size === '2xs' && 'h-2.5 w-2.5',
|
||||
color === 'default' && 'inherit',
|
||||
color === 'danger' && 'text-danger',
|
||||
color === 'warning' && 'text-warning',
|
||||
color === 'notice' && 'text-notice',
|
||||
color === 'info' && 'text-info',
|
||||
color === 'success' && 'text-success',
|
||||
color === 'primary' && 'text-primary',
|
||||
color === 'secondary' && 'text-secondary',
|
||||
!spin && "transform-gpu",
|
||||
spin && "animate-spin",
|
||||
"flex-shrink-0",
|
||||
size === "xl" && "h-6 w-6",
|
||||
size === "lg" && "h-5 w-5",
|
||||
size === "md" && "h-4 w-4",
|
||||
size === "sm" && "h-3.5 w-3.5",
|
||||
size === "xs" && "h-3 w-3",
|
||||
size === "2xs" && "h-2.5 w-2.5",
|
||||
color === "default" && "inherit",
|
||||
color === "danger" && "text-danger",
|
||||
color === "warning" && "text-warning",
|
||||
color === "notice" && "text-notice",
|
||||
color === "info" && "text-info",
|
||||
color === "success" && "text-success",
|
||||
color === "primary" && "text-primary",
|
||||
color === "secondary" && "text-secondary",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
|
||||
return (
|
||||
<code
|
||||
className={classNames(
|
||||
className,
|
||||
'font-mono text-shrink bg-surface-highlight border border-border-subtle flex-grow-0',
|
||||
'px-1.5 py-0.5 rounded text shadow-inner break-words',
|
||||
"font-mono text-shrink bg-surface-highlight border border-border-subtle flex-grow-0",
|
||||
"px-1.5 py-0.5 rounded text shadow-inner break-words",
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import classNames from 'classnames';
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
size?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LoadingIcon({ size = 'md', className }: Props) {
|
||||
export function LoadingIcon({ size = "md", className }: Props) {
|
||||
const classes = classNames(
|
||||
className,
|
||||
'text-inherit flex-shrink-0',
|
||||
size === 'xl' && 'h-6 w-6',
|
||||
size === 'lg' && 'h-5 w-5',
|
||||
size === 'md' && 'h-4 w-4',
|
||||
size === 'sm' && 'h-3.5 w-3.5',
|
||||
size === 'xs' && 'h-3 w-3',
|
||||
size === '2xs' && 'h-2.5 w-2.5',
|
||||
'animate-spin',
|
||||
"text-inherit flex-shrink-0",
|
||||
size === "xl" && "h-6 w-6",
|
||||
size === "lg" && "h-5 w-5",
|
||||
size === "md" && "h-4 w-4",
|
||||
size === "sm" && "h-3.5 w-3.5",
|
||||
size === "xs" && "h-3 w-3",
|
||||
size === "2xs" && "h-2.5 w-2.5",
|
||||
"animate-spin",
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
classes,
|
||||
'border-[currentColor] border-b-transparent rounded-full',
|
||||
size === 'xl' && 'border-[0.2rem]',
|
||||
size === 'lg' && 'border-[0.16rem]',
|
||||
size === 'md' && 'border-[0.13rem]',
|
||||
size === 'sm' && 'border-[0.1rem]',
|
||||
size === 'xs' && 'border-[0.08rem]',
|
||||
size === '2xs' && 'border-[0.06rem]',
|
||||
"border-[currentColor] border-b-transparent rounded-full",
|
||||
size === "xl" && "border-[0.2rem]",
|
||||
size === "lg" && "border-[0.16rem]",
|
||||
size === "md" && "border-[0.13rem]",
|
||||
size === "sm" && "border-[0.1rem]",
|
||||
size === "xs" && "border-[0.08rem]",
|
||||
size === "2xs" && "border-[0.06rem]",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import classNames from 'classnames';
|
||||
import { FocusTrap } from 'focus-trap-react';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { Portal } from './Portal';
|
||||
import classNames from "classnames";
|
||||
import { FocusTrap } from "focus-trap-react";
|
||||
import * as m from "motion/react-m";
|
||||
import type { ReactNode } from "react";
|
||||
import { useRef } from "react";
|
||||
import { Portal } from "./Portal";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
@@ -11,20 +11,20 @@ interface Props {
|
||||
open: boolean;
|
||||
onClose?: () => void;
|
||||
zIndex?: keyof typeof zIndexes;
|
||||
variant?: 'default' | 'transparent';
|
||||
variant?: "default" | "transparent";
|
||||
noBackdrop?: boolean;
|
||||
}
|
||||
|
||||
const zIndexes: Record<number, string> = {
|
||||
10: 'z-10',
|
||||
20: 'z-20',
|
||||
30: 'z-30',
|
||||
40: 'z-40',
|
||||
50: 'z-50',
|
||||
10: "z-10",
|
||||
20: "z-20",
|
||||
30: "z-30",
|
||||
40: "z-40",
|
||||
50: "z-50",
|
||||
};
|
||||
|
||||
export function Overlay({
|
||||
variant = 'default',
|
||||
variant = "default",
|
||||
zIndex = 30,
|
||||
open,
|
||||
onClose,
|
||||
@@ -63,7 +63,7 @@ export function Overlay({
|
||||
>
|
||||
<m.div
|
||||
ref={containerRef}
|
||||
className={classNames('fixed inset-0', zIndexes[zIndex])}
|
||||
className={classNames("fixed inset-0", zIndexes[zIndex])}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
@@ -71,14 +71,14 @@ export function Overlay({
|
||||
aria-hidden
|
||||
onClick={onClose}
|
||||
className={classNames(
|
||||
'absolute inset-0',
|
||||
variant === 'default' && 'bg-backdrop backdrop-blur-sm',
|
||||
"absolute inset-0",
|
||||
variant === "default" && "bg-backdrop backdrop-blur-sm",
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Show the draggable region at the top */}
|
||||
{/* TODO: Figure out tauri drag region and also make clickable still */}
|
||||
{variant === 'default' && (
|
||||
{variant === "default" && (
|
||||
<div data-tauri-drag-region className="absolute top-0 left-0 h-md right-0" />
|
||||
)}
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { usePortal } from '../hooks/usePortal';
|
||||
import type { ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { usePortal } from "../hooks/usePortal";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
const START_DISTANCE = 7;
|
||||
|
||||
@@ -18,8 +18,8 @@ interface Props {
|
||||
onResizeEnd?: () => void;
|
||||
onResizeMove?: (e: ResizeHandleEvent) => void;
|
||||
onReset?: () => void;
|
||||
side: 'left' | 'right' | 'top';
|
||||
justify: 'center' | 'end' | 'start';
|
||||
side: "left" | "right" | "top";
|
||||
justify: "center" | "end" | "start";
|
||||
}
|
||||
|
||||
export function ResizeHandle({
|
||||
@@ -32,7 +32,7 @@ export function ResizeHandle({
|
||||
onReset,
|
||||
side,
|
||||
}: Props) {
|
||||
const vertical = side === 'top';
|
||||
const vertical = side === "top";
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
const moveState = useRef<{
|
||||
move: (e: MouseEvent) => void;
|
||||
@@ -67,15 +67,15 @@ export function ResizeHandle({
|
||||
function up() {
|
||||
setIsResizing(false);
|
||||
moveState.current = null;
|
||||
document.documentElement.removeEventListener('mousemove', move);
|
||||
document.documentElement.removeEventListener('mouseup', up);
|
||||
document.documentElement.removeEventListener("mousemove", move);
|
||||
document.documentElement.removeEventListener("mouseup", up);
|
||||
onResizeEnd?.();
|
||||
}
|
||||
|
||||
moveState.current = { calledStart: false, xStart: e.clientX, yStart: e.clientY, move, up };
|
||||
|
||||
document.documentElement.addEventListener('mousemove', move);
|
||||
document.documentElement.addEventListener('mouseup', up);
|
||||
document.documentElement.addEventListener("mousemove", move);
|
||||
document.documentElement.addEventListener("mouseup", up);
|
||||
},
|
||||
[onResizeEnd, onResizeMove, onResizeStart, vertical],
|
||||
);
|
||||
@@ -88,22 +88,22 @@ export function ResizeHandle({
|
||||
onPointerDown={handlePointerDown}
|
||||
className={classNames(
|
||||
className,
|
||||
'group z-10 flex select-none transition-colors hover:bg-surface-active rounded-full',
|
||||
vertical ? 'w-full h-1.5 cursor-row-resize' : 'h-full w-1.5 cursor-col-resize',
|
||||
justify === 'center' && 'justify-center',
|
||||
justify === 'end' && 'justify-end',
|
||||
justify === 'start' && 'justify-start',
|
||||
side === 'right' && 'right-0',
|
||||
side === 'left' && 'left-0',
|
||||
side === 'top' && 'top-0',
|
||||
"group z-10 flex select-none transition-colors hover:bg-surface-active rounded-full",
|
||||
vertical ? "w-full h-1.5 cursor-row-resize" : "h-full w-1.5 cursor-col-resize",
|
||||
justify === "center" && "justify-center",
|
||||
justify === "end" && "justify-end",
|
||||
justify === "start" && "justify-start",
|
||||
side === "right" && "right-0",
|
||||
side === "left" && "left-0",
|
||||
side === "top" && "top-0",
|
||||
)}
|
||||
>
|
||||
{isResizing && (
|
||||
<div
|
||||
className={classNames(
|
||||
'fixed -left-[100vw] -right-[100vw] -top-[100vh] -bottom-[100vh]',
|
||||
vertical && 'cursor-row-resize',
|
||||
!vertical && 'cursor-col-resize',
|
||||
"fixed -left-[100vw] -right-[100vw] -top-[100vh] -bottom-[100vh]",
|
||||
vertical && "cursor-row-resize",
|
||||
!vertical && "cursor-col-resize",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import classNames from 'classnames';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useContainerSize } from '../hooks/useContainerSize';
|
||||
import { Overlay } from './Overlay';
|
||||
import type { ResizeHandleEvent } from './ResizeHandle';
|
||||
import { ResizeHandle } from './ResizeHandle';
|
||||
import classNames from "classnames";
|
||||
import * as m from "motion/react-m";
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useContainerSize } from "../hooks/useContainerSize";
|
||||
import { Overlay } from "./Overlay";
|
||||
import type { ResizeHandleEvent } from "./ResizeHandle";
|
||||
import { ResizeHandle } from "./ResizeHandle";
|
||||
|
||||
const FLOATING_BREAKPOINT = 600;
|
||||
|
||||
const side = { gridArea: 'side', minWidth: 0 };
|
||||
const drag = { gridArea: 'drag' };
|
||||
const body = { gridArea: 'body', minWidth: 0 };
|
||||
const side = { gridArea: "side", minWidth: 0 };
|
||||
const drag = { gridArea: "drag" };
|
||||
const body = { gridArea: "body", minWidth: 0 };
|
||||
|
||||
interface Props {
|
||||
width: number;
|
||||
@@ -98,7 +98,7 @@ export function SidebarLayout({
|
||||
|
||||
if (floating) {
|
||||
return (
|
||||
<div ref={containerRef} className={classNames(className, 'w-full h-full min-h-0')}>
|
||||
<div ref={containerRef} className={classNames(className, "w-full h-full min-h-0")}>
|
||||
<Overlay
|
||||
open={!floatingHidden}
|
||||
portalName="sidebar"
|
||||
@@ -123,7 +123,7 @@ export function SidebarLayout({
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={styles}
|
||||
className={classNames(className, 'grid w-full h-full', !isResizing && 'transition-grid')}
|
||||
className={classNames(className, "grid w-full h-full", !isResizing && "transition-grid")}
|
||||
>
|
||||
<div style={side} className="overflow-hidden">
|
||||
{sidebar}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useContainerSize } from '../hooks/useContainerSize';
|
||||
import { clamp } from '../lib/clamp';
|
||||
import type { ResizeHandleEvent } from './ResizeHandle';
|
||||
import { ResizeHandle } from './ResizeHandle';
|
||||
import classNames from "classnames";
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { useLocalStorage } from "react-use";
|
||||
import { useContainerSize } from "../hooks/useContainerSize";
|
||||
import { clamp } from "../lib/clamp";
|
||||
import type { ResizeHandleEvent } from "./ResizeHandle";
|
||||
import { ResizeHandle } from "./ResizeHandle";
|
||||
|
||||
export type SplitLayoutLayout = 'responsive' | 'horizontal' | 'vertical';
|
||||
export type SplitLayoutLayout = "responsive" | "horizontal" | "vertical";
|
||||
|
||||
export interface SlotProps {
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
orientation: "horizontal" | "vertical";
|
||||
style: CSSProperties;
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ interface Props {
|
||||
}
|
||||
|
||||
const baseProperties = { minWidth: 0 };
|
||||
const areaL = { ...baseProperties, gridArea: 'left' };
|
||||
const areaR = { ...baseProperties, gridArea: 'right' };
|
||||
const areaD = { ...baseProperties, gridArea: 'drag' };
|
||||
const areaL = { ...baseProperties, gridArea: "left" };
|
||||
const areaR = { ...baseProperties, gridArea: "right" };
|
||||
const areaD = { ...baseProperties, gridArea: "drag" };
|
||||
|
||||
const STACK_VERTICAL_WIDTH = 500;
|
||||
|
||||
@@ -40,7 +40,7 @@ export function SplitLayout({
|
||||
secondSlot,
|
||||
className,
|
||||
storageKey,
|
||||
layout = 'responsive',
|
||||
layout = "responsive",
|
||||
resizeHandleClassName,
|
||||
defaultRatio = 0.5,
|
||||
minHeightPx = 10,
|
||||
@@ -59,7 +59,7 @@ export function SplitLayout({
|
||||
|
||||
const size = useContainerSize(containerRef);
|
||||
const verticalBasedOnSize = size.width !== 0 && size.width < STACK_VERTICAL_WIDTH;
|
||||
const vertical = layout !== 'horizontal' && (layout === 'vertical' || verticalBasedOnSize);
|
||||
const vertical = layout !== "horizontal" && (layout === "vertical" || verticalBasedOnSize);
|
||||
|
||||
const styles = useMemo<CSSProperties>(() => {
|
||||
return {
|
||||
@@ -118,23 +118,23 @@ export function SplitLayout({
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={styles}
|
||||
className={classNames(className, 'grid w-full h-full overflow-hidden')}
|
||||
className={classNames(className, "grid w-full h-full overflow-hidden")}
|
||||
>
|
||||
{firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
|
||||
{firstSlot({ style: areaL, orientation: vertical ? "vertical" : "horizontal" })}
|
||||
{secondSlot && (
|
||||
<>
|
||||
<ResizeHandle
|
||||
style={areaD}
|
||||
className={classNames(
|
||||
resizeHandleClassName,
|
||||
vertical ? '-translate-y-1' : '-translate-x-1',
|
||||
vertical ? "-translate-y-1" : "-translate-x-1",
|
||||
)}
|
||||
onResizeMove={handleResizeMove}
|
||||
onReset={handleReset}
|
||||
side={vertical ? 'top' : 'left'}
|
||||
side={vertical ? "top" : "left"}
|
||||
justify="center"
|
||||
/>
|
||||
{secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
|
||||
{secondSlot({ style: areaR, orientation: vertical ? "vertical" : "horizontal" })}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ComponentType, ForwardedRef, HTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { ComponentType, ForwardedRef, HTMLAttributes, ReactNode } from "react";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
const gapClasses = {
|
||||
0: 'gap-0',
|
||||
0.5: 'gap-0.5',
|
||||
1: 'gap-1',
|
||||
1.5: 'gap-1.5',
|
||||
2: 'gap-2',
|
||||
3: 'gap-3',
|
||||
4: 'gap-4',
|
||||
5: 'gap-5',
|
||||
6: 'gap-6',
|
||||
0: "gap-0",
|
||||
0.5: "gap-0.5",
|
||||
1: "gap-1",
|
||||
1.5: "gap-1.5",
|
||||
2: "gap-2",
|
||||
3: "gap-3",
|
||||
4: "gap-4",
|
||||
5: "gap-5",
|
||||
6: "gap-6",
|
||||
};
|
||||
|
||||
interface HStackProps extends BaseStackProps {
|
||||
@@ -19,14 +19,14 @@ interface HStackProps extends BaseStackProps {
|
||||
}
|
||||
|
||||
export const HStack = forwardRef(function HStack(
|
||||
{ className, space, children, alignItems = 'center', ...props }: HStackProps,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
||||
{ className, space, children, alignItems = "center", ...props }: HStackProps,
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
return (
|
||||
<BaseStack
|
||||
ref={ref}
|
||||
className={classNames(className, 'flex-row', space != null && gapClasses[space])}
|
||||
className={classNames(className, "flex-row", space != null && gapClasses[space])}
|
||||
alignItems={alignItems}
|
||||
{...props}
|
||||
>
|
||||
@@ -41,13 +41,13 @@ export type VStackProps = BaseStackProps & {
|
||||
|
||||
export const VStack = forwardRef(function VStack(
|
||||
{ className, space, children, ...props }: VStackProps,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
return (
|
||||
<BaseStack
|
||||
ref={ref}
|
||||
className={classNames(className, 'flex-col', space != null && gapClasses[space])}
|
||||
className={classNames(className, "flex-col", space != null && gapClasses[space])}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -56,34 +56,34 @@ export const VStack = forwardRef(function VStack(
|
||||
});
|
||||
|
||||
type BaseStackProps = HTMLAttributes<HTMLElement> & {
|
||||
as?: ComponentType | 'ul' | 'label' | 'form' | 'p';
|
||||
as?: ComponentType | "ul" | "label" | "form" | "p";
|
||||
space?: keyof typeof gapClasses;
|
||||
alignItems?: 'start' | 'center' | 'stretch' | 'end';
|
||||
justifyContent?: 'start' | 'center' | 'end' | 'between';
|
||||
alignItems?: "start" | "center" | "stretch" | "end";
|
||||
justifyContent?: "start" | "center" | "end" | "between";
|
||||
wrap?: boolean;
|
||||
};
|
||||
|
||||
const BaseStack = forwardRef(function BaseStack(
|
||||
{ className, alignItems, justifyContent, wrap, children, as, ...props }: BaseStackProps,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
const Component = as ?? 'div';
|
||||
const Component = as ?? "div";
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
className,
|
||||
'flex',
|
||||
wrap && 'flex-wrap',
|
||||
alignItems === 'center' && 'items-center',
|
||||
alignItems === 'start' && 'items-start',
|
||||
alignItems === 'stretch' && 'items-stretch',
|
||||
alignItems === 'end' && 'items-end',
|
||||
justifyContent === 'start' && 'justify-start',
|
||||
justifyContent === 'center' && 'justify-center',
|
||||
justifyContent === 'end' && 'justify-end',
|
||||
justifyContent === 'between' && 'justify-between',
|
||||
"flex",
|
||||
wrap && "flex-wrap",
|
||||
alignItems === "center" && "items-center",
|
||||
alignItems === "start" && "items-start",
|
||||
alignItems === "stretch" && "items-stretch",
|
||||
alignItems === "end" && "items-end",
|
||||
justifyContent === "start" && "justify-start",
|
||||
justifyContent === "center" && "justify-center",
|
||||
justifyContent === "end" && "justify-end",
|
||||
justifyContent === "between" && "justify-between",
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function Table({
|
||||
children,
|
||||
@@ -13,13 +13,13 @@ export function Table({
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<div style={style} className={classNames('w-full', scrollable && 'h-full overflow-y-auto')}>
|
||||
<div style={style} className={classNames("w-full", scrollable && "h-full overflow-y-auto")}>
|
||||
<table
|
||||
className={classNames(
|
||||
className,
|
||||
'w-full text-sm mb-auto min-w-full max-w-full',
|
||||
'border-separate border-spacing-0',
|
||||
scrollable && '[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-10',
|
||||
"w-full text-sm mb-auto min-w-full max-w-full",
|
||||
"border-separate border-spacing-0",
|
||||
scrollable && "[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-10",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -41,7 +41,7 @@ export function TableHead({ children, className }: { children: ReactNode; classN
|
||||
<thead
|
||||
className={classNames(
|
||||
className,
|
||||
'bg-surface [&_th]:border-b [&_th]:border-b-surface-highlight',
|
||||
"bg-surface [&_th]:border-b [&_th]:border-b-surface-highlight",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -56,18 +56,18 @@ export function TableRow({ children }: { children: ReactNode }) {
|
||||
export function TableCell({
|
||||
children,
|
||||
className,
|
||||
align = 'left',
|
||||
align = "left",
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
align?: "left" | "center" | "right";
|
||||
}) {
|
||||
return (
|
||||
<td
|
||||
className={classNames(
|
||||
className,
|
||||
'py-2 [&:not(:first-child)]:pl-4 whitespace-nowrap',
|
||||
align === 'left' ? 'text-left' : align === 'center' ? 'text-center' : 'text-right',
|
||||
"py-2 [&:not(:first-child)]:pl-4 whitespace-nowrap",
|
||||
align === "left" ? "text-left" : align === "center" ? "text-center" : "text-right",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -83,7 +83,7 @@ export function TruncatedWideTableCell({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<TableCell className={classNames(className, 'truncate max-w-0 w-full')}>{children}</TableCell>
|
||||
<TableCell className={classNames(className, "truncate max-w-0 w-full")}>{children}</TableCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export function TableHeaderCell({
|
||||
<th
|
||||
className={classNames(
|
||||
className,
|
||||
'py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle',
|
||||
"py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { WINDOW_CONTROLS_WIDTH } from '../lib/constants';
|
||||
import { Button } from './Button';
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import classNames from "classnames";
|
||||
import { useState } from "react";
|
||||
import { WINDOW_CONTROLS_WIDTH } from "../lib/constants";
|
||||
import { Button } from "./Button";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -12,17 +12,26 @@ interface Props {
|
||||
useNativeTitlebar: boolean;
|
||||
}
|
||||
|
||||
export function WindowControls({ className, onlyX, osType, hideWindowControls, useNativeTitlebar }: Props) {
|
||||
export function WindowControls({
|
||||
className,
|
||||
onlyX,
|
||||
osType,
|
||||
hideWindowControls,
|
||||
useNativeTitlebar,
|
||||
}: Props) {
|
||||
const [maximized, setMaximized] = useState<boolean>(false);
|
||||
|
||||
// Never show controls on macOS or if hideWindowControls is true
|
||||
if (osType === 'macos' || hideWindowControls || useNativeTitlebar) {
|
||||
if (osType === "macos" || hideWindowControls || useNativeTitlebar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(className, 'ml-4 absolute right-0 top-0 bottom-0 flex items-center justify-end')}
|
||||
className={classNames(
|
||||
className,
|
||||
"ml-4 absolute right-0 top-0 bottom-0 flex items-center justify-end",
|
||||
)}
|
||||
style={{ width: WINDOW_CONTROLS_WIDTH }}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
|
||||
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from "@dnd-kit/core";
|
||||
import {
|
||||
DndContext,
|
||||
MeasuringStrategy,
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
useDroppable,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import classNames from 'classnames';
|
||||
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from 'react';
|
||||
} from "@dnd-kit/core";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import classNames from "classnames";
|
||||
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from "react";
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
@@ -20,24 +20,19 @@ import {
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import { computeSideForDragMove } from '../../lib/dnd';
|
||||
import { useStore } from 'jotai';
|
||||
import {
|
||||
draggingIdsFamily,
|
||||
focusIdsFamily,
|
||||
hoveredParentFamily,
|
||||
selectedIdsFamily,
|
||||
} from './atoms';
|
||||
import { type CollapsedAtom, CollapsedAtomContext } from './context';
|
||||
import type { ContextMenuRenderer, JotaiStore, SelectableTreeNode, TreeNode } from './common';
|
||||
import { closestVisibleNode, equalSubtree, getSelectedItems, hasAncestor } from './common';
|
||||
import { TreeDragOverlay } from './TreeDragOverlay';
|
||||
import type { TreeItemClickEvent, TreeItemHandle, TreeItemProps } from './TreeItem';
|
||||
import type { TreeItemListProps } from './TreeItemList';
|
||||
import { TreeItemList } from './TreeItemList';
|
||||
import { useSelectableItems } from './useSelectableItems';
|
||||
} from "react";
|
||||
import { useKey, useKeyPressEvent } from "react-use";
|
||||
import { computeSideForDragMove } from "../../lib/dnd";
|
||||
import { useStore } from "jotai";
|
||||
import { draggingIdsFamily, focusIdsFamily, hoveredParentFamily, selectedIdsFamily } from "./atoms";
|
||||
import { type CollapsedAtom, CollapsedAtomContext } from "./context";
|
||||
import type { ContextMenuRenderer, JotaiStore, SelectableTreeNode, TreeNode } from "./common";
|
||||
import { closestVisibleNode, equalSubtree, getSelectedItems, hasAncestor } from "./common";
|
||||
import { TreeDragOverlay } from "./TreeDragOverlay";
|
||||
import type { TreeItemClickEvent, TreeItemHandle, TreeItemProps } from "./TreeItem";
|
||||
import type { TreeItemListProps } from "./TreeItemList";
|
||||
import { TreeItemList } from "./TreeItemList";
|
||||
import { useSelectableItems } from "./useSelectableItems";
|
||||
|
||||
/** So we re-calculate after expanding a folder during drag */
|
||||
const measuring = { droppable: { strategy: MeasuringStrategy.Always } };
|
||||
@@ -137,7 +132,7 @@ function TreeInner<T extends { id: string }>(
|
||||
return false;
|
||||
}
|
||||
$el.focus();
|
||||
$el.scrollIntoView({ block: 'nearest' });
|
||||
$el.scrollIntoView({ block: "nearest" });
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
@@ -239,7 +234,7 @@ function TreeInner<T extends { id: string }>(
|
||||
};
|
||||
}, [getContextMenu, selectableItems, setSelected, treeId]);
|
||||
|
||||
const handleSelect = useCallback<NonNullable<TreeItemProps<T>['onClick']>>(
|
||||
const handleSelect = useCallback<NonNullable<TreeItemProps<T>["onClick"]>>(
|
||||
(item, { shiftKey, metaKey, ctrlKey }) => {
|
||||
const anchorSelectedId = store.get(focusIdsFamily(treeId)).anchorId;
|
||||
const selectedIdsAtom = selectedIdsFamily(treeId);
|
||||
@@ -279,7 +274,7 @@ function TreeInner<T extends { id: string }>(
|
||||
} else {
|
||||
setSelected([item.id], true);
|
||||
}
|
||||
} else if (type() === 'macos' ? metaKey : ctrlKey) {
|
||||
} else if (type() === "macos" ? metaKey : ctrlKey) {
|
||||
const withoutCurr = selectedIds.filter((id) => id !== item.id);
|
||||
if (withoutCurr.length === selectedIds.length) {
|
||||
// It wasn't in there, so add it
|
||||
@@ -297,7 +292,7 @@ function TreeInner<T extends { id: string }>(
|
||||
[selectableItems, setSelected, treeId],
|
||||
);
|
||||
|
||||
const handleClick = useCallback<NonNullable<TreeItemProps<T>['onClick']>>(
|
||||
const handleClick = useCallback<NonNullable<TreeItemProps<T>["onClick"]>>(
|
||||
(item, e) => {
|
||||
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
||||
handleSelect(item, e);
|
||||
@@ -348,7 +343,7 @@ function TreeInner<T extends { id: string }>(
|
||||
);
|
||||
|
||||
useKey(
|
||||
(e) => e.key === 'ArrowUp' || e.key.toLowerCase() === 'k',
|
||||
(e) => e.key === "ArrowUp" || e.key.toLowerCase() === "k",
|
||||
(e) => {
|
||||
if (!isTreeFocused()) return;
|
||||
e.preventDefault();
|
||||
@@ -359,7 +354,7 @@ function TreeInner<T extends { id: string }>(
|
||||
);
|
||||
|
||||
useKey(
|
||||
(e) => e.key === 'ArrowDown' || e.key.toLowerCase() === 'j',
|
||||
(e) => e.key === "ArrowDown" || e.key.toLowerCase() === "j",
|
||||
(e) => {
|
||||
if (!isTreeFocused()) return;
|
||||
e.preventDefault();
|
||||
@@ -371,7 +366,7 @@ function TreeInner<T extends { id: string }>(
|
||||
|
||||
// If the selected item is a collapsed folder, expand it. Otherwise, select next item
|
||||
useKey(
|
||||
(e) => e.key === 'ArrowRight' || e.key === 'l',
|
||||
(e) => e.key === "ArrowRight" || e.key === "l",
|
||||
(e) => {
|
||||
if (!isTreeFocused()) return;
|
||||
e.preventDefault();
|
||||
@@ -397,7 +392,7 @@ function TreeInner<T extends { id: string }>(
|
||||
// If the selected item is in a folder, select its parent.
|
||||
// If the selected item is an expanded folder, collapse it.
|
||||
useKey(
|
||||
(e) => e.key === 'ArrowLeft' || e.key === 'h',
|
||||
(e) => e.key === "ArrowLeft" || e.key === "h",
|
||||
(e) => {
|
||||
if (!isTreeFocused()) return;
|
||||
e.preventDefault();
|
||||
@@ -420,7 +415,7 @@ function TreeInner<T extends { id: string }>(
|
||||
[selectableItems, handleSelect],
|
||||
);
|
||||
|
||||
useKeyPressEvent('Escape', async () => {
|
||||
useKeyPressEvent("Escape", async () => {
|
||||
if (!treeRef.current?.contains(document.activeElement)) return;
|
||||
clearDragState();
|
||||
const lastSelectedId = store.get(focusIdsFamily(treeId)).lastId;
|
||||
@@ -484,11 +479,11 @@ function TreeInner<T extends { id: string }>(
|
||||
let hoveredParent = node.parent;
|
||||
const dragIndex = selectableItems.findIndex((n) => n.node.item.id === item.id) ?? -1;
|
||||
const hovered = selectableItems[dragIndex]?.node ?? null;
|
||||
const hoveredIndex = dragIndex + (side === 'before' ? 0 : 1);
|
||||
let hoveredChildIndex = overSelectableItem.index + (side === 'before' ? 0 : 1);
|
||||
const hoveredIndex = dragIndex + (side === "before" ? 0 : 1);
|
||||
let hoveredChildIndex = overSelectableItem.index + (side === "before" ? 0 : 1);
|
||||
|
||||
// Move into the folder if it's open and we're moving after it
|
||||
if (hovered?.children != null && side === 'after') {
|
||||
if (hovered?.children != null && side === "after") {
|
||||
hoveredParent = hovered;
|
||||
hoveredChildIndex = 0;
|
||||
}
|
||||
@@ -618,7 +613,7 @@ function TreeInner<T extends { id: string }>(
|
||||
|
||||
const treeItemListProps: Omit<
|
||||
TreeItemListProps<T>,
|
||||
'nodes' | 'treeId' | 'activeIdAtom' | 'hoveredParent' | 'hoveredIndex'
|
||||
"nodes" | "treeId" | "activeIdAtom" | "hoveredParent" | "hoveredIndex"
|
||||
> = {
|
||||
getItemKey,
|
||||
getContextMenu: handleGetContextMenu,
|
||||
@@ -667,24 +662,24 @@ function TreeInner<T extends { id: string }>(
|
||||
ref={treeRef}
|
||||
className={classNames(
|
||||
className,
|
||||
'outline-none h-full',
|
||||
'overflow-y-auto overflow-x-hidden',
|
||||
'grid grid-rows-[auto_1fr]',
|
||||
"outline-none h-full",
|
||||
"overflow-y-auto overflow-x-hidden",
|
||||
"grid grid-rows-[auto_1fr]",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'[&_.tree-item.selected_.tree-item-inner]:text-text',
|
||||
'[&:focus-within]:[&_.tree-item.selected]:bg-surface-active',
|
||||
'[&:not(:focus-within)]:[&_.tree-item.selected:not([data-context-menu-open])]:bg-surface-highlight',
|
||||
'[&_.tree-item.selected[data-context-menu-open]]:bg-surface-active',
|
||||
"[&_.tree-item.selected_.tree-item-inner]:text-text",
|
||||
"[&:focus-within]:[&_.tree-item.selected]:bg-surface-active",
|
||||
"[&:not(:focus-within)]:[&_.tree-item.selected:not([data-context-menu-open])]:bg-surface-highlight",
|
||||
"[&_.tree-item.selected[data-context-menu-open]]:bg-surface-active",
|
||||
// Round the items, but only if the ends of the selection.
|
||||
// Also account for the drop marker being in between items
|
||||
'[&_.tree-item]:rounded-md',
|
||||
'[&_.tree-item.selected+.tree-item.selected]:rounded-t-none',
|
||||
'[&_.tree-item.selected+.drop-marker+.tree-item.selected]:rounded-t-none',
|
||||
'[&_.tree-item.selected:has(+.tree-item.selected)]:rounded-b-none',
|
||||
'[&_.tree-item.selected:has(+.drop-marker+.tree-item.selected)]:rounded-b-none',
|
||||
"[&_.tree-item]:rounded-md",
|
||||
"[&_.tree-item.selected+.tree-item.selected]:rounded-t-none",
|
||||
"[&_.tree-item.selected+.drop-marker+.tree-item.selected]:rounded-t-none",
|
||||
"[&_.tree-item.selected:has(+.tree-item.selected)]:rounded-b-none",
|
||||
"[&_.tree-item.selected:has(+.drop-marker+.tree-item.selected)]:rounded-b-none",
|
||||
)}
|
||||
>
|
||||
<TreeItemList
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { DragOverlay } from '@dnd-kit/core';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { draggingIdsFamily } from './atoms';
|
||||
import type { SelectableTreeNode } from './common';
|
||||
import type { TreeProps } from './Tree';
|
||||
import { TreeItemList } from './TreeItemList';
|
||||
import { DragOverlay } from "@dnd-kit/core";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { draggingIdsFamily } from "./atoms";
|
||||
import type { SelectableTreeNode } from "./common";
|
||||
import type { TreeProps } from "./Tree";
|
||||
import { TreeItemList } from "./TreeItemList";
|
||||
|
||||
export function TreeDragOverlay<T extends { id: string }>({
|
||||
treeId,
|
||||
@@ -14,7 +14,7 @@ export function TreeDragOverlay<T extends { id: string }>({
|
||||
}: {
|
||||
treeId: string;
|
||||
selectableItems: SelectableTreeNode<T>[];
|
||||
} & Pick<TreeProps<T>, 'getItemKey' | 'ItemInner' | 'ItemLeftSlotInner'>) {
|
||||
} & Pick<TreeProps<T>, "getItemKey" | "ItemInner" | "ItemLeftSlotInner">) {
|
||||
const draggingItems = useAtomValue(draggingIdsFamily(treeId));
|
||||
return (
|
||||
<DragOverlay dropAnimation={null}>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { memo } from 'react';
|
||||
import { DropMarker } from '../DropMarker';
|
||||
import { hoveredParentDepthFamily, isIndexHoveredFamily } from './atoms';
|
||||
import type { TreeNode } from './common';
|
||||
import { useIsCollapsed } from './context';
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo } from "react";
|
||||
import { DropMarker } from "../DropMarker";
|
||||
import { hoveredParentDepthFamily, isIndexHoveredFamily } from "./atoms";
|
||||
import type { TreeNode } from "./common";
|
||||
import { useIsCollapsed } from "./context";
|
||||
|
||||
export const TreeDropMarker = memo(function TreeDropMarker<T extends { id: string }>({
|
||||
className,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { memo } from 'react';
|
||||
import { hoveredParentDepthFamily, isAncestorHoveredFamily } from './atoms';
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo } from "react";
|
||||
import { hoveredParentDepthFamily, isAncestorHoveredFamily } from "./atoms";
|
||||
|
||||
export const TreeIndentGuide = memo(function TreeIndentGuide({
|
||||
treeId,
|
||||
@@ -19,11 +19,11 @@ export const TreeIndentGuide = memo(function TreeIndentGuide({
|
||||
<div className="flex">
|
||||
{Array.from({ length: depth }).map((_, i) => (
|
||||
<div
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
||||
// oxlint-disable-next-line react/no-array-index-key
|
||||
key={i}
|
||||
className={classNames(
|
||||
'w-[calc(1rem+0.5px)] border-r border-r-text-subtlest',
|
||||
!(parentDepth === i + 1 && isHovered) && 'opacity-30',
|
||||
"w-[calc(1rem+0.5px)] border-r border-r-text-subtlest",
|
||||
!(parentDepth === i + 1 && isHovered) && "opacity-30",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import type { DragMoveEvent } from '@dnd-kit/core';
|
||||
import { useDndContext, useDndMonitor, useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue, useStore } from 'jotai';
|
||||
import type { DragMoveEvent } from "@dnd-kit/core";
|
||||
import { useDndContext, useDndMonitor, useDraggable, useDroppable } from "@dnd-kit/core";
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue, useStore } from "jotai";
|
||||
import type {
|
||||
MouseEvent,
|
||||
PointerEvent,
|
||||
FocusEvent as ReactFocusEvent,
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
} from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { computeSideForDragMove } from '../../lib/dnd';
|
||||
import { Icon } from '../Icon';
|
||||
import { isLastFocusedFamily, isSelectedFamily } from './atoms';
|
||||
import { useCollapsedAtom, useIsAncestorCollapsed, useIsCollapsed, useSetCollapsed } from './context';
|
||||
import type { TreeNode } from './common';
|
||||
import { getNodeKey } from './common';
|
||||
import type { TreeProps } from './Tree';
|
||||
import { TreeIndentGuide } from './TreeIndentGuide';
|
||||
} from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { computeSideForDragMove } from "../../lib/dnd";
|
||||
import { Icon } from "../Icon";
|
||||
import { isLastFocusedFamily, isSelectedFamily } from "./atoms";
|
||||
import {
|
||||
useCollapsedAtom,
|
||||
useIsAncestorCollapsed,
|
||||
useIsCollapsed,
|
||||
useSetCollapsed,
|
||||
} from "./context";
|
||||
import type { TreeNode } from "./common";
|
||||
import { getNodeKey } from "./common";
|
||||
import type { TreeProps } from "./Tree";
|
||||
import { TreeIndentGuide } from "./TreeIndentGuide";
|
||||
|
||||
export interface TreeItemClickEvent {
|
||||
shiftKey: boolean;
|
||||
@@ -26,7 +31,13 @@ export interface TreeItemClickEvent {
|
||||
|
||||
export type TreeItemProps<T extends { id: string }> = Pick<
|
||||
TreeProps<T>,
|
||||
'ItemInner' | 'ItemLeftSlotInner' | 'ItemRightSlot' | 'treeId' | 'getEditOptions' | 'getItemKey' | 'renderContextMenu'
|
||||
| "ItemInner"
|
||||
| "ItemLeftSlotInner"
|
||||
| "ItemRightSlot"
|
||||
| "treeId"
|
||||
| "getEditOptions"
|
||||
| "getItemKey"
|
||||
| "renderContextMenu"
|
||||
> & {
|
||||
node: TreeNode<T>;
|
||||
className?: string;
|
||||
@@ -69,7 +80,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
const setCollapsed = useSetCollapsed(node.item.id);
|
||||
const isLastSelected = useAtomValue(isLastFocusedFamily({ treeId, itemId: node.item.id }));
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const [dropHover, setDropHover] = useState<null | 'drop' | 'animate'>(null);
|
||||
const [dropHover, setDropHover] = useState<null | "drop" | "animate">(null);
|
||||
const startedHoverTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||
const handle = useMemo<TreeItemHandle>(
|
||||
() => ({
|
||||
@@ -89,7 +100,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
return listItemRef.current.getBoundingClientRect();
|
||||
},
|
||||
scrollIntoView: () => {
|
||||
listItemRef.current?.scrollIntoView({ block: 'nearest' });
|
||||
listItemRef.current?.scrollIntoView({ block: "nearest" });
|
||||
},
|
||||
}),
|
||||
[editing, getEditOptions],
|
||||
@@ -154,13 +165,13 @@ function TreeItem_<T extends { id: string }>({
|
||||
async (e: ReactKeyboardEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation(); // Don't trigger other tree keys (like arrows)
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
case "Enter":
|
||||
if (editing) {
|
||||
e.preventDefault();
|
||||
await handleSubmitNameEdit(e.currentTarget);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
case "Escape":
|
||||
if (editing) {
|
||||
e.preventDefault();
|
||||
setEditing(false);
|
||||
@@ -201,8 +212,8 @@ function TreeItem_<T extends { id: string }>({
|
||||
const hasChildren = (node.children?.length ?? 0) > 0;
|
||||
const collapsedMap = store.get(collapsedAtom);
|
||||
const itemCollapsed = !!collapsedMap[node.item.id];
|
||||
if (itemCollapsed && isFolder && hasChildren && side === 'after') {
|
||||
setDropHover('animate');
|
||||
if (itemCollapsed && isFolder && hasChildren && side === "after") {
|
||||
setDropHover("animate");
|
||||
clearTimeout(startedHoverTimeout.current);
|
||||
startedHoverTimeout.current = setTimeout(() => {
|
||||
store.set(collapsedAtom, { ...store.get(collapsedAtom), [node.item.id]: false });
|
||||
@@ -214,8 +225,8 @@ function TreeItem_<T extends { id: string }>({
|
||||
);
|
||||
});
|
||||
}, HOVER_CLOSED_FOLDER_DELAY);
|
||||
} else if (isFolder && !hasChildren && side === 'after') {
|
||||
setDropHover('drop');
|
||||
} else if (isFolder && !hasChildren && side === "after") {
|
||||
setDropHover("drop");
|
||||
} else {
|
||||
clearDropHover();
|
||||
}
|
||||
@@ -231,7 +242,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
|
||||
// Set data attribute on the list item to preserve active state
|
||||
if (listItemRef.current) {
|
||||
listItemRef.current.setAttribute('data-context-menu-open', 'true');
|
||||
listItemRef.current.setAttribute("data-context-menu-open", "true");
|
||||
}
|
||||
|
||||
const items = await getContextMenu(node.item);
|
||||
@@ -243,7 +254,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
const handleCloseContextMenu = useCallback(() => {
|
||||
// Remove data attribute when context menu closes
|
||||
if (listItemRef.current) {
|
||||
listItemRef.current.removeAttribute('data-context-menu-open');
|
||||
listItemRef.current.removeAttribute("data-context-menu-open");
|
||||
}
|
||||
setShowContextMenu(null);
|
||||
}, []);
|
||||
@@ -283,20 +294,20 @@ function TreeItem_<T extends { id: string }>({
|
||||
onContextMenu={handleContextMenu}
|
||||
className={classNames(
|
||||
className,
|
||||
'tree-item',
|
||||
'h-sm',
|
||||
'grid grid-cols-[auto_minmax(0,1fr)]',
|
||||
editing && 'ring-1 focus-within:ring-focus',
|
||||
dropHover != null && 'relative z-10 ring-2 ring-primary',
|
||||
dropHover === 'animate' && 'animate-blinkRing',
|
||||
isSelected && 'selected',
|
||||
"tree-item",
|
||||
"h-sm",
|
||||
"grid grid-cols-[auto_minmax(0,1fr)]",
|
||||
editing && "ring-1 focus-within:ring-focus",
|
||||
dropHover != null && "relative z-10 ring-2 ring-primary",
|
||||
dropHover === "animate" && "animate-blinkRing",
|
||||
isSelected && "selected",
|
||||
)}
|
||||
>
|
||||
<TreeIndentGuide treeId={treeId} depth={depth} ancestorIds={ancestorIds} />
|
||||
<div
|
||||
className={classNames(
|
||||
'text-text-subtle',
|
||||
'grid grid-cols-[auto_minmax(0,1fr)_auto] gap-x-2 items-center rounded-md',
|
||||
"text-text-subtle",
|
||||
"grid grid-cols-[auto_minmax(0,1fr)_auto] gap-x-2 items-center rounded-md",
|
||||
)}
|
||||
>
|
||||
{showContextMenu &&
|
||||
@@ -313,12 +324,12 @@ function TreeItem_<T extends { id: string }>({
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
<Icon
|
||||
icon={node.children.length === 0 ? 'dot' : 'chevron_right'}
|
||||
icon={node.children.length === 0 ? "dot" : "chevron_right"}
|
||||
className={classNames(
|
||||
'transition-transform text-text-subtlest',
|
||||
'ml-auto',
|
||||
'w-[1rem] h-[1rem]',
|
||||
!isCollapsed && node.children.length > 0 && 'rotate-90',
|
||||
"transition-transform text-text-subtlest",
|
||||
"ml-auto",
|
||||
"w-[1rem] h-[1rem]",
|
||||
!isCollapsed && node.children.length > 0 && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import type { SelectableTreeNode } from './common';
|
||||
import type { TreeProps } from './Tree';
|
||||
import { TreeDropMarker } from './TreeDropMarker';
|
||||
import type { TreeItemHandle, TreeItemProps } from './TreeItem';
|
||||
import { TreeItem } from './TreeItem';
|
||||
import type { CSSProperties } from "react";
|
||||
import { Fragment } from "react";
|
||||
import type { SelectableTreeNode } from "./common";
|
||||
import type { TreeProps } from "./Tree";
|
||||
import { TreeDropMarker } from "./TreeDropMarker";
|
||||
import type { TreeItemHandle, TreeItemProps } from "./TreeItem";
|
||||
import { TreeItem } from "./TreeItem";
|
||||
|
||||
export type TreeItemListProps<T extends { id: string }> = Pick<
|
||||
TreeProps<T>,
|
||||
'ItemInner' | 'ItemLeftSlotInner' | 'ItemRightSlot' | 'treeId' | 'getItemKey' | 'getEditOptions' | 'renderContextMenu'
|
||||
| "ItemInner"
|
||||
| "ItemLeftSlotInner"
|
||||
| "ItemRightSlot"
|
||||
| "treeId"
|
||||
| "getItemKey"
|
||||
| "getEditOptions"
|
||||
| "renderContextMenu"
|
||||
> &
|
||||
Pick<TreeItemProps<T>, 'onClick' | 'getContextMenu'> & {
|
||||
Pick<TreeItemProps<T>, "onClick" | "getContextMenu"> & {
|
||||
nodes: SelectableTreeNode<T>[];
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { atom } from 'jotai';
|
||||
import { atomFamily, selectAtom } from 'jotai/utils';
|
||||
import { atom } from "jotai";
|
||||
import { atomFamily, selectAtom } from "jotai/utils";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const selectedIdsFamily = atomFamily((_treeId: string) => {
|
||||
@@ -57,7 +57,7 @@ export const isAncestorHoveredFamily = atomFamily(
|
||||
(v) => v.parentId && ancestorIds.includes(v.parentId),
|
||||
Object.is,
|
||||
),
|
||||
(a, b) => a.treeId === b.treeId && a.ancestorIds.join(',') === b.ancestorIds.join(','),
|
||||
(a, b) => a.treeId === b.treeId && a.ancestorIds.join(",") === b.ancestorIds.join(","),
|
||||
);
|
||||
|
||||
export const isIndexHoveredFamily = atomFamily(
|
||||
@@ -73,4 +73,3 @@ export const hoveredParentDepthFamily = atomFamily((treeId: string) =>
|
||||
(a, b) => Object.is(a, b), // prevents re-render unless the value changes
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { createStore } from 'jotai';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { CollapsedAtom } from './context';
|
||||
import { selectedIdsFamily } from './atoms';
|
||||
import type { createStore } from "jotai";
|
||||
import type { ReactNode } from "react";
|
||||
import type { CollapsedAtom } from "./context";
|
||||
import { selectedIdsFamily } from "./atoms";
|
||||
|
||||
export type JotaiStore = ReturnType<typeof createStore>;
|
||||
|
||||
@@ -71,7 +71,11 @@ export function hasAncestor<T extends { id: string }>(node: TreeNode<T>, ancesto
|
||||
return hasAncestor(node.parent, ancestorId);
|
||||
}
|
||||
|
||||
export function isVisibleNode<T extends { id: string }>(store: JotaiStore, collapsedAtom: CollapsedAtom, node: TreeNode<T>) {
|
||||
export function isVisibleNode<T extends { id: string }>(
|
||||
store: JotaiStore,
|
||||
collapsedAtom: CollapsedAtom,
|
||||
node: TreeNode<T>,
|
||||
) {
|
||||
const collapsed = store.get(collapsedAtom);
|
||||
let p = node.parent;
|
||||
while (p) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { WritableAtom } from 'jotai';
|
||||
import { useAtomValue, useStore } from 'jotai';
|
||||
import { selectAtom } from 'jotai/utils';
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
import type { WritableAtom } from "jotai";
|
||||
import { useAtomValue, useStore } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||
|
||||
type CollapsedMap = Record<string, boolean>;
|
||||
type SetAction = CollapsedMap | ((prev: CollapsedMap) => CollapsedMap);
|
||||
@@ -11,14 +11,14 @@ export const CollapsedAtomContext = createContext<CollapsedAtom | null>(null);
|
||||
|
||||
export function useCollapsedAtom(): CollapsedAtom {
|
||||
const atom = useContext(CollapsedAtomContext);
|
||||
if (!atom) throw new Error('CollapsedAtomContext not provided');
|
||||
if (!atom) throw new Error("CollapsedAtomContext not provided");
|
||||
return atom;
|
||||
}
|
||||
|
||||
export function useIsCollapsed(itemId: string | undefined) {
|
||||
const collapsedAtom = useCollapsedAtom();
|
||||
const derivedAtom = useMemo(
|
||||
() => selectAtom(collapsedAtom, (map) => !!map[itemId ?? 'n/a'], Object.is),
|
||||
() => selectAtom(collapsedAtom, (map) => !!map[itemId ?? "n/a"], Object.is),
|
||||
[collapsedAtom, itemId],
|
||||
);
|
||||
return useAtomValue(derivedAtom);
|
||||
@@ -29,10 +29,10 @@ export function useSetCollapsed(itemId: string | undefined) {
|
||||
const store = useStore();
|
||||
return useCallback(
|
||||
(next: boolean | ((prev: boolean) => boolean)) => {
|
||||
const key = itemId ?? 'n/a';
|
||||
const key = itemId ?? "n/a";
|
||||
const prevMap = store.get(collapsedAtom);
|
||||
const prevValue = !!prevMap[key];
|
||||
const value = typeof next === 'function' ? next(prevValue) : next;
|
||||
const value = typeof next === "function" ? next(prevValue) : next;
|
||||
if (value === prevValue) return;
|
||||
store.set(collapsedAtom, { ...prevMap, [key]: value });
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { SelectableTreeNode, TreeNode } from './common';
|
||||
import { useMemo } from "react";
|
||||
import type { SelectableTreeNode, TreeNode } from "./common";
|
||||
|
||||
export function useSelectableItems<T extends { id: string }>(root: TreeNode<T>) {
|
||||
return useMemo(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import type { RefObject } from "react";
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
|
||||
export function useContainerSize(ref: RefObject<HTMLElement | null>) {
|
||||
const [size, setSize] = useState<{ width: number; height: number }>({ width: 0, height: 0 });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { debounce } from '@yaakapp-internal/lib';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { debounce } from "@yaakapp-internal/lib";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export function useDebouncedState<T>(
|
||||
defaultValue: T,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useDebouncedState } from './useDebouncedState';
|
||||
import { useEffect } from "react";
|
||||
import { useDebouncedState } from "./useDebouncedState";
|
||||
|
||||
export function useDebouncedValue<T>(value: T, delay = 500) {
|
||||
const [state, setState] = useDebouncedState<T>(value, delay);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import { useDebouncedValue } from './useDebouncedValue';
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { useWindowSize } from "react-use";
|
||||
import { useDebouncedValue } from "./useDebouncedValue";
|
||||
|
||||
export function useIsFullscreen() {
|
||||
const windowSize = useWindowSize();
|
||||
@@ -13,7 +13,7 @@ export function useIsFullscreen() {
|
||||
|
||||
return (
|
||||
useQuery({
|
||||
queryKey: ['is_fullscreen', debouncedWindowWidth],
|
||||
queryKey: ["is_fullscreen", debouncedWindowWidth],
|
||||
queryFn: async () => {
|
||||
return getCurrentWebviewWindow().isFullscreen();
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRef } from 'react';
|
||||
import { useRef } from "react";
|
||||
|
||||
const PORTAL_CONTAINER_ID = 'react-portal';
|
||||
const PORTAL_CONTAINER_ID = "react-portal";
|
||||
|
||||
export function usePortal(name: string) {
|
||||
const ref = useRef(getOrCreatePortal(name));
|
||||
@@ -10,15 +10,15 @@ export function usePortal(name: string) {
|
||||
function getOrCreatePortal(name: string) {
|
||||
let portalContainer = document.getElementById(PORTAL_CONTAINER_ID);
|
||||
if (!portalContainer) {
|
||||
portalContainer = document.createElement('div');
|
||||
portalContainer = document.createElement("div");
|
||||
portalContainer.id = PORTAL_CONTAINER_ID;
|
||||
document.body.appendChild(portalContainer);
|
||||
}
|
||||
|
||||
let existing = portalContainer.querySelector(`:scope > [data-portal-name="${name}"]`);
|
||||
if (!existing) {
|
||||
const el = document.createElement('div');
|
||||
el.setAttribute('data-portal-name', name);
|
||||
const el = document.createElement("div");
|
||||
el.setAttribute("data-portal-name", name);
|
||||
portalContainer.appendChild(el);
|
||||
existing = el;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useUnmount } from 'react-use';
|
||||
import { useRef, useState } from "react";
|
||||
import { useUnmount } from "react-use";
|
||||
|
||||
/** Returns a boolean that is true for a given number of milliseconds. */
|
||||
export function useTimedBoolean(millis = 1500): [boolean, () => void] {
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
export type { BannerProps } from './components/Banner';
|
||||
export { Banner } from './components/Banner';
|
||||
export type { ButtonProps } from './components/Button';
|
||||
export { Button } from './components/Button';
|
||||
export { DropMarker } from './components/DropMarker';
|
||||
export { HeaderSize } from './components/HeaderSize';
|
||||
export { Heading } from './components/Heading';
|
||||
export type { IconProps } from './components/Icon';
|
||||
export { Icon } from './components/Icon';
|
||||
export type { IconButtonProps } from './components/IconButton';
|
||||
export { IconButton } from './components/IconButton';
|
||||
export { FormattedError } from './components/FormattedError';
|
||||
export { InlineCode } from './components/InlineCode';
|
||||
export { LoadingIcon } from './components/LoadingIcon';
|
||||
export { Overlay } from './components/Overlay';
|
||||
export { Portal } from './components/Portal';
|
||||
export type { ResizeHandleEvent } from './components/ResizeHandle';
|
||||
export { ResizeHandle } from './components/ResizeHandle';
|
||||
export { SidebarLayout } from './components/SidebarLayout';
|
||||
export type { SlotProps, SplitLayoutLayout } from './components/SplitLayout';
|
||||
export { SplitLayout } from './components/SplitLayout';
|
||||
export type { VStackProps } from './components/Stacks';
|
||||
export { HStack, VStack } from './components/Stacks';
|
||||
export type { BannerProps } from "./components/Banner";
|
||||
export { Banner } from "./components/Banner";
|
||||
export type { ButtonProps } from "./components/Button";
|
||||
export { Button } from "./components/Button";
|
||||
export { DropMarker } from "./components/DropMarker";
|
||||
export { HeaderSize } from "./components/HeaderSize";
|
||||
export { Heading } from "./components/Heading";
|
||||
export type { IconProps } from "./components/Icon";
|
||||
export { Icon } from "./components/Icon";
|
||||
export type { IconButtonProps } from "./components/IconButton";
|
||||
export { IconButton } from "./components/IconButton";
|
||||
export { FormattedError } from "./components/FormattedError";
|
||||
export { InlineCode } from "./components/InlineCode";
|
||||
export { LoadingIcon } from "./components/LoadingIcon";
|
||||
export { Overlay } from "./components/Overlay";
|
||||
export { Portal } from "./components/Portal";
|
||||
export type { ResizeHandleEvent } from "./components/ResizeHandle";
|
||||
export { ResizeHandle } from "./components/ResizeHandle";
|
||||
export { SidebarLayout } from "./components/SidebarLayout";
|
||||
export type { SlotProps, SplitLayoutLayout } from "./components/SplitLayout";
|
||||
export { SplitLayout } from "./components/SplitLayout";
|
||||
export type { VStackProps } from "./components/Stacks";
|
||||
export { HStack, VStack } from "./components/Stacks";
|
||||
export {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -29,20 +29,20 @@ export {
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
TruncatedWideTableCell,
|
||||
} from './components/Table';
|
||||
export { isSelectedFamily, selectedIdsFamily } from './components/tree/atoms';
|
||||
export type { TreeNode } from './components/tree/common';
|
||||
export type { TreeHandle, TreeProps } from './components/tree/Tree';
|
||||
export { Tree } from './components/tree/Tree';
|
||||
export type { TreeItemProps } from './components/tree/TreeItem';
|
||||
export { WindowControls } from './components/WindowControls';
|
||||
export { useContainerSize } from './hooks/useContainerSize';
|
||||
export { useDebouncedState } from './hooks/useDebouncedState';
|
||||
export { useDebouncedValue } from './hooks/useDebouncedValue';
|
||||
export { useIsFullscreen } from './hooks/useIsFullscreen';
|
||||
export { usePortal } from './hooks/usePortal';
|
||||
export { useTimedBoolean } from './hooks/useTimedBoolean';
|
||||
export { clamp } from './lib/clamp';
|
||||
export { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from './lib/constants';
|
||||
export { computeSideForDragMove } from './lib/dnd';
|
||||
export { minPromiseMillis } from './lib/minPromiseMillis';
|
||||
} from "./components/Table";
|
||||
export { isSelectedFamily, selectedIdsFamily } from "./components/tree/atoms";
|
||||
export type { TreeNode } from "./components/tree/common";
|
||||
export type { TreeHandle, TreeProps } from "./components/tree/Tree";
|
||||
export { Tree } from "./components/tree/Tree";
|
||||
export type { TreeItemProps } from "./components/tree/TreeItem";
|
||||
export { WindowControls } from "./components/WindowControls";
|
||||
export { useContainerSize } from "./hooks/useContainerSize";
|
||||
export { useDebouncedState } from "./hooks/useDebouncedState";
|
||||
export { useDebouncedValue } from "./hooks/useDebouncedValue";
|
||||
export { useIsFullscreen } from "./hooks/useIsFullscreen";
|
||||
export { usePortal } from "./hooks/usePortal";
|
||||
export { useTimedBoolean } from "./hooks/useTimedBoolean";
|
||||
export { clamp } from "./lib/clamp";
|
||||
export { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from "./lib/constants";
|
||||
export { computeSideForDragMove } from "./lib/dnd";
|
||||
export { minPromiseMillis } from "./lib/minPromiseMillis";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const HEADER_SIZE_MD = '30px';
|
||||
export const HEADER_SIZE_LG = '40px';
|
||||
export const HEADER_SIZE_MD = "30px";
|
||||
export const HEADER_SIZE_LG = "40px";
|
||||
|
||||
export const WINDOW_CONTROLS_WIDTH = '10.5rem';
|
||||
export const WINDOW_CONTROLS_WIDTH = "10.5rem";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { DragMoveEvent } from '@dnd-kit/core';
|
||||
import type { DragMoveEvent } from "@dnd-kit/core";
|
||||
|
||||
export function computeSideForDragMove(
|
||||
id: string,
|
||||
e: DragMoveEvent,
|
||||
orientation: 'vertical' | 'horizontal' = 'vertical',
|
||||
): 'before' | 'after' | null {
|
||||
orientation: "vertical" | "horizontal" = "vertical",
|
||||
): "before" | "after" | null {
|
||||
if (e.over == null || e.over.id !== id) {
|
||||
return null;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export function computeSideForDragMove(
|
||||
|
||||
const overRect = e.over.rect;
|
||||
|
||||
if (orientation === 'horizontal') {
|
||||
if (orientation === "horizontal") {
|
||||
// For horizontal layouts (tabs side-by-side), use left/right logic
|
||||
const activeLeft =
|
||||
e.active.rect.current.translated?.left ?? e.active.rect.current.initial.left + e.delta.x;
|
||||
@@ -22,7 +22,7 @@ export function computeSideForDragMove(
|
||||
const hoverRight = overRect.right;
|
||||
const hoverMiddleX = hoverLeft + (hoverRight - hoverLeft) / 2;
|
||||
|
||||
return pointerX < hoverMiddleX ? 'before' : 'after'; // 'before' = left, 'after' = right
|
||||
return pointerX < hoverMiddleX ? "before" : "after"; // 'before' = left, 'after' = right
|
||||
} else {
|
||||
// For vertical layouts, use top/bottom logic
|
||||
const activeTop =
|
||||
@@ -34,6 +34,6 @@ export function computeSideForDragMove(
|
||||
const hoverMiddleY = (hoverBottom - hoverTop) / 2;
|
||||
const hoverClientY = pointerY - hoverTop;
|
||||
|
||||
return hoverClientY < hoverMiddleY ? 'before' : 'after';
|
||||
return hoverClientY < hoverMiddleY ? "before" : "after";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user