camox 0.38.0 → 0.40.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.
@@ -0,0 +1,303 @@
1
+ import { useAuthContext } from "../../../lib/auth.js";
2
+ import { projectQueries } from "../../../lib/queries.js";
3
+ import { cn } from "../../../lib/utils.js";
4
+ import { previewStore, selectionBlockId } from "../previewStore.js";
5
+ import { UserButton } from "../../studio/components/UserButton.js";
6
+ import { AddBlockSidebar } from "./AddBlockSidebar.js";
7
+ import { PageTree } from "./PageTree.js";
8
+ import { PageNavigatorHeader } from "./PageNavigatorSidebar.js";
9
+ import { PageEditorSidebar } from "./PageEditorSidebar.js";
10
+ import { PageInfoSidebar, PageMarkdownContent, PageSeoContent } from "./PageInfoSidebar.js";
11
+ import { c } from "react/compiler-runtime";
12
+ import { useQuery } from "@tanstack/react-query";
13
+ import * as React from "react";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ import { useSelector } from "@xstate/store-react";
16
+ import { Button } from "@camox/ui/button";
17
+ import { ArrowLeft, FileText, Info, Layers, Search, Settings } from "lucide-react";
18
+
19
+ //#region src/features/preview/components/MobilePreviewDrawer.tsx
20
+ const COLLAPSED_HEIGHT = 80;
21
+ const DRAG_THRESHOLD = 4;
22
+ const screenTitles = {
23
+ root: "Page",
24
+ blocks: "Page content",
25
+ about: "About this page",
26
+ seo: "SEO metadata",
27
+ markdown: "Page markdown"
28
+ };
29
+ function MobilePreviewDrawer({ page, projectName }) {
30
+ const [height, setHeight] = React.useState(COLLAPSED_HEIGHT);
31
+ const [isExpanded, setIsExpanded] = React.useState(false);
32
+ const [isDragging, setIsDragging] = React.useState(false);
33
+ const [screen, setScreen] = React.useState("root");
34
+ const authCtx = useAuthContext();
35
+ const { data: project } = useQuery(projectQueries.getBySlug(authCtx.projectSlug));
36
+ const dragStartRef = React.useRef({
37
+ y: 0,
38
+ height: COLLAPSED_HEIGHT
39
+ });
40
+ const dragDistanceRef = React.useRef(0);
41
+ const rootContentRef = React.useRef(null);
42
+ const selection = useSelector(previewStore, (state) => state.context.selection);
43
+ const isAddBlockSidebarOpen = useSelector(previewStore, (state_0) => state_0.context.isAddBlockSidebarOpen);
44
+ const selectedBlockId = selectionBlockId(selection);
45
+ const isContentScreen = selectedBlockId != null || isAddBlockSidebarOpen;
46
+ const isTileScreen = screen !== "root" || isContentScreen;
47
+ const showScreenHeader = isExpanded && isTileScreen;
48
+ const getExpandedHeight = React.useCallback(() => {
49
+ if (typeof window === "undefined") return 720;
50
+ if (isTileScreen) return window.innerHeight;
51
+ const contentHeight = rootContentRef.current?.getBoundingClientRect().height ?? 0;
52
+ return Math.max(COLLAPSED_HEIGHT, Math.min(window.innerHeight, COLLAPSED_HEIGHT + contentHeight + 2));
53
+ }, [isTileScreen]);
54
+ const goBack = () => {
55
+ if (isContentScreen) {
56
+ previewStore.send({ type: "clearSelection" });
57
+ previewStore.send({ type: "closeAddBlockSidebar" });
58
+ setScreen("blocks");
59
+ return;
60
+ }
61
+ setScreen("root");
62
+ };
63
+ const snapTo = React.useCallback((expanded) => {
64
+ setIsExpanded(expanded);
65
+ setHeight(expanded ? getExpandedHeight() : COLLAPSED_HEIGHT);
66
+ }, [getExpandedHeight]);
67
+ React.useEffect(() => {
68
+ setScreen("root");
69
+ }, [page.id]);
70
+ React.useEffect(() => {
71
+ const handleResize = () => {
72
+ if (!isExpanded || isDragging) return;
73
+ setHeight(getExpandedHeight());
74
+ };
75
+ handleResize();
76
+ const observer = new ResizeObserver(handleResize);
77
+ if (rootContentRef.current) observer.observe(rootContentRef.current);
78
+ window.addEventListener("resize", handleResize);
79
+ return () => {
80
+ observer.disconnect();
81
+ window.removeEventListener("resize", handleResize);
82
+ };
83
+ }, [
84
+ isExpanded,
85
+ isDragging,
86
+ getExpandedHeight
87
+ ]);
88
+ const handlePointerDown = (event) => {
89
+ event.currentTarget.setPointerCapture(event.pointerId);
90
+ dragStartRef.current = {
91
+ y: event.clientY,
92
+ height
93
+ };
94
+ dragDistanceRef.current = 0;
95
+ setIsDragging(true);
96
+ };
97
+ const handlePointerMove = (event_0) => {
98
+ if (!event_0.currentTarget.hasPointerCapture(event_0.pointerId)) return;
99
+ const distance = dragStartRef.current.y - event_0.clientY;
100
+ dragDistanceRef.current = distance;
101
+ const nextHeight = dragStartRef.current.height + distance;
102
+ setHeight(Math.min(getExpandedHeight(), Math.max(COLLAPSED_HEIGHT, nextHeight)));
103
+ };
104
+ const finishDrag = (event_1, onTap = () => snapTo(!isExpanded)) => {
105
+ if (event_1.currentTarget.hasPointerCapture(event_1.pointerId)) event_1.currentTarget.releasePointerCapture(event_1.pointerId);
106
+ setIsDragging(false);
107
+ const distance_0 = dragDistanceRef.current;
108
+ if (Math.abs(distance_0) < DRAG_THRESHOLD) {
109
+ onTap();
110
+ return;
111
+ }
112
+ if (isExpanded && distance_0 < -40) {
113
+ snapTo(false);
114
+ return;
115
+ }
116
+ snapTo(dragStartRef.current.height + distance_0 >= (COLLAPSED_HEIGHT + getExpandedHeight()) / 2);
117
+ };
118
+ const cancelDrag = (event_2) => {
119
+ if (event_2.currentTarget.hasPointerCapture(event_2.pointerId)) event_2.currentTarget.releasePointerCapture(event_2.pointerId);
120
+ setIsDragging(false);
121
+ snapTo(isExpanded);
122
+ };
123
+ return /* @__PURE__ */ jsxs("section", {
124
+ className: "bg-background relative z-10 w-full shrink-0 overflow-hidden border-t-2 shadow-[0_-8px_24px_rgba(0,0,0,0.08)]",
125
+ style: {
126
+ height,
127
+ transition: isDragging ? "none" : "height 220ms cubic-bezier(0.22, 1, 0.36, 1)"
128
+ },
129
+ "aria-label": "Project drawer",
130
+ children: [showScreenHeader ? /* @__PURE__ */ jsxs("header", {
131
+ className: "relative h-12 touch-none border-b px-3 pt-2.5 pb-1.5 select-none",
132
+ onPointerDown: handlePointerDown,
133
+ onPointerMove: handlePointerMove,
134
+ onPointerUp: (event_3) => finishDrag(event_3, goBack),
135
+ onPointerCancel: cancelDrag,
136
+ children: [/* @__PURE__ */ jsx("span", { className: "bg-muted-foreground/60 absolute top-1 left-1/2 h-1 w-12 -translate-x-1/2 rounded-full" }), /* @__PURE__ */ jsxs("button", {
137
+ type: "button",
138
+ onClick: (event_4) => {
139
+ if (event_4.detail === 0) goBack();
140
+ },
141
+ className: "hover:bg-accent flex h-8 w-full items-center gap-2 rounded-lg px-2 text-left text-sm font-semibold",
142
+ "aria-label": isContentScreen ? "Back to page content" : "Back to page overview",
143
+ children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "text-muted-foreground size-4" }), isContentScreen ? screenTitles.blocks : screenTitles[screen]]
144
+ })]
145
+ }) : /* @__PURE__ */ jsxs("div", {
146
+ className: "relative h-20",
147
+ children: [/* @__PURE__ */ jsxs("button", {
148
+ type: "button",
149
+ className: "relative flex h-20 w-full touch-none items-center px-5 pt-2 pr-20 text-left select-none",
150
+ "aria-controls": "camox-mobile-drawer-content",
151
+ "aria-expanded": isExpanded,
152
+ onPointerDown: handlePointerDown,
153
+ onPointerMove: handlePointerMove,
154
+ onPointerUp: (event_5) => finishDrag(event_5),
155
+ onPointerCancel: cancelDrag,
156
+ onClick: (event_6) => {
157
+ if (event_6.detail === 0) snapTo(!isExpanded);
158
+ },
159
+ children: [/* @__PURE__ */ jsx("span", { className: "bg-muted-foreground/60 absolute top-2 left-1/2 h-1 w-12 -translate-x-1/2 rounded-full" }), /* @__PURE__ */ jsx("span", {
160
+ className: "truncate pr-4 text-sm font-medium",
161
+ children: projectName
162
+ })]
163
+ }), /* @__PURE__ */ jsx("div", {
164
+ className: "absolute top-6 right-5",
165
+ children: /* @__PURE__ */ jsx(UserButton, {})
166
+ })]
167
+ }), /* @__PURE__ */ jsx("div", {
168
+ id: "camox-mobile-drawer-content",
169
+ inert: !isExpanded,
170
+ className: cn("flex min-h-0 flex-col overflow-hidden", isTileScreen && "pb-[env(safe-area-inset-bottom)]", showScreenHeader ? "h-[calc(100%-3rem)]" : "h-[calc(100%-5rem)]"),
171
+ children: selectedBlockId != null ? /* @__PURE__ */ jsx(PageEditorSidebar, {}) : isAddBlockSidebarOpen ? /* @__PURE__ */ jsx(AddBlockSidebar, {}) : /* @__PURE__ */ jsxs("div", {
172
+ className: "min-h-0 flex-1 overflow-auto",
173
+ children: [
174
+ screen === "root" && /* @__PURE__ */ jsxs("div", {
175
+ ref: rootContentRef,
176
+ className: "pb-[env(safe-area-inset-bottom)]",
177
+ children: [
178
+ /* @__PURE__ */ jsx(PageNavigatorHeader, { page }),
179
+ /* @__PURE__ */ jsx("div", {
180
+ className: "px-3 pt-3",
181
+ children: /* @__PURE__ */ jsx(Button, {
182
+ type: "button",
183
+ variant: "outline",
184
+ className: "w-full",
185
+ disabled: page.status === "published" || page.livePublishedCheckpointId == null,
186
+ onClick: () => previewStore.send({ type: "viewLivePage" }),
187
+ children: "View live site"
188
+ })
189
+ }),
190
+ /* @__PURE__ */ jsxs("div", {
191
+ className: "grid grid-cols-2 gap-2 p-3",
192
+ children: [
193
+ /* @__PURE__ */ jsx(DrawerTile, {
194
+ title: "Page content",
195
+ icon: Layers,
196
+ onClick: () => setScreen("blocks"),
197
+ fullWidth: true
198
+ }),
199
+ /* @__PURE__ */ jsx(DrawerTile, {
200
+ title: "About this page",
201
+ icon: Info,
202
+ onClick: () => setScreen("about")
203
+ }),
204
+ /* @__PURE__ */ jsx(DrawerTile, {
205
+ title: "SEO metadata",
206
+ icon: Search,
207
+ onClick: () => setScreen("seo")
208
+ }),
209
+ /* @__PURE__ */ jsx(DrawerTile, {
210
+ title: "Page markdown",
211
+ icon: FileText,
212
+ onClick: () => setScreen("markdown")
213
+ }),
214
+ /* @__PURE__ */ jsx(DrawerTile, {
215
+ title: "Project settings",
216
+ icon: Settings,
217
+ onClick: () => {
218
+ if (!project) return;
219
+ window.open(`${authCtx.authenticationUrl}/${project.organizationSlug}/${project.slug}/overview`, "_blank", "noopener,noreferrer");
220
+ },
221
+ disabled: !project
222
+ })
223
+ ]
224
+ })
225
+ ]
226
+ }),
227
+ screen === "blocks" && /* @__PURE__ */ jsx("div", {
228
+ className: "flex flex-col gap-2 p-2",
229
+ children: /* @__PURE__ */ jsx(PageTree, {})
230
+ }),
231
+ screen === "about" && /* @__PURE__ */ jsx(PageInfoSidebar, {
232
+ pageId: page.id,
233
+ scrollable: false,
234
+ aboutOnly: true
235
+ }),
236
+ screen === "seo" && /* @__PURE__ */ jsx("div", {
237
+ className: "p-3",
238
+ children: /* @__PURE__ */ jsx(PageSeoContent, { pageId: page.id })
239
+ }),
240
+ screen === "markdown" && /* @__PURE__ */ jsxs("div", {
241
+ className: "space-y-4 p-3",
242
+ children: [/* @__PURE__ */ jsx("p", {
243
+ className: "text-muted-foreground text-sm",
244
+ children: "How your content will be served to AI agents."
245
+ }), /* @__PURE__ */ jsx(PageMarkdownContent, { pageId: page.id })]
246
+ })
247
+ ]
248
+ }, screen)
249
+ })]
250
+ });
251
+ }
252
+ function DrawerTile(t0) {
253
+ const $ = c(13);
254
+ if ($[0] !== "a15836a2c406a6e26e3450f2952542d7c3ef6e1f52eed0eca720954850f751a5") {
255
+ for (let $i = 0; $i < 13; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
256
+ $[0] = "a15836a2c406a6e26e3450f2952542d7c3ef6e1f52eed0eca720954850f751a5";
257
+ }
258
+ const { title, icon: Icon, onClick, fullWidth: t1, disabled: t2 } = t0;
259
+ const fullWidth = t1 === void 0 ? false : t1;
260
+ const disabled = t2 === void 0 ? false : t2;
261
+ const t3 = fullWidth && "col-span-2";
262
+ let t4;
263
+ if ($[1] !== t3) {
264
+ t4 = cn("bg-muted/40 hover:bg-muted flex flex-col items-start justify-between rounded-xl border p-3 text-left transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", t3);
265
+ $[1] = t3;
266
+ $[2] = t4;
267
+ } else t4 = $[2];
268
+ let t5;
269
+ if ($[3] !== Icon) {
270
+ t5 = /* @__PURE__ */ jsx(Icon, { className: "text-muted-foreground size-5" });
271
+ $[3] = Icon;
272
+ $[4] = t5;
273
+ } else t5 = $[4];
274
+ let t6;
275
+ if ($[5] !== title) {
276
+ t6 = /* @__PURE__ */ jsx("span", {
277
+ className: "mt-3 text-xs font-medium",
278
+ children: title
279
+ });
280
+ $[5] = title;
281
+ $[6] = t6;
282
+ } else t6 = $[6];
283
+ let t7;
284
+ if ($[7] !== disabled || $[8] !== onClick || $[9] !== t4 || $[10] !== t5 || $[11] !== t6) {
285
+ t7 = /* @__PURE__ */ jsxs("button", {
286
+ type: "button",
287
+ onClick,
288
+ disabled,
289
+ className: t4,
290
+ children: [t5, t6]
291
+ });
292
+ $[7] = disabled;
293
+ $[8] = onClick;
294
+ $[9] = t4;
295
+ $[10] = t5;
296
+ $[11] = t6;
297
+ $[12] = t7;
298
+ } else t7 = $[12];
299
+ return t7;
300
+ }
301
+
302
+ //#endregion
303
+ export { MobilePreviewDrawer };
@@ -281,7 +281,7 @@ const PageEditorSidebar = () => {
281
281
  },
282
282
  {
283
283
  key: "block",
284
- label: block.summary || blockDef._internal.title,
284
+ label: blockDef._internal.title,
285
285
  isCurrent: ancestorChain.length === 0 && !fieldHasOwnView,
286
286
  onClick: () => previewStore.send({
287
287
  type: "setFocusedBlock",
@@ -290,7 +290,7 @@ const PageEditorSidebar = () => {
290
290
  },
291
291
  ...ancestorChain.map((ancestor) => ({
292
292
  key: `item-${ancestor.id}`,
293
- label: ancestor.summary || formatFieldName(ancestor.fieldName),
293
+ label: getArraySchemaForItem(blockDef._internal.contentSchema, ancestor.id, itemsMap)?.title ?? formatFieldName(ancestor.fieldName),
294
294
  isCurrent: ancestor.id === currentItemId && !fieldHasOwnView && ancestor.id === ancestorChain[ancestorChain.length - 1]?.id,
295
295
  onClick: () => previewStore.send({
296
296
  type: "selectItem",