@btst/stack 2.3.0 → 2.5.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 (208) hide show
  1. package/dist/packages/stack/src/client/components/compose.cjs +1 -2
  2. package/dist/packages/stack/src/client/components/compose.mjs +1 -2
  3. package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.cjs +71 -0
  4. package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.mjs +68 -0
  5. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +87 -54
  6. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +87 -54
  7. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +2 -2
  8. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +2 -2
  9. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +89 -22
  10. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +90 -23
  11. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +110 -33
  12. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +112 -35
  13. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +1 -1
  14. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +1 -1
  15. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.cjs +14 -21
  16. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +15 -22
  17. package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +17 -1
  18. package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +17 -1
  19. package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +28 -45
  20. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +22 -39
  21. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.cjs +15 -2
  22. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.mjs +16 -3
  23. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.cjs +24 -1
  24. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.mjs +24 -1
  25. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.cjs +26 -0
  26. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.mjs +24 -0
  27. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.cjs +30 -1
  28. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.mjs +30 -1
  29. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -0
  30. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -0
  31. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +23 -27
  32. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +24 -28
  33. package/dist/packages/stack/src/plugins/cms/api/mutations.cjs +48 -0
  34. package/dist/packages/stack/src/plugins/cms/api/mutations.mjs +46 -0
  35. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +21 -18
  36. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +21 -18
  37. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +11 -15
  38. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +12 -16
  39. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +58 -62
  40. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +58 -62
  41. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +12 -12
  42. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +13 -13
  43. package/dist/packages/stack/src/plugins/kanban/api/mutations.cjs +91 -0
  44. package/dist/packages/stack/src/plugins/kanban/api/mutations.mjs +87 -0
  45. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +92 -118
  46. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +89 -115
  47. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +7 -3
  48. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +7 -3
  49. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +22 -29
  50. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +23 -30
  51. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.cjs +89 -0
  52. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +89 -0
  53. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +8 -8
  54. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +9 -9
  55. package/dist/packages/stack/src/plugins/utils.cjs +42 -0
  56. package/dist/packages/stack/src/plugins/utils.mjs +41 -1
  57. package/dist/plugins/ai-chat/api/index.d.cts +1 -1
  58. package/dist/plugins/ai-chat/api/index.d.mts +1 -1
  59. package/dist/plugins/ai-chat/api/index.d.ts +1 -1
  60. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  61. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  62. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  63. package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +92 -0
  64. package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +84 -0
  65. package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +84 -0
  66. package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +84 -0
  67. package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +88 -0
  68. package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
  69. package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
  70. package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
  71. package/dist/plugins/ai-chat/client/index.d.cts +10 -10
  72. package/dist/plugins/ai-chat/client/index.d.mts +10 -10
  73. package/dist/plugins/ai-chat/client/index.d.ts +10 -10
  74. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  75. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  76. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  77. package/dist/plugins/blog/api/index.d.cts +2 -2
  78. package/dist/plugins/blog/api/index.d.mts +2 -2
  79. package/dist/plugins/blog/api/index.d.ts +2 -2
  80. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  81. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  82. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  83. package/dist/plugins/blog/client/index.d.cts +13 -13
  84. package/dist/plugins/blog/client/index.d.mts +13 -13
  85. package/dist/plugins/blog/client/index.d.ts +13 -13
  86. package/dist/plugins/blog/query-keys.d.cts +2 -2
  87. package/dist/plugins/blog/query-keys.d.mts +2 -2
  88. package/dist/plugins/blog/query-keys.d.ts +2 -2
  89. package/dist/plugins/client/index.cjs +1 -0
  90. package/dist/plugins/client/index.d.cts +8 -1
  91. package/dist/plugins/client/index.d.mts +8 -1
  92. package/dist/plugins/client/index.d.ts +8 -1
  93. package/dist/plugins/client/index.mjs +1 -1
  94. package/dist/plugins/cms/api/index.cjs +2 -0
  95. package/dist/plugins/cms/api/index.d.cts +2 -2
  96. package/dist/plugins/cms/api/index.d.mts +2 -2
  97. package/dist/plugins/cms/api/index.d.ts +2 -2
  98. package/dist/plugins/cms/api/index.mjs +1 -0
  99. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  100. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  101. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  102. package/dist/plugins/cms/client/index.d.cts +6 -6
  103. package/dist/plugins/cms/client/index.d.mts +6 -6
  104. package/dist/plugins/cms/client/index.d.ts +6 -6
  105. package/dist/plugins/cms/query-keys.d.cts +2 -2
  106. package/dist/plugins/cms/query-keys.d.mts +2 -2
  107. package/dist/plugins/cms/query-keys.d.ts +2 -2
  108. package/dist/plugins/form-builder/api/index.d.cts +2 -2
  109. package/dist/plugins/form-builder/api/index.d.mts +2 -2
  110. package/dist/plugins/form-builder/api/index.d.ts +2 -2
  111. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  112. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  113. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  114. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  115. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  116. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  117. package/dist/plugins/form-builder/client/index.d.cts +6 -6
  118. package/dist/plugins/form-builder/client/index.d.mts +6 -6
  119. package/dist/plugins/form-builder/client/index.d.ts +6 -6
  120. package/dist/plugins/form-builder/query-keys.d.cts +2 -2
  121. package/dist/plugins/form-builder/query-keys.d.mts +2 -2
  122. package/dist/plugins/form-builder/query-keys.d.ts +2 -2
  123. package/dist/plugins/kanban/api/index.cjs +4 -0
  124. package/dist/plugins/kanban/api/index.d.cts +1 -1
  125. package/dist/plugins/kanban/api/index.d.mts +1 -1
  126. package/dist/plugins/kanban/api/index.d.ts +1 -1
  127. package/dist/plugins/kanban/api/index.mjs +1 -0
  128. package/dist/plugins/kanban/client/index.d.cts +12 -12
  129. package/dist/plugins/kanban/client/index.d.mts +12 -12
  130. package/dist/plugins/kanban/client/index.d.ts +12 -12
  131. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  132. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  133. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  134. package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
  135. package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
  136. package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
  137. package/dist/plugins/ui-builder/client/index.d.cts +3 -3
  138. package/dist/plugins/ui-builder/client/index.d.mts +3 -3
  139. package/dist/plugins/ui-builder/client/index.d.ts +3 -3
  140. package/dist/plugins/ui-builder/index.d.cts +2 -2
  141. package/dist/plugins/ui-builder/index.d.mts +2 -2
  142. package/dist/plugins/ui-builder/index.d.ts +2 -2
  143. package/dist/shared/{stack.C-WUPMT6.d.cts → stack.B2xZTSiO.d.cts} +4 -4
  144. package/dist/shared/{stack.B1EeBt1b.d.ts → stack.B58oHdqm.d.mts} +33 -3
  145. package/dist/shared/{stack.CVDTkMoO.d.mts → stack.B8QD11QU.d.cts} +7 -7
  146. package/dist/shared/{stack.CVDTkMoO.d.cts → stack.B8QD11QU.d.mts} +7 -7
  147. package/dist/shared/{stack.CVDTkMoO.d.ts → stack.B8QD11QU.d.ts} +7 -7
  148. package/dist/shared/{stack.CIP6QS9l.d.ts → stack.BDVEpue1.d.ts} +1 -1
  149. package/dist/shared/{stack.C5dtIncc.d.mts → stack.BTvbxZvw.d.cts} +1 -1
  150. package/dist/shared/{stack.DaOcgmrM.d.ts → stack.BV9hnvu4.d.cts} +31 -7
  151. package/dist/shared/{stack.DaOcgmrM.d.cts → stack.BV9hnvu4.d.mts} +31 -7
  152. package/dist/shared/{stack.DaOcgmrM.d.mts → stack.BV9hnvu4.d.ts} +31 -7
  153. package/dist/shared/{stack.DdI5W6MB.d.mts → stack.BozPgbrZ.d.cts} +19 -19
  154. package/dist/shared/{stack.DdI5W6MB.d.ts → stack.BozPgbrZ.d.mts} +19 -19
  155. package/dist/shared/{stack.DdI5W6MB.d.cts → stack.BozPgbrZ.d.ts} +19 -19
  156. package/dist/shared/{stack.CP68pFEH.d.mts → stack.C9Mg2Q46.d.cts} +33 -3
  157. package/dist/shared/{stack.BeSm90va.d.ts → stack.CTDVxbrA.d.ts} +72 -14
  158. package/dist/shared/{stack.C-Ptrz8s.d.ts → stack.Cj_zKww4.d.ts} +4 -4
  159. package/dist/shared/{stack.TIBF2AOx.d.ts → stack.CxaFNQCV.d.mts} +89 -34
  160. package/dist/shared/{stack.CMh_EdxW.d.cts → stack.D-b5zbPm.d.cts} +72 -14
  161. package/dist/shared/{stack.Dw0Ly2TM.d.cts → stack.DTtmJPQO.d.mts} +1 -1
  162. package/dist/shared/{stack.BKfolAyK.d.ts → stack.DXnclTG7.d.ts} +11 -11
  163. package/dist/shared/{stack.snB1EDP7.d.cts → stack.DaZM10cp.d.cts} +11 -11
  164. package/dist/shared/{stack.Dg09R0oB.d.mts → stack.FVWf2JhZ.d.mts} +72 -14
  165. package/dist/shared/{stack.BIXEI6v_.d.mts → stack.cfCkioTe.d.mts} +11 -11
  166. package/dist/shared/{stack.6fUOjLs9.d.mts → stack.dH7u-TJH.d.mts} +4 -4
  167. package/dist/shared/{stack.BpolpQpf.d.cts → stack.j75TpKh2.d.ts} +89 -34
  168. package/dist/shared/{stack.rTy7-wQU.d.mts → stack.n1_i1p2B.d.cts} +89 -34
  169. package/dist/shared/{stack.IdtKDRka.d.cts → stack.sO33ZDhK.d.ts} +33 -3
  170. package/package.json +14 -1
  171. package/src/client/components/compose.tsx +7 -4
  172. package/src/plugins/ai-chat/api/page-tools.ts +111 -0
  173. package/src/plugins/ai-chat/api/plugin.ts +228 -72
  174. package/src/plugins/ai-chat/client/components/chat-input.tsx +2 -2
  175. package/src/plugins/ai-chat/client/components/chat-interface.tsx +154 -58
  176. package/src/plugins/ai-chat/client/components/chat-layout.tsx +166 -32
  177. package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +1 -1
  178. package/src/plugins/ai-chat/client/context/page-ai-context.tsx +240 -0
  179. package/src/plugins/ai-chat/client/plugin.tsx +23 -31
  180. package/src/plugins/ai-chat/schemas.ts +16 -0
  181. package/src/plugins/blog/api/plugin.ts +31 -47
  182. package/src/plugins/blog/client/components/forms/post-forms.tsx +29 -2
  183. package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +28 -0
  184. package/src/plugins/blog/client/components/pages/fill-blog-form-handler.ts +38 -0
  185. package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +33 -1
  186. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +20 -0
  187. package/src/plugins/blog/client/plugin.tsx +36 -39
  188. package/src/plugins/client/index.ts +5 -1
  189. package/src/plugins/cms/api/index.ts +4 -0
  190. package/src/plugins/cms/api/mutations.ts +84 -0
  191. package/src/plugins/cms/api/plugin.ts +23 -17
  192. package/src/plugins/cms/client/plugin.tsx +18 -21
  193. package/src/plugins/cms/types.ts +7 -7
  194. package/src/plugins/form-builder/api/plugin.ts +64 -64
  195. package/src/plugins/form-builder/client/plugin.tsx +19 -18
  196. package/src/plugins/form-builder/types.ts +19 -24
  197. package/src/plugins/kanban/api/index.ts +6 -0
  198. package/src/plugins/kanban/api/mutations.ts +169 -0
  199. package/src/plugins/kanban/api/plugin.ts +123 -136
  200. package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
  201. package/src/plugins/kanban/client/plugin.tsx +35 -41
  202. package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
  203. package/src/plugins/ui-builder/client/plugin.tsx +11 -10
  204. package/src/plugins/ui-builder/types.ts +4 -4
  205. package/src/plugins/utils.ts +92 -1
  206. package/dist/shared/{stack.CBON0dWL.d.mts → stack.BQmuNl5p.d.cts} +2 -2
  207. package/dist/shared/{stack.CBON0dWL.d.ts → stack.BQmuNl5p.d.mts} +2 -2
  208. package/dist/shared/{stack.CBON0dWL.d.cts → stack.BQmuNl5p.d.ts} +2 -2
@@ -22,9 +22,12 @@ import { toast } from "sonner";
22
22
  import UIBuilder from "@workspace/ui/components/ui-builder";
23
23
  import type {
24
24
  ComponentLayer,
25
+ ComponentRegistry,
25
26
  Variable,
26
27
  } from "@workspace/ui/components/ui-builder/types";
27
28
 
29
+ import { useLayerStore } from "@workspace/ui/lib/ui-builder/store/layer-store";
30
+ import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
28
31
  import {
29
32
  useSuspenseUIBuilderPage,
30
33
  useCreateUIBuilderPage,
@@ -39,6 +42,104 @@ export interface PageBuilderPageProps {
39
42
  id?: string;
40
43
  }
41
44
 
45
+ /**
46
+ * Generate a concise AI-readable description of the available components
47
+ * in the component registry, including their prop names.
48
+ */
49
+ function buildRegistryDescription(registry: ComponentRegistry): string {
50
+ const lines: string[] = [];
51
+ for (const [name, entry] of Object.entries(registry) as [
52
+ string,
53
+ { schema?: unknown },
54
+ ][]) {
55
+ let propsLine = "";
56
+ try {
57
+ const shape = (entry.schema as any)?.shape as
58
+ | Record<string, unknown>
59
+ | undefined;
60
+ if (shape) {
61
+ const fields = Object.keys(shape).join(", ");
62
+ propsLine = ` — props: ${fields}`;
63
+ }
64
+ } catch {
65
+ // ignore schema introspection errors
66
+ }
67
+ lines.push(`- ${name}${propsLine}`);
68
+ }
69
+ return lines.join("\n");
70
+ }
71
+
72
+ /**
73
+ * Build the full page description string for the AI context.
74
+ * Stays within the 8,000-character pageContext limit.
75
+ */
76
+ function buildPageDescription(
77
+ id: string | undefined,
78
+ slug: string,
79
+ layers: ComponentLayer[],
80
+ registry: ComponentRegistry,
81
+ ): string {
82
+ const header = id
83
+ ? `UI Builder — editing page (slug: "${slug}")`
84
+ : "UI Builder — creating new page";
85
+
86
+ const layersJson = JSON.stringify(layers, null, 2);
87
+
88
+ const registryDesc = buildRegistryDescription(registry);
89
+
90
+ const layerFormat = `Each layer: { id: string, type: string, name: string, props: Record<string,any>, children?: ComponentLayer[] | string }`;
91
+
92
+ const full = [
93
+ header,
94
+ "",
95
+ `## Current Layers (${layers.length})`,
96
+ layersJson,
97
+ "",
98
+ `## Available Component Types`,
99
+ registryDesc,
100
+ "",
101
+ `## ComponentLayer format`,
102
+ layerFormat,
103
+ ].join("\n");
104
+
105
+ // Trim to fit the 16,000-char server-side limit, cutting the layers JSON if needed
106
+ if (full.length <= 16000) return full;
107
+
108
+ // Re-build with truncated layers JSON
109
+ const overhead =
110
+ [
111
+ header,
112
+ "",
113
+ `## Current Layers (${layers.length})`,
114
+ "",
115
+ "",
116
+ `## Available Component Types`,
117
+ registryDesc,
118
+ "",
119
+ `## ComponentLayer format`,
120
+ layerFormat,
121
+ ].join("\n").length + 30; // 30-char buffer for "...(truncated)"
122
+
123
+ const budget = Math.max(0, 16000 - overhead);
124
+ const truncatedLayers =
125
+ layersJson.length > budget
126
+ ? layersJson.slice(0, budget) + "\n...(truncated)"
127
+ : layersJson;
128
+
129
+ return [
130
+ header,
131
+ "",
132
+ `## Current Layers (${layers.length})`,
133
+ truncatedLayers,
134
+ "",
135
+ `## Available Component Types`,
136
+ registryDesc,
137
+ "",
138
+ `## ComponentLayer format`,
139
+ layerFormat,
140
+ ].join("\n");
141
+ }
142
+
42
143
  /**
43
144
  * Slugify a string for URL-friendly slugs
44
145
  */
@@ -139,6 +240,37 @@ function PageBuilderPageContent({
139
240
  // Auto-generate slug from first page name
140
241
  const [autoSlug, setAutoSlug] = useState(!id);
141
242
 
243
+ // Register AI context so the chat can update the page layout
244
+ useRegisterPageAIContext({
245
+ routeName: id ? "ui-builder-edit-page" : "ui-builder-new-page",
246
+ pageDescription: buildPageDescription(id, slug, layers, componentRegistry),
247
+ suggestions: [
248
+ "Add a hero section",
249
+ "Add a 3-column feature grid",
250
+ "Make the layout full-width",
251
+ "Add a card with a title, description, and button",
252
+ "Replace the layout with a centered single-column design",
253
+ ],
254
+ clientTools: {
255
+ updatePageLayers: async ({ layers: newLayers }) => {
256
+ // Drive the UIBuilder's Zustand store directly so the editor
257
+ // and layers panel update immediately. The store's onChange
258
+ // callback will propagate back to the parent's `layers` state.
259
+ const store = useLayerStore.getState();
260
+ store.initialize(
261
+ newLayers,
262
+ store.selectedPageId || newLayers[0]?.id,
263
+ undefined,
264
+ store.variables,
265
+ );
266
+ return {
267
+ success: true,
268
+ message: `Applied ${newLayers.length} layer(s) to the page`,
269
+ };
270
+ },
271
+ },
272
+ });
273
+
142
274
  // Handle layers change from UIBuilder
143
275
  const handleLayersChange = useCallback(
144
276
  (newLayers: ComponentLayer[]) => {
@@ -3,6 +3,7 @@ import { lazy } from "react";
3
3
  import {
4
4
  defineClientPlugin,
5
5
  createApiClient,
6
+ runClientHookWithShim,
6
7
  } from "@btst/stack/plugins/client";
7
8
  import { createRoute } from "@btst/yar";
8
9
  import type { QueryClient } from "@tanstack/react-query";
@@ -69,10 +70,10 @@ function createPageListLoader(config: UIBuilderClientConfig) {
69
70
  try {
70
71
  // Before hook - authorization check
71
72
  if (hooks?.beforeLoadPageList) {
72
- const canLoad = await hooks.beforeLoadPageList(context);
73
- if (!canLoad) {
74
- throw new Error("Load prevented by beforeLoadPageList hook");
75
- }
73
+ await runClientHookWithShim(
74
+ () => hooks.beforeLoadPageList!(context),
75
+ "Load prevented by beforeLoadPageList hook",
76
+ );
76
77
  }
77
78
 
78
79
  const client = createApiClient<CMSApiRouter>({
@@ -161,10 +162,10 @@ function createPageBuilderLoader(
161
162
  try {
162
163
  // Before hook - authorization check
163
164
  if (hooks?.beforeLoadPageBuilder) {
164
- const canLoad = await hooks.beforeLoadPageBuilder(id, context);
165
- if (!canLoad) {
166
- throw new Error("Load prevented by beforeLoadPageBuilder hook");
167
- }
165
+ await runClientHookWithShim(
166
+ () => hooks.beforeLoadPageBuilder!(id, context),
167
+ "Load prevented by beforeLoadPageBuilder hook",
168
+ );
168
169
  }
169
170
 
170
171
  const client = createApiClient<CMSApiRouter>({
@@ -273,11 +274,11 @@ function createPageBuilderMeta(
273
274
  * hooks: {
274
275
  * beforeLoadPageList: async (ctx) => {
275
276
  * const session = await getSession(ctx.headers)
276
- * return session?.user?.isAdmin === true
277
+ * if (!session?.user?.isAdmin) throw new Error("Admin access required")
277
278
  * },
278
279
  * beforeLoadPageBuilder: async (pageId, ctx) => {
279
280
  * const session = await getSession(ctx.headers)
280
- * return session?.user?.isAdmin === true
281
+ * if (!session?.user?.isAdmin) throw new Error("Admin access required")
281
282
  * },
282
283
  * onLoadError: () => redirect("/auth/sign-in"),
283
284
  * },
@@ -92,24 +92,24 @@ export interface LoaderContext {
92
92
  */
93
93
  export interface UIBuilderClientHooks {
94
94
  /**
95
- * Called before loading the page list. Return false to cancel loading.
95
+ * Called before loading the page list. Throw an error to cancel loading.
96
96
  * @param context - Loader context with path, params, etc.
97
97
  */
98
- beforeLoadPageList?: (context: LoaderContext) => Promise<boolean> | boolean;
98
+ beforeLoadPageList?: (context: LoaderContext) => Promise<void> | void;
99
99
  /**
100
100
  * Called after the page list is loaded.
101
101
  * @param context - Loader context
102
102
  */
103
103
  afterLoadPageList?: (context: LoaderContext) => Promise<void> | void;
104
104
  /**
105
- * Called before loading the page builder. Return false to cancel loading.
105
+ * Called before loading the page builder. Throw an error to cancel loading.
106
106
  * @param pageId - The page ID (undefined for new pages)
107
107
  * @param context - Loader context
108
108
  */
109
109
  beforeLoadPageBuilder?: (
110
110
  pageId: string | undefined,
111
111
  context: LoaderContext,
112
- ) => Promise<boolean> | boolean;
112
+ ) => Promise<void> | void;
113
113
  /**
114
114
  * Called after the page builder is loaded.
115
115
  * @param pageId - The page ID (undefined for new pages)
@@ -1,5 +1,96 @@
1
1
  import { createClient } from "better-call/client";
2
2
 
3
+ /**
4
+ * Runs a hook with backward-compatible denial handling.
5
+ * Hooks may deny by returning a falsy value (old) or throwing (new).
6
+ * Both are normalized to an HTTP error via `createError` (`ctx.error`).
7
+ * Returns the hook's result so transform hooks can apply mutations.
8
+ *
9
+ * ## Migration note (v2.4 → v2.5)
10
+ *
11
+ * Old-style hooks signalled denial by returning `false` and allowance by returning `true`.
12
+ * Pre-shim call sites used `if (!result)` which treated `undefined` (fall-through) as deny.
13
+ * New-style hooks throw an Error to deny and return void/undefined to allow.
14
+ *
15
+ * The shim detects old-style boolean returns at runtime and emits a deprecation warning so
16
+ * that any hook with a code path returning a boolean is surfaced immediately. Hooks that fall
17
+ * through to `undefined` on **every** code path (no boolean return anywhere) cannot be
18
+ * distinguished from new-style void hooks — those hooks must be audited manually and updated
19
+ * to throw explicitly when they intend to deny access.
20
+ */
21
+ export async function runHookWithShim<T>(
22
+ hookFn: () => Promise<T> | T,
23
+ createError: (
24
+ status: keyof typeof statusCodes | Status,
25
+ body: { message: string },
26
+ ) => any,
27
+ defaultMessage: string,
28
+ errorStatus = 403 as keyof typeof statusCodes | Status,
29
+ ): Promise<Exclude<Awaited<T>, false>> {
30
+ let result: Awaited<T>;
31
+ try {
32
+ result = await hookFn();
33
+ } catch (e) {
34
+ throw createError(errorStatus, {
35
+ message: e instanceof Error ? e.message : defaultMessage,
36
+ });
37
+ }
38
+ // Detect old-style boolean returns (pre-v2.5 pattern).
39
+ // Emitting a warning here is the only reliable way to surface hooks that still rely on
40
+ // boolean returns — including hooks where one branch returns `false` and another falls
41
+ // through to `undefined`, which was previously denied by `if (!result)` at the call site
42
+ // but would now silently allow if the warning is not acted on.
43
+ if (typeof result === "boolean") {
44
+ if (process.env.NODE_ENV !== "production") {
45
+ console.warn(
46
+ `[btst] A lifecycle hook returned a boolean (${result}). ` +
47
+ `Boolean returns are deprecated — throw an Error to deny access instead. ` +
48
+ `IMPORTANT: any code path in this hook that falls through to undefined ` +
49
+ `now ALLOWS access (previously denied). ` +
50
+ `Update the hook to throw new Error("Unauthorized") to deny.`,
51
+ );
52
+ }
53
+ if (!result) {
54
+ throw createError(errorStatus, { message: defaultMessage });
55
+ }
56
+ }
57
+ return result as Exclude<Awaited<T>, false>;
58
+ }
59
+
60
+ /**
61
+ * Client-side equivalent of runHookWithShim — throws a plain Error instead of an HTTP error.
62
+ * Hooks may deny by returning false (old) or throwing (new); both normalize to an Error.
63
+ *
64
+ * See `runHookWithShim` for the full migration note on boolean-vs-void semantics.
65
+ */
66
+ export async function runClientHookWithShim<T>(
67
+ hookFn: () => Promise<T> | T,
68
+ defaultMessage: string,
69
+ ): Promise<Exclude<Awaited<T>, false>> {
70
+ let result: Awaited<T>;
71
+ try {
72
+ result = await hookFn();
73
+ } catch (e) {
74
+ throw e instanceof Error ? e : new Error(defaultMessage);
75
+ }
76
+ // Detect old-style boolean returns and warn; see runHookWithShim for rationale.
77
+ if (typeof result === "boolean") {
78
+ if (process.env.NODE_ENV !== "production") {
79
+ console.warn(
80
+ `[btst] A lifecycle hook returned a boolean (${result}). ` +
81
+ `Boolean returns are deprecated — throw an Error to deny access instead. ` +
82
+ `IMPORTANT: any code path in this hook that falls through to undefined ` +
83
+ `now ALLOWS access (previously denied). ` +
84
+ `Update the hook to throw new Error("Unauthorized") to deny.`,
85
+ );
86
+ }
87
+ if (!result) {
88
+ throw new Error(defaultMessage);
89
+ }
90
+ }
91
+ return result as Exclude<Awaited<T>, false>;
92
+ }
93
+
3
94
  /**
4
95
  * Returns true when a fetch error is a connection-refused / no-server error.
5
96
  * Used in SSR loaders to emit an actionable build-time warning when
@@ -18,7 +109,7 @@ export function isConnectionError(err: unknown): boolean {
18
109
  code === "ERR_CONNECTION_REFUSED"
19
110
  );
20
111
  }
21
- import type { Router, Endpoint } from "better-call";
112
+ import type { Router, Endpoint, Status, statusCodes } from "better-call";
22
113
 
23
114
  interface CreateApiClientOptions {
24
115
  baseURL?: string;
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
42
42
  name: z.ZodString;
43
43
  slug: z.ZodString;
44
44
  }, z.core.$strip>]>>>>;
45
+ title: z.ZodString;
45
46
  slug: z.ZodOptional<z.ZodString>;
46
- publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
47
47
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
48
48
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
49
- title: z.ZodString;
49
+ publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
50
50
  content: z.ZodString;
51
51
  excerpt: z.ZodString;
52
52
  image: z.ZodOptional<z.ZodString>;
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
42
42
  name: z.ZodString;
43
43
  slug: z.ZodString;
44
44
  }, z.core.$strip>]>>>>;
45
+ title: z.ZodString;
45
46
  slug: z.ZodOptional<z.ZodString>;
46
- publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
47
47
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
48
48
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
49
- title: z.ZodString;
49
+ publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
50
50
  content: z.ZodString;
51
51
  excerpt: z.ZodString;
52
52
  image: z.ZodOptional<z.ZodString>;
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
42
42
  name: z.ZodString;
43
43
  slug: z.ZodString;
44
44
  }, z.core.$strip>]>>>>;
45
+ title: z.ZodString;
45
46
  slug: z.ZodOptional<z.ZodString>;
46
- publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
47
47
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
48
48
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
49
- title: z.ZodString;
49
+ publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
50
50
  content: z.ZodString;
51
51
  excerpt: z.ZodString;
52
52
  image: z.ZodOptional<z.ZodString>;