@webmaster-droid/web 0.1.0-alpha.1 → 0.1.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 (3) hide show
  1. package/dist/index.d.ts +13 -11
  2. package/dist/index.js +700 -442
  3. package/package.json +5 -3
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,28 @@ 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) {
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
+ throw new Error(
209
+ `${componentName} missing content for "${path}". Provide a CMS value or set \`${fallbackPropName}\`.`
210
+ );
211
+ }
212
+ function sanitizeRichTextHtml(html) {
213
+ return sanitizeHtml(html, {
214
+ allowedTags: [...RICH_TEXT_ALLOWED_TAGS],
215
+ allowedAttributes: {
216
+ a: [...RICH_TEXT_ALLOWED_ATTRS]
217
+ },
218
+ allowedSchemes: ["http", "https", "mailto", "tel"],
219
+ allowProtocolRelative: false,
220
+ disallowedTagsMode: "discard"
221
+ });
178
222
  }
179
223
  function EditableText({
180
224
  path,
@@ -185,7 +229,7 @@ function EditableText({
185
229
  ...rest
186
230
  }) {
187
231
  const { document: document2, enabled } = useEditableDocument();
188
- const value = pickStringValue(document2, path, fallback);
232
+ const value = pickStringValue(document2, path, fallback, "EditableText", "fallback");
189
233
  const attrs = enabled ? editableMeta({
190
234
  path,
191
235
  label: label ?? path,
@@ -203,7 +247,8 @@ function EditableRichText({
203
247
  ...rest
204
248
  }) {
205
249
  const { document: document2, enabled } = useEditableDocument();
206
- const value = pickStringValue(document2, path, fallback);
250
+ const value = pickStringValue(document2, path, fallback, "EditableRichText", "fallback");
251
+ const sanitizedHtml = sanitizeRichTextHtml(value);
207
252
  const attrs = enabled ? editableMeta({
208
253
  path,
209
254
  label: label ?? path,
@@ -213,7 +258,7 @@ function EditableRichText({
213
258
  return createElement(as, {
214
259
  ...rest,
215
260
  ...attrs,
216
- dangerouslySetInnerHTML: { __html: value }
261
+ dangerouslySetInnerHTML: { __html: sanitizedHtml }
217
262
  });
218
263
  }
219
264
  function EditableImage({
@@ -225,8 +270,8 @@ function EditableImage({
225
270
  ...rest
226
271
  }) {
227
272
  const { document: document2, enabled } = useEditableDocument();
228
- const src = pickStringValue(document2, path, fallbackSrc);
229
- const alt = altPath ? pickStringValue(document2, altPath, fallbackAlt) : fallbackAlt;
273
+ const src = pickStringValue(document2, path, fallbackSrc, "EditableImage", "fallbackSrc");
274
+ const alt = altPath ? pickStringValue(document2, altPath, fallbackAlt, "EditableImage", "fallbackAlt") : fallbackAlt ?? "";
230
275
  const attrs = enabled ? editableMeta({
231
276
  path,
232
277
  label: label ?? path,
@@ -245,8 +290,14 @@ function EditableLink({
245
290
  ...rest
246
291
  }) {
247
292
  const { document: document2, enabled } = useEditableDocument();
248
- const href = pickStringValue(document2, hrefPath, fallbackHref);
249
- const text = pickStringValue(document2, labelPath, fallbackLabel);
293
+ const href = pickStringValue(document2, hrefPath, fallbackHref, "EditableLink", "fallbackHref");
294
+ const text = pickStringValue(
295
+ document2,
296
+ labelPath,
297
+ fallbackLabel,
298
+ "EditableLink",
299
+ "fallbackLabel"
300
+ );
250
301
  const attrs = enabled ? editableMeta({
251
302
  path: labelPath,
252
303
  label: label ?? labelPath,
@@ -686,18 +737,7 @@ function useWebmasterDroid() {
686
737
  return context;
687
738
  }
688
739
 
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";
740
+ // src/overlay/utils.ts
701
741
  function createMessage(role, text, status) {
702
742
  return {
703
743
  id: `${role}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
@@ -786,7 +826,394 @@ function kindIcon(kind) {
786
826
  }
787
827
  return "TXT";
788
828
  }
789
- function WebmasterDroidOverlay() {
829
+ var OVERLAY_FONT_FAMILY = "var(--font-ibm-plex-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace";
830
+
831
+ // src/overlay/components.tsx
832
+ import ReactMarkdown from "react-markdown";
833
+ import remarkGfm from "remark-gfm";
834
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
835
+ function OverlayHeader({
836
+ isAuthenticated,
837
+ publishState,
838
+ activeTab,
839
+ historyCount,
840
+ clearChatDisabled,
841
+ onPublish,
842
+ onTabChange,
843
+ onClearChat,
844
+ onClose
845
+ }) {
846
+ 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: [
847
+ isAuthenticated ? /* @__PURE__ */ jsxs(Fragment, { children: [
848
+ /* @__PURE__ */ jsx3(
849
+ "span",
850
+ {
851
+ 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"}`,
852
+ children: publishState
853
+ }
854
+ ),
855
+ /* @__PURE__ */ jsx3(
856
+ "button",
857
+ {
858
+ type: "button",
859
+ 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",
860
+ onClick: onPublish,
861
+ disabled: !isAuthenticated,
862
+ children: "Publish"
863
+ }
864
+ ),
865
+ /* @__PURE__ */ jsxs("div", { className: "inline-flex rounded-md border border-stone-300 bg-[#e8dfd1] p-0.5", children: [
866
+ /* @__PURE__ */ jsx3(
867
+ "button",
868
+ {
869
+ type: "button",
870
+ 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"}`,
871
+ onClick: () => onTabChange("chat"),
872
+ children: "Chat"
873
+ }
874
+ ),
875
+ /* @__PURE__ */ jsxs(
876
+ "button",
877
+ {
878
+ type: "button",
879
+ 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"}`,
880
+ onClick: () => onTabChange("history"),
881
+ children: [
882
+ "History (",
883
+ historyCount,
884
+ ")"
885
+ ]
886
+ }
887
+ )
888
+ ] })
889
+ ] }) : /* @__PURE__ */ jsx3("h2", { className: "text-[12px] font-semibold text-stone-700", children: "Login" }),
890
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
891
+ isAuthenticated ? /* @__PURE__ */ jsx3(
892
+ "button",
893
+ {
894
+ type: "button",
895
+ "aria-label": "Clear chat",
896
+ title: "Clear chat",
897
+ disabled: clearChatDisabled,
898
+ 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",
899
+ onClick: onClearChat,
900
+ children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3.5 w-3.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
901
+ "path",
902
+ {
903
+ 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",
904
+ stroke: "currentColor",
905
+ strokeWidth: "1.4",
906
+ strokeLinecap: "round",
907
+ strokeLinejoin: "round"
908
+ }
909
+ ) })
910
+ }
911
+ ) : null,
912
+ /* @__PURE__ */ jsx3(
913
+ "button",
914
+ {
915
+ type: "button",
916
+ className: "rounded border border-stone-300 px-2 py-1 text-[11px] leading-4 text-stone-700 hover:bg-[#efe8dc]",
917
+ onClick: onClose,
918
+ children: "Close"
919
+ }
920
+ )
921
+ ] })
922
+ ] }) });
923
+ }
924
+ function OverlayLoginPanel({
925
+ authConfigured,
926
+ email,
927
+ password,
928
+ signingIn,
929
+ onEmailChange,
930
+ onPasswordChange,
931
+ onSignIn
932
+ }) {
933
+ 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: [
934
+ /* @__PURE__ */ jsx3("h3", { className: "mb-2 text-[12px] font-semibold text-stone-700", children: "Sign in" }),
935
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
936
+ /* @__PURE__ */ jsx3(
937
+ "input",
938
+ {
939
+ type: "text",
940
+ value: email,
941
+ onChange: (event) => onEmailChange(event.target.value),
942
+ placeholder: "login",
943
+ 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"
944
+ }
945
+ ),
946
+ /* @__PURE__ */ jsx3(
947
+ "input",
948
+ {
949
+ type: "password",
950
+ value: password,
951
+ onChange: (event) => onPasswordChange(event.target.value),
952
+ placeholder: "Password",
953
+ 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"
954
+ }
955
+ ),
956
+ /* @__PURE__ */ jsx3(
957
+ "button",
958
+ {
959
+ type: "button",
960
+ onClick: onSignIn,
961
+ disabled: signingIn || !email.trim() || !password,
962
+ 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",
963
+ children: signingIn ? "Signing in" : "Sign in"
964
+ }
965
+ )
966
+ ] })
967
+ ] }) });
968
+ }
969
+ function OverlayChatPanel({
970
+ messages,
971
+ chatEndRef,
972
+ showAssistantAvatarImage,
973
+ assistantAvatarUrl,
974
+ assistantAvatarFallbackLabel,
975
+ onAssistantAvatarError,
976
+ showModelPicker,
977
+ selectableModels,
978
+ modelId,
979
+ sending,
980
+ onModelChange,
981
+ selectedElement,
982
+ onClearSelectedElement,
983
+ message,
984
+ onMessageChange,
985
+ onMessageKeyDown,
986
+ onSend,
987
+ isAuthenticated
988
+ }) {
989
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
990
+ /* @__PURE__ */ jsxs("section", { className: "flex-1 space-y-1 overflow-auto bg-[#ece7dd] p-2", children: [
991
+ messages.map((entry) => {
992
+ const isAssistant = entry.role === "assistant";
993
+ const isPendingAssistant = isAssistant && entry.status === "pending";
994
+ return /* @__PURE__ */ jsx3(
995
+ "div",
996
+ {
997
+ 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"}`,
998
+ children: entry.role === "tool" ? /* @__PURE__ */ jsx3("span", { children: entry.text }) : /* @__PURE__ */ jsxs(Fragment, { children: [
999
+ isAssistant ? showAssistantAvatarImage ? /* @__PURE__ */ jsx3(
1000
+ "img",
1001
+ {
1002
+ src: assistantAvatarUrl,
1003
+ alt: "",
1004
+ "aria-hidden": "true",
1005
+ 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" : ""}`,
1006
+ onError: onAssistantAvatarError
1007
+ }
1008
+ ) : /* @__PURE__ */ jsx3(
1009
+ "span",
1010
+ {
1011
+ "aria-hidden": "true",
1012
+ 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" : ""}`,
1013
+ children: assistantAvatarFallbackLabel
1014
+ }
1015
+ ) : null,
1016
+ /* @__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 }) })
1017
+ ] })
1018
+ },
1019
+ entry.id
1020
+ );
1021
+ }),
1022
+ /* @__PURE__ */ jsx3("div", { ref: chatEndRef })
1023
+ ] }),
1024
+ /* @__PURE__ */ jsxs("footer", { className: "border-t border-stone-300 bg-[#f3eee5] p-2", children: [
1025
+ showModelPicker && selectableModels.length > 1 ? /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center gap-1.5", children: [
1026
+ /* @__PURE__ */ jsx3(
1027
+ "label",
1028
+ {
1029
+ htmlFor: "admin-model-picker",
1030
+ className: "text-[10px] font-semibold uppercase tracking-wide text-stone-600",
1031
+ children: "Model"
1032
+ }
1033
+ ),
1034
+ /* @__PURE__ */ jsx3(
1035
+ "select",
1036
+ {
1037
+ id: "admin-model-picker",
1038
+ value: modelId ?? selectableModels[0]?.id,
1039
+ onChange: (event) => onModelChange(event.target.value),
1040
+ disabled: sending,
1041
+ 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",
1042
+ children: selectableModels.map((option) => /* @__PURE__ */ jsx3("option", { value: option.id, children: option.label }, option.id))
1043
+ }
1044
+ )
1045
+ ] }) : null,
1046
+ 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: [
1047
+ /* @__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) }),
1048
+ /* @__PURE__ */ jsxs("p", { className: "min-w-0 flex-1 truncate text-[10px] leading-3.5 text-stone-600", children: [
1049
+ /* @__PURE__ */ jsx3("span", { className: "font-semibold text-stone-800", children: selectedElement.label }),
1050
+ /* @__PURE__ */ jsxs("span", { children: [
1051
+ " \xB7 ",
1052
+ selectedElement.path
1053
+ ] }),
1054
+ selectedElement.preview ? /* @__PURE__ */ jsxs("span", { children: [
1055
+ " \xB7 ",
1056
+ selectedElement.preview
1057
+ ] }) : null
1058
+ ] }),
1059
+ /* @__PURE__ */ jsx3(
1060
+ "button",
1061
+ {
1062
+ type: "button",
1063
+ "aria-label": "Clear selected element",
1064
+ title: "Clear selected element",
1065
+ 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]",
1066
+ onClick: onClearSelectedElement,
1067
+ children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3 w-3", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
1068
+ "path",
1069
+ {
1070
+ d: "M5 5L15 15M15 5L5 15",
1071
+ stroke: "currentColor",
1072
+ strokeWidth: "1.5",
1073
+ strokeLinecap: "round"
1074
+ }
1075
+ ) })
1076
+ }
1077
+ )
1078
+ ] }) : null,
1079
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
1080
+ /* @__PURE__ */ jsx3(
1081
+ "textarea",
1082
+ {
1083
+ value: message,
1084
+ onChange: (event) => onMessageChange(event.target.value),
1085
+ onKeyDown: onMessageKeyDown,
1086
+ rows: 2,
1087
+ placeholder: "Ask the agent to edit text, image URLs, or theme tokens",
1088
+ 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"
1089
+ }
1090
+ ),
1091
+ /* @__PURE__ */ jsx3(
1092
+ "button",
1093
+ {
1094
+ type: "button",
1095
+ onClick: onSend,
1096
+ disabled: !isAuthenticated || sending || !message.trim(),
1097
+ 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",
1098
+ children: sending ? "Sending" : "Send"
1099
+ }
1100
+ )
1101
+ ] })
1102
+ ] })
1103
+ ] });
1104
+ }
1105
+ function OverlayHistoryPanel({
1106
+ history,
1107
+ deletingCheckpointId,
1108
+ onRestorePublished,
1109
+ onRestoreCheckpoint,
1110
+ onDeleteCheckpoint
1111
+ }) {
1112
+ 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: [
1113
+ /* @__PURE__ */ jsxs("div", { className: "rounded border border-stone-300 bg-[#f8f3e9]", children: [
1114
+ /* @__PURE__ */ jsxs("div", { className: "border-b border-stone-200 px-2 py-1 font-semibold text-stone-700", children: [
1115
+ "Published (",
1116
+ history.published.length,
1117
+ ")"
1118
+ ] }),
1119
+ /* @__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(
1120
+ "div",
1121
+ {
1122
+ className: "flex items-center justify-between gap-2 rounded border border-stone-200 bg-[#f2ecdf] px-2 py-1",
1123
+ children: [
1124
+ /* @__PURE__ */ jsx3("span", { className: "truncate text-[10px] text-stone-700", children: formatHistoryTime(item.createdAt) }),
1125
+ /* @__PURE__ */ jsx3(
1126
+ "button",
1127
+ {
1128
+ type: "button",
1129
+ className: "rounded border border-stone-300 bg-[#f7f2e8] px-1.5 py-0.5 text-[10px] text-stone-700 hover:bg-[#efe8dc]",
1130
+ onClick: () => onRestorePublished(item),
1131
+ children: "Restore"
1132
+ }
1133
+ )
1134
+ ]
1135
+ },
1136
+ `pub-${item.id}`
1137
+ )) }) : /* @__PURE__ */ jsx3("p", { className: "text-[10px] text-stone-500", children: "No published snapshots." }) })
1138
+ ] }),
1139
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col rounded border border-stone-300 bg-[#f8f3e9]", children: [
1140
+ /* @__PURE__ */ jsxs("div", { className: "border-b border-stone-200 px-2 py-1 font-semibold text-stone-700", children: [
1141
+ "Checkpoints (",
1142
+ history.checkpoints.length,
1143
+ ")"
1144
+ ] }),
1145
+ /* @__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(
1146
+ "div",
1147
+ {
1148
+ className: "flex items-start justify-between gap-2 rounded border border-stone-200 bg-[#f2ecdf] px-2 py-1",
1149
+ children: [
1150
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1151
+ /* @__PURE__ */ jsx3("p", { className: "truncate text-[10px] text-stone-700", children: formatHistoryTime(item.createdAt) }),
1152
+ item.reason ? /* @__PURE__ */ jsx3("p", { className: "truncate text-[10px] text-stone-500", children: item.reason }) : null
1153
+ ] }),
1154
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1155
+ /* @__PURE__ */ jsx3(
1156
+ "button",
1157
+ {
1158
+ type: "button",
1159
+ disabled: deletingCheckpointId === item.id,
1160
+ 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",
1161
+ onClick: () => onRestoreCheckpoint(item),
1162
+ children: "Restore"
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ jsx3(
1166
+ "button",
1167
+ {
1168
+ type: "button",
1169
+ "aria-label": "Delete checkpoint",
1170
+ title: "Delete checkpoint",
1171
+ disabled: deletingCheckpointId === item.id,
1172
+ 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",
1173
+ onClick: () => onDeleteCheckpoint(item),
1174
+ children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3.5 w-3.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
1175
+ "path",
1176
+ {
1177
+ 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",
1178
+ stroke: "currentColor",
1179
+ strokeWidth: "1.4",
1180
+ strokeLinecap: "round",
1181
+ strokeLinejoin: "round"
1182
+ }
1183
+ ) })
1184
+ }
1185
+ )
1186
+ ] })
1187
+ ]
1188
+ },
1189
+ `cp-${item.id}`
1190
+ )) }) : /* @__PURE__ */ jsx3("p", { className: "text-[10px] text-stone-500", children: "No checkpoints yet." }) })
1191
+ ] })
1192
+ ] }) });
1193
+ }
1194
+ function OverlayLauncherButton({ onOpen }) {
1195
+ return /* @__PURE__ */ jsx3(
1196
+ "button",
1197
+ {
1198
+ type: "button",
1199
+ onClick: onOpen,
1200
+ 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",
1201
+ style: { fontFamily: OVERLAY_FONT_FAMILY },
1202
+ children: "Chat to Webmaster"
1203
+ }
1204
+ );
1205
+ }
1206
+
1207
+ // src/overlay/controller.ts
1208
+ import {
1209
+ useCallback,
1210
+ useEffect as useEffect2,
1211
+ useMemo as useMemo2,
1212
+ useRef,
1213
+ useState as useState2
1214
+ } from "react";
1215
+ import { REQUIRED_PUBLISH_CONFIRMATION } from "@webmaster-droid/contracts";
1216
+ function useOverlayController() {
790
1217
  const {
791
1218
  config,
792
1219
  isAdminMode,
@@ -878,11 +1305,7 @@ function WebmasterDroidOverlay() {
878
1305
  document.removeEventListener("click", onDocumentClickCapture, true);
879
1306
  };
880
1307
  }, [isAdminMode, isOpen, setSelectedElement]);
881
- const selectableModels = modelOptions;
882
- if (!isAdminMode) {
883
- return null;
884
- }
885
- const signInWithPassword = async () => {
1308
+ const signInWithPassword = useCallback(async () => {
886
1309
  if (!supabase) {
887
1310
  setMessages((prev) => [
888
1311
  ...prev,
@@ -915,8 +1338,8 @@ function WebmasterDroidOverlay() {
915
1338
  } finally {
916
1339
  setSigningIn(false);
917
1340
  }
918
- };
919
- const onSend = async () => {
1341
+ }, [email, password, supabase]);
1342
+ const onSend = useCallback(async () => {
920
1343
  if (!token || !message.trim() || sending) {
921
1344
  return;
922
1345
  }
@@ -1020,14 +1443,28 @@ function WebmasterDroidOverlay() {
1020
1443
  } catch (error) {
1021
1444
  const detail = error instanceof Error ? error.message : "Chat failed.";
1022
1445
  const pendingAssistantId = pendingAssistantIdRef.current;
1023
- setMessages((prev) => [...removeMessageById(prev, pendingAssistantId), createMessage("system", detail)]);
1446
+ setMessages((prev) => [
1447
+ ...removeMessageById(prev, pendingAssistantId),
1448
+ createMessage("system", detail)
1449
+ ]);
1024
1450
  pendingAssistantIdRef.current = null;
1025
1451
  } finally {
1026
1452
  pendingAssistantIdRef.current = null;
1027
1453
  setSending(false);
1028
1454
  }
1029
- };
1030
- const onPublish = async () => {
1455
+ }, [
1456
+ config.apiBaseUrl,
1457
+ includeThinking,
1458
+ message,
1459
+ messages,
1460
+ modelId,
1461
+ requestRefresh,
1462
+ refreshHistory,
1463
+ selectedElement,
1464
+ sending,
1465
+ token
1466
+ ]);
1467
+ const onPublish = useCallback(async () => {
1031
1468
  if (!token) {
1032
1469
  return;
1033
1470
  }
@@ -1051,53 +1488,59 @@ function WebmasterDroidOverlay() {
1051
1488
  const detail = error instanceof Error ? error.message : "Publish failed.";
1052
1489
  setMessages((prev) => [...prev, createMessage("system", detail)]);
1053
1490
  }
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 ? `
1491
+ }, [config.apiBaseUrl, requestRefresh, refreshHistory, token]);
1492
+ const onRollback = useCallback(
1493
+ async (request, label) => {
1494
+ if (!token) {
1495
+ return;
1496
+ }
1497
+ try {
1498
+ await rollbackDraft(config.apiBaseUrl, token, request);
1499
+ requestRefresh();
1500
+ await refreshHistory(false);
1501
+ setMessages((prev) => [
1502
+ ...prev,
1503
+ createMessage("system", `Draft restored from ${label}.`)
1504
+ ]);
1505
+ } catch (error) {
1506
+ const detail = error instanceof Error ? error.message : "Rollback failed.";
1507
+ setMessages((prev) => [...prev, createMessage("system", detail)]);
1508
+ }
1509
+ },
1510
+ [config.apiBaseUrl, requestRefresh, refreshHistory, token]
1511
+ );
1512
+ const onDeleteCheckpoint = useCallback(
1513
+ async (checkpoint) => {
1514
+ if (!token || deletingCheckpointId) {
1515
+ return;
1516
+ }
1517
+ const timestampLabel = formatHistoryTime(checkpoint.createdAt);
1518
+ const reasonLine = checkpoint.reason ? `
1078
1519
  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 = () => {
1520
+ const approved = window.confirm(
1521
+ `Delete checkpoint from ${timestampLabel}? This cannot be undone.${reasonLine}`
1522
+ );
1523
+ if (!approved) {
1524
+ return;
1525
+ }
1526
+ setDeletingCheckpointId(checkpoint.id);
1527
+ try {
1528
+ await deleteCheckpoint(config.apiBaseUrl, token, { checkpointId: checkpoint.id });
1529
+ await refreshHistory(false);
1530
+ setMessages((prev) => [
1531
+ ...prev,
1532
+ createMessage("system", `Deleted checkpoint from ${timestampLabel}.`)
1533
+ ]);
1534
+ } catch (error) {
1535
+ const detail = error instanceof Error ? error.message : "Delete checkpoint failed.";
1536
+ setMessages((prev) => [...prev, createMessage("system", detail)]);
1537
+ } finally {
1538
+ setDeletingCheckpointId((current) => current === checkpoint.id ? null : current);
1539
+ }
1540
+ },
1541
+ [config.apiBaseUrl, deletingCheckpointId, refreshHistory, token]
1542
+ );
1543
+ const onClearChat = useCallback(() => {
1101
1544
  if (sending) {
1102
1545
  return;
1103
1546
  }
@@ -1108,372 +1551,180 @@ Reason: ${checkpoint.reason}` : "";
1108
1551
  setMessages([]);
1109
1552
  setMessage("");
1110
1553
  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);
1554
+ }, [clearSelectedElement, message, messages.length, selectedElement, sending]);
1555
+ const onMessageKeyDown = useCallback(
1556
+ (event) => {
1557
+ if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) {
1558
+ return;
1559
+ }
1560
+ event.preventDefault();
1561
+ if (!isAuthenticated || sending || !message.trim()) {
1562
+ return;
1563
+ }
1564
+ void onSend();
1565
+ },
1566
+ [isAuthenticated, message, onSend, sending]
1567
+ );
1568
+ const latestPublished = useMemo2(
1569
+ () => history.published.reduce((max, item) => {
1570
+ const value = historyTimestamp(item.createdAt);
1571
+ if (value === null) {
1572
+ return max;
1573
+ }
1574
+ return max === null ? value : Math.max(max, value);
1575
+ }, null),
1576
+ [history.published]
1577
+ );
1578
+ const latestCheckpoint = useMemo2(
1579
+ () => history.checkpoints.reduce((max, item) => {
1580
+ const value = historyTimestamp(item.createdAt);
1581
+ if (value === null) {
1582
+ return max;
1583
+ }
1584
+ return max === null ? value : Math.max(max, value);
1585
+ }, null),
1586
+ [history.checkpoints]
1587
+ );
1136
1588
  const publishState = latestCheckpoint !== null && (latestPublished === null || latestCheckpoint > latestPublished) ? "Unpublished" : "Published";
1137
1589
  const assistantAvatarFallbackLabel = (config.assistantAvatarFallback || "W").trim().charAt(0).toUpperCase() || "W";
1138
1590
  const showAssistantAvatarImage = Boolean(config.assistantAvatarUrl) && !assistantAvatarFailed;
1139
- return /* @__PURE__ */ jsx3(Fragment, { children: isOpen ? /* @__PURE__ */ jsxs(
1591
+ return {
1592
+ isAdminMode,
1593
+ isAuthenticated,
1594
+ authConfigured,
1595
+ isOpen,
1596
+ setIsOpen,
1597
+ activeTab,
1598
+ setActiveTab,
1599
+ overlayRootRef,
1600
+ chatEndRef,
1601
+ publishState,
1602
+ selectableModels: modelOptions,
1603
+ showModelPicker,
1604
+ modelId,
1605
+ setModelId,
1606
+ includeThinking,
1607
+ selectedElement,
1608
+ clearSelectedElement,
1609
+ email,
1610
+ setEmail,
1611
+ password,
1612
+ setPassword,
1613
+ signingIn,
1614
+ signInWithPassword,
1615
+ message,
1616
+ setMessage,
1617
+ messages,
1618
+ sending,
1619
+ onSend,
1620
+ onPublish,
1621
+ onRollback,
1622
+ history,
1623
+ deletingCheckpointId,
1624
+ onDeleteCheckpoint,
1625
+ onClearChat,
1626
+ onMessageKeyDown,
1627
+ assistantAvatarFallbackLabel,
1628
+ showAssistantAvatarImage,
1629
+ assistantAvatarUrl: config.assistantAvatarUrl,
1630
+ setAssistantAvatarFailed,
1631
+ clearChatDisabled: sending || messages.length === 0 && !message.trim() && !selectedElement
1632
+ };
1633
+ }
1634
+
1635
+ // src/overlay.tsx
1636
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1637
+ function WebmasterDroidOverlay() {
1638
+ const controller = useOverlayController();
1639
+ if (!controller.isAdminMode) {
1640
+ return null;
1641
+ }
1642
+ return /* @__PURE__ */ jsx4(Fragment2, { children: controller.isOpen ? /* @__PURE__ */ jsxs2(
1140
1643
  "div",
1141
1644
  {
1142
- ref: overlayRootRef,
1645
+ ref: controller.overlayRootRef,
1143
1646
  "data-admin-overlay-root": true,
1144
1647
  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
- },
1648
+ style: { fontFamily: OVERLAY_FONT_FAMILY },
1148
1649
  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
1650
+ /* @__PURE__ */ jsx4(
1651
+ OverlayHeader,
1652
+ {
1653
+ isAuthenticated: controller.isAuthenticated,
1654
+ publishState: controller.publishState,
1655
+ activeTab: controller.activeTab,
1656
+ historyCount: controller.history.published.length + controller.history.checkpoints.length,
1657
+ clearChatDisabled: controller.clearChatDisabled,
1658
+ onPublish: () => {
1659
+ void controller.onPublish();
1660
+ },
1661
+ onTabChange: controller.setActiveTab,
1662
+ onClearChat: controller.onClearChat,
1663
+ onClose: () => controller.setIsOpen(false)
1664
+ }
1665
+ ),
1666
+ !controller.isAuthenticated ? /* @__PURE__ */ jsx4(
1667
+ OverlayLoginPanel,
1668
+ {
1669
+ authConfigured: controller.authConfigured,
1670
+ email: controller.email,
1671
+ password: controller.password,
1672
+ signingIn: controller.signingIn,
1673
+ onEmailChange: controller.setEmail,
1674
+ onPasswordChange: controller.setPassword,
1675
+ onSignIn: () => {
1676
+ void controller.signInWithPassword();
1677
+ }
1678
+ }
1679
+ ) : controller.activeTab === "chat" ? /* @__PURE__ */ jsx4(
1680
+ OverlayChatPanel,
1681
+ {
1682
+ messages: controller.messages,
1683
+ chatEndRef: controller.chatEndRef,
1684
+ showAssistantAvatarImage: controller.showAssistantAvatarImage,
1685
+ assistantAvatarUrl: controller.assistantAvatarUrl,
1686
+ assistantAvatarFallbackLabel: controller.assistantAvatarFallbackLabel,
1687
+ onAssistantAvatarError: () => controller.setAssistantAvatarFailed(true),
1688
+ showModelPicker: controller.showModelPicker,
1689
+ selectableModels: controller.selectableModels,
1690
+ modelId: controller.modelId,
1691
+ sending: controller.sending,
1692
+ onModelChange: controller.setModelId,
1693
+ selectedElement: controller.selectedElement,
1694
+ onClearSelectedElement: controller.clearSelectedElement,
1695
+ message: controller.message,
1696
+ onMessageChange: controller.setMessage,
1697
+ onMessageKeyDown: controller.onMessageKeyDown,
1698
+ onSend: () => {
1699
+ void controller.onSend();
1700
+ },
1701
+ isAuthenticated: controller.isAuthenticated
1702
+ }
1703
+ ) : /* @__PURE__ */ jsx4(
1704
+ OverlayHistoryPanel,
1705
+ {
1706
+ history: controller.history,
1707
+ deletingCheckpointId: controller.deletingCheckpointId,
1708
+ onRestorePublished: (item) => {
1709
+ void controller.onRollback(
1710
+ { sourceType: "published", sourceId: item.id },
1711
+ `published snapshot at ${formatHistoryTime(item.createdAt)}`
1291
1712
  );
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
- ] }) })
1713
+ },
1714
+ onRestoreCheckpoint: (item) => {
1715
+ void controller.onRollback(
1716
+ { sourceType: "checkpoint", sourceId: item.id },
1717
+ `checkpoint at ${formatHistoryTime(item.createdAt)}`
1718
+ );
1719
+ },
1720
+ onDeleteCheckpoint: (item) => {
1721
+ void controller.onDeleteCheckpoint(item);
1722
+ }
1723
+ }
1724
+ )
1463
1725
  ]
1464
1726
  }
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
- ) });
1727
+ ) : /* @__PURE__ */ jsx4(OverlayLauncherButton, { onOpen: () => controller.setIsOpen(true) }) });
1477
1728
  }
1478
1729
 
1479
1730
  // src/runtime.tsx
@@ -1484,7 +1735,10 @@ import {
1484
1735
  useMemo as useMemo3,
1485
1736
  useState as useState3
1486
1737
  } from "react";
1487
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1738
+ import {
1739
+ createDefaultCmsDocument
1740
+ } from "@webmaster-droid/contracts";
1741
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1488
1742
  var CmsRuntimeContext = createContext3(null);
1489
1743
  function createThemeCssVariables(tokens) {
1490
1744
  return {
@@ -1499,6 +1753,10 @@ function createThemeCssVariables(tokens) {
1499
1753
  }
1500
1754
  function CmsRuntimeBridge(props) {
1501
1755
  const { config, isAdminMode, isAuthenticated, token, refreshKey } = useWebmasterDroid();
1756
+ const defaultDocument = useMemo3(
1757
+ () => props.fallbackDocument ?? createDefaultCmsDocument(),
1758
+ [props.fallbackDocument]
1759
+ );
1502
1760
  const stage = useMemo3(
1503
1761
  () => isAdminMode && isAuthenticated ? "draft" : "live",
1504
1762
  [isAdminMode, isAuthenticated]
@@ -1509,7 +1767,7 @@ function CmsRuntimeBridge(props) {
1509
1767
  );
1510
1768
  const [state, setState] = useState3({
1511
1769
  requestKey: "",
1512
- document: props.fallbackDocument,
1770
+ document: defaultDocument,
1513
1771
  error: null
1514
1772
  });
1515
1773
  useEffect3(() => {
@@ -1530,14 +1788,14 @@ function CmsRuntimeBridge(props) {
1530
1788
  const message = error2 instanceof Error ? error2.message : "Failed to load content.";
1531
1789
  setState({
1532
1790
  requestKey,
1533
- document: props.fallbackDocument,
1791
+ document: defaultDocument,
1534
1792
  error: message
1535
1793
  });
1536
1794
  });
1537
1795
  return () => {
1538
1796
  ignore = true;
1539
1797
  };
1540
- }, [config.apiBaseUrl, props.fallbackDocument, requestKey, stage, token]);
1798
+ }, [config.apiBaseUrl, defaultDocument, requestKey, stage, token]);
1541
1799
  const loading = state.requestKey !== requestKey;
1542
1800
  const error = loading ? null : state.error;
1543
1801
  const value = useMemo3(
@@ -1549,14 +1807,14 @@ function CmsRuntimeBridge(props) {
1549
1807
  }),
1550
1808
  [error, loading, stage, state.document]
1551
1809
  );
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
1810
+ const content = props.applyThemeTokens ? /* @__PURE__ */ jsx5("div", { style: createThemeCssVariables(value.document.themeTokens), children: props.children }) : props.children;
1811
+ return /* @__PURE__ */ jsxs3(CmsRuntimeContext.Provider, { value, children: [
1812
+ /* @__PURE__ */ jsx5(EditableProvider, { document: value.document, mode: stage, enabled: isAdminMode, children: content }),
1813
+ props.includeOverlay ? /* @__PURE__ */ jsx5(WebmasterDroidOverlay, {}) : null
1556
1814
  ] });
1557
1815
  }
1558
1816
  function WebmasterDroidRuntime(props) {
1559
- return /* @__PURE__ */ jsx4(WebmasterDroidProvider, { config: props.config, children: /* @__PURE__ */ jsx4(
1817
+ return /* @__PURE__ */ jsx5(WebmasterDroidProvider, { config: props.config, children: /* @__PURE__ */ jsx5(
1560
1818
  CmsRuntimeBridge,
1561
1819
  {
1562
1820
  fallbackDocument: props.fallbackDocument,