@webmaster-droid/web 0.1.0-alpha.2 → 0.1.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.
package/dist/index.js CHANGED
@@ -6,11 +6,36 @@ import {
6
6
  createElement,
7
7
  useContext
8
8
  } from "react";
9
+ import sanitizeHtml from "sanitize-html";
9
10
  import { jsx } from "react/jsx-runtime";
10
11
  var EDITABLE_ROOTS = ["pages.", "layout.", "seo.", "themeTokens."];
11
12
  var MAX_PATH_LENGTH = 320;
12
13
  var MAX_LABEL_LENGTH = 120;
13
14
  var MAX_PREVIEW_LENGTH = 140;
15
+ var RICH_TEXT_ALLOWED_TAGS = [
16
+ "a",
17
+ "b",
18
+ "blockquote",
19
+ "br",
20
+ "code",
21
+ "em",
22
+ "h1",
23
+ "h2",
24
+ "h3",
25
+ "h4",
26
+ "h5",
27
+ "h6",
28
+ "hr",
29
+ "i",
30
+ "li",
31
+ "ol",
32
+ "p",
33
+ "pre",
34
+ "strong",
35
+ "u",
36
+ "ul"
37
+ ];
38
+ var RICH_TEXT_ALLOWED_ATTRS = ["href", "title", "aria-label", "rel"];
14
39
  var EditableContext = createContext(null);
15
40
  function EditableProvider(props) {
16
41
  return /* @__PURE__ */ jsx(
@@ -172,9 +197,31 @@ function parseSelectedEditableFromTarget(target, pagePath) {
172
197
  }
173
198
  return selected;
174
199
  }
175
- function pickStringValue(document2, path, fallback) {
200
+ function pickStringValue(document2, path, fallback, componentName, fallbackPropName, missingBehavior = "throw") {
176
201
  const value = readByPath(document2, path);
177
- return typeof value === "string" && value.trim() ? value : fallback;
202
+ if (typeof value === "string" && value.trim()) {
203
+ return value;
204
+ }
205
+ if (typeof fallback === "string") {
206
+ return fallback;
207
+ }
208
+ const message = `${componentName} missing content for "${path}". Provide a CMS value or set \`${fallbackPropName}\`.`;
209
+ if (missingBehavior === "empty") {
210
+ console.error(message);
211
+ return "";
212
+ }
213
+ throw new Error(message);
214
+ }
215
+ function sanitizeRichTextHtml(html) {
216
+ return sanitizeHtml(html, {
217
+ allowedTags: [...RICH_TEXT_ALLOWED_TAGS],
218
+ allowedAttributes: {
219
+ a: [...RICH_TEXT_ALLOWED_ATTRS]
220
+ },
221
+ allowedSchemes: ["http", "https", "mailto", "tel"],
222
+ allowProtocolRelative: false,
223
+ disallowedTagsMode: "discard"
224
+ });
178
225
  }
179
226
  function EditableText({
180
227
  path,
@@ -185,7 +232,7 @@ function EditableText({
185
232
  ...rest
186
233
  }) {
187
234
  const { document: document2, enabled } = useEditableDocument();
188
- const value = pickStringValue(document2, path, fallback);
235
+ const value = pickStringValue(document2, path, fallback, "EditableText", "fallback", "empty");
189
236
  const attrs = enabled ? editableMeta({
190
237
  path,
191
238
  label: label ?? path,
@@ -203,7 +250,15 @@ function EditableRichText({
203
250
  ...rest
204
251
  }) {
205
252
  const { document: document2, enabled } = useEditableDocument();
206
- const value = pickStringValue(document2, path, fallback);
253
+ const value = pickStringValue(
254
+ document2,
255
+ path,
256
+ fallback,
257
+ "EditableRichText",
258
+ "fallback",
259
+ "empty"
260
+ );
261
+ const sanitizedHtml = sanitizeRichTextHtml(value);
207
262
  const attrs = enabled ? editableMeta({
208
263
  path,
209
264
  label: label ?? path,
@@ -213,7 +268,7 @@ function EditableRichText({
213
268
  return createElement(as, {
214
269
  ...rest,
215
270
  ...attrs,
216
- dangerouslySetInnerHTML: { __html: value }
271
+ dangerouslySetInnerHTML: { __html: sanitizedHtml }
217
272
  });
218
273
  }
219
274
  function EditableImage({
@@ -225,8 +280,22 @@ function EditableImage({
225
280
  ...rest
226
281
  }) {
227
282
  const { document: document2, enabled } = useEditableDocument();
228
- const src = pickStringValue(document2, path, fallbackSrc);
229
- const alt = altPath ? pickStringValue(document2, altPath, fallbackAlt) : fallbackAlt;
283
+ const src = pickStringValue(
284
+ document2,
285
+ path,
286
+ fallbackSrc,
287
+ "EditableImage",
288
+ "fallbackSrc",
289
+ "empty"
290
+ );
291
+ const alt = altPath ? pickStringValue(
292
+ document2,
293
+ altPath,
294
+ fallbackAlt,
295
+ "EditableImage",
296
+ "fallbackAlt",
297
+ "empty"
298
+ ) : fallbackAlt ?? "";
230
299
  const attrs = enabled ? editableMeta({
231
300
  path,
232
301
  label: label ?? path,
@@ -245,8 +314,22 @@ function EditableLink({
245
314
  ...rest
246
315
  }) {
247
316
  const { document: document2, enabled } = useEditableDocument();
248
- const href = pickStringValue(document2, hrefPath, fallbackHref);
249
- const text = pickStringValue(document2, labelPath, fallbackLabel);
317
+ const href = pickStringValue(
318
+ document2,
319
+ hrefPath,
320
+ fallbackHref,
321
+ "EditableLink",
322
+ "fallbackHref",
323
+ "empty"
324
+ );
325
+ const text = pickStringValue(
326
+ document2,
327
+ labelPath,
328
+ fallbackLabel,
329
+ "EditableLink",
330
+ "fallbackLabel",
331
+ "empty"
332
+ );
250
333
  const attrs = enabled ? editableMeta({
251
334
  path: labelPath,
252
335
  label: label ?? labelPath,
@@ -686,18 +769,7 @@ function useWebmasterDroid() {
686
769
  return context;
687
770
  }
688
771
 
689
- // src/overlay.tsx
690
- import {
691
- useCallback,
692
- useEffect as useEffect2,
693
- useMemo as useMemo2,
694
- useRef,
695
- useState as useState2
696
- } from "react";
697
- import ReactMarkdown from "react-markdown";
698
- import remarkGfm from "remark-gfm";
699
- import { REQUIRED_PUBLISH_CONFIRMATION } from "@webmaster-droid/contracts";
700
- import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
772
+ // src/overlay/utils.ts
701
773
  function createMessage(role, text, status) {
702
774
  return {
703
775
  id: `${role}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
@@ -786,7 +858,394 @@ function kindIcon(kind) {
786
858
  }
787
859
  return "TXT";
788
860
  }
789
- function WebmasterDroidOverlay() {
861
+ var OVERLAY_FONT_FAMILY = "var(--font-ibm-plex-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace";
862
+
863
+ // src/overlay/components.tsx
864
+ import ReactMarkdown from "react-markdown";
865
+ import remarkGfm from "remark-gfm";
866
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
867
+ function OverlayHeader({
868
+ isAuthenticated,
869
+ publishState,
870
+ activeTab,
871
+ historyCount,
872
+ clearChatDisabled,
873
+ onPublish,
874
+ onTabChange,
875
+ onClearChat,
876
+ onClose
877
+ }) {
878
+ return /* @__PURE__ */ jsx3("header", { className: "border-b border-stone-300 bg-[#f3eee5] p-2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
879
+ isAuthenticated ? /* @__PURE__ */ jsxs(Fragment, { children: [
880
+ /* @__PURE__ */ jsx3(
881
+ "span",
882
+ {
883
+ className: `rounded border px-1.5 py-0.5 text-[10px] font-medium leading-4 ${publishState === "Published" ? "border-stone-300 bg-[#ece5d9] text-stone-600" : "border-stone-500 bg-[#ded4c3] text-stone-800"}`,
884
+ children: publishState
885
+ }
886
+ ),
887
+ /* @__PURE__ */ jsx3(
888
+ "button",
889
+ {
890
+ type: "button",
891
+ className: "rounded border border-stone-700 bg-stone-800 px-2 py-1 text-[11px] font-semibold leading-4 text-stone-100 hover:bg-stone-700 disabled:cursor-not-allowed disabled:opacity-50",
892
+ onClick: onPublish,
893
+ disabled: !isAuthenticated,
894
+ children: "Publish"
895
+ }
896
+ ),
897
+ /* @__PURE__ */ jsxs("div", { className: "inline-flex rounded-md border border-stone-300 bg-[#e8dfd1] p-0.5", children: [
898
+ /* @__PURE__ */ jsx3(
899
+ "button",
900
+ {
901
+ type: "button",
902
+ className: `rounded px-2 py-1 text-[11px] font-medium leading-4 ${activeTab === "chat" ? "bg-[#f7f2e8] text-stone-900 shadow-sm" : "text-stone-600 hover:text-stone-900"}`,
903
+ onClick: () => onTabChange("chat"),
904
+ children: "Chat"
905
+ }
906
+ ),
907
+ /* @__PURE__ */ jsxs(
908
+ "button",
909
+ {
910
+ type: "button",
911
+ className: `rounded px-2 py-1 text-[11px] font-medium leading-4 ${activeTab === "history" ? "bg-[#f7f2e8] text-stone-900 shadow-sm" : "text-stone-600 hover:text-stone-900"}`,
912
+ onClick: () => onTabChange("history"),
913
+ children: [
914
+ "History (",
915
+ historyCount,
916
+ ")"
917
+ ]
918
+ }
919
+ )
920
+ ] })
921
+ ] }) : /* @__PURE__ */ jsx3("h2", { className: "text-[12px] font-semibold text-stone-700", children: "Login" }),
922
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
923
+ isAuthenticated ? /* @__PURE__ */ jsx3(
924
+ "button",
925
+ {
926
+ type: "button",
927
+ "aria-label": "Clear chat",
928
+ title: "Clear chat",
929
+ disabled: clearChatDisabled,
930
+ className: "inline-flex h-6 w-6 items-center justify-center rounded border border-stone-300 text-stone-600 hover:bg-[#efe8dc] hover:text-stone-800 disabled:cursor-not-allowed disabled:opacity-50",
931
+ onClick: onClearChat,
932
+ children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3.5 w-3.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
933
+ "path",
934
+ {
935
+ d: "M4.5 5.5H15.5M8 3.75H12M7 7.5V13.5M10 7.5V13.5M13 7.5V13.5M6.5 5.5L7 15C7.03 15.6 7.53 16.08 8.13 16.08H11.87C12.47 16.08 12.97 15.6 13 15L13.5 5.5",
936
+ stroke: "currentColor",
937
+ strokeWidth: "1.4",
938
+ strokeLinecap: "round",
939
+ strokeLinejoin: "round"
940
+ }
941
+ ) })
942
+ }
943
+ ) : null,
944
+ /* @__PURE__ */ jsx3(
945
+ "button",
946
+ {
947
+ type: "button",
948
+ className: "rounded border border-stone-300 px-2 py-1 text-[11px] leading-4 text-stone-700 hover:bg-[#efe8dc]",
949
+ onClick: onClose,
950
+ children: "Close"
951
+ }
952
+ )
953
+ ] })
954
+ ] }) });
955
+ }
956
+ function OverlayLoginPanel({
957
+ authConfigured,
958
+ email,
959
+ password,
960
+ signingIn,
961
+ onEmailChange,
962
+ onPasswordChange,
963
+ onSignIn
964
+ }) {
965
+ return /* @__PURE__ */ jsx3("section", { className: "flex min-h-0 flex-1 items-center justify-center bg-[#ece7dd] p-3", children: !authConfigured ? /* @__PURE__ */ jsx3("div", { className: "w-full max-w-sm rounded border border-red-300 bg-[#f8f3e9] p-3 text-[11px] leading-4 text-red-700", children: "Missing Supabase config (`supabaseUrl` / `supabaseAnonKey`)." }) : /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm rounded border border-stone-300 bg-[#f8f3e9] p-3", children: [
966
+ /* @__PURE__ */ jsx3("h3", { className: "mb-2 text-[12px] font-semibold text-stone-700", children: "Sign in" }),
967
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
968
+ /* @__PURE__ */ jsx3(
969
+ "input",
970
+ {
971
+ type: "text",
972
+ value: email,
973
+ onChange: (event) => onEmailChange(event.target.value),
974
+ placeholder: "login",
975
+ className: "w-full rounded border border-stone-300 bg-[#f4efe6] px-2 py-1.5 text-[12px] text-stone-900 outline-none focus:border-stone-500"
976
+ }
977
+ ),
978
+ /* @__PURE__ */ jsx3(
979
+ "input",
980
+ {
981
+ type: "password",
982
+ value: password,
983
+ onChange: (event) => onPasswordChange(event.target.value),
984
+ placeholder: "Password",
985
+ className: "w-full rounded border border-stone-300 bg-[#f4efe6] px-2 py-1.5 text-[12px] text-stone-900 outline-none focus:border-stone-500"
986
+ }
987
+ ),
988
+ /* @__PURE__ */ jsx3(
989
+ "button",
990
+ {
991
+ type: "button",
992
+ onClick: onSignIn,
993
+ disabled: signingIn || !email.trim() || !password,
994
+ className: "w-full rounded border border-stone-700 bg-stone-800 px-2 py-1.5 text-[12px] font-medium text-stone-100 hover:bg-stone-700 disabled:cursor-not-allowed disabled:opacity-50",
995
+ children: signingIn ? "Signing in" : "Sign in"
996
+ }
997
+ )
998
+ ] })
999
+ ] }) });
1000
+ }
1001
+ function OverlayChatPanel({
1002
+ messages,
1003
+ chatEndRef,
1004
+ showAssistantAvatarImage,
1005
+ assistantAvatarUrl,
1006
+ assistantAvatarFallbackLabel,
1007
+ onAssistantAvatarError,
1008
+ showModelPicker,
1009
+ selectableModels,
1010
+ modelId,
1011
+ sending,
1012
+ onModelChange,
1013
+ selectedElement,
1014
+ onClearSelectedElement,
1015
+ message,
1016
+ onMessageChange,
1017
+ onMessageKeyDown,
1018
+ onSend,
1019
+ isAuthenticated
1020
+ }) {
1021
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1022
+ /* @__PURE__ */ jsxs("section", { className: "flex-1 space-y-1 overflow-auto bg-[#ece7dd] p-2", children: [
1023
+ messages.map((entry) => {
1024
+ const isAssistant = entry.role === "assistant";
1025
+ const isPendingAssistant = isAssistant && entry.status === "pending";
1026
+ return /* @__PURE__ */ jsx3(
1027
+ "div",
1028
+ {
1029
+ className: entry.role === "tool" ? "max-w-[96%] px-0.5 py-0 text-[10px] leading-tight text-stone-500" : `max-w-[92%] rounded-md py-1.5 text-[12px] leading-4 ${entry.role === "user" ? "ml-auto bg-[#2e2b27] px-2 text-stone-50" : entry.role === "thinking" ? "bg-[#e3dbce] px-2 text-stone-700" : isAssistant ? "relative border border-[#d6ccbb] bg-[#f8f3e9] pl-8 pr-2 text-stone-800" : "bg-[#ddd2bf] px-2 text-stone-800"}`,
1030
+ children: entry.role === "tool" ? /* @__PURE__ */ jsx3("span", { children: entry.text }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1031
+ isAssistant ? showAssistantAvatarImage ? /* @__PURE__ */ jsx3(
1032
+ "img",
1033
+ {
1034
+ src: assistantAvatarUrl,
1035
+ alt: "",
1036
+ "aria-hidden": "true",
1037
+ className: `pointer-events-none absolute left-2 top-1.5 h-[18px] w-[18px] select-none rounded-full border border-[#d6ccbb] bg-[#efe8dc] object-cover ${isPendingAssistant ? "animate-pulse" : ""}`,
1038
+ onError: onAssistantAvatarError
1039
+ }
1040
+ ) : /* @__PURE__ */ jsx3(
1041
+ "span",
1042
+ {
1043
+ "aria-hidden": "true",
1044
+ className: `pointer-events-none absolute left-2 top-1.5 inline-flex h-[18px] w-[18px] select-none items-center justify-center rounded-full border border-[#d6ccbb] bg-[#efe8dc] text-[9px] font-semibold text-stone-700 ${isPendingAssistant ? "animate-pulse" : ""}`,
1045
+ children: assistantAvatarFallbackLabel
1046
+ }
1047
+ ) : null,
1048
+ /* @__PURE__ */ jsx3("div", { className: "max-w-none text-inherit [&_code]:rounded [&_code]:bg-stone-900/10 [&_code]:px-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_p]:mb-1 [&_p:last-child]:mb-0 [&_ul]:list-disc [&_ul]:pl-4", children: isPendingAssistant && !entry.text.trim() ? /* @__PURE__ */ jsx3("span", { className: "block h-4", "aria-hidden": "true" }) : /* @__PURE__ */ jsx3(ReactMarkdown, { remarkPlugins: [remarkGfm], children: entry.text }) })
1049
+ ] })
1050
+ },
1051
+ entry.id
1052
+ );
1053
+ }),
1054
+ /* @__PURE__ */ jsx3("div", { ref: chatEndRef })
1055
+ ] }),
1056
+ /* @__PURE__ */ jsxs("footer", { className: "border-t border-stone-300 bg-[#f3eee5] p-2", children: [
1057
+ showModelPicker && selectableModels.length > 1 ? /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center gap-1.5", children: [
1058
+ /* @__PURE__ */ jsx3(
1059
+ "label",
1060
+ {
1061
+ htmlFor: "admin-model-picker",
1062
+ className: "text-[10px] font-semibold uppercase tracking-wide text-stone-600",
1063
+ children: "Model"
1064
+ }
1065
+ ),
1066
+ /* @__PURE__ */ jsx3(
1067
+ "select",
1068
+ {
1069
+ id: "admin-model-picker",
1070
+ value: modelId ?? selectableModels[0]?.id,
1071
+ onChange: (event) => onModelChange(event.target.value),
1072
+ disabled: sending,
1073
+ className: "h-7 min-w-0 flex-1 rounded border border-stone-300 bg-[#f7f2e8] px-2 text-[11px] text-stone-800 outline-none focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60",
1074
+ children: selectableModels.map((option) => /* @__PURE__ */ jsx3("option", { value: option.id, children: option.label }, option.id))
1075
+ }
1076
+ )
1077
+ ] }) : null,
1078
+ selectedElement ? /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center gap-1 rounded border border-stone-300 bg-[#e8dfd1] px-1.5 py-1", children: [
1079
+ /* @__PURE__ */ jsx3("span", { className: "inline-flex shrink-0 items-center justify-center rounded border border-stone-300 bg-[#f7f2e8] px-1 py-0.5 text-[9px] font-semibold text-stone-700", children: kindIcon(selectedElement.kind) }),
1080
+ /* @__PURE__ */ jsxs("p", { className: "min-w-0 flex-1 truncate text-[10px] leading-3.5 text-stone-600", children: [
1081
+ /* @__PURE__ */ jsx3("span", { className: "font-semibold text-stone-800", children: selectedElement.label }),
1082
+ /* @__PURE__ */ jsxs("span", { children: [
1083
+ " \xB7 ",
1084
+ selectedElement.path
1085
+ ] }),
1086
+ selectedElement.preview ? /* @__PURE__ */ jsxs("span", { children: [
1087
+ " \xB7 ",
1088
+ selectedElement.preview
1089
+ ] }) : null
1090
+ ] }),
1091
+ /* @__PURE__ */ jsx3(
1092
+ "button",
1093
+ {
1094
+ type: "button",
1095
+ "aria-label": "Clear selected element",
1096
+ title: "Clear selected element",
1097
+ className: "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded border border-stone-300 bg-[#f7f2e8] text-stone-700 hover:bg-[#efe8dc]",
1098
+ onClick: onClearSelectedElement,
1099
+ children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3 w-3", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
1100
+ "path",
1101
+ {
1102
+ d: "M5 5L15 15M15 5L5 15",
1103
+ stroke: "currentColor",
1104
+ strokeWidth: "1.5",
1105
+ strokeLinecap: "round"
1106
+ }
1107
+ ) })
1108
+ }
1109
+ )
1110
+ ] }) : null,
1111
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
1112
+ /* @__PURE__ */ jsx3(
1113
+ "textarea",
1114
+ {
1115
+ value: message,
1116
+ onChange: (event) => onMessageChange(event.target.value),
1117
+ onKeyDown: onMessageKeyDown,
1118
+ rows: 2,
1119
+ placeholder: "Ask the agent to edit text, image URLs, or theme tokens",
1120
+ className: "flex-1 resize-none rounded border border-stone-300 bg-[#f4efe6] px-2 py-1.5 text-[12px] leading-4 text-stone-900 outline-none placeholder:text-stone-500 focus:border-stone-500"
1121
+ }
1122
+ ),
1123
+ /* @__PURE__ */ jsx3(
1124
+ "button",
1125
+ {
1126
+ type: "button",
1127
+ onClick: onSend,
1128
+ disabled: !isAuthenticated || sending || !message.trim(),
1129
+ className: "rounded border border-stone-500 bg-stone-600 px-3 py-1.5 text-[12px] font-semibold text-stone-100 hover:bg-stone-700 disabled:cursor-not-allowed disabled:opacity-50",
1130
+ children: sending ? "Sending" : "Send"
1131
+ }
1132
+ )
1133
+ ] })
1134
+ ] })
1135
+ ] });
1136
+ }
1137
+ function OverlayHistoryPanel({
1138
+ history,
1139
+ deletingCheckpointId,
1140
+ onRestorePublished,
1141
+ onRestoreCheckpoint,
1142
+ onDeleteCheckpoint
1143
+ }) {
1144
+ return /* @__PURE__ */ jsx3("section", { className: "flex min-h-0 flex-1 flex-col p-2 text-[11px] leading-4", children: /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 overflow-hidden", children: [
1145
+ /* @__PURE__ */ jsxs("div", { className: "rounded border border-stone-300 bg-[#f8f3e9]", children: [
1146
+ /* @__PURE__ */ jsxs("div", { className: "border-b border-stone-200 px-2 py-1 font-semibold text-stone-700", children: [
1147
+ "Published (",
1148
+ history.published.length,
1149
+ ")"
1150
+ ] }),
1151
+ /* @__PURE__ */ jsx3("div", { className: "max-h-40 overflow-auto px-2 py-1.5", children: history.published.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "space-y-1", children: history.published.map((item) => /* @__PURE__ */ jsxs(
1152
+ "div",
1153
+ {
1154
+ className: "flex items-center justify-between gap-2 rounded border border-stone-200 bg-[#f2ecdf] px-2 py-1",
1155
+ children: [
1156
+ /* @__PURE__ */ jsx3("span", { className: "truncate text-[10px] text-stone-700", children: formatHistoryTime(item.createdAt) }),
1157
+ /* @__PURE__ */ jsx3(
1158
+ "button",
1159
+ {
1160
+ type: "button",
1161
+ className: "rounded border border-stone-300 bg-[#f7f2e8] px-1.5 py-0.5 text-[10px] text-stone-700 hover:bg-[#efe8dc]",
1162
+ onClick: () => onRestorePublished(item),
1163
+ children: "Restore"
1164
+ }
1165
+ )
1166
+ ]
1167
+ },
1168
+ `pub-${item.id}`
1169
+ )) }) : /* @__PURE__ */ jsx3("p", { className: "text-[10px] text-stone-500", children: "No published snapshots." }) })
1170
+ ] }),
1171
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col rounded border border-stone-300 bg-[#f8f3e9]", children: [
1172
+ /* @__PURE__ */ jsxs("div", { className: "border-b border-stone-200 px-2 py-1 font-semibold text-stone-700", children: [
1173
+ "Checkpoints (",
1174
+ history.checkpoints.length,
1175
+ ")"
1176
+ ] }),
1177
+ /* @__PURE__ */ jsx3("div", { className: "min-h-0 flex-1 overflow-auto px-2 py-1.5", children: history.checkpoints.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "space-y-1", children: history.checkpoints.map((item) => /* @__PURE__ */ jsxs(
1178
+ "div",
1179
+ {
1180
+ className: "flex items-start justify-between gap-2 rounded border border-stone-200 bg-[#f2ecdf] px-2 py-1",
1181
+ children: [
1182
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1183
+ /* @__PURE__ */ jsx3("p", { className: "truncate text-[10px] text-stone-700", children: formatHistoryTime(item.createdAt) }),
1184
+ item.reason ? /* @__PURE__ */ jsx3("p", { className: "truncate text-[10px] text-stone-500", children: item.reason }) : null
1185
+ ] }),
1186
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1187
+ /* @__PURE__ */ jsx3(
1188
+ "button",
1189
+ {
1190
+ type: "button",
1191
+ disabled: deletingCheckpointId === item.id,
1192
+ className: "rounded border border-stone-300 bg-[#f7f2e8] px-1.5 py-0.5 text-[10px] text-stone-700 hover:bg-[#efe8dc] disabled:cursor-not-allowed disabled:opacity-50",
1193
+ onClick: () => onRestoreCheckpoint(item),
1194
+ children: "Restore"
1195
+ }
1196
+ ),
1197
+ /* @__PURE__ */ jsx3(
1198
+ "button",
1199
+ {
1200
+ type: "button",
1201
+ "aria-label": "Delete checkpoint",
1202
+ title: "Delete checkpoint",
1203
+ disabled: deletingCheckpointId === item.id,
1204
+ className: "inline-flex h-6 w-6 items-center justify-center rounded border border-stone-300 bg-[#f7f2e8] text-stone-700 hover:bg-[#efe8dc] disabled:cursor-not-allowed disabled:opacity-50",
1205
+ onClick: () => onDeleteCheckpoint(item),
1206
+ children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3.5 w-3.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
1207
+ "path",
1208
+ {
1209
+ d: "M4.5 5.5H15.5M8 3.75H12M7 7.5V13.5M10 7.5V13.5M13 7.5V13.5M6.5 5.5L7 15C7.03 15.6 7.53 16.08 8.13 16.08H11.87C12.47 16.08 12.97 15.6 13 15L13.5 5.5",
1210
+ stroke: "currentColor",
1211
+ strokeWidth: "1.4",
1212
+ strokeLinecap: "round",
1213
+ strokeLinejoin: "round"
1214
+ }
1215
+ ) })
1216
+ }
1217
+ )
1218
+ ] })
1219
+ ]
1220
+ },
1221
+ `cp-${item.id}`
1222
+ )) }) : /* @__PURE__ */ jsx3("p", { className: "text-[10px] text-stone-500", children: "No checkpoints yet." }) })
1223
+ ] })
1224
+ ] }) });
1225
+ }
1226
+ function OverlayLauncherButton({ onOpen }) {
1227
+ return /* @__PURE__ */ jsx3(
1228
+ "button",
1229
+ {
1230
+ type: "button",
1231
+ onClick: onOpen,
1232
+ className: "fixed bottom-4 right-4 z-[100] rounded-full border border-stone-600 bg-stone-700 px-4 py-2 text-[12px] font-semibold text-stone-100 shadow-xl hover:bg-stone-800",
1233
+ style: { fontFamily: OVERLAY_FONT_FAMILY },
1234
+ children: "Chat to Webmaster"
1235
+ }
1236
+ );
1237
+ }
1238
+
1239
+ // src/overlay/controller.ts
1240
+ import {
1241
+ useCallback,
1242
+ useEffect as useEffect2,
1243
+ useMemo as useMemo2,
1244
+ useRef,
1245
+ useState as useState2
1246
+ } from "react";
1247
+ import { REQUIRED_PUBLISH_CONFIRMATION } from "@webmaster-droid/contracts";
1248
+ function useOverlayController() {
790
1249
  const {
791
1250
  config,
792
1251
  isAdminMode,
@@ -878,11 +1337,7 @@ function WebmasterDroidOverlay() {
878
1337
  document.removeEventListener("click", onDocumentClickCapture, true);
879
1338
  };
880
1339
  }, [isAdminMode, isOpen, setSelectedElement]);
881
- const selectableModels = modelOptions;
882
- if (!isAdminMode) {
883
- return null;
884
- }
885
- const signInWithPassword = async () => {
1340
+ const signInWithPassword = useCallback(async () => {
886
1341
  if (!supabase) {
887
1342
  setMessages((prev) => [
888
1343
  ...prev,
@@ -915,8 +1370,8 @@ function WebmasterDroidOverlay() {
915
1370
  } finally {
916
1371
  setSigningIn(false);
917
1372
  }
918
- };
919
- const onSend = async () => {
1373
+ }, [email, password, supabase]);
1374
+ const onSend = useCallback(async () => {
920
1375
  if (!token || !message.trim() || sending) {
921
1376
  return;
922
1377
  }
@@ -1020,14 +1475,28 @@ function WebmasterDroidOverlay() {
1020
1475
  } catch (error) {
1021
1476
  const detail = error instanceof Error ? error.message : "Chat failed.";
1022
1477
  const pendingAssistantId = pendingAssistantIdRef.current;
1023
- setMessages((prev) => [...removeMessageById(prev, pendingAssistantId), createMessage("system", detail)]);
1478
+ setMessages((prev) => [
1479
+ ...removeMessageById(prev, pendingAssistantId),
1480
+ createMessage("system", detail)
1481
+ ]);
1024
1482
  pendingAssistantIdRef.current = null;
1025
1483
  } finally {
1026
1484
  pendingAssistantIdRef.current = null;
1027
1485
  setSending(false);
1028
1486
  }
1029
- };
1030
- const onPublish = async () => {
1487
+ }, [
1488
+ config.apiBaseUrl,
1489
+ includeThinking,
1490
+ message,
1491
+ messages,
1492
+ modelId,
1493
+ requestRefresh,
1494
+ refreshHistory,
1495
+ selectedElement,
1496
+ sending,
1497
+ token
1498
+ ]);
1499
+ const onPublish = useCallback(async () => {
1031
1500
  if (!token) {
1032
1501
  return;
1033
1502
  }
@@ -1051,53 +1520,59 @@ function WebmasterDroidOverlay() {
1051
1520
  const detail = error instanceof Error ? error.message : "Publish failed.";
1052
1521
  setMessages((prev) => [...prev, createMessage("system", detail)]);
1053
1522
  }
1054
- };
1055
- const onRollback = async (request, label) => {
1056
- if (!token) {
1057
- return;
1058
- }
1059
- try {
1060
- await rollbackDraft(config.apiBaseUrl, token, request);
1061
- requestRefresh();
1062
- await refreshHistory(false);
1063
- setMessages((prev) => [
1064
- ...prev,
1065
- createMessage("system", `Draft restored from ${label}.`)
1066
- ]);
1067
- } catch (error) {
1068
- const detail = error instanceof Error ? error.message : "Rollback failed.";
1069
- setMessages((prev) => [...prev, createMessage("system", detail)]);
1070
- }
1071
- };
1072
- const onDeleteCheckpoint = async (checkpoint) => {
1073
- if (!token || deletingCheckpointId) {
1074
- return;
1075
- }
1076
- const timestampLabel = formatHistoryTime(checkpoint.createdAt);
1077
- const reasonLine = checkpoint.reason ? `
1523
+ }, [config.apiBaseUrl, requestRefresh, refreshHistory, token]);
1524
+ const onRollback = useCallback(
1525
+ async (request, label) => {
1526
+ if (!token) {
1527
+ return;
1528
+ }
1529
+ try {
1530
+ await rollbackDraft(config.apiBaseUrl, token, request);
1531
+ requestRefresh();
1532
+ await refreshHistory(false);
1533
+ setMessages((prev) => [
1534
+ ...prev,
1535
+ createMessage("system", `Draft restored from ${label}.`)
1536
+ ]);
1537
+ } catch (error) {
1538
+ const detail = error instanceof Error ? error.message : "Rollback failed.";
1539
+ setMessages((prev) => [...prev, createMessage("system", detail)]);
1540
+ }
1541
+ },
1542
+ [config.apiBaseUrl, requestRefresh, refreshHistory, token]
1543
+ );
1544
+ const onDeleteCheckpoint = useCallback(
1545
+ async (checkpoint) => {
1546
+ if (!token || deletingCheckpointId) {
1547
+ return;
1548
+ }
1549
+ const timestampLabel = formatHistoryTime(checkpoint.createdAt);
1550
+ const reasonLine = checkpoint.reason ? `
1078
1551
  Reason: ${checkpoint.reason}` : "";
1079
- const approved = window.confirm(
1080
- `Delete checkpoint from ${timestampLabel}? This cannot be undone.${reasonLine}`
1081
- );
1082
- if (!approved) {
1083
- return;
1084
- }
1085
- setDeletingCheckpointId(checkpoint.id);
1086
- try {
1087
- await deleteCheckpoint(config.apiBaseUrl, token, { checkpointId: checkpoint.id });
1088
- await refreshHistory(false);
1089
- setMessages((prev) => [
1090
- ...prev,
1091
- createMessage("system", `Deleted checkpoint from ${timestampLabel}.`)
1092
- ]);
1093
- } catch (error) {
1094
- const detail = error instanceof Error ? error.message : "Delete checkpoint failed.";
1095
- setMessages((prev) => [...prev, createMessage("system", detail)]);
1096
- } finally {
1097
- setDeletingCheckpointId((current) => current === checkpoint.id ? null : current);
1098
- }
1099
- };
1100
- const onClearChat = () => {
1552
+ const approved = window.confirm(
1553
+ `Delete checkpoint from ${timestampLabel}? This cannot be undone.${reasonLine}`
1554
+ );
1555
+ if (!approved) {
1556
+ return;
1557
+ }
1558
+ setDeletingCheckpointId(checkpoint.id);
1559
+ try {
1560
+ await deleteCheckpoint(config.apiBaseUrl, token, { checkpointId: checkpoint.id });
1561
+ await refreshHistory(false);
1562
+ setMessages((prev) => [
1563
+ ...prev,
1564
+ createMessage("system", `Deleted checkpoint from ${timestampLabel}.`)
1565
+ ]);
1566
+ } catch (error) {
1567
+ const detail = error instanceof Error ? error.message : "Delete checkpoint failed.";
1568
+ setMessages((prev) => [...prev, createMessage("system", detail)]);
1569
+ } finally {
1570
+ setDeletingCheckpointId((current) => current === checkpoint.id ? null : current);
1571
+ }
1572
+ },
1573
+ [config.apiBaseUrl, deletingCheckpointId, refreshHistory, token]
1574
+ );
1575
+ const onClearChat = useCallback(() => {
1101
1576
  if (sending) {
1102
1577
  return;
1103
1578
  }
@@ -1108,372 +1583,180 @@ Reason: ${checkpoint.reason}` : "";
1108
1583
  setMessages([]);
1109
1584
  setMessage("");
1110
1585
  clearSelectedElement();
1111
- };
1112
- const onMessageKeyDown = (event) => {
1113
- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) {
1114
- return;
1115
- }
1116
- event.preventDefault();
1117
- if (!isAuthenticated || sending || !message.trim()) {
1118
- return;
1119
- }
1120
- void onSend();
1121
- };
1122
- const latestPublished = history.published.reduce((max, item) => {
1123
- const value = historyTimestamp(item.createdAt);
1124
- if (value === null) {
1125
- return max;
1126
- }
1127
- return max === null ? value : Math.max(max, value);
1128
- }, null);
1129
- const latestCheckpoint = history.checkpoints.reduce((max, item) => {
1130
- const value = historyTimestamp(item.createdAt);
1131
- if (value === null) {
1132
- return max;
1133
- }
1134
- return max === null ? value : Math.max(max, value);
1135
- }, null);
1586
+ }, [clearSelectedElement, message, messages.length, selectedElement, sending]);
1587
+ const onMessageKeyDown = useCallback(
1588
+ (event) => {
1589
+ if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) {
1590
+ return;
1591
+ }
1592
+ event.preventDefault();
1593
+ if (!isAuthenticated || sending || !message.trim()) {
1594
+ return;
1595
+ }
1596
+ void onSend();
1597
+ },
1598
+ [isAuthenticated, message, onSend, sending]
1599
+ );
1600
+ const latestPublished = useMemo2(
1601
+ () => history.published.reduce((max, item) => {
1602
+ const value = historyTimestamp(item.createdAt);
1603
+ if (value === null) {
1604
+ return max;
1605
+ }
1606
+ return max === null ? value : Math.max(max, value);
1607
+ }, null),
1608
+ [history.published]
1609
+ );
1610
+ const latestCheckpoint = useMemo2(
1611
+ () => history.checkpoints.reduce((max, item) => {
1612
+ const value = historyTimestamp(item.createdAt);
1613
+ if (value === null) {
1614
+ return max;
1615
+ }
1616
+ return max === null ? value : Math.max(max, value);
1617
+ }, null),
1618
+ [history.checkpoints]
1619
+ );
1136
1620
  const publishState = latestCheckpoint !== null && (latestPublished === null || latestCheckpoint > latestPublished) ? "Unpublished" : "Published";
1137
1621
  const assistantAvatarFallbackLabel = (config.assistantAvatarFallback || "W").trim().charAt(0).toUpperCase() || "W";
1138
1622
  const showAssistantAvatarImage = Boolean(config.assistantAvatarUrl) && !assistantAvatarFailed;
1139
- return /* @__PURE__ */ jsx3(Fragment, { children: isOpen ? /* @__PURE__ */ jsxs(
1623
+ return {
1624
+ isAdminMode,
1625
+ isAuthenticated,
1626
+ authConfigured,
1627
+ isOpen,
1628
+ setIsOpen,
1629
+ activeTab,
1630
+ setActiveTab,
1631
+ overlayRootRef,
1632
+ chatEndRef,
1633
+ publishState,
1634
+ selectableModels: modelOptions,
1635
+ showModelPicker,
1636
+ modelId,
1637
+ setModelId,
1638
+ includeThinking,
1639
+ selectedElement,
1640
+ clearSelectedElement,
1641
+ email,
1642
+ setEmail,
1643
+ password,
1644
+ setPassword,
1645
+ signingIn,
1646
+ signInWithPassword,
1647
+ message,
1648
+ setMessage,
1649
+ messages,
1650
+ sending,
1651
+ onSend,
1652
+ onPublish,
1653
+ onRollback,
1654
+ history,
1655
+ deletingCheckpointId,
1656
+ onDeleteCheckpoint,
1657
+ onClearChat,
1658
+ onMessageKeyDown,
1659
+ assistantAvatarFallbackLabel,
1660
+ showAssistantAvatarImage,
1661
+ assistantAvatarUrl: config.assistantAvatarUrl,
1662
+ setAssistantAvatarFailed,
1663
+ clearChatDisabled: sending || messages.length === 0 && !message.trim() && !selectedElement
1664
+ };
1665
+ }
1666
+
1667
+ // src/overlay.tsx
1668
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1669
+ function WebmasterDroidOverlay() {
1670
+ const controller = useOverlayController();
1671
+ if (!controller.isAdminMode) {
1672
+ return null;
1673
+ }
1674
+ return /* @__PURE__ */ jsx4(Fragment2, { children: controller.isOpen ? /* @__PURE__ */ jsxs2(
1140
1675
  "div",
1141
1676
  {
1142
- ref: overlayRootRef,
1677
+ ref: controller.overlayRootRef,
1143
1678
  "data-admin-overlay-root": true,
1144
1679
  className: "fixed bottom-4 right-4 z-[100] flex h-[62vh] w-[min(480px,calc(100vw-1.5rem))] flex-col overflow-hidden rounded-lg border border-stone-300 bg-[#f6f2eb] text-stone-900 shadow-2xl",
1145
- style: {
1146
- fontFamily: "var(--font-ibm-plex-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"
1147
- },
1680
+ style: { fontFamily: OVERLAY_FONT_FAMILY },
1148
1681
  children: [
1149
- /* @__PURE__ */ jsx3("header", { className: "border-b border-stone-300 bg-[#f3eee5] p-2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1150
- isAuthenticated ? /* @__PURE__ */ jsxs(Fragment, { children: [
1151
- /* @__PURE__ */ jsx3(
1152
- "span",
1153
- {
1154
- className: `rounded border px-1.5 py-0.5 text-[10px] font-medium leading-4 ${publishState === "Published" ? "border-stone-300 bg-[#ece5d9] text-stone-600" : "border-stone-500 bg-[#ded4c3] text-stone-800"}`,
1155
- children: publishState
1156
- }
1157
- ),
1158
- /* @__PURE__ */ jsx3(
1159
- "button",
1160
- {
1161
- type: "button",
1162
- className: "rounded border border-stone-700 bg-stone-800 px-2 py-1 text-[11px] font-semibold leading-4 text-stone-100 hover:bg-stone-700 disabled:cursor-not-allowed disabled:opacity-50",
1163
- onClick: onPublish,
1164
- disabled: !isAuthenticated,
1165
- children: "Publish"
1166
- }
1167
- ),
1168
- /* @__PURE__ */ jsxs("div", { className: "inline-flex rounded-md border border-stone-300 bg-[#e8dfd1] p-0.5", children: [
1169
- /* @__PURE__ */ jsx3(
1170
- "button",
1171
- {
1172
- type: "button",
1173
- className: `rounded px-2 py-1 text-[11px] font-medium leading-4 ${activeTab === "chat" ? "bg-[#f7f2e8] text-stone-900 shadow-sm" : "text-stone-600 hover:text-stone-900"}`,
1174
- onClick: () => setActiveTab("chat"),
1175
- children: "Chat"
1176
- }
1177
- ),
1178
- /* @__PURE__ */ jsxs(
1179
- "button",
1180
- {
1181
- type: "button",
1182
- className: `rounded px-2 py-1 text-[11px] font-medium leading-4 ${activeTab === "history" ? "bg-[#f7f2e8] text-stone-900 shadow-sm" : "text-stone-600 hover:text-stone-900"}`,
1183
- onClick: () => setActiveTab("history"),
1184
- children: [
1185
- "History (",
1186
- history.published.length + history.checkpoints.length,
1187
- ")"
1188
- ]
1189
- }
1190
- )
1191
- ] })
1192
- ] }) : /* @__PURE__ */ jsx3("h2", { className: "text-[12px] font-semibold text-stone-700", children: "Login" }),
1193
- /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
1194
- isAuthenticated ? /* @__PURE__ */ jsx3(
1195
- "button",
1196
- {
1197
- type: "button",
1198
- "aria-label": "Clear chat",
1199
- title: "Clear chat",
1200
- disabled: sending || messages.length === 0 && !message.trim() && !selectedElement,
1201
- className: "inline-flex h-6 w-6 items-center justify-center rounded border border-stone-300 text-stone-600 hover:bg-[#efe8dc] hover:text-stone-800 disabled:cursor-not-allowed disabled:opacity-50",
1202
- onClick: onClearChat,
1203
- children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3.5 w-3.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
1204
- "path",
1205
- {
1206
- d: "M4.5 5.5H15.5M8 3.75H12M7 7.5V13.5M10 7.5V13.5M13 7.5V13.5M6.5 5.5L7 15C7.03 15.6 7.53 16.08 8.13 16.08H11.87C12.47 16.08 12.97 15.6 13 15L13.5 5.5",
1207
- stroke: "currentColor",
1208
- strokeWidth: "1.4",
1209
- strokeLinecap: "round",
1210
- strokeLinejoin: "round"
1211
- }
1212
- ) })
1213
- }
1214
- ) : null,
1215
- /* @__PURE__ */ jsx3(
1216
- "button",
1217
- {
1218
- type: "button",
1219
- className: "rounded border border-stone-300 px-2 py-1 text-[11px] leading-4 text-stone-700 hover:bg-[#efe8dc]",
1220
- onClick: () => setIsOpen(false),
1221
- children: "Close"
1222
- }
1223
- )
1224
- ] })
1225
- ] }) }),
1226
- !isAuthenticated ? /* @__PURE__ */ jsx3("section", { className: "flex min-h-0 flex-1 items-center justify-center bg-[#ece7dd] p-3", children: !authConfigured ? /* @__PURE__ */ jsx3("div", { className: "w-full max-w-sm rounded border border-red-300 bg-[#f8f3e9] p-3 text-[11px] leading-4 text-red-700", children: "Missing Supabase config (`supabaseUrl` / `supabaseAnonKey`)." }) : /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm rounded border border-stone-300 bg-[#f8f3e9] p-3", children: [
1227
- /* @__PURE__ */ jsx3("h3", { className: "mb-2 text-[12px] font-semibold text-stone-700", children: "Sign in" }),
1228
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1229
- /* @__PURE__ */ jsx3(
1230
- "input",
1231
- {
1232
- type: "text",
1233
- value: email,
1234
- onChange: (event) => setEmail(event.target.value),
1235
- placeholder: "login",
1236
- className: "w-full rounded border border-stone-300 bg-[#f4efe6] px-2 py-1.5 text-[12px] text-stone-900 outline-none focus:border-stone-500"
1237
- }
1238
- ),
1239
- /* @__PURE__ */ jsx3(
1240
- "input",
1241
- {
1242
- type: "password",
1243
- value: password,
1244
- onChange: (event) => setPassword(event.target.value),
1245
- placeholder: "Password",
1246
- className: "w-full rounded border border-stone-300 bg-[#f4efe6] px-2 py-1.5 text-[12px] text-stone-900 outline-none focus:border-stone-500"
1247
- }
1248
- ),
1249
- /* @__PURE__ */ jsx3(
1250
- "button",
1251
- {
1252
- type: "button",
1253
- onClick: signInWithPassword,
1254
- disabled: signingIn || !email.trim() || !password,
1255
- className: "w-full rounded border border-stone-700 bg-stone-800 px-2 py-1.5 text-[12px] font-medium text-stone-100 hover:bg-stone-700 disabled:cursor-not-allowed disabled:opacity-50",
1256
- children: signingIn ? "Signing in" : "Sign in"
1257
- }
1258
- )
1259
- ] })
1260
- ] }) }) : activeTab === "chat" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1261
- /* @__PURE__ */ jsxs("section", { className: "flex-1 space-y-1 overflow-auto bg-[#ece7dd] p-2", children: [
1262
- messages.map((entry) => {
1263
- const isAssistant = entry.role === "assistant";
1264
- const isPendingAssistant = isAssistant && entry.status === "pending";
1265
- return /* @__PURE__ */ jsx3(
1266
- "div",
1267
- {
1268
- className: entry.role === "tool" ? "max-w-[96%] px-0.5 py-0 text-[10px] leading-tight text-stone-500" : `max-w-[92%] rounded-md py-1.5 text-[12px] leading-4 ${entry.role === "user" ? "ml-auto bg-[#2e2b27] px-2 text-stone-50" : entry.role === "thinking" ? "bg-[#e3dbce] px-2 text-stone-700" : isAssistant ? "relative border border-[#d6ccbb] bg-[#f8f3e9] pl-8 pr-2 text-stone-800" : "bg-[#ddd2bf] px-2 text-stone-800"}`,
1269
- children: entry.role === "tool" ? /* @__PURE__ */ jsx3("span", { children: entry.text }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1270
- isAssistant ? showAssistantAvatarImage ? /* @__PURE__ */ jsx3(
1271
- "img",
1272
- {
1273
- src: config.assistantAvatarUrl,
1274
- alt: "",
1275
- "aria-hidden": "true",
1276
- className: `pointer-events-none absolute left-2 top-1.5 h-[18px] w-[18px] select-none rounded-full border border-[#d6ccbb] bg-[#efe8dc] object-cover ${isPendingAssistant ? "animate-pulse" : ""}`,
1277
- onError: () => setAssistantAvatarFailed(true)
1278
- }
1279
- ) : /* @__PURE__ */ jsx3(
1280
- "span",
1281
- {
1282
- "aria-hidden": "true",
1283
- className: `pointer-events-none absolute left-2 top-1.5 inline-flex h-[18px] w-[18px] select-none items-center justify-center rounded-full border border-[#d6ccbb] bg-[#efe8dc] text-[9px] font-semibold text-stone-700 ${isPendingAssistant ? "animate-pulse" : ""}`,
1284
- children: assistantAvatarFallbackLabel
1285
- }
1286
- ) : null,
1287
- /* @__PURE__ */ jsx3("div", { className: "max-w-none text-inherit [&_code]:rounded [&_code]:bg-stone-900/10 [&_code]:px-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_p]:mb-1 [&_p:last-child]:mb-0 [&_ul]:list-disc [&_ul]:pl-4", children: isPendingAssistant && !entry.text.trim() ? /* @__PURE__ */ jsx3("span", { className: "block h-4", "aria-hidden": "true" }) : /* @__PURE__ */ jsx3(ReactMarkdown, { remarkPlugins: [remarkGfm], children: entry.text }) })
1288
- ] })
1289
- },
1290
- entry.id
1682
+ /* @__PURE__ */ jsx4(
1683
+ OverlayHeader,
1684
+ {
1685
+ isAuthenticated: controller.isAuthenticated,
1686
+ publishState: controller.publishState,
1687
+ activeTab: controller.activeTab,
1688
+ historyCount: controller.history.published.length + controller.history.checkpoints.length,
1689
+ clearChatDisabled: controller.clearChatDisabled,
1690
+ onPublish: () => {
1691
+ void controller.onPublish();
1692
+ },
1693
+ onTabChange: controller.setActiveTab,
1694
+ onClearChat: controller.onClearChat,
1695
+ onClose: () => controller.setIsOpen(false)
1696
+ }
1697
+ ),
1698
+ !controller.isAuthenticated ? /* @__PURE__ */ jsx4(
1699
+ OverlayLoginPanel,
1700
+ {
1701
+ authConfigured: controller.authConfigured,
1702
+ email: controller.email,
1703
+ password: controller.password,
1704
+ signingIn: controller.signingIn,
1705
+ onEmailChange: controller.setEmail,
1706
+ onPasswordChange: controller.setPassword,
1707
+ onSignIn: () => {
1708
+ void controller.signInWithPassword();
1709
+ }
1710
+ }
1711
+ ) : controller.activeTab === "chat" ? /* @__PURE__ */ jsx4(
1712
+ OverlayChatPanel,
1713
+ {
1714
+ messages: controller.messages,
1715
+ chatEndRef: controller.chatEndRef,
1716
+ showAssistantAvatarImage: controller.showAssistantAvatarImage,
1717
+ assistantAvatarUrl: controller.assistantAvatarUrl,
1718
+ assistantAvatarFallbackLabel: controller.assistantAvatarFallbackLabel,
1719
+ onAssistantAvatarError: () => controller.setAssistantAvatarFailed(true),
1720
+ showModelPicker: controller.showModelPicker,
1721
+ selectableModels: controller.selectableModels,
1722
+ modelId: controller.modelId,
1723
+ sending: controller.sending,
1724
+ onModelChange: controller.setModelId,
1725
+ selectedElement: controller.selectedElement,
1726
+ onClearSelectedElement: controller.clearSelectedElement,
1727
+ message: controller.message,
1728
+ onMessageChange: controller.setMessage,
1729
+ onMessageKeyDown: controller.onMessageKeyDown,
1730
+ onSend: () => {
1731
+ void controller.onSend();
1732
+ },
1733
+ isAuthenticated: controller.isAuthenticated
1734
+ }
1735
+ ) : /* @__PURE__ */ jsx4(
1736
+ OverlayHistoryPanel,
1737
+ {
1738
+ history: controller.history,
1739
+ deletingCheckpointId: controller.deletingCheckpointId,
1740
+ onRestorePublished: (item) => {
1741
+ void controller.onRollback(
1742
+ { sourceType: "published", sourceId: item.id },
1743
+ `published snapshot at ${formatHistoryTime(item.createdAt)}`
1291
1744
  );
1292
- }),
1293
- /* @__PURE__ */ jsx3("div", { ref: chatEndRef })
1294
- ] }),
1295
- /* @__PURE__ */ jsxs("footer", { className: "border-t border-stone-300 bg-[#f3eee5] p-2", children: [
1296
- showModelPicker && selectableModels.length > 1 ? /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center gap-1.5", children: [
1297
- /* @__PURE__ */ jsx3(
1298
- "label",
1299
- {
1300
- htmlFor: "admin-model-picker",
1301
- className: "text-[10px] font-semibold uppercase tracking-wide text-stone-600",
1302
- children: "Model"
1303
- }
1304
- ),
1305
- /* @__PURE__ */ jsx3(
1306
- "select",
1307
- {
1308
- id: "admin-model-picker",
1309
- value: modelId ?? selectableModels[0]?.id,
1310
- onChange: (event) => setModelId(event.target.value),
1311
- disabled: sending,
1312
- className: "h-7 min-w-0 flex-1 rounded border border-stone-300 bg-[#f7f2e8] px-2 text-[11px] text-stone-800 outline-none focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60",
1313
- children: selectableModels.map((option) => /* @__PURE__ */ jsx3("option", { value: option.id, children: option.label }, option.id))
1314
- }
1315
- )
1316
- ] }) : null,
1317
- selectedElement ? /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center gap-1 rounded border border-stone-300 bg-[#e8dfd1] px-1.5 py-1", children: [
1318
- /* @__PURE__ */ jsx3("span", { className: "inline-flex shrink-0 items-center justify-center rounded border border-stone-300 bg-[#f7f2e8] px-1 py-0.5 text-[9px] font-semibold text-stone-700", children: kindIcon(selectedElement.kind) }),
1319
- /* @__PURE__ */ jsxs("p", { className: "min-w-0 flex-1 truncate text-[10px] leading-3.5 text-stone-600", children: [
1320
- /* @__PURE__ */ jsx3("span", { className: "font-semibold text-stone-800", children: selectedElement.label }),
1321
- /* @__PURE__ */ jsxs("span", { children: [
1322
- " \xB7 ",
1323
- selectedElement.path
1324
- ] }),
1325
- selectedElement.preview ? /* @__PURE__ */ jsxs("span", { children: [
1326
- " \xB7 ",
1327
- selectedElement.preview
1328
- ] }) : null
1329
- ] }),
1330
- /* @__PURE__ */ jsx3(
1331
- "button",
1332
- {
1333
- type: "button",
1334
- "aria-label": "Clear selected element",
1335
- title: "Clear selected element",
1336
- className: "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded border border-stone-300 bg-[#f7f2e8] text-stone-700 hover:bg-[#efe8dc]",
1337
- onClick: clearSelectedElement,
1338
- children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3 w-3", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
1339
- "path",
1340
- {
1341
- d: "M5 5L15 15M15 5L5 15",
1342
- stroke: "currentColor",
1343
- strokeWidth: "1.5",
1344
- strokeLinecap: "round"
1345
- }
1346
- ) })
1347
- }
1348
- )
1349
- ] }) : null,
1350
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
1351
- /* @__PURE__ */ jsx3(
1352
- "textarea",
1353
- {
1354
- value: message,
1355
- onChange: (event) => setMessage(event.target.value),
1356
- onKeyDown: onMessageKeyDown,
1357
- rows: 2,
1358
- placeholder: "Ask the agent to edit text, image URLs, or theme tokens",
1359
- className: "flex-1 resize-none rounded border border-stone-300 bg-[#f4efe6] px-2 py-1.5 text-[12px] leading-4 text-stone-900 outline-none placeholder:text-stone-500 focus:border-stone-500"
1360
- }
1361
- ),
1362
- /* @__PURE__ */ jsx3(
1363
- "button",
1364
- {
1365
- type: "button",
1366
- onClick: onSend,
1367
- disabled: !isAuthenticated || sending || !message.trim(),
1368
- className: "rounded border border-stone-500 bg-stone-600 px-3 py-1.5 text-[12px] font-semibold text-stone-100 hover:bg-stone-700 disabled:cursor-not-allowed disabled:opacity-50",
1369
- children: sending ? "Sending" : "Send"
1370
- }
1371
- )
1372
- ] })
1373
- ] })
1374
- ] }) : /* @__PURE__ */ jsx3("section", { className: "flex min-h-0 flex-1 flex-col p-2 text-[11px] leading-4", children: /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 overflow-hidden", children: [
1375
- /* @__PURE__ */ jsxs("div", { className: "rounded border border-stone-300 bg-[#f8f3e9]", children: [
1376
- /* @__PURE__ */ jsxs("div", { className: "border-b border-stone-200 px-2 py-1 font-semibold text-stone-700", children: [
1377
- "Published (",
1378
- history.published.length,
1379
- ")"
1380
- ] }),
1381
- /* @__PURE__ */ jsx3("div", { className: "max-h-40 overflow-auto px-2 py-1.5", children: history.published.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "space-y-1", children: history.published.map((item) => /* @__PURE__ */ jsxs(
1382
- "div",
1383
- {
1384
- className: "flex items-center justify-between gap-2 rounded border border-stone-200 bg-[#f2ecdf] px-2 py-1",
1385
- children: [
1386
- /* @__PURE__ */ jsx3("span", { className: "truncate text-[10px] text-stone-700", children: formatHistoryTime(item.createdAt) }),
1387
- /* @__PURE__ */ jsx3(
1388
- "button",
1389
- {
1390
- type: "button",
1391
- className: "rounded border border-stone-300 bg-[#f7f2e8] px-1.5 py-0.5 text-[10px] text-stone-700 hover:bg-[#efe8dc]",
1392
- onClick: () => onRollback(
1393
- { sourceType: "published", sourceId: item.id },
1394
- `published snapshot at ${formatHistoryTime(item.createdAt)}`
1395
- ),
1396
- children: "Restore"
1397
- }
1398
- )
1399
- ]
1400
- },
1401
- `pub-${item.id}`
1402
- )) }) : /* @__PURE__ */ jsx3("p", { className: "text-[10px] text-stone-500", children: "No published snapshots." }) })
1403
- ] }),
1404
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col rounded border border-stone-300 bg-[#f8f3e9]", children: [
1405
- /* @__PURE__ */ jsxs("div", { className: "border-b border-stone-200 px-2 py-1 font-semibold text-stone-700", children: [
1406
- "Checkpoints (",
1407
- history.checkpoints.length,
1408
- ")"
1409
- ] }),
1410
- /* @__PURE__ */ jsx3("div", { className: "min-h-0 flex-1 overflow-auto px-2 py-1.5", children: history.checkpoints.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "space-y-1", children: history.checkpoints.map((item) => /* @__PURE__ */ jsxs(
1411
- "div",
1412
- {
1413
- className: "flex items-start justify-between gap-2 rounded border border-stone-200 bg-[#f2ecdf] px-2 py-1",
1414
- children: [
1415
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1416
- /* @__PURE__ */ jsx3("p", { className: "truncate text-[10px] text-stone-700", children: formatHistoryTime(item.createdAt) }),
1417
- item.reason ? /* @__PURE__ */ jsx3("p", { className: "truncate text-[10px] text-stone-500", children: item.reason }) : null
1418
- ] }),
1419
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1420
- /* @__PURE__ */ jsx3(
1421
- "button",
1422
- {
1423
- type: "button",
1424
- disabled: deletingCheckpointId === item.id,
1425
- className: "rounded border border-stone-300 bg-[#f7f2e8] px-1.5 py-0.5 text-[10px] text-stone-700 hover:bg-[#efe8dc] disabled:cursor-not-allowed disabled:opacity-50",
1426
- onClick: () => onRollback(
1427
- { sourceType: "checkpoint", sourceId: item.id },
1428
- `checkpoint at ${formatHistoryTime(item.createdAt)}`
1429
- ),
1430
- children: "Restore"
1431
- }
1432
- ),
1433
- /* @__PURE__ */ jsx3(
1434
- "button",
1435
- {
1436
- type: "button",
1437
- "aria-label": "Delete checkpoint",
1438
- title: "Delete checkpoint",
1439
- disabled: deletingCheckpointId === item.id,
1440
- className: "inline-flex h-6 w-6 items-center justify-center rounded border border-stone-300 bg-[#f7f2e8] text-stone-700 hover:bg-[#efe8dc] disabled:cursor-not-allowed disabled:opacity-50",
1441
- onClick: () => {
1442
- void onDeleteCheckpoint(item);
1443
- },
1444
- children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3.5 w-3.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
1445
- "path",
1446
- {
1447
- d: "M4.5 5.5H15.5M8 3.75H12M7 7.5V13.5M10 7.5V13.5M13 7.5V13.5M6.5 5.5L7 15C7.03 15.6 7.53 16.08 8.13 16.08H11.87C12.47 16.08 12.97 15.6 13 15L13.5 5.5",
1448
- stroke: "currentColor",
1449
- strokeWidth: "1.4",
1450
- strokeLinecap: "round",
1451
- strokeLinejoin: "round"
1452
- }
1453
- ) })
1454
- }
1455
- )
1456
- ] })
1457
- ]
1458
- },
1459
- `cp-${item.id}`
1460
- )) }) : /* @__PURE__ */ jsx3("p", { className: "text-[10px] text-stone-500", children: "No checkpoints yet." }) })
1461
- ] })
1462
- ] }) })
1745
+ },
1746
+ onRestoreCheckpoint: (item) => {
1747
+ void controller.onRollback(
1748
+ { sourceType: "checkpoint", sourceId: item.id },
1749
+ `checkpoint at ${formatHistoryTime(item.createdAt)}`
1750
+ );
1751
+ },
1752
+ onDeleteCheckpoint: (item) => {
1753
+ void controller.onDeleteCheckpoint(item);
1754
+ }
1755
+ }
1756
+ )
1463
1757
  ]
1464
1758
  }
1465
- ) : /* @__PURE__ */ jsx3(
1466
- "button",
1467
- {
1468
- type: "button",
1469
- onClick: () => setIsOpen(true),
1470
- className: "fixed bottom-4 right-4 z-[100] rounded-full border border-stone-600 bg-stone-700 px-4 py-2 text-[12px] font-semibold text-stone-100 shadow-xl hover:bg-stone-800",
1471
- style: {
1472
- fontFamily: "var(--font-ibm-plex-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"
1473
- },
1474
- children: "Chat to Webmaster"
1475
- }
1476
- ) });
1759
+ ) : /* @__PURE__ */ jsx4(OverlayLauncherButton, { onOpen: () => controller.setIsOpen(true) }) });
1477
1760
  }
1478
1761
 
1479
1762
  // src/runtime.tsx
@@ -1484,7 +1767,10 @@ import {
1484
1767
  useMemo as useMemo3,
1485
1768
  useState as useState3
1486
1769
  } from "react";
1487
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1770
+ import {
1771
+ createDefaultCmsDocument
1772
+ } from "@webmaster-droid/contracts";
1773
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1488
1774
  var CmsRuntimeContext = createContext3(null);
1489
1775
  function createThemeCssVariables(tokens) {
1490
1776
  return {
@@ -1499,6 +1785,10 @@ function createThemeCssVariables(tokens) {
1499
1785
  }
1500
1786
  function CmsRuntimeBridge(props) {
1501
1787
  const { config, isAdminMode, isAuthenticated, token, refreshKey } = useWebmasterDroid();
1788
+ const defaultDocument = useMemo3(
1789
+ () => props.fallbackDocument ?? createDefaultCmsDocument(),
1790
+ [props.fallbackDocument]
1791
+ );
1502
1792
  const stage = useMemo3(
1503
1793
  () => isAdminMode && isAuthenticated ? "draft" : "live",
1504
1794
  [isAdminMode, isAuthenticated]
@@ -1509,7 +1799,7 @@ function CmsRuntimeBridge(props) {
1509
1799
  );
1510
1800
  const [state, setState] = useState3({
1511
1801
  requestKey: "",
1512
- document: props.fallbackDocument,
1802
+ document: defaultDocument,
1513
1803
  error: null
1514
1804
  });
1515
1805
  useEffect3(() => {
@@ -1530,14 +1820,14 @@ function CmsRuntimeBridge(props) {
1530
1820
  const message = error2 instanceof Error ? error2.message : "Failed to load content.";
1531
1821
  setState({
1532
1822
  requestKey,
1533
- document: props.fallbackDocument,
1823
+ document: defaultDocument,
1534
1824
  error: message
1535
1825
  });
1536
1826
  });
1537
1827
  return () => {
1538
1828
  ignore = true;
1539
1829
  };
1540
- }, [config.apiBaseUrl, props.fallbackDocument, requestKey, stage, token]);
1830
+ }, [config.apiBaseUrl, defaultDocument, requestKey, stage, token]);
1541
1831
  const loading = state.requestKey !== requestKey;
1542
1832
  const error = loading ? null : state.error;
1543
1833
  const value = useMemo3(
@@ -1549,14 +1839,14 @@ function CmsRuntimeBridge(props) {
1549
1839
  }),
1550
1840
  [error, loading, stage, state.document]
1551
1841
  );
1552
- const content = props.applyThemeTokens ? /* @__PURE__ */ jsx4("div", { style: createThemeCssVariables(value.document.themeTokens), children: props.children }) : props.children;
1553
- return /* @__PURE__ */ jsxs2(CmsRuntimeContext.Provider, { value, children: [
1554
- /* @__PURE__ */ jsx4(EditableProvider, { document: value.document, mode: stage, enabled: isAdminMode, children: content }),
1555
- props.includeOverlay ? /* @__PURE__ */ jsx4(WebmasterDroidOverlay, {}) : null
1842
+ const content = props.applyThemeTokens ? /* @__PURE__ */ jsx5("div", { style: createThemeCssVariables(value.document.themeTokens), children: props.children }) : props.children;
1843
+ return /* @__PURE__ */ jsxs3(CmsRuntimeContext.Provider, { value, children: [
1844
+ /* @__PURE__ */ jsx5(EditableProvider, { document: value.document, mode: stage, enabled: isAdminMode, children: content }),
1845
+ props.includeOverlay ? /* @__PURE__ */ jsx5(WebmasterDroidOverlay, {}) : null
1556
1846
  ] });
1557
1847
  }
1558
1848
  function WebmasterDroidRuntime(props) {
1559
- return /* @__PURE__ */ jsx4(WebmasterDroidProvider, { config: props.config, children: /* @__PURE__ */ jsx4(
1849
+ return /* @__PURE__ */ jsx5(WebmasterDroidProvider, { config: props.config, children: /* @__PURE__ */ jsx5(
1560
1850
  CmsRuntimeBridge,
1561
1851
  {
1562
1852
  fallbackDocument: props.fallbackDocument,