@tulip-systems/core 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands.d.mts +3 -2
- package/dist/commands.mjs +2 -1
- package/dist/components/ui/badge.d.mts +1 -1
- package/dist/components/ui/button-group.d.mts +1 -1
- package/dist/components/ui/button.d.mts +2 -2
- package/dist/components/ui/field.client.d.mts +1 -1
- package/dist/data-tables/client.d.mts +2 -1
- package/dist/data-tables/client.mjs +2 -1
- package/dist/modules/commands/components/menus/context-menu.client.d.mts +6 -7
- package/dist/modules/commands/components/menus/dropdown-menu.client.d.mts +6 -7
- package/dist/modules/commands/components/menus/floating-menu.client.d.mts +6 -7
- package/dist/modules/commands/components/menus/inline-menu.client.d.mts +6 -7
- package/dist/modules/commands/components/menus/responsive-menu.client.d.mts +5 -6
- package/dist/modules/commands/components/render-command.mjs +3 -5
- package/dist/modules/commands/lib/builder.d.mts +114 -18
- package/dist/modules/commands/lib/builder.mjs +42 -7
- package/dist/modules/commands/lib/registery.d.mts +47 -14
- package/dist/modules/commands/lib/registery.mjs +76 -16
- package/dist/modules/commands/lib/utils.d.mts +11 -0
- package/dist/modules/commands/lib/utils.mjs +14 -0
- package/dist/modules/data-tables/components/footer.mjs +3 -0
- package/dist/modules/data-tables/hooks/use-context.client.d.mts +2 -2
- package/dist/modules/data-tables/lib/types.d.mts +3 -3
- package/dist/modules/data-tables/strategies/local/components.mjs +25 -0
- package/dist/modules/data-tables/strategies/local/strategy.d.mts +12 -0
- package/dist/modules/data-tables/strategies/local/strategy.mjs +31 -0
- package/dist/modules/inline/components/inputs/combobox-dropdown.client.d.mts +4 -3
- package/dist/modules/inline/components/inputs/combobox.client.d.mts +4 -3
- package/dist/modules/inline/components/inputs/date-time.client.d.mts +4 -3
- package/dist/modules/inline/components/inputs/editor.client.d.mts +4 -3
- package/dist/modules/inline/components/inputs/input.client.d.mts +7 -5
- package/dist/modules/inline/components/inputs/select.client.d.mts +4 -3
- package/dist/modules/inline/components/inputs/switch.client.d.mts +4 -3
- package/dist/modules/inline/lib/variants.d.mts +1 -1
- package/dist/modules/router/lib/query-client.d.mts +2 -1
- package/dist/modules/router/lib/query-client.mjs +2 -2
- package/dist/modules/storage/lib/service.server.d.mts +9 -9
- package/dist/router.d.mts +2 -1
- package/dist/router.mjs +2 -1
- package/dist/src/components/ui/badge.d.mts +1 -1
- package/dist/src/components/ui/button-group.d.mts +1 -1
- package/dist/src/components/ui/button.d.mts +2 -2
- package/dist/src/components/ui/field.client.d.mts +1 -1
- package/dist/src/components/ui/item.d.mts +1 -1
- package/dist/src/modules/auth/handler/create-client.client.d.mts +2 -2
- package/dist/src/modules/commands/components/menus/context-menu.client.d.mts +6 -7
- package/dist/src/modules/commands/components/menus/dropdown-menu.client.d.mts +6 -7
- package/dist/src/modules/commands/components/menus/floating-menu.client.d.mts +6 -7
- package/dist/src/modules/commands/components/menus/inline-menu.client.d.mts +6 -7
- package/dist/src/modules/commands/components/menus/responsive-menu.client.d.mts +5 -6
- package/dist/src/modules/commands/components/render-command.mjs +3 -5
- package/dist/src/modules/commands/lib/builder.d.mts +114 -18
- package/dist/src/modules/commands/lib/builder.mjs +42 -7
- package/dist/src/modules/commands/lib/registery.d.mts +47 -14
- package/dist/src/modules/commands/lib/registery.mjs +76 -16
- package/dist/src/modules/commands/lib/utils.d.mts +11 -0
- package/dist/src/modules/commands/lib/utils.mjs +14 -0
- package/dist/src/modules/data-tables/components/footer.mjs +3 -0
- package/dist/src/modules/data-tables/hooks/use-context.client.d.mts +2 -2
- package/dist/src/modules/data-tables/lib/types.d.mts +3 -3
- package/dist/src/modules/data-tables/strategies/local/components.mjs +25 -0
- package/dist/src/modules/data-tables/strategies/local/strategy.d.mts +12 -0
- package/dist/src/modules/data-tables/strategies/local/strategy.mjs +31 -0
- package/dist/src/modules/inline/components/inputs/combobox-dropdown.client.d.mts +4 -3
- package/dist/src/modules/inline/components/inputs/combobox.client.d.mts +4 -3
- package/dist/src/modules/inline/components/inputs/date-time.client.d.mts +4 -3
- package/dist/src/modules/inline/components/inputs/editor.client.d.mts +4 -3
- package/dist/src/modules/inline/components/inputs/input.client.d.mts +7 -5
- package/dist/src/modules/inline/components/inputs/select.client.d.mts +4 -3
- package/dist/src/modules/inline/components/inputs/switch.client.d.mts +4 -3
- package/dist/src/modules/inline/lib/variants.d.mts +1 -1
- package/dist/src/modules/router/lib/query-client.d.mts +2 -1
- package/dist/src/modules/router/lib/query-client.mjs +2 -2
- package/dist/src/modules/storage/lib/service.server.d.mts +21 -21
- package/package.json +1 -1
- package/src/modules/commands/components/menus/context-menu.client.tsx +11 -11
- package/src/modules/commands/components/menus/dropdown-menu.client.tsx +9 -10
- package/src/modules/commands/components/menus/floating-menu.client.tsx +9 -10
- package/src/modules/commands/components/menus/inline-menu.client.tsx +9 -10
- package/src/modules/commands/components/menus/responsive-menu.client.tsx +7 -8
- package/src/modules/commands/components/render-command.tsx +17 -13
- package/src/modules/commands/entry.ts +1 -0
- package/src/modules/commands/lib/builder.ts +216 -36
- package/src/modules/commands/lib/registery.ts +210 -47
- package/src/modules/commands/lib/utils.ts +10 -0
- package/src/modules/data-tables/components/footer.tsx +9 -0
- package/src/modules/data-tables/entry.client.ts +1 -0
- package/src/modules/data-tables/hooks/use-context.client.tsx +2 -2
- package/src/modules/data-tables/lib/types.ts +3 -3
- package/src/modules/data-tables/strategies/local/components.tsx +17 -0
- package/src/modules/data-tables/strategies/local/strategy.ts +33 -0
- package/src/modules/inline/components/inputs/combobox-dropdown.client.tsx +11 -5
- package/src/modules/inline/components/inputs/combobox.client.tsx +11 -5
- package/src/modules/inline/components/inputs/date-time.client.tsx +11 -5
- package/src/modules/inline/components/inputs/editor.client.tsx +11 -5
- package/src/modules/inline/components/inputs/input.client.tsx +21 -9
- package/src/modules/inline/components/inputs/select.client.tsx +11 -5
- package/src/modules/inline/components/inputs/switch.client.tsx +11 -5
- package/src/modules/router/entry.ts +1 -0
- package/src/modules/router/lib/query-client.ts +2 -2
- package/src/styles.css +91 -0
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
|
|
4
|
-
import type z from "zod";
|
|
5
4
|
import { Skeleton } from "@/components/entry";
|
|
6
5
|
import { cn } from "@/lib/utils/cn";
|
|
7
6
|
import type { CommandContextLifecycle } from "../../hooks/use-command.client";
|
|
8
|
-
import type { CommandDef } from "../../lib/builder";
|
|
7
|
+
import type { AnyCommandDef, CommandDef, ExtractCommandData } from "../../lib/builder";
|
|
9
8
|
import { RenderCommand } from "../render-command";
|
|
10
9
|
|
|
11
10
|
export type InlineCommandMenuProps<
|
|
12
|
-
|
|
13
|
-
TMeta,
|
|
11
|
+
TCommand extends AnyCommandDef,
|
|
12
|
+
TMeta = object,
|
|
14
13
|
> = HTMLAttributes<HTMLElement> &
|
|
15
14
|
CommandContextLifecycle & {
|
|
16
|
-
data:
|
|
17
|
-
commands:
|
|
15
|
+
data: NoInfer<ExtractCommandData<TCommand>>;
|
|
16
|
+
commands: TCommand[];
|
|
18
17
|
meta?: TMeta;
|
|
19
18
|
fallback?: ReactNode;
|
|
20
19
|
};
|
|
21
20
|
|
|
22
|
-
export function InlineCommandMenu<
|
|
21
|
+
export function InlineCommandMenu<TCommand extends AnyCommandDef, TMeta = object>({
|
|
23
22
|
data,
|
|
24
23
|
meta,
|
|
25
24
|
commands,
|
|
@@ -29,7 +28,7 @@ export function InlineCommandMenu<TSchema extends z.ZodTypeAny, TMeta>({
|
|
|
29
28
|
onSettled,
|
|
30
29
|
className,
|
|
31
30
|
...props
|
|
32
|
-
}: InlineCommandMenuProps<
|
|
31
|
+
}: InlineCommandMenuProps<TCommand, TMeta>) {
|
|
33
32
|
if (!commands.length) return null;
|
|
34
33
|
|
|
35
34
|
return (
|
|
@@ -38,8 +37,8 @@ export function InlineCommandMenu<TSchema extends z.ZodTypeAny, TMeta>({
|
|
|
38
37
|
{commands.map((command, index) => (
|
|
39
38
|
<RenderCommand
|
|
40
39
|
key={index}
|
|
41
|
-
command={command}
|
|
42
|
-
data={data}
|
|
40
|
+
command={command as CommandDef<unknown, TMeta>}
|
|
41
|
+
data={data as unknown}
|
|
43
42
|
meta={meta}
|
|
44
43
|
fallback={fallback}
|
|
45
44
|
ui="inline"
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
-
import type z from "zod";
|
|
5
4
|
import type { CommandContextLifecycle } from "../../hooks/use-command.client";
|
|
6
|
-
import type {
|
|
5
|
+
import type { AnyCommandDef, ExtractCommandData } from "../../lib/builder";
|
|
7
6
|
import { DropdownCommandMenu, DropdownCommandMenuLoading } from "./dropdown-menu.client";
|
|
8
7
|
import { InlineCommandMenu, InlineCommandMenuLoading } from "./inline-menu.client";
|
|
9
8
|
|
|
10
9
|
export type ResponsiveCommandMenuProps<
|
|
11
|
-
|
|
12
|
-
TMeta,
|
|
10
|
+
TCommand extends AnyCommandDef,
|
|
11
|
+
TMeta = object,
|
|
13
12
|
> = CommandContextLifecycle & {
|
|
14
|
-
data:
|
|
15
|
-
commands:
|
|
13
|
+
data: NoInfer<ExtractCommandData<TCommand>>;
|
|
14
|
+
commands: TCommand[];
|
|
16
15
|
meta?: TMeta;
|
|
17
16
|
fallback?: ReactNode;
|
|
18
17
|
};
|
|
19
18
|
|
|
20
|
-
export function ResponsiveCommandMenu<
|
|
21
|
-
props: ResponsiveCommandMenuProps<
|
|
19
|
+
export function ResponsiveCommandMenu<TCommand extends AnyCommandDef, TMeta = object>(
|
|
20
|
+
props: ResponsiveCommandMenuProps<TCommand, TMeta>,
|
|
22
21
|
) {
|
|
23
22
|
return (
|
|
24
23
|
<>
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
-
import type z from "zod";
|
|
5
4
|
import { Allowed } from "@/modules/auth/components/allowed.client";
|
|
6
5
|
import { type CommandContextLifecycle, CommandContextProvider } from "../hooks/use-command.client";
|
|
7
6
|
import type { CommandDef, CommandUI } from "../lib/builder";
|
|
8
7
|
|
|
9
|
-
type RenderCommandProps<
|
|
10
|
-
command: CommandDef<
|
|
11
|
-
data:
|
|
8
|
+
type RenderCommandProps<TData, TMeta> = {
|
|
9
|
+
command: CommandDef<TData, TMeta>;
|
|
10
|
+
data: TData;
|
|
12
11
|
meta?: TMeta;
|
|
13
12
|
fallback?: ReactNode;
|
|
14
13
|
ui: CommandUI;
|
|
@@ -17,22 +16,27 @@ type RenderCommandProps<TSchema extends z.ZodTypeAny, TMeta> = {
|
|
|
17
16
|
/**
|
|
18
17
|
* Command renderer.
|
|
19
18
|
*
|
|
20
|
-
* It
|
|
21
|
-
*
|
|
19
|
+
* It applies cheap visibility/disabled rules, exposes command status via context,
|
|
20
|
+
* and then renders the command content. Validation belongs at execution boundaries.
|
|
22
21
|
*/
|
|
23
|
-
export function RenderCommand<
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
export function RenderCommand<TData, TMeta>(props: RenderCommandProps<TData, TMeta>) {
|
|
23
|
+
// Apply the command's transformer once before dispatching to conditions and render.
|
|
24
|
+
// This lets commands declare a union input type (e.g. Template | Template[]) while
|
|
25
|
+
// working with a single transformed type (e.g. Template[]) internally.
|
|
26
|
+
const data = (
|
|
27
|
+
props.command.transform
|
|
28
|
+
? (props.command.transform as (data: unknown) => unknown)(props.data)
|
|
29
|
+
: props.data
|
|
30
|
+
) as TData;
|
|
29
31
|
|
|
30
32
|
const meta = (props.meta ?? {}) as TMeta;
|
|
31
33
|
|
|
32
34
|
const visibleResult = props.command.visibleWhen?.({ data, meta });
|
|
33
35
|
const disabledResult = props.command.disabledWhen?.({ data, meta });
|
|
34
36
|
|
|
35
|
-
const isVisible = Array.isArray(visibleResult)
|
|
37
|
+
const isVisible = Array.isArray(visibleResult)
|
|
38
|
+
? visibleResult.every(Boolean)
|
|
39
|
+
: (visibleResult ?? true);
|
|
36
40
|
if (!isVisible) return <>{props.fallback}</>;
|
|
37
41
|
|
|
38
42
|
const isDisabled = Array.isArray(disabledResult)
|
|
@@ -1,17 +1,39 @@
|
|
|
1
|
-
import type { z } from "zod";
|
|
2
1
|
import type { Permission } from "@/modules/auth/lib/permissions";
|
|
3
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The different UI contexts in which a command can be rendered.
|
|
5
|
+
*/
|
|
4
6
|
export type CommandUI = "dropdown" | "context" | "table" | "inline" | "custom";
|
|
5
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The status of a command, which can be used to style the command's UI appropriately.
|
|
10
|
+
*/
|
|
6
11
|
export type CommandStatus = "idle" | "pending" | "success" | "error";
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
/**
|
|
14
|
+
* A string tag that can be added to a command for organizational and filtering purposes.
|
|
15
|
+
*/
|
|
16
|
+
export type CommandTag = string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Represents a command definition with its associated types and properties.
|
|
20
|
+
*/
|
|
21
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional — holds heterogeneous command data/meta/tag types.
|
|
22
|
+
export type AnyCommandDef = CommandDef<any, any, any, any>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Condition function that determines whether a command is visible or disabled based on the provided data and meta.
|
|
26
|
+
*/
|
|
27
|
+
export type CommandCondition<TData, TMeta> = (params: {
|
|
28
|
+
data: TData;
|
|
10
29
|
meta: TMeta;
|
|
11
30
|
}) => boolean | boolean[];
|
|
12
31
|
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Render function that defines how a command should be rendered based on the provided data, meta, and UI context.
|
|
34
|
+
*/
|
|
35
|
+
export type CommandRender<TData, TMeta> = (params: {
|
|
36
|
+
data: TData;
|
|
15
37
|
meta: TMeta;
|
|
16
38
|
ui: CommandUI;
|
|
17
39
|
}) => React.ReactNode;
|
|
@@ -19,69 +41,227 @@ export type CommandRender<TSchema extends z.ZodTypeAny, TMeta> = (params: {
|
|
|
19
41
|
/**
|
|
20
42
|
* Final command definition (without name).
|
|
21
43
|
* Name is derived from the object key in `defineCommands`.
|
|
44
|
+
*
|
|
45
|
+
* @typeParam TData - The type `visibleWhen`, `disabledWhen`, and `render` receive.
|
|
46
|
+
* When `.transform()` is used this is the post-transform type.
|
|
47
|
+
* @typeParam TMeta - Shared contextual metadata passed alongside data.
|
|
48
|
+
* @typeParam TTags - Literal tuple of tag strings preserved from the builder's
|
|
49
|
+
* `.tags()` call. Enables compile-time tag filtering in the
|
|
50
|
+
* registry. Defaults to `string[]` when not provided.
|
|
51
|
+
* @typeParam TInput - The type that menus must pass as `data`. Equals `TData`
|
|
52
|
+
* when no `.transform()` is used. When `.transform(fn)` is
|
|
53
|
+
* called, `TInput` captures the pre-transform type so menus
|
|
54
|
+
* remain correctly typed while `render` sees the transformed type.
|
|
55
|
+
* Structurally expressed as the parameter of `transform`.
|
|
22
56
|
*/
|
|
23
|
-
export type CommandDef<
|
|
57
|
+
export type CommandDef<
|
|
58
|
+
TData = unknown,
|
|
59
|
+
TMeta = object,
|
|
60
|
+
TTags extends readonly string[] = string[],
|
|
61
|
+
TInput = TData,
|
|
62
|
+
> = {
|
|
63
|
+
id?: string;
|
|
64
|
+
tags: TTags;
|
|
65
|
+
transform?: (data: TInput) => TData;
|
|
24
66
|
permission?: Permission;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
render: CommandRender<TSchema, TMeta>;
|
|
67
|
+
visibleWhen?: CommandCondition<TData, TMeta>;
|
|
68
|
+
disabledWhen?: CommandCondition<TData, TMeta>;
|
|
69
|
+
render: CommandRender<TData, TMeta>;
|
|
29
70
|
};
|
|
30
71
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Command typed by accepted input data.
|
|
74
|
+
*
|
|
75
|
+
* Useful for APIs that only care about what can be passed in (e.g. table
|
|
76
|
+
* providers that always pass selected rows), not the command's normalized
|
|
77
|
+
* render-time shape.
|
|
78
|
+
*/
|
|
79
|
+
// biome-ignore lint/suspicious/noExplicitAny: hides non-input generics for call-site ergonomics.
|
|
80
|
+
export type CommandFor<TInput, TMeta = object> = CommandDef<any, TMeta, any, TInput>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extracts the **input** data type of a command — what menus must pass as `data`.
|
|
84
|
+
*
|
|
85
|
+
* - When `.transform()` is used, this is the pre-transform type (e.g. `Template | Template[]`).
|
|
86
|
+
* - Otherwise it equals the render-time type.
|
|
87
|
+
*
|
|
88
|
+
* The type is read from the `transform` field's parameter when present, and
|
|
89
|
+
* falls back to the `render` parameter when there is no transformer.
|
|
90
|
+
*/
|
|
91
|
+
export type ExtractCommandData<TCommand extends AnyCommandDef> = [
|
|
92
|
+
NonNullable<TCommand["transform"]>,
|
|
93
|
+
] extends [never]
|
|
94
|
+
? TCommand extends { render: (params: { data: infer TData; meta: any; ui: any }) => any }
|
|
95
|
+
? TData
|
|
96
|
+
: never
|
|
97
|
+
: NonNullable<TCommand["transform"]> extends (data: infer TInput) => any
|
|
98
|
+
? TInput
|
|
99
|
+
: never;
|
|
100
|
+
|
|
101
|
+
// ─── Builder interfaces ────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Builder state after `.$type<TData>()` has been called (or at initial state
|
|
105
|
+
* with `TData = unknown`).
|
|
106
|
+
*
|
|
107
|
+
* @typeParam TData - Current render-time data type (post-transform if used).
|
|
108
|
+
* @typeParam TTags - Literal tag tuple, carried through every method so that
|
|
109
|
+
* the final `CommandDef` retains the literal type.
|
|
110
|
+
* @typeParam TInput - Input data type for menus. Starts equal to `TData`.
|
|
111
|
+
* Freezes to the value of `TData` at the moment `.transform()`
|
|
112
|
+
* is called, while `TData` changes to the transformer's output.
|
|
113
|
+
*/
|
|
114
|
+
type ReadyCommandBuilder<
|
|
115
|
+
TData,
|
|
116
|
+
TMeta,
|
|
117
|
+
TTags extends readonly string[] = string[],
|
|
118
|
+
TInput = TData,
|
|
119
|
+
> = {
|
|
120
|
+
/** Override the data (and optionally meta) type. Resets `TInput` to the new `TData`. */
|
|
121
|
+
$type<TNewData, TNewMeta = TMeta>(): ReadyCommandBuilder<TNewData, TNewMeta, TTags, TNewData>;
|
|
122
|
+
/**
|
|
123
|
+
* Set the command's tags as a literal tuple. The `const` modifier infers
|
|
124
|
+
* string arguments as literals, enabling compile-time tag narrowing in the registry.
|
|
125
|
+
*/
|
|
126
|
+
tags<const TNewTags extends string[]>(
|
|
127
|
+
...tags: TNewTags
|
|
128
|
+
): ReadyCommandBuilder<TData, TMeta, TNewTags, TInput>;
|
|
129
|
+
permission(permission: Permission): ReadyCommandBuilder<TData, TMeta, TTags, TInput>;
|
|
130
|
+
/**
|
|
131
|
+
* Declares a lightweight transformer that runs once per render — before `visibleWhen`,
|
|
132
|
+
* `disabledWhen`, and `render` — collapsing the input type into a single usable shape.
|
|
133
|
+
*
|
|
134
|
+
* Freezes `TInput` to the current `TData` (the pre-transform type) and advances
|
|
135
|
+
* `TData` to `TNewData` (the transformer's return type). This ensures:
|
|
136
|
+
* - Menus are typed against the union input (e.g. `Template | Template[]`)
|
|
137
|
+
* - `visibleWhen` and `render` work with the clean transformed type (e.g. `Template[]`)
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* commandBuilder
|
|
141
|
+
* .$type<Template | Template[]>()
|
|
142
|
+
* .transform((data) => (Array.isArray(data) ? data : [data]))
|
|
143
|
+
* .visibleWhen(({ data }) => data.length > 0) // data: Template[]
|
|
144
|
+
* .render(({ data }) => <ArchiveCommand ids={data.map(i => i.id)} />)
|
|
145
|
+
*/
|
|
146
|
+
transform<TNewData>(
|
|
147
|
+
fn: (data: TData) => TNewData,
|
|
148
|
+
): ReadyCommandBuilder<TNewData, TMeta, TTags, TData>;
|
|
149
|
+
visibleWhen(
|
|
150
|
+
condition: CommandCondition<TData, TMeta>,
|
|
151
|
+
): ReadyCommandBuilder<TData, TMeta, TTags, TInput>;
|
|
152
|
+
disabledWhen(
|
|
153
|
+
condition: CommandCondition<TData, TMeta>,
|
|
154
|
+
): ReadyCommandBuilder<TData, TMeta, TTags, TInput>;
|
|
155
|
+
/** Terminal — produces the final `CommandDef`. */
|
|
156
|
+
render(render: CommandRender<TData, TMeta>): CommandDef<TData, TMeta, TTags, TInput>;
|
|
36
157
|
};
|
|
37
158
|
|
|
38
|
-
|
|
39
|
-
|
|
159
|
+
/**
|
|
160
|
+
* The initial builder returned by `commandBuilder` / `createCommandBuilder()`.
|
|
161
|
+
* Before `.$type<>()` is called, `TData` is `unknown`.
|
|
162
|
+
*/
|
|
163
|
+
type InitialCommandBuilder<TMeta> = ReadyCommandBuilder<unknown, TMeta, string[], unknown> & {
|
|
164
|
+
$type<TNewData, TNewMeta = TMeta>(): ReadyCommandBuilder<TNewData, TNewMeta, string[], TNewData>;
|
|
165
|
+
tags<const TNewTags extends string[]>(
|
|
166
|
+
...tags: TNewTags
|
|
167
|
+
): ReadyCommandBuilder<unknown, TMeta, TNewTags, unknown>;
|
|
40
168
|
};
|
|
41
169
|
|
|
170
|
+
// ─── Internal builder state ────────────────────────────────────────────────────
|
|
171
|
+
|
|
42
172
|
type StartState<TMeta> = {
|
|
173
|
+
tags?: readonly string[];
|
|
43
174
|
permission?: Permission;
|
|
44
175
|
meta?: TMeta;
|
|
45
176
|
};
|
|
46
177
|
|
|
47
|
-
type ReadyState<
|
|
178
|
+
type ReadyState<TData, TMeta, TTags extends readonly string[]> = {
|
|
179
|
+
tags: TTags;
|
|
48
180
|
permission?: Permission;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
181
|
+
// Erased to (data: unknown) => unknown at storage level; CommandDef.transform is typed as
|
|
182
|
+
// CommandNormalize<TInput, TData> and restored via cast in the render() terminal.
|
|
183
|
+
transform?: (data: unknown) => unknown;
|
|
184
|
+
visibleWhen?: CommandCondition<TData, TMeta>;
|
|
185
|
+
disabledWhen?: CommandCondition<TData, TMeta>;
|
|
52
186
|
};
|
|
53
187
|
|
|
188
|
+
// ─── Builder factory functions ─────────────────────────────────────────────────
|
|
189
|
+
|
|
54
190
|
function createInitialBuilder<TMeta>(state: StartState<TMeta>): InitialCommandBuilder<TMeta> {
|
|
191
|
+
const readyBuilder = createReadyBuilder<unknown, TMeta, string[]>({
|
|
192
|
+
tags: (state.tags as string[]) ?? [],
|
|
193
|
+
permission: state.permission,
|
|
194
|
+
});
|
|
195
|
+
|
|
55
196
|
return {
|
|
56
|
-
|
|
57
|
-
|
|
197
|
+
...readyBuilder,
|
|
198
|
+
tags<const TNewTags extends string[]>(...tags: TNewTags) {
|
|
199
|
+
return createReadyBuilder<unknown, TMeta, TNewTags>({
|
|
200
|
+
tags,
|
|
201
|
+
permission: state.permission,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
$type<TNewData, TNewMeta = TMeta>() {
|
|
205
|
+
return createReadyBuilder<TNewData, TNewMeta, string[]>({
|
|
206
|
+
tags: (state.tags as string[]) ?? [],
|
|
207
|
+
permission: state.permission,
|
|
208
|
+
});
|
|
58
209
|
},
|
|
59
|
-
}
|
|
210
|
+
} as InitialCommandBuilder<TMeta>;
|
|
60
211
|
}
|
|
61
212
|
|
|
62
|
-
function createReadyBuilder<
|
|
63
|
-
state: ReadyState<
|
|
64
|
-
): ReadyCommandBuilder<
|
|
213
|
+
function createReadyBuilder<TData, TMeta, TTags extends readonly string[]>(
|
|
214
|
+
state: ReadyState<TData, TMeta, TTags>,
|
|
215
|
+
): ReadyCommandBuilder<TData, TMeta, TTags, TData> {
|
|
65
216
|
return {
|
|
217
|
+
$type<TNewData, TNewMeta = TMeta>() {
|
|
218
|
+
return createReadyBuilder<TNewData, TNewMeta, TTags>({
|
|
219
|
+
tags: state.tags,
|
|
220
|
+
permission: state.permission,
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
tags<const TNewTags extends string[]>(...tags: TNewTags) {
|
|
224
|
+
return createReadyBuilder<TData, TMeta, TNewTags>({
|
|
225
|
+
tags,
|
|
226
|
+
permission: state.permission,
|
|
227
|
+
transform: state.transform,
|
|
228
|
+
visibleWhen: state.visibleWhen as CommandCondition<TData, TMeta> | undefined,
|
|
229
|
+
disabledWhen: state.disabledWhen as CommandCondition<TData, TMeta> | undefined,
|
|
230
|
+
});
|
|
231
|
+
},
|
|
66
232
|
permission(permission: Permission) {
|
|
67
|
-
return createReadyBuilder<
|
|
233
|
+
return createReadyBuilder<TData, TMeta, TTags>({ ...state, permission });
|
|
234
|
+
},
|
|
235
|
+
transform<TNewData>(fn: (data: TData) => TNewData) {
|
|
236
|
+
// TInput freezes to the current TData; TData advances to TNewData.
|
|
237
|
+
// The returned builder is typed ReadyCommandBuilder<TNewData, TMeta, TTags, TData>
|
|
238
|
+
// but createReadyBuilder only tracks TData (output); TInput is enforced via the cast.
|
|
239
|
+
return createReadyBuilder<TNewData, TMeta, TTags>({
|
|
240
|
+
tags: state.tags,
|
|
241
|
+
permission: state.permission,
|
|
242
|
+
transform: fn as (data: unknown) => unknown,
|
|
243
|
+
}) as unknown as ReadyCommandBuilder<TNewData, TMeta, TTags, TData>;
|
|
68
244
|
},
|
|
69
|
-
visibleWhen(condition: CommandCondition<
|
|
70
|
-
return createReadyBuilder<
|
|
245
|
+
visibleWhen(condition: CommandCondition<TData, TMeta>) {
|
|
246
|
+
return createReadyBuilder<TData, TMeta, TTags>({ ...state, visibleWhen: condition });
|
|
71
247
|
},
|
|
72
|
-
disabledWhen(condition: CommandCondition<
|
|
73
|
-
return createReadyBuilder<
|
|
248
|
+
disabledWhen(condition: CommandCondition<TData, TMeta>) {
|
|
249
|
+
return createReadyBuilder<TData, TMeta, TTags>({ ...state, disabledWhen: condition });
|
|
74
250
|
},
|
|
75
|
-
render(render: CommandRender<
|
|
251
|
+
render(render: CommandRender<TData, TMeta>) {
|
|
252
|
+
// The transform field is stored as (data: unknown) => unknown in ReadyState but
|
|
253
|
+
// CommandDef.transform is typed as (data: TInput) => TData. The cast bridges the gap;
|
|
254
|
+
// runtime correctness is guaranteed by the builder's type constraints.
|
|
76
255
|
return {
|
|
256
|
+
tags: state.tags,
|
|
257
|
+
transform: state.transform,
|
|
77
258
|
permission: state.permission,
|
|
78
|
-
schema: state.schema,
|
|
79
259
|
visibleWhen: state.visibleWhen,
|
|
80
260
|
disabledWhen: state.disabledWhen,
|
|
81
261
|
render,
|
|
82
|
-
}
|
|
262
|
+
} as unknown as CommandDef<TData, TMeta, TTags, TData>;
|
|
83
263
|
},
|
|
84
|
-
}
|
|
264
|
+
} as ReadyCommandBuilder<TData, TMeta, TTags, TData>;
|
|
85
265
|
}
|
|
86
266
|
|
|
87
267
|
export function createCommandBuilder<TMeta = object>(): InitialCommandBuilder<TMeta> {
|