@tulip-systems/core 0.9.0 → 0.10.1

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.
Files changed (135) hide show
  1. package/dist/commands.d.mts +3 -2
  2. package/dist/commands.mjs +2 -1
  3. package/dist/components/client.d.mts +2 -1
  4. package/dist/components/client.mjs +2 -1
  5. package/dist/components/editor/extensions/file-handler/extension.d.mts +1 -1
  6. package/dist/components/editor/extensions/image/extension.d.mts +1 -1
  7. package/dist/components/editor/extensions/skeleton/extension.mjs +1 -1
  8. package/dist/components/editor/lib/constants.d.mts +1 -1
  9. package/dist/components/editor/lib/extensions.d.mts +1 -1
  10. package/dist/components/editor/lib/helpers.d.mts +5 -1
  11. package/dist/components/editor/lib/helpers.mjs +8 -1
  12. package/dist/components/layouts/root-layout.server.d.mts +3 -2
  13. package/dist/components/layouts/root-layout.server.mjs +1 -3
  14. package/dist/components/server.d.mts +2 -2
  15. package/dist/components/themes/color-theme-provider.client.d.mts +27 -0
  16. package/dist/components/themes/color-theme-provider.client.mjs +59 -0
  17. package/dist/components/themes/color-theme.d.mts +29 -0
  18. package/dist/components/themes/color-theme.mjs +32 -0
  19. package/dist/components/ui/badge.d.mts +1 -1
  20. package/dist/components/ui/button.d.mts +1 -1
  21. package/dist/components/ui/item.d.mts +1 -1
  22. package/dist/components.d.mts +3 -2
  23. package/dist/components.mjs +3 -2
  24. package/dist/data-tables/client.d.mts +2 -1
  25. package/dist/data-tables/client.mjs +2 -1
  26. package/dist/modules/auth/handler/create-client.client.d.mts +4 -4
  27. package/dist/modules/commands/components/menus/context-menu.client.d.mts +6 -7
  28. package/dist/modules/commands/components/menus/dropdown-menu.client.d.mts +6 -7
  29. package/dist/modules/commands/components/menus/floating-menu.client.d.mts +6 -7
  30. package/dist/modules/commands/components/menus/inline-menu.client.d.mts +6 -7
  31. package/dist/modules/commands/components/menus/responsive-menu.client.d.mts +5 -6
  32. package/dist/modules/commands/components/render-command.mjs +3 -5
  33. package/dist/modules/commands/lib/builder.d.mts +114 -18
  34. package/dist/modules/commands/lib/builder.mjs +42 -7
  35. package/dist/modules/commands/lib/registery.d.mts +47 -14
  36. package/dist/modules/commands/lib/registery.mjs +76 -16
  37. package/dist/modules/commands/lib/utils.d.mts +11 -0
  38. package/dist/modules/commands/lib/utils.mjs +14 -0
  39. package/dist/modules/data-tables/components/footer.mjs +3 -0
  40. package/dist/modules/data-tables/hooks/use-context.client.d.mts +2 -2
  41. package/dist/modules/data-tables/lib/types.d.mts +3 -3
  42. package/dist/modules/data-tables/strategies/local/components.mjs +25 -0
  43. package/dist/modules/data-tables/strategies/local/strategy.d.mts +12 -0
  44. package/dist/modules/data-tables/strategies/local/strategy.mjs +31 -0
  45. package/dist/modules/inline/components/inputs/combobox-dropdown.client.d.mts +4 -3
  46. package/dist/modules/inline/components/inputs/combobox.client.d.mts +4 -3
  47. package/dist/modules/inline/components/inputs/date-time.client.d.mts +4 -3
  48. package/dist/modules/inline/components/inputs/editor.client.d.mts +4 -3
  49. package/dist/modules/inline/components/inputs/input.client.d.mts +7 -5
  50. package/dist/modules/inline/components/inputs/select.client.d.mts +4 -3
  51. package/dist/modules/inline/components/inputs/switch.client.d.mts +4 -3
  52. package/dist/modules/inline/lib/variants.d.mts +1 -1
  53. package/dist/modules/router/lib/query-client.d.mts +2 -1
  54. package/dist/modules/router/lib/query-client.mjs +2 -2
  55. package/dist/modules/storage/components/dropzone.client.d.mts +2 -2
  56. package/dist/modules/storage/lib/service.server.d.mts +21 -21
  57. package/dist/router.d.mts +2 -1
  58. package/dist/router.mjs +2 -1
  59. package/dist/src/components/editor/extensions/file-handler/extension.d.mts +1 -1
  60. package/dist/src/components/editor/extensions/image/extension.d.mts +1 -1
  61. package/dist/src/components/editor/extensions/skeleton/extension.mjs +1 -1
  62. package/dist/src/components/editor/lib/constants.d.mts +1 -1
  63. package/dist/src/components/editor/lib/extensions.d.mts +1 -1
  64. package/dist/src/components/editor/lib/helpers.d.mts +5 -1
  65. package/dist/src/components/editor/lib/helpers.mjs +8 -1
  66. package/dist/src/components/layouts/root-layout.server.d.mts +3 -2
  67. package/dist/src/components/layouts/root-layout.server.mjs +1 -3
  68. package/dist/src/components/themes/color-theme-provider.client.d.mts +27 -0
  69. package/dist/src/components/themes/color-theme-provider.client.mjs +59 -0
  70. package/dist/src/components/themes/color-theme.d.mts +29 -0
  71. package/dist/src/components/themes/color-theme.mjs +32 -0
  72. package/dist/src/components/ui/badge.d.mts +1 -1
  73. package/dist/src/components/ui/button-group.d.mts +1 -1
  74. package/dist/src/components/ui/button.d.mts +2 -2
  75. package/dist/src/components/ui/field.client.d.mts +1 -1
  76. package/dist/src/modules/commands/components/menus/context-menu.client.d.mts +6 -7
  77. package/dist/src/modules/commands/components/menus/dropdown-menu.client.d.mts +6 -7
  78. package/dist/src/modules/commands/components/menus/floating-menu.client.d.mts +6 -7
  79. package/dist/src/modules/commands/components/menus/inline-menu.client.d.mts +6 -7
  80. package/dist/src/modules/commands/components/menus/responsive-menu.client.d.mts +5 -6
  81. package/dist/src/modules/commands/components/render-command.mjs +3 -5
  82. package/dist/src/modules/commands/lib/builder.d.mts +114 -18
  83. package/dist/src/modules/commands/lib/builder.mjs +42 -7
  84. package/dist/src/modules/commands/lib/registery.d.mts +47 -14
  85. package/dist/src/modules/commands/lib/registery.mjs +76 -16
  86. package/dist/src/modules/commands/lib/utils.d.mts +11 -0
  87. package/dist/src/modules/commands/lib/utils.mjs +14 -0
  88. package/dist/src/modules/data-tables/components/footer.mjs +3 -0
  89. package/dist/src/modules/data-tables/hooks/use-context.client.d.mts +2 -2
  90. package/dist/src/modules/data-tables/lib/types.d.mts +3 -3
  91. package/dist/src/modules/data-tables/strategies/local/components.mjs +25 -0
  92. package/dist/src/modules/data-tables/strategies/local/strategy.d.mts +12 -0
  93. package/dist/src/modules/data-tables/strategies/local/strategy.mjs +31 -0
  94. package/dist/src/modules/inline/components/inputs/combobox-dropdown.client.d.mts +4 -3
  95. package/dist/src/modules/inline/components/inputs/combobox.client.d.mts +4 -3
  96. package/dist/src/modules/inline/components/inputs/date-time.client.d.mts +4 -3
  97. package/dist/src/modules/inline/components/inputs/editor.client.d.mts +4 -3
  98. package/dist/src/modules/inline/components/inputs/input.client.d.mts +7 -5
  99. package/dist/src/modules/inline/components/inputs/select.client.d.mts +4 -3
  100. package/dist/src/modules/inline/components/inputs/switch.client.d.mts +4 -3
  101. package/dist/src/modules/router/lib/query-client.d.mts +2 -1
  102. package/dist/src/modules/router/lib/query-client.mjs +2 -2
  103. package/package.json +1 -1
  104. package/src/components/editor/lib/helpers.ts +8 -0
  105. package/src/components/entry.client.ts +1 -1
  106. package/src/components/entry.ts +1 -1
  107. package/src/components/layouts/root-layout.server.tsx +4 -2
  108. package/src/components/themes/color-theme-provider.client.tsx +82 -0
  109. package/src/components/themes/color-theme.ts +32 -0
  110. package/src/modules/commands/components/menus/context-menu.client.tsx +11 -11
  111. package/src/modules/commands/components/menus/dropdown-menu.client.tsx +9 -10
  112. package/src/modules/commands/components/menus/floating-menu.client.tsx +9 -10
  113. package/src/modules/commands/components/menus/inline-menu.client.tsx +9 -10
  114. package/src/modules/commands/components/menus/responsive-menu.client.tsx +7 -8
  115. package/src/modules/commands/components/render-command.tsx +17 -13
  116. package/src/modules/commands/entry.ts +1 -0
  117. package/src/modules/commands/lib/builder.ts +216 -36
  118. package/src/modules/commands/lib/registery.ts +210 -47
  119. package/src/modules/commands/lib/utils.ts +10 -0
  120. package/src/modules/data-tables/components/footer.tsx +9 -0
  121. package/src/modules/data-tables/entry.client.ts +1 -0
  122. package/src/modules/data-tables/hooks/use-context.client.tsx +2 -2
  123. package/src/modules/data-tables/lib/types.ts +3 -3
  124. package/src/modules/data-tables/strategies/local/components.tsx +17 -0
  125. package/src/modules/data-tables/strategies/local/strategy.ts +33 -0
  126. package/src/modules/inline/components/inputs/combobox-dropdown.client.tsx +11 -5
  127. package/src/modules/inline/components/inputs/combobox.client.tsx +11 -5
  128. package/src/modules/inline/components/inputs/date-time.client.tsx +11 -5
  129. package/src/modules/inline/components/inputs/editor.client.tsx +11 -5
  130. package/src/modules/inline/components/inputs/input.client.tsx +21 -9
  131. package/src/modules/inline/components/inputs/select.client.tsx +11 -5
  132. package/src/modules/inline/components/inputs/switch.client.tsx +11 -5
  133. package/src/modules/router/entry.ts +1 -0
  134. package/src/modules/router/lib/query-client.ts +2 -2
  135. package/src/styles.css +317 -2
@@ -0,0 +1,32 @@
1
+ import type { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
2
+ import z from "zod";
3
+
4
+ /**
5
+ * Shared cookie key used by both server reads and client writes.
6
+ */
7
+ export const COLOR_THEME_STORAGE_KEY = "tulip-color-theme";
8
+
9
+ /**
10
+ * All supported color theme names.
11
+ */
12
+ export const colorThemes = ["tulip", "rose", "iris", "sage"] as const;
13
+ export type ColorTheme = (typeof colorThemes)[number];
14
+
15
+ /**
16
+ * Validators
17
+ */
18
+ export const colorThemeSchema = z.enum(colorThemes, { error: "Invalid color theme" });
19
+
20
+ /**
21
+ * Resolves a validated color theme from either a raw string value or a cookie store.
22
+ * Invalid, missing, or unsupported values are treated as undefined.
23
+ */
24
+ export function getColorThemeValue(
25
+ cookieStore: ReadonlyRequestCookies,
26
+ fallback: ColorTheme = "tulip",
27
+ ): ColorTheme {
28
+ const theme = cookieStore.get(COLOR_THEME_STORAGE_KEY)?.value;
29
+ const parsed = colorThemeSchema.safeParse(theme);
30
+
31
+ return parsed.success ? parsed.data : fallback;
32
+ }
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
 
3
3
  import type { ComponentProps, ReactNode } from "react";
4
- import type z from "zod";
5
4
  import {
6
5
  ContextMenu,
7
6
  ContextMenuContent,
@@ -10,15 +9,16 @@ import {
10
9
  } from "@/components/ui/context-menu.client";
11
10
  import { cn } from "@/lib/entry";
12
11
  import type { CommandContextLifecycle } from "../../hooks/use-command.client";
13
- import type { CommandDef } from "../../lib/builder";
12
+ import type { AnyCommandDef, CommandDef, ExtractCommandData } from "../../lib/builder";
14
13
  import { RenderCommand } from "../render-command";
15
14
 
16
- export type ContextCommandMenuContentProps<TSchema extends z.ZodTypeAny, TMeta> = ComponentProps<
17
- typeof ContextMenuContent
18
- > &
15
+ export type ContextCommandMenuContentProps<
16
+ TCommand extends AnyCommandDef,
17
+ TMeta = object,
18
+ > = ComponentProps<typeof ContextMenuContent> &
19
19
  CommandContextLifecycle & {
20
- data: unknown;
21
- commands: Array<CommandDef<z.ZodTypeAny, TMeta>>;
20
+ data: NoInfer<ExtractCommandData<TCommand>>;
21
+ commands: TCommand[];
22
22
  meta?: TMeta;
23
23
  fallback?: ReactNode;
24
24
  emptyLabel?: string;
@@ -37,7 +37,7 @@ export const ContextCommandMenuTrigger = ContextMenuTrigger;
37
37
  /**
38
38
  * ContextCommandMenuContent
39
39
  */
40
- export function ContextCommandMenuContent<TSchema extends z.ZodTypeAny, TMeta>({
40
+ export function ContextCommandMenuContent<TCommand extends AnyCommandDef, TMeta = object>({
41
41
  data,
42
42
  commands,
43
43
  meta,
@@ -48,7 +48,7 @@ export function ContextCommandMenuContent<TSchema extends z.ZodTypeAny, TMeta>({
48
48
  className,
49
49
  emptyLabel = "Geen acties beschikbaar",
50
50
  ...props
51
- }: ContextCommandMenuContentProps<TSchema, TMeta>) {
51
+ }: ContextCommandMenuContentProps<TCommand, TMeta>) {
52
52
  if (!commands.length) {
53
53
  return (
54
54
  <ContextMenuContent {...props} className={cn("min-w-40", className)}>
@@ -64,8 +64,8 @@ export function ContextCommandMenuContent<TSchema extends z.ZodTypeAny, TMeta>({
64
64
  {commands.map((command, index) => (
65
65
  <RenderCommand
66
66
  key={index}
67
- command={command}
68
- data={data}
67
+ command={command as CommandDef<unknown, TMeta>}
68
+ data={data as unknown}
69
69
  meta={meta}
70
70
  fallback={fallback}
71
71
  ui="context"
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { MoreHorizontal } from "lucide-react";
4
4
  import React, { type ComponentProps, type HTMLAttributes, type ReactNode } from "react";
5
- import type z from "zod";
6
5
  import { Skeleton } from "@/components/entry";
7
6
  import { Button } from "@/components/ui/button";
8
7
  import {
@@ -12,21 +11,21 @@ import {
12
11
  } from "@/components/ui/dropdown-menu.client";
13
12
  import { cn } from "@/lib/utils/cn";
14
13
  import type { CommandContextLifecycle } from "../../hooks/use-command.client";
15
- import type { CommandDef } from "../../lib/builder";
14
+ import type { AnyCommandDef, CommandDef, ExtractCommandData } from "../../lib/builder";
16
15
  import { RenderCommand } from "../render-command";
17
16
 
18
17
  export type DropdownCommandMenuProps<
19
- TSchema extends z.ZodTypeAny,
20
- TMeta,
18
+ TCommand extends AnyCommandDef,
19
+ TMeta = object,
21
20
  > = HTMLAttributes<HTMLElement> &
22
21
  CommandContextLifecycle & {
23
- data: unknown;
24
- commands: Array<CommandDef<z.ZodTypeAny, TMeta>>;
22
+ data: NoInfer<ExtractCommandData<TCommand>>;
23
+ commands: TCommand[];
25
24
  meta?: TMeta;
26
25
  fallback?: ReactNode;
27
26
  };
28
27
 
29
- export function DropdownCommandMenu<TSchema extends z.ZodTypeAny, TMeta>({
28
+ export function DropdownCommandMenu<TCommand extends AnyCommandDef, TMeta = object>({
30
29
  data,
31
30
  commands,
32
31
  meta,
@@ -36,7 +35,7 @@ export function DropdownCommandMenu<TSchema extends z.ZodTypeAny, TMeta>({
36
35
  onSettled,
37
36
  className,
38
37
  ...props
39
- }: DropdownCommandMenuProps<TSchema, TMeta>) {
38
+ }: DropdownCommandMenuProps<TCommand, TMeta>) {
40
39
  const [open, setOpen] = React.useState(false);
41
40
 
42
41
  if (!commands.length) return null;
@@ -58,8 +57,8 @@ export function DropdownCommandMenu<TSchema extends z.ZodTypeAny, TMeta>({
58
57
  {commands.map((command, index) => (
59
58
  <RenderCommand
60
59
  key={index}
61
- command={command}
62
- data={data}
60
+ command={command as CommandDef<unknown, TMeta>}
61
+ data={data as unknown}
63
62
  meta={meta}
64
63
  fallback={fallback}
65
64
  ui="dropdown"
@@ -1,26 +1,25 @@
1
1
  "use client";
2
2
 
3
3
  import type { HTMLAttributes, ReactNode } from "react";
4
- import type z from "zod";
5
4
  import { cn } from "@/lib/utils/cn";
6
5
  import type { CommandContextLifecycle } from "../../hooks/use-command.client";
7
- import type { CommandDef } from "../../lib/builder";
6
+ import type { AnyCommandDef, CommandDef, ExtractCommandData } from "../../lib/builder";
8
7
  import { RenderCommand } from "../render-command";
9
8
 
10
9
  export type FloatingCommandMenuProps<
11
- TSchema extends z.ZodTypeAny,
12
- TMeta,
10
+ TCommand extends AnyCommandDef,
11
+ TMeta = object,
13
12
  > = HTMLAttributes<HTMLElement> &
14
13
  CommandContextLifecycle & {
15
- data: unknown;
16
- commands: Array<CommandDef<z.ZodTypeAny, TMeta>>;
14
+ data: NoInfer<ExtractCommandData<TCommand>>;
15
+ commands: TCommand[];
17
16
  meta?: TMeta;
18
17
  fallback?: ReactNode;
19
18
  state: "open" | "closed";
20
19
  emptyLabel?: string;
21
20
  };
22
21
 
23
- export function FloatingCommandMenu<TSchema extends z.ZodTypeAny, TMeta>({
22
+ export function FloatingCommandMenu<TCommand extends AnyCommandDef, TMeta = object>({
24
23
  data,
25
24
  commands,
26
25
  meta,
@@ -32,7 +31,7 @@ export function FloatingCommandMenu<TSchema extends z.ZodTypeAny, TMeta>({
32
31
  className,
33
32
  emptyLabel = "Geen acties beschikbaar",
34
33
  ...props
35
- }: FloatingCommandMenuProps<TSchema, TMeta>) {
34
+ }: FloatingCommandMenuProps<TCommand, TMeta>) {
36
35
  return (
37
36
  <div
38
37
  {...props}
@@ -46,8 +45,8 @@ export function FloatingCommandMenu<TSchema extends z.ZodTypeAny, TMeta>({
46
45
  {commands.map((command, index) => (
47
46
  <RenderCommand
48
47
  key={index}
49
- command={command}
50
- data={data}
48
+ command={command as CommandDef<unknown, TMeta>}
49
+ data={data as unknown}
51
50
  meta={meta}
52
51
  fallback={fallback}
53
52
  ui="table"
@@ -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
- TSchema extends z.ZodTypeAny,
13
- TMeta,
11
+ TCommand extends AnyCommandDef,
12
+ TMeta = object,
14
13
  > = HTMLAttributes<HTMLElement> &
15
14
  CommandContextLifecycle & {
16
- data: unknown;
17
- commands: Array<CommandDef<z.ZodTypeAny, TMeta>>;
15
+ data: NoInfer<ExtractCommandData<TCommand>>;
16
+ commands: TCommand[];
18
17
  meta?: TMeta;
19
18
  fallback?: ReactNode;
20
19
  };
21
20
 
22
- export function InlineCommandMenu<TSchema extends z.ZodTypeAny, TMeta>({
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<TSchema, TMeta>) {
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 { CommandDef } from "../../lib/builder";
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
- TSchema extends z.ZodTypeAny,
12
- TMeta,
10
+ TCommand extends AnyCommandDef,
11
+ TMeta = object,
13
12
  > = CommandContextLifecycle & {
14
- data: unknown;
15
- commands: Array<CommandDef<z.ZodTypeAny, TMeta>>;
13
+ data: NoInfer<ExtractCommandData<TCommand>>;
14
+ commands: TCommand[];
16
15
  meta?: TMeta;
17
16
  fallback?: ReactNode;
18
17
  };
19
18
 
20
- export function ResponsiveCommandMenu<TSchema extends z.ZodTypeAny, TMeta>(
21
- props: ResponsiveCommandMenuProps<TSchema, TMeta>,
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<TSchema extends z.ZodTypeAny, TMeta> = {
10
- command: CommandDef<TSchema, TMeta>;
11
- data: z.input<TSchema>;
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 validates input data through the command schema, applies optional visibility/disabled rules,
21
- * exposes command status via context, and then renders the command content.
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<TSchema extends z.ZodTypeAny, TMeta>(
24
- props: RenderCommandProps<TSchema, TMeta>,
25
- ) {
26
- const parsedData = props.command.schema.safeParse(props.data);
27
- if (!parsedData.success) return <>{props.fallback}</>;
28
- const data = parsedData.data;
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) ? visibleResult.every(Boolean) : (visibleResult ?? true);
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)
@@ -3,3 +3,4 @@
3
3
  */
4
4
  export * from "./lib/builder";
5
5
  export * from "./lib/registery";
6
+ export * from "./lib/utils";
@@ -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
- export type CommandCondition<TSchema extends z.ZodTypeAny, TMeta> = (params: {
9
- data: z.output<TSchema>;
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
- export type CommandRender<TSchema extends z.ZodTypeAny, TMeta> = (params: {
14
- data: z.output<TSchema>;
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<TSchema extends z.ZodTypeAny = z.ZodTypeAny, TMeta = object> = {
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
- schema: TSchema;
26
- visibleWhen?: CommandCondition<TSchema, TMeta>;
27
- disabledWhen?: CommandCondition<TSchema, TMeta>;
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
- type ReadyCommandBuilder<TSchema extends z.ZodTypeAny, TMeta> = {
32
- permission(permission: Permission): ReadyCommandBuilder<TSchema, TMeta>;
33
- visibleWhen(condition: CommandCondition<TSchema, TMeta>): ReadyCommandBuilder<TSchema, TMeta>;
34
- disabledWhen(condition: CommandCondition<TSchema, TMeta>): ReadyCommandBuilder<TSchema, TMeta>;
35
- render(render: CommandRender<TSchema, TMeta>): CommandDef<TSchema, TMeta>;
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
- type InitialCommandBuilder<TMeta> = {
39
- input<TNextSchema extends z.ZodTypeAny>(schema: TNextSchema): ReadyCommandBuilder<TNextSchema, TMeta>;
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<TSchema extends z.ZodTypeAny, TMeta> = {
178
+ type ReadyState<TData, TMeta, TTags extends readonly string[]> = {
179
+ tags: TTags;
48
180
  permission?: Permission;
49
- schema: TSchema;
50
- visibleWhen?: CommandCondition<TSchema, TMeta>;
51
- disabledWhen?: CommandCondition<TSchema, TMeta>;
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
- input<TNextSchema extends z.ZodTypeAny>(schema: TNextSchema) {
57
- return createReadyBuilder<TNextSchema, TMeta>({ permission: state.permission, schema });
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<TSchema extends z.ZodTypeAny, TMeta>(
63
- state: ReadyState<TSchema, TMeta>,
64
- ): ReadyCommandBuilder<TSchema, TMeta> {
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<TSchema, TMeta>({ ...state, permission });
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<TSchema, TMeta>) {
70
- return createReadyBuilder<TSchema, TMeta>({ ...state, visibleWhen: condition });
245
+ visibleWhen(condition: CommandCondition<TData, TMeta>) {
246
+ return createReadyBuilder<TData, TMeta, TTags>({ ...state, visibleWhen: condition });
71
247
  },
72
- disabledWhen(condition: CommandCondition<TSchema, TMeta>) {
73
- return createReadyBuilder<TSchema, TMeta>({ ...state, disabledWhen: condition });
248
+ disabledWhen(condition: CommandCondition<TData, TMeta>) {
249
+ return createReadyBuilder<TData, TMeta, TTags>({ ...state, disabledWhen: condition });
74
250
  },
75
- render(render: CommandRender<TSchema, TMeta>) {
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> {