@btst/stack 2.5.5 → 2.6.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.
Files changed (101) hide show
  1. package/README.md +23 -0
  2. package/dist/client/components/index.d.cts +9 -9
  3. package/dist/client/components/index.d.mts +9 -9
  4. package/dist/client/components/index.d.ts +9 -9
  5. package/dist/packages/stack/src/plugins/ai-chat/client/components/shared/default-error.cjs +1 -1
  6. package/dist/packages/stack/src/plugins/ai-chat/client/components/shared/default-error.mjs +1 -1
  7. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.cjs +44 -35
  8. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +44 -35
  9. package/dist/packages/stack/src/plugins/blog/client/components/shared/default-error.cjs +1 -1
  10. package/dist/packages/stack/src/plugins/blog/client/components/shared/default-error.mjs +1 -1
  11. package/dist/packages/stack/src/plugins/blog/client/hooks/use-debounce.cjs +22 -0
  12. package/dist/packages/stack/src/plugins/blog/client/hooks/use-debounce.mjs +23 -2
  13. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +12 -6
  14. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +12 -6
  15. package/dist/packages/stack/src/plugins/cms/client/components/shared/default-error.cjs +1 -1
  16. package/dist/packages/stack/src/plugins/cms/client/components/shared/default-error.mjs +1 -1
  17. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +38 -26
  18. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +38 -26
  19. package/dist/packages/stack/src/plugins/form-builder/client/components/shared/default-error.cjs +1 -1
  20. package/dist/packages/stack/src/plugins/form-builder/client/components/shared/default-error.mjs +1 -1
  21. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +32 -20
  22. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +32 -20
  23. package/dist/packages/stack/src/plugins/kanban/client/components/shared/default-error.cjs +1 -1
  24. package/dist/packages/stack/src/plugins/kanban/client/components/shared/default-error.mjs +1 -1
  25. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +6 -3
  26. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +6 -3
  27. package/dist/packages/stack/src/plugins/ui-builder/client/components/page-renderer.cjs +1 -1
  28. package/dist/packages/stack/src/plugins/ui-builder/client/components/page-renderer.mjs +1 -1
  29. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.cjs +3 -1
  30. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +3 -1
  31. package/dist/packages/stack/src/plugins/ui-builder/client/components/shared/default-error.cjs +1 -1
  32. package/dist/packages/stack/src/plugins/ui-builder/client/components/shared/default-error.mjs +1 -1
  33. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +24 -15
  34. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +24 -15
  35. package/dist/packages/ui/src/components/search-select.cjs +13 -3
  36. package/dist/packages/ui/src/components/search-select.mjs +14 -4
  37. package/dist/plugins/ai-chat/client/index.d.cts +17 -4
  38. package/dist/plugins/ai-chat/client/index.d.mts +17 -4
  39. package/dist/plugins/ai-chat/client/index.d.ts +17 -4
  40. package/dist/plugins/blog/client/hooks/index.cjs +3 -0
  41. package/dist/plugins/blog/client/hooks/index.d.cts +7 -226
  42. package/dist/plugins/blog/client/hooks/index.d.mts +7 -226
  43. package/dist/plugins/blog/client/hooks/index.d.ts +7 -226
  44. package/dist/plugins/blog/client/hooks/index.mjs +1 -0
  45. package/dist/plugins/blog/client/index.d.cts +45 -21
  46. package/dist/plugins/blog/client/index.d.mts +45 -21
  47. package/dist/plugins/blog/client/index.d.ts +45 -21
  48. package/dist/plugins/cms/client/index.d.cts +35 -14
  49. package/dist/plugins/cms/client/index.d.mts +35 -14
  50. package/dist/plugins/cms/client/index.d.ts +35 -14
  51. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  52. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  53. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  54. package/dist/plugins/form-builder/client/index.d.cts +32 -14
  55. package/dist/plugins/form-builder/client/index.d.mts +32 -14
  56. package/dist/plugins/form-builder/client/index.d.ts +32 -14
  57. package/dist/plugins/kanban/api/index.d.cts +1 -1
  58. package/dist/plugins/kanban/api/index.d.mts +1 -1
  59. package/dist/plugins/kanban/api/index.d.ts +1 -1
  60. package/dist/plugins/kanban/client/components/index.d.cts +5 -5
  61. package/dist/plugins/kanban/client/components/index.d.mts +5 -5
  62. package/dist/plugins/kanban/client/components/index.d.ts +5 -5
  63. package/dist/plugins/kanban/client/index.d.cts +25 -10
  64. package/dist/plugins/kanban/client/index.d.mts +25 -10
  65. package/dist/plugins/kanban/client/index.d.ts +25 -10
  66. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  67. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  68. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  69. package/dist/plugins/route-docs/client/index.d.cts +4 -4
  70. package/dist/plugins/route-docs/client/index.d.mts +4 -4
  71. package/dist/plugins/route-docs/client/index.d.ts +4 -4
  72. package/dist/plugins/ui-builder/client/components/index.d.cts +1 -1
  73. package/dist/plugins/ui-builder/client/components/index.d.mts +1 -1
  74. package/dist/plugins/ui-builder/client/components/index.d.ts +1 -1
  75. package/dist/plugins/ui-builder/client/index.d.cts +35 -16
  76. package/dist/plugins/ui-builder/client/index.d.mts +35 -16
  77. package/dist/plugins/ui-builder/client/index.d.ts +35 -16
  78. package/dist/shared/stack.CNLHlv7r.d.mts +228 -0
  79. package/dist/shared/stack.CQAZwXhV.d.cts +228 -0
  80. package/dist/shared/stack.D3BsrpAz.d.ts +228 -0
  81. package/package.json +19 -2
  82. package/src/__tests__/page-component-overrides.test.tsx +147 -0
  83. package/src/plugins/ai-chat/client/components/shared/default-error.tsx +1 -1
  84. package/src/plugins/ai-chat/client/plugin.tsx +60 -32
  85. package/src/plugins/blog/client/components/shared/default-error.tsx +2 -1
  86. package/src/plugins/blog/client/hooks/index.tsx +1 -0
  87. package/src/plugins/blog/client/plugin.tsx +41 -6
  88. package/src/plugins/cms/client/components/shared/default-error.tsx +3 -2
  89. package/src/plugins/cms/client/plugin.tsx +65 -32
  90. package/src/plugins/form-builder/client/components/shared/default-error.tsx +3 -2
  91. package/src/plugins/form-builder/client/plugin.tsx +56 -23
  92. package/src/plugins/kanban/client/components/shared/default-error.tsx +3 -2
  93. package/src/plugins/kanban/client/plugin.tsx +23 -3
  94. package/src/plugins/ui-builder/client/components/page-renderer.tsx +5 -3
  95. package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +2 -0
  96. package/src/plugins/ui-builder/client/components/shared/default-error.tsx +3 -2
  97. package/src/plugins/ui-builder/client/overrides.ts +10 -1
  98. package/src/plugins/ui-builder/client/plugin.tsx +41 -15
  99. package/dist/shared/{stack.CxNeGV2z.d.mts → stack.Ba_Ks8qi.d.mts} +9 -9
  100. package/dist/shared/{stack.DSxTDZBQ.d.cts → stack.CFqqZUes.d.cts} +9 -9
  101. package/dist/shared/{stack.BFcg0tDz.d.ts → stack.DMobugrZ.d.ts} +9 -9
@@ -4,7 +4,7 @@ import { AlertCircle } from "lucide-react";
4
4
  import { Button } from "@workspace/ui/components/button";
5
5
 
6
6
  interface DefaultErrorProps {
7
- error: Error;
7
+ error: unknown;
8
8
  resetErrorBoundary?: () => void;
9
9
  }
10
10
 
@@ -18,7 +18,8 @@ export function DefaultError({ error, resetErrorBoundary }: DefaultErrorProps) {
18
18
  Something went wrong
19
19
  </h3>
20
20
  <p className="text-sm text-muted-foreground mb-4 max-w-sm">
21
- {error.message || "An unexpected error occurred"}
21
+ {(error instanceof Error ? error.message : undefined) ||
22
+ "An unexpected error occurred"}
22
23
  </p>
23
24
  {resetErrorBoundary && (
24
25
  <Button variant="outline" onClick={resetErrorBoundary}>
@@ -6,6 +6,7 @@ import {
6
6
  runClientHookWithShim,
7
7
  } from "@btst/stack/plugins/client";
8
8
  import { createRoute } from "@btst/yar";
9
+ import type { ComponentType } from "react";
9
10
  import type { QueryClient } from "@tanstack/react-query";
10
11
  import type { CMSApiRouter } from "../api";
11
12
  import { createCMSQueryKeys } from "../query-keys";
@@ -129,6 +130,22 @@ export interface CMSClientConfig {
129
130
  headers?: Headers;
130
131
  /** Optional hooks for customizing behavior (authorization, redirects, etc.) */
131
132
  hooks?: CMSClientHooks;
133
+
134
+ /**
135
+ * Optional page component overrides.
136
+ * Replace any plugin page with a custom React component.
137
+ * The built-in component is used as the fallback when not provided.
138
+ */
139
+ pageComponents?: {
140
+ /** Replaces the CMS dashboard page */
141
+ dashboard?: ComponentType;
142
+ /** Replaces the content list page */
143
+ contentList?: ComponentType<{ typeSlug: string }>;
144
+ /** Replaces the new content editor page */
145
+ newContent?: ComponentType<{ typeSlug: string }>;
146
+ /** Replaces the edit content editor page */
147
+ editContent?: ComponentType<{ typeSlug: string; id: string }>;
148
+ };
132
149
  }
133
150
 
134
151
  /**
@@ -469,38 +486,54 @@ export const cmsClientPlugin = (config: CMSClientConfig) =>
469
486
  name: "cms",
470
487
 
471
488
  routes: () => ({
472
- dashboard: createRoute("/cms", () => ({
473
- PageComponent: () => <DashboardPageComponent />,
474
- loader: createDashboardLoader(config),
475
- meta: createDashboardMeta(),
476
- })),
477
-
478
- contentList: createRoute("/cms/:typeSlug", ({ params }) => ({
479
- PageComponent: () => (
480
- <ContentListPageComponent typeSlug={params.typeSlug} />
481
- ),
482
- loader: createContentListLoader(params.typeSlug, config),
483
- meta: createContentListMeta(params.typeSlug, config),
484
- })),
485
-
486
- newContent: createRoute("/cms/:typeSlug/new", ({ params }) => ({
487
- PageComponent: () => (
488
- <ContentEditorPageComponent typeSlug={params.typeSlug} />
489
- ),
490
- loader: createContentEditorLoader(params.typeSlug, undefined, config),
491
- meta: createContentEditorMeta(params.typeSlug, undefined, config),
492
- })),
493
-
494
- editContent: createRoute("/cms/:typeSlug/:id", ({ params }) => ({
495
- PageComponent: () => (
496
- <ContentEditorPageComponent
497
- typeSlug={params.typeSlug}
498
- id={params.id}
499
- />
500
- ),
501
- loader: createContentEditorLoader(params.typeSlug, params.id, config),
502
- meta: createContentEditorMeta(params.typeSlug, params.id, config),
503
- })),
489
+ dashboard: createRoute("/cms", () => {
490
+ const CustomDashboard = config.pageComponents?.dashboard;
491
+ return {
492
+ PageComponent: CustomDashboard ?? (() => <DashboardPageComponent />),
493
+ loader: createDashboardLoader(config),
494
+ meta: createDashboardMeta(),
495
+ };
496
+ }),
497
+
498
+ contentList: createRoute("/cms/:typeSlug", ({ params }) => {
499
+ const CustomContentList = config.pageComponents?.contentList;
500
+ return {
501
+ PageComponent: CustomContentList
502
+ ? () => <CustomContentList typeSlug={params.typeSlug} />
503
+ : () => <ContentListPageComponent typeSlug={params.typeSlug} />,
504
+ loader: createContentListLoader(params.typeSlug, config),
505
+ meta: createContentListMeta(params.typeSlug, config),
506
+ };
507
+ }),
508
+
509
+ newContent: createRoute("/cms/:typeSlug/new", ({ params }) => {
510
+ const CustomNewContent = config.pageComponents?.newContent;
511
+ return {
512
+ PageComponent: CustomNewContent
513
+ ? () => <CustomNewContent typeSlug={params.typeSlug} />
514
+ : () => <ContentEditorPageComponent typeSlug={params.typeSlug} />,
515
+ loader: createContentEditorLoader(params.typeSlug, undefined, config),
516
+ meta: createContentEditorMeta(params.typeSlug, undefined, config),
517
+ };
518
+ }),
519
+
520
+ editContent: createRoute("/cms/:typeSlug/:id", ({ params }) => {
521
+ const CustomEditContent = config.pageComponents?.editContent;
522
+ return {
523
+ PageComponent: CustomEditContent
524
+ ? () => (
525
+ <CustomEditContent typeSlug={params.typeSlug} id={params.id} />
526
+ )
527
+ : () => (
528
+ <ContentEditorPageComponent
529
+ typeSlug={params.typeSlug}
530
+ id={params.id}
531
+ />
532
+ ),
533
+ loader: createContentEditorLoader(params.typeSlug, params.id, config),
534
+ meta: createContentEditorMeta(params.typeSlug, params.id, config),
535
+ };
536
+ }),
504
537
  }),
505
538
 
506
539
  sitemap: async () => {
@@ -4,7 +4,7 @@ import { AlertCircle } from "lucide-react";
4
4
  import { Button } from "@workspace/ui/components/button";
5
5
 
6
6
  interface DefaultErrorProps {
7
- error: Error;
7
+ error: unknown;
8
8
  resetErrorBoundary?: () => void;
9
9
  }
10
10
 
@@ -18,7 +18,8 @@ export function DefaultError({ error, resetErrorBoundary }: DefaultErrorProps) {
18
18
  Something went wrong
19
19
  </h3>
20
20
  <p className="text-sm text-muted-foreground mb-4 max-w-sm">
21
- {error.message || "An unexpected error occurred"}
21
+ {(error instanceof Error ? error.message : undefined) ||
22
+ "An unexpected error occurred"}
22
23
  </p>
23
24
  {resetErrorBoundary && (
24
25
  <Button variant="outline" onClick={resetErrorBoundary}>
@@ -7,6 +7,7 @@ import {
7
7
  runClientHookWithShim,
8
8
  } from "@btst/stack/plugins/client";
9
9
  import { createRoute } from "@btst/yar";
10
+ import type { ComponentType } from "react";
10
11
  import type { QueryClient } from "@tanstack/react-query";
11
12
  import type { FormBuilderApiRouter } from "../api";
12
13
  import { createFormBuilderQueryKeys } from "../query-keys";
@@ -126,6 +127,22 @@ export interface FormBuilderClientConfig {
126
127
  headers?: Headers;
127
128
  /** Optional hooks for customizing behavior (authorization, redirects, etc.) */
128
129
  hooks?: FormBuilderClientHooks;
130
+
131
+ /**
132
+ * Optional page component overrides.
133
+ * Replace any plugin page with a custom React component.
134
+ * The built-in component is used as the fallback when not provided.
135
+ */
136
+ pageComponents?: {
137
+ /** Replaces the form list page */
138
+ formList?: ComponentType;
139
+ /** Replaces the new form page */
140
+ newForm?: ComponentType;
141
+ /** Replaces the form editor page */
142
+ editForm?: ComponentType<{ id: string }>;
143
+ /** Replaces the form submissions page */
144
+ submissions?: ComponentType<{ formId: string }>;
145
+ };
129
146
  }
130
147
 
131
148
  /**
@@ -476,29 +493,45 @@ export const formBuilderClientPlugin = (config: FormBuilderClientConfig) =>
476
493
  name: "form-builder",
477
494
 
478
495
  routes: () => ({
479
- formList: createRoute("/forms", () => ({
480
- PageComponent: () => <FormListPageComponent />,
481
- loader: createFormListLoader(config),
482
- meta: createFormListMeta(),
483
- })),
484
-
485
- newForm: createRoute("/forms/new", () => ({
486
- PageComponent: () => <FormBuilderPageComponent />,
487
- loader: createFormBuilderLoader(undefined, config),
488
- meta: createFormBuilderMeta(undefined, config),
489
- })),
490
-
491
- editForm: createRoute("/forms/:id/edit", ({ params }) => ({
492
- PageComponent: () => <FormBuilderPageComponent id={params.id} />,
493
- loader: createFormBuilderLoader(params.id, config),
494
- meta: createFormBuilderMeta(params.id, config),
495
- })),
496
-
497
- submissions: createRoute("/forms/:id/submissions", ({ params }) => ({
498
- PageComponent: () => <SubmissionsPageComponent formId={params.id} />,
499
- loader: createSubmissionsLoader(params.id, config),
500
- meta: createSubmissionsMeta(params.id, config),
501
- })),
496
+ formList: createRoute("/forms", () => {
497
+ const CustomFormList = config.pageComponents?.formList;
498
+ return {
499
+ PageComponent: CustomFormList ?? (() => <FormListPageComponent />),
500
+ loader: createFormListLoader(config),
501
+ meta: createFormListMeta(),
502
+ };
503
+ }),
504
+
505
+ newForm: createRoute("/forms/new", () => {
506
+ const CustomNewForm = config.pageComponents?.newForm;
507
+ return {
508
+ PageComponent: CustomNewForm ?? (() => <FormBuilderPageComponent />),
509
+ loader: createFormBuilderLoader(undefined, config),
510
+ meta: createFormBuilderMeta(undefined, config),
511
+ };
512
+ }),
513
+
514
+ editForm: createRoute("/forms/:id/edit", ({ params }) => {
515
+ const CustomEditForm = config.pageComponents?.editForm;
516
+ return {
517
+ PageComponent: CustomEditForm
518
+ ? () => <CustomEditForm id={params.id} />
519
+ : () => <FormBuilderPageComponent id={params.id} />,
520
+ loader: createFormBuilderLoader(params.id, config),
521
+ meta: createFormBuilderMeta(params.id, config),
522
+ };
523
+ }),
524
+
525
+ submissions: createRoute("/forms/:id/submissions", ({ params }) => {
526
+ const CustomSubmissions = config.pageComponents?.submissions;
527
+ return {
528
+ PageComponent: CustomSubmissions
529
+ ? () => <CustomSubmissions formId={params.id} />
530
+ : () => <SubmissionsPageComponent formId={params.id} />,
531
+ loader: createSubmissionsLoader(params.id, config),
532
+ meta: createSubmissionsMeta(params.id, config),
533
+ };
534
+ }),
502
535
  }),
503
536
 
504
537
  sitemap: async () => {
@@ -4,7 +4,7 @@ import { AlertCircle, RefreshCw } from "lucide-react";
4
4
  import { Button } from "@workspace/ui/components/button";
5
5
 
6
6
  interface DefaultErrorProps {
7
- error?: Error;
7
+ error?: unknown;
8
8
  reset?: () => void;
9
9
  }
10
10
 
@@ -19,7 +19,8 @@ export function DefaultError({ error, reset }: DefaultErrorProps) {
19
19
  </div>
20
20
  <h3 className="text-lg font-semibold mb-2">Something went wrong</h3>
21
21
  <p className="text-muted-foreground max-w-md mb-4">
22
- {error?.message || "An unexpected error occurred. Please try again."}
22
+ {(error instanceof Error ? error.message : undefined) ||
23
+ "An unexpected error occurred. Please try again."}
23
24
  </p>
24
25
  {reset && (
25
26
  <Button onClick={reset} variant="outline">
@@ -5,6 +5,7 @@ import {
5
5
  runClientHookWithShim,
6
6
  } from "@btst/stack/plugins/client";
7
7
  import { createRoute } from "@btst/yar";
8
+ import type { ComponentType } from "react";
8
9
  import type { QueryClient } from "@tanstack/react-query";
9
10
  import type { KanbanApiRouter } from "../api";
10
11
  import { createKanbanQueryKeys } from "../query-keys";
@@ -79,6 +80,20 @@ export interface KanbanClientConfig {
79
80
 
80
81
  /** Optional headers for SSR (e.g., forwarding cookies) */
81
82
  headers?: Headers;
83
+
84
+ /**
85
+ * Optional page component overrides.
86
+ * Replace any plugin page with a custom React component.
87
+ * The built-in component is used as the fallback when not provided.
88
+ */
89
+ pageComponents?: {
90
+ /** Replaces the boards list page */
91
+ boards?: ComponentType;
92
+ /** Replaces the new board page */
93
+ newBoard?: ComponentType;
94
+ /** Replaces the board detail page */
95
+ board?: ComponentType<{ boardId: string }>;
96
+ };
82
97
  }
83
98
 
84
99
  /**
@@ -407,22 +422,27 @@ export const kanbanClientPlugin = (config: KanbanClientConfig) =>
407
422
 
408
423
  routes: () => ({
409
424
  boards: createRoute("/kanban", () => {
425
+ const CustomBoards = config.pageComponents?.boards;
410
426
  return {
411
- PageComponent: () => <BoardsListPageComponent />,
427
+ PageComponent: CustomBoards ?? (() => <BoardsListPageComponent />),
412
428
  loader: createBoardsLoader(config),
413
429
  meta: createBoardsListMeta(config),
414
430
  };
415
431
  }),
416
432
  newBoard: createRoute("/kanban/new", () => {
433
+ const CustomNewBoard = config.pageComponents?.newBoard;
417
434
  return {
418
- PageComponent: NewBoardPageComponent,
435
+ PageComponent: CustomNewBoard ?? NewBoardPageComponent,
419
436
  loader: createNewBoardLoader(config),
420
437
  meta: createNewBoardMeta(config),
421
438
  };
422
439
  }),
423
440
  board: createRoute("/kanban/:boardId", ({ params: { boardId } }) => {
441
+ const CustomBoard = config.pageComponents?.board;
424
442
  return {
425
- PageComponent: () => <BoardPageComponent boardId={boardId} />,
443
+ PageComponent: CustomBoard
444
+ ? () => <CustomBoard boardId={boardId} />
445
+ : () => <BoardPageComponent boardId={boardId} />,
426
446
  loader: createBoardLoader(boardId, config),
427
447
  meta: createBoardMeta(boardId, config),
428
448
  };
@@ -29,13 +29,15 @@ function DefaultLoadingComponent(): ReactNode {
29
29
  /**
30
30
  * Default error component for PageRenderer
31
31
  */
32
- function DefaultErrorComponent({ error }: { error: Error }): ReactNode {
32
+ function DefaultErrorComponent({ error }: { error: unknown }): ReactNode {
33
33
  return (
34
34
  <div className="flex flex-col items-center justify-center min-h-[200px] p-4">
35
35
  <div className="text-destructive font-medium">
36
36
  {uiBuilderLocalization.pageRenderer.error}
37
37
  </div>
38
- <div className="text-sm text-muted-foreground mt-2">{error.message}</div>
38
+ <div className="text-sm text-muted-foreground mt-2">
39
+ {error instanceof Error ? error.message : String(error)}
40
+ </div>
39
41
  </div>
40
42
  );
41
43
  }
@@ -65,7 +67,7 @@ export interface PageRendererProps {
65
67
  /** Custom loading component */
66
68
  LoadingComponent?: ComponentType;
67
69
  /** Custom error component */
68
- ErrorComponent?: ComponentType<{ error: Error }>;
70
+ ErrorComponent?: ComponentType<{ error: unknown }>;
69
71
  /** Custom not found component */
70
72
  NotFoundComponent?: ComponentType;
71
73
  /** Additional className for the container */
@@ -215,6 +215,7 @@ function PageBuilderPageContent({
215
215
  navigate,
216
216
  Link,
217
217
  componentRegistry: customRegistry,
218
+ functionRegistry,
218
219
  } = usePluginOverrides<UIBuilderPluginOverrides>("ui-builder");
219
220
  const basePath = useBasePath();
220
221
 
@@ -463,6 +464,7 @@ function PageBuilderPageContent({
463
464
  }
464
465
  onVariablesChange={handleVariablesChange}
465
466
  componentRegistry={componentRegistry}
467
+ functionRegistry={functionRegistry}
466
468
  persistLayerStore={false}
467
469
  allowVariableEditing={true}
468
470
  allowPagesCreation={false}
@@ -4,7 +4,7 @@ import { AlertCircle } from "lucide-react";
4
4
  import { Button } from "@workspace/ui/components/button";
5
5
 
6
6
  interface DefaultErrorProps {
7
- error: Error;
7
+ error: unknown;
8
8
  resetErrorBoundary?: () => void;
9
9
  }
10
10
 
@@ -18,7 +18,8 @@ export function DefaultError({ error, resetErrorBoundary }: DefaultErrorProps) {
18
18
  Something went wrong
19
19
  </h3>
20
20
  <p className="text-sm text-muted-foreground mb-4 max-w-sm">
21
- {error.message || "An unexpected error occurred"}
21
+ {(error instanceof Error ? error.message : undefined) ||
22
+ "An unexpected error occurred"}
22
23
  </p>
23
24
  {resetErrorBoundary && (
24
25
  <Button variant="outline" onClick={resetErrorBoundary}>
@@ -1,5 +1,8 @@
1
1
  import type { ComponentType } from "react";
2
- import type { ComponentRegistry } from "@workspace/ui/components/ui-builder/types";
2
+ import type {
3
+ ComponentRegistry,
4
+ FunctionRegistry,
5
+ } from "@workspace/ui/components/ui-builder/types";
3
6
  import type { UIBuilderClientHooks } from "../types";
4
7
 
5
8
  /**
@@ -63,6 +66,12 @@ export interface UIBuilderPluginOverrides {
63
66
  */
64
67
  componentRegistry?: ComponentRegistry;
65
68
 
69
+ /**
70
+ * Function registry for resolving bindable event handlers (onClick, onSubmit, etc.)
71
+ * in the preview modal and layer renderer.
72
+ */
73
+ functionRegistry?: FunctionRegistry;
74
+
66
75
  /**
67
76
  * Base path for UI Builder admin pages (default: /pages/ui-builder)
68
77
  */
@@ -6,6 +6,7 @@ import {
6
6
  runClientHookWithShim,
7
7
  } from "@btst/stack/plugins/client";
8
8
  import { createRoute } from "@btst/yar";
9
+ import type { ComponentType } from "react";
9
10
  import type { QueryClient } from "@tanstack/react-query";
10
11
  import type { CMSApiRouter } from "../../cms/api";
11
12
  import { createCMSQueryKeys } from "../../cms/query-keys";
@@ -48,6 +49,20 @@ export interface UIBuilderClientConfig {
48
49
  hooks?: UIBuilderClientHooks;
49
50
  /** Component registry to use for the UI Builder */
50
51
  componentRegistry?: ComponentRegistry;
52
+
53
+ /**
54
+ * Optional page component overrides.
55
+ * Replace any plugin page with a custom React component.
56
+ * The built-in component is used as the fallback when not provided.
57
+ */
58
+ pageComponents?: {
59
+ /** Replaces the page list page */
60
+ pageList?: ComponentType;
61
+ /** Replaces the new page builder page */
62
+ newPage?: ComponentType;
63
+ /** Replaces the edit page builder page */
64
+ editPage?: ComponentType<{ id: string }>;
65
+ };
51
66
  }
52
67
 
53
68
  /**
@@ -290,23 +305,34 @@ export const uiBuilderClientPlugin = (config: UIBuilderClientConfig) =>
290
305
  name: "ui-builder",
291
306
 
292
307
  routes: () => ({
293
- pageList: createRoute("/ui-builder", () => ({
294
- PageComponent: () => <PageListPageComponent />,
295
- loader: createPageListLoader(config),
296
- meta: createPageListMeta(),
297
- })),
308
+ pageList: createRoute("/ui-builder", () => {
309
+ const CustomPageList = config.pageComponents?.pageList;
310
+ return {
311
+ PageComponent: CustomPageList ?? (() => <PageListPageComponent />),
312
+ loader: createPageListLoader(config),
313
+ meta: createPageListMeta(),
314
+ };
315
+ }),
298
316
 
299
- newPage: createRoute("/ui-builder/new", () => ({
300
- PageComponent: () => <PageBuilderPageComponent />,
301
- loader: createPageBuilderLoader(undefined, config),
302
- meta: createPageBuilderMeta(undefined, config),
303
- })),
317
+ newPage: createRoute("/ui-builder/new", () => {
318
+ const CustomNewPage = config.pageComponents?.newPage;
319
+ return {
320
+ PageComponent: CustomNewPage ?? (() => <PageBuilderPageComponent />),
321
+ loader: createPageBuilderLoader(undefined, config),
322
+ meta: createPageBuilderMeta(undefined, config),
323
+ };
324
+ }),
304
325
 
305
- editPage: createRoute("/ui-builder/:id/edit", ({ params }) => ({
306
- PageComponent: () => <PageBuilderPageComponent id={params.id} />,
307
- loader: createPageBuilderLoader(params.id, config),
308
- meta: createPageBuilderMeta(params.id, config),
309
- })),
326
+ editPage: createRoute("/ui-builder/:id/edit", ({ params }) => {
327
+ const CustomEditPage = config.pageComponents?.editPage;
328
+ return {
329
+ PageComponent: CustomEditPage
330
+ ? () => <CustomEditPage id={params.id} />
331
+ : () => <PageBuilderPageComponent id={params.id} />,
332
+ loader: createPageBuilderLoader(params.id, config),
333
+ meta: createPageBuilderMeta(params.id, config),
334
+ };
335
+ }),
310
336
  }),
311
337
 
312
338
  sitemap: async () => {
@@ -8,11 +8,11 @@ import { QueryClient } from '@tanstack/react-query';
8
8
  declare const createBoardSchema: z.ZodObject<{
9
9
  description: z.ZodOptional<z.ZodString>;
10
10
  name: z.ZodString;
11
+ createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
12
+ updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
11
13
  slug: z.ZodOptional<z.ZodString>;
12
14
  ownerId: z.ZodOptional<z.ZodString>;
13
15
  organizationId: z.ZodOptional<z.ZodString>;
14
- createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
15
- updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
16
16
  }, z.core.$strip>;
17
17
  declare const updateBoardSchema: z.ZodObject<{
18
18
  createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
@@ -26,9 +26,9 @@ declare const updateBoardSchema: z.ZodObject<{
26
26
  }, z.core.$strip>;
27
27
  declare const createColumnSchema: z.ZodObject<{
28
28
  title: z.ZodString;
29
- boardId: z.ZodString;
30
29
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
31
30
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
31
+ boardId: z.ZodString;
32
32
  order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
33
33
  }, z.core.$strip>;
34
34
  declare const updateColumnSchema: z.ZodObject<{
@@ -324,11 +324,11 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
324
324
  body: z.ZodObject<{
325
325
  description: z.ZodOptional<z.ZodString>;
326
326
  name: z.ZodString;
327
+ createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
328
+ updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
327
329
  slug: z.ZodOptional<z.ZodString>;
328
330
  ownerId: z.ZodOptional<z.ZodString>;
329
331
  organizationId: z.ZodOptional<z.ZodString>;
330
- createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
331
- updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
332
332
  }, z.core.$strip>;
333
333
  }, {
334
334
  columns: ColumnWithTasks[];
@@ -346,11 +346,11 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
346
346
  body: z.ZodObject<{
347
347
  description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
348
348
  name: z.ZodOptional<z.ZodString>;
349
+ createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
350
+ updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
349
351
  slug: z.ZodOptional<z.ZodString>;
350
352
  ownerId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
351
353
  organizationId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
352
- createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
353
- updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
354
354
  }, z.core.$strip>;
355
355
  }, Board>;
356
356
  readonly deleteBoard: better_call.StrictEndpoint<"/boards/:id", {
@@ -362,9 +362,9 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
362
362
  method: "POST";
363
363
  body: z.ZodObject<{
364
364
  title: z.ZodString;
365
- boardId: z.ZodString;
366
365
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
367
366
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
367
+ boardId: z.ZodString;
368
368
  order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
369
369
  }, z.core.$strip>;
370
370
  }, Column>;
@@ -372,9 +372,9 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
372
372
  method: "PUT";
373
373
  body: z.ZodObject<{
374
374
  title: z.ZodOptional<z.ZodString>;
375
- boardId: z.ZodOptional<z.ZodString>;
376
375
  createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
377
376
  updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
377
+ boardId: z.ZodOptional<z.ZodString>;
378
378
  order: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodNumber>>>;
379
379
  }, z.core.$strip>;
380
380
  }, Column>;
@@ -8,11 +8,11 @@ import { QueryClient } from '@tanstack/react-query';
8
8
  declare const createBoardSchema: z.ZodObject<{
9
9
  description: z.ZodOptional<z.ZodString>;
10
10
  name: z.ZodString;
11
+ createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
12
+ updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
11
13
  slug: z.ZodOptional<z.ZodString>;
12
14
  ownerId: z.ZodOptional<z.ZodString>;
13
15
  organizationId: z.ZodOptional<z.ZodString>;
14
- createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
15
- updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
16
16
  }, z.core.$strip>;
17
17
  declare const updateBoardSchema: z.ZodObject<{
18
18
  createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
@@ -26,9 +26,9 @@ declare const updateBoardSchema: z.ZodObject<{
26
26
  }, z.core.$strip>;
27
27
  declare const createColumnSchema: z.ZodObject<{
28
28
  title: z.ZodString;
29
- boardId: z.ZodString;
30
29
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
31
30
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
31
+ boardId: z.ZodString;
32
32
  order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
33
33
  }, z.core.$strip>;
34
34
  declare const updateColumnSchema: z.ZodObject<{
@@ -324,11 +324,11 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
324
324
  body: z.ZodObject<{
325
325
  description: z.ZodOptional<z.ZodString>;
326
326
  name: z.ZodString;
327
+ createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
328
+ updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
327
329
  slug: z.ZodOptional<z.ZodString>;
328
330
  ownerId: z.ZodOptional<z.ZodString>;
329
331
  organizationId: z.ZodOptional<z.ZodString>;
330
- createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
331
- updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
332
332
  }, z.core.$strip>;
333
333
  }, {
334
334
  columns: ColumnWithTasks[];
@@ -346,11 +346,11 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
346
346
  body: z.ZodObject<{
347
347
  description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
348
348
  name: z.ZodOptional<z.ZodString>;
349
+ createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
350
+ updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
349
351
  slug: z.ZodOptional<z.ZodString>;
350
352
  ownerId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
351
353
  organizationId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
352
- createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
353
- updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
354
354
  }, z.core.$strip>;
355
355
  }, Board>;
356
356
  readonly deleteBoard: better_call.StrictEndpoint<"/boards/:id", {
@@ -362,9 +362,9 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
362
362
  method: "POST";
363
363
  body: z.ZodObject<{
364
364
  title: z.ZodString;
365
- boardId: z.ZodString;
366
365
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
367
366
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
367
+ boardId: z.ZodString;
368
368
  order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
369
369
  }, z.core.$strip>;
370
370
  }, Column>;
@@ -372,9 +372,9 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
372
372
  method: "PUT";
373
373
  body: z.ZodObject<{
374
374
  title: z.ZodOptional<z.ZodString>;
375
- boardId: z.ZodOptional<z.ZodString>;
376
375
  createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
377
376
  updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
377
+ boardId: z.ZodOptional<z.ZodString>;
378
378
  order: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodNumber>>>;
379
379
  }, z.core.$strip>;
380
380
  }, Column>;