@alpaca-editor/core 1.0.4026 → 1.0.4030

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 (224) hide show
  1. package/dist/components/ActionButton.js +2 -2
  2. package/dist/components/ActionButton.js.map +1 -1
  3. package/dist/components/SimpleLanguageSelector.js +3 -1
  4. package/dist/components/SimpleLanguageSelector.js.map +1 -1
  5. package/dist/components/ui/button.d.ts +1 -1
  6. package/dist/config/config.js +1 -1
  7. package/dist/config/config.js.map +1 -1
  8. package/dist/config/types.d.ts +1 -1
  9. package/dist/editor/ContextMenu.js +0 -1
  10. package/dist/editor/ContextMenu.js.map +1 -1
  11. package/dist/editor/Editor.js +9 -3
  12. package/dist/editor/Editor.js.map +1 -1
  13. package/dist/editor/FieldListField.js +16 -24
  14. package/dist/editor/FieldListField.js.map +1 -1
  15. package/dist/editor/ImageEditButton.js +1 -1
  16. package/dist/editor/ImageEditButton.js.map +1 -1
  17. package/dist/editor/ImageEditor.js +1 -1
  18. package/dist/editor/ImageEditor.js.map +1 -1
  19. package/dist/editor/MainLayout.js +2 -2
  20. package/dist/editor/MainLayout.js.map +1 -1
  21. package/dist/editor/Terminal.js +1 -1
  22. package/dist/editor/Terminal.js.map +1 -1
  23. package/dist/editor/Titlebar.js +0 -1
  24. package/dist/editor/Titlebar.js.map +1 -1
  25. package/dist/editor/ai/AgentCostDisplay.d.ts +26 -0
  26. package/dist/editor/ai/AgentCostDisplay.js +65 -0
  27. package/dist/editor/ai/AgentCostDisplay.js.map +1 -0
  28. package/dist/editor/ai/Agents.js +83 -29
  29. package/dist/editor/ai/Agents.js.map +1 -1
  30. package/dist/editor/ai/AiPromptPopover.d.ts +7 -0
  31. package/dist/editor/ai/AiPromptPopover.js +111 -0
  32. package/dist/editor/ai/AiPromptPopover.js.map +1 -0
  33. package/dist/editor/ai/AiResponseMessage.d.ts +1 -0
  34. package/dist/editor/ai/AiResponseMessage.js +104 -18
  35. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  36. package/dist/editor/ai/AiTerminal.d.ts +18 -2
  37. package/dist/editor/ai/AiTerminal.js +423 -63
  38. package/dist/editor/ai/AiTerminal.js.map +1 -1
  39. package/dist/editor/ai/EditorAiTerminal.d.ts +2 -1
  40. package/dist/editor/ai/EditorAiTerminal.js +2 -2
  41. package/dist/editor/ai/EditorAiTerminal.js.map +1 -1
  42. package/dist/editor/ai/editorAiContext.d.ts +0 -1
  43. package/dist/editor/ai/editorAiContext.js +0 -2
  44. package/dist/editor/ai/editorAiContext.js.map +1 -1
  45. package/dist/editor/client/EditorClient.d.ts +3 -2
  46. package/dist/editor/client/EditorClient.js +326 -68
  47. package/dist/editor/client/EditorClient.js.map +1 -1
  48. package/dist/editor/client/editContext.d.ts +6 -4
  49. package/dist/editor/client/editContext.js.map +1 -1
  50. package/dist/editor/client/fieldModificationStore.d.ts +19 -0
  51. package/dist/editor/client/fieldModificationStore.js +125 -0
  52. package/dist/editor/client/fieldModificationStore.js.map +1 -0
  53. package/dist/editor/client/itemsRepository.d.ts +1 -1
  54. package/dist/editor/client/itemsRepository.js +38 -28
  55. package/dist/editor/client/itemsRepository.js.map +1 -1
  56. package/dist/editor/client/operations.d.ts +1 -0
  57. package/dist/editor/client/operations.js +39 -31
  58. package/dist/editor/client/operations.js.map +1 -1
  59. package/dist/editor/commands/componentCommands.js +5 -3
  60. package/dist/editor/commands/componentCommands.js.map +1 -1
  61. package/dist/editor/commands/itemCommands.js.map +1 -1
  62. package/dist/editor/component-designer/aiContext.js +0 -2
  63. package/dist/editor/component-designer/aiContext.js.map +1 -1
  64. package/dist/editor/field-types/DropLinkEditor.js +1 -1
  65. package/dist/editor/field-types/DropLinkEditor.js.map +1 -1
  66. package/dist/editor/field-types/MultiLineText.js +5 -7
  67. package/dist/editor/field-types/MultiLineText.js.map +1 -1
  68. package/dist/editor/field-types/RichTextEditorComponent.js +5 -7
  69. package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
  70. package/dist/editor/field-types/SingleLineText.js +5 -7
  71. package/dist/editor/field-types/SingleLineText.js.map +1 -1
  72. package/dist/editor/hooks/useEditorSettings.d.ts +17 -0
  73. package/dist/editor/hooks/useEditorSettings.js +61 -0
  74. package/dist/editor/hooks/useEditorSettings.js.map +1 -0
  75. package/dist/editor/menubar/ItemActionsMenu.js +2 -2
  76. package/dist/editor/menubar/ItemActionsMenu.js.map +1 -1
  77. package/dist/editor/menubar/PageSelector.js +1 -1
  78. package/dist/editor/menubar/PageSelector.js.map +1 -1
  79. package/dist/editor/menubar/toolbar-sections/EditControls.js +1 -1
  80. package/dist/editor/menubar/toolbar-sections/EditControls.js.map +1 -1
  81. package/dist/editor/menubar/toolbar-sections/InsertControls.js +1 -1
  82. package/dist/editor/menubar/toolbar-sections/InsertControls.js.map +1 -1
  83. package/dist/editor/menubar/toolbar-sections/UtilityControls.js +1 -1
  84. package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
  85. package/dist/editor/menubar/toolbar-sections/ViewportControls.js +1 -1
  86. package/dist/editor/menubar/toolbar-sections/ViewportControls.js.map +1 -1
  87. package/dist/editor/page-editor-chrome/FieldEditedIndicators.js +4 -3
  88. package/dist/editor/page-editor-chrome/FieldEditedIndicators.js.map +1 -1
  89. package/dist/editor/page-editor-chrome/FrameMenu.js +9 -1
  90. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  91. package/dist/editor/page-editor-chrome/useInlineAICompletion.js +0 -1
  92. package/dist/editor/page-editor-chrome/useInlineAICompletion.js.map +1 -1
  93. package/dist/editor/page-viewer/EditorForm.js +1 -1
  94. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  95. package/dist/editor/page-viewer/PageViewer.js +9 -8
  96. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  97. package/dist/editor/page-viewer/PageViewerFrame.js +7 -1
  98. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  99. package/dist/editor/page-viewer/pageViewContext.js +40 -6
  100. package/dist/editor/page-viewer/pageViewContext.js.map +1 -1
  101. package/dist/editor/reviews/Comment.js +7 -6
  102. package/dist/editor/reviews/Comment.js.map +1 -1
  103. package/dist/editor/services/agentService.d.ts +84 -12
  104. package/dist/editor/services/agentService.js +256 -15
  105. package/dist/editor/services/agentService.js.map +1 -1
  106. package/dist/editor/services/aiService.d.ts +18 -3
  107. package/dist/editor/services/aiService.js +5 -3
  108. package/dist/editor/services/aiService.js.map +1 -1
  109. package/dist/editor/services/contextService.js +0 -1
  110. package/dist/editor/services/contextService.js.map +1 -1
  111. package/dist/editor/services/systemService.d.ts +2 -1
  112. package/dist/editor/services/systemService.js +3 -0
  113. package/dist/editor/services/systemService.js.map +1 -1
  114. package/dist/editor/sidebar/ComponentPalette.js +1 -1
  115. package/dist/editor/sidebar/ComponentPalette.js.map +1 -1
  116. package/dist/editor/sidebar/EditHistory.js +2 -2
  117. package/dist/editor/sidebar/EditHistory.js.map +1 -1
  118. package/dist/editor/sidebar/GraphQL.d.ts +1 -0
  119. package/dist/editor/sidebar/GraphQL.js +8 -2
  120. package/dist/editor/sidebar/GraphQL.js.map +1 -1
  121. package/dist/editor/sidebar/MainContentTree.js +1 -1
  122. package/dist/editor/sidebar/MainContentTree.js.map +1 -1
  123. package/dist/editor/sidebar/SEOInfo.js +1 -1
  124. package/dist/editor/sidebar/SEOInfo.js.map +1 -1
  125. package/dist/editor/sidebar/ViewSelector.d.ts +4 -1
  126. package/dist/editor/sidebar/ViewSelector.js +64 -48
  127. package/dist/editor/sidebar/ViewSelector.js.map +1 -1
  128. package/dist/editor/ui/PerfectTree.js +2 -11
  129. package/dist/editor/ui/PerfectTree.js.map +1 -1
  130. package/dist/editor/ui/SimpleIconButton.d.ts +2 -0
  131. package/dist/editor/ui/SimpleIconButton.js +8 -4
  132. package/dist/editor/ui/SimpleIconButton.js.map +1 -1
  133. package/dist/index.d.ts +2 -0
  134. package/dist/index.js +2 -0
  135. package/dist/index.js.map +1 -1
  136. package/dist/page-wizard/steps/CollectStep.js +1 -1
  137. package/dist/page-wizard/steps/CollectStep.js.map +1 -1
  138. package/dist/page-wizard/steps/StructureStep.js +1 -1
  139. package/dist/page-wizard/steps/StructureStep.js.map +1 -1
  140. package/dist/page-wizard/steps/TranslateStep.js +233 -18
  141. package/dist/page-wizard/steps/TranslateStep.js.map +1 -1
  142. package/dist/revision.d.ts +2 -2
  143. package/dist/revision.js +2 -2
  144. package/dist/splash-screen/RecentPages.js +1 -13
  145. package/dist/splash-screen/RecentPages.js.map +1 -1
  146. package/dist/splash-screen/SplashScreen.js +1 -1
  147. package/dist/splash-screen/SplashScreen.js.map +1 -1
  148. package/dist/styles.css +88 -3
  149. package/dist/types.d.ts +6 -0
  150. package/package.json +2 -2
  151. package/src/components/ActionButton.tsx +3 -2
  152. package/src/components/SimpleLanguageSelector.tsx +6 -1
  153. package/src/config/config.tsx +1 -1
  154. package/src/config/types.ts +1 -1
  155. package/src/editor/ContextMenu.tsx +0 -3
  156. package/src/editor/Editor.tsx +11 -3
  157. package/src/editor/FieldListField.tsx +22 -31
  158. package/src/editor/ImageEditButton.tsx +1 -0
  159. package/src/editor/ImageEditor.tsx +1 -0
  160. package/src/editor/MainLayout.tsx +2 -2
  161. package/src/editor/Terminal.tsx +1 -1
  162. package/src/editor/Titlebar.tsx +0 -2
  163. package/src/editor/ai/AgentCostDisplay.tsx +237 -0
  164. package/src/editor/ai/Agents.tsx +147 -65
  165. package/src/editor/ai/AiPromptPopover.tsx +209 -0
  166. package/src/editor/ai/AiResponseMessage.tsx +232 -45
  167. package/src/editor/ai/AiTerminal.tsx +559 -88
  168. package/src/editor/ai/EditorAiTerminal.tsx +3 -0
  169. package/src/editor/ai/editorAiContext.ts +0 -3
  170. package/src/editor/client/EditorClient.tsx +409 -117
  171. package/src/editor/client/editContext.ts +7 -5
  172. package/src/editor/client/fieldModificationStore.ts +196 -0
  173. package/src/editor/client/itemsRepository.ts +41 -31
  174. package/src/editor/client/operations.ts +95 -76
  175. package/src/editor/commands/componentCommands.tsx +9 -3
  176. package/src/editor/commands/itemCommands.tsx +0 -1
  177. package/src/editor/component-designer/aiContext.ts +0 -2
  178. package/src/editor/field-types/DropLinkEditor.tsx +1 -1
  179. package/src/editor/field-types/MultiLineText.tsx +9 -9
  180. package/src/editor/field-types/RichTextEditorComponent.tsx +8 -8
  181. package/src/editor/field-types/SingleLineText.tsx +9 -9
  182. package/src/editor/hooks/useEditorSettings.ts +68 -0
  183. package/src/editor/menubar/ItemActionsMenu.tsx +3 -2
  184. package/src/editor/menubar/PageSelector.tsx +1 -1
  185. package/src/editor/menubar/toolbar-sections/EditControls.tsx +1 -0
  186. package/src/editor/menubar/toolbar-sections/InsertControls.tsx +1 -0
  187. package/src/editor/menubar/toolbar-sections/UtilityControls.tsx +2 -0
  188. package/src/editor/menubar/toolbar-sections/ViewportControls.tsx +2 -0
  189. package/src/editor/page-editor-chrome/FieldEditedIndicators.tsx +4 -3
  190. package/src/editor/page-editor-chrome/FrameMenu.tsx +10 -1
  191. package/src/editor/page-editor-chrome/useInlineAICompletion.tsx +0 -1
  192. package/src/editor/page-viewer/EditorForm.tsx +1 -0
  193. package/src/editor/page-viewer/PageViewer.tsx +9 -8
  194. package/src/editor/page-viewer/PageViewerFrame.tsx +7 -1
  195. package/src/editor/page-viewer/pageViewContext.ts +40 -5
  196. package/src/editor/reviews/Comment.tsx +7 -7
  197. package/src/editor/services/agentService.ts +405 -31
  198. package/src/editor/services/aiService.ts +24 -5
  199. package/src/editor/services/contextService.ts +0 -1
  200. package/src/editor/services/systemService.ts +7 -1
  201. package/src/editor/sidebar/ComponentPalette.tsx +4 -1
  202. package/src/editor/sidebar/EditHistory.tsx +2 -0
  203. package/src/editor/sidebar/GraphQL.tsx +19 -7
  204. package/src/editor/sidebar/MainContentTree.tsx +1 -1
  205. package/src/editor/sidebar/SEOInfo.tsx +1 -1
  206. package/src/editor/sidebar/ViewSelector.tsx +80 -64
  207. package/src/editor/ui/PerfectTree.tsx +2 -18
  208. package/src/editor/ui/SimpleIconButton.tsx +56 -38
  209. package/src/index.ts +2 -0
  210. package/src/page-wizard/steps/CollectStep.tsx +0 -2
  211. package/src/page-wizard/steps/StructureStep.tsx +3 -0
  212. package/src/page-wizard/steps/TranslateStep.tsx +473 -62
  213. package/src/revision.ts +2 -2
  214. package/src/splash-screen/RecentPages.tsx +0 -14
  215. package/src/splash-screen/SplashScreen.tsx +3 -2
  216. package/src/types.ts +7 -0
  217. package/dist/editor/ai/AiPopup.d.ts +0 -10
  218. package/dist/editor/ai/AiPopup.js +0 -23
  219. package/dist/editor/ai/AiPopup.js.map +0 -1
  220. package/dist/editor/ai/AiToolCall.d.ts +0 -5
  221. package/dist/editor/ai/AiToolCall.js +0 -28
  222. package/dist/editor/ai/AiToolCall.js.map +0 -1
  223. package/src/editor/ai/AiPopup.tsx +0 -59
  224. package/src/editor/ai/AiToolCall.tsx +0 -71
@@ -64,6 +64,7 @@ export function getOperationsContext(
64
64
  setSuggestedEdits: (suggestedEdits: SuggestedEdit[]) => void;
65
65
  mode: EditorMode;
66
66
  setFocusFieldComponentId: (componentId: string) => void;
67
+ setInsertMode: (insertMode: boolean) => void;
67
68
  },
68
69
  ui: {
69
70
  showErrorToast: (error: { summary?: string; details?: string }) => void;
@@ -87,25 +88,31 @@ export function getOperationsContext(
87
88
  stateRef.current = state;
88
89
  }, [state]);
89
90
 
90
- const executeOp = async (
91
- op: EditOperation,
92
- options: ExecuteEditOptions = {
93
- refresh: "immediate",
91
+ // Extract values to avoid state object reference issues
92
+ const itemsRepository = state.itemsRepository;
93
+
94
+ const executeOp = useCallback(
95
+ async (
96
+ op: EditOperation,
97
+ options: ExecuteEditOptions = {
98
+ refresh: "immediate",
99
+ },
100
+ ): Promise<EditOperation> => {
101
+ console.log("Executing op", op);
102
+ setExecutingEditOperations((ops) => (ops ? [...ops, op] : [op]));
103
+ const result = await executeEditOperation(op, state.sessionId);
104
+ // console.log("Result", result);
105
+ handleResult(result, options);
106
+ await state.refreshHistory(op.mainItem);
107
+ //state.addToEditHistory(result.data as EditOperation);
108
+ setEditOperationExecuted(result.data as EditOperation);
109
+ setExecutingEditOperations((ops) =>
110
+ ops ? ops.filter((x: EditOperation) => x.id !== op.id) : [],
111
+ );
112
+ return result.data as EditOperation;
94
113
  },
95
- ): Promise<EditOperation> => {
96
- console.log("Executing op", op);
97
- setExecutingEditOperations((ops) => (ops ? [...ops, op] : [op]));
98
- const result = await executeEditOperation(op, state.sessionId);
99
- // console.log("Result", result);
100
- handleResult(result, options);
101
- await state.refreshHistory(op.mainItem);
102
- //state.addToEditHistory(result.data as EditOperation);
103
- setEditOperationExecuted(result.data as EditOperation);
104
- setExecutingEditOperations((ops) =>
105
- ops ? ops.filter((x: EditOperation) => x.id !== op.id) : [],
106
- );
107
- return result.data as EditOperation;
108
- };
114
+ [state.sessionId, state.refreshHistory],
115
+ );
109
116
 
110
117
  const duplicateComponents = useCallback(
111
118
  ({ componentIds }: { componentIds: string[] }) => {
@@ -123,37 +130,40 @@ export function getOperationsContext(
123
130
  [state.page],
124
131
  );
125
132
 
126
- const addComponent = async (
127
- componentTypeId: string,
128
- placeholderKey: string,
129
- index: number,
130
- pageItem: ItemDescriptor,
131
- ) => {
132
- // if (!page) {
133
- // console.log("Add component: No page found");
134
- // return;
135
- // }
136
-
137
- const op: AddComponentOperation = {
138
- type: "add-component",
139
- mainItem: pageItem,
140
- // parent: parentId
141
- // ? {
142
- // id: parentId,
143
- // language: page.item.language,
144
- // version: page.item.version,
145
- // }
146
- // : undefined,
147
- placeholderKey,
148
- placeholderIndex: index,
149
- componentTypeId,
150
- date: new Date().toISOString(),
151
- id: uuid(),
152
- description: "Add component",
153
- };
154
- const result = await executeOp(op);
155
- return result as AddComponentOperation;
156
- };
133
+ const addComponent = useCallback(
134
+ async (
135
+ componentTypeId: string,
136
+ placeholderKey: string,
137
+ index: number,
138
+ pageItem: ItemDescriptor,
139
+ ) => {
140
+ // if (!page) {
141
+ // console.log("Add component: No page found");
142
+ // return;
143
+ // }
144
+
145
+ const op: AddComponentOperation = {
146
+ type: "add-component",
147
+ mainItem: pageItem,
148
+ // parent: parentId
149
+ // ? {
150
+ // id: parentId,
151
+ // language: page.item.language,
152
+ // version: page.item.version,
153
+ // }
154
+ // : undefined,
155
+ placeholderKey,
156
+ placeholderIndex: index,
157
+ componentTypeId,
158
+ date: new Date().toISOString(),
159
+ id: uuid(),
160
+ description: "Add component",
161
+ };
162
+ const result = await executeOp(op);
163
+ return result as AddComponentOperation;
164
+ },
165
+ [executeOp],
166
+ );
157
167
 
158
168
  const handleResult = (
159
169
  result: ExecutionResult<unknown>,
@@ -177,6 +187,11 @@ export function getOperationsContext(
177
187
  if (executedOp.focus) {
178
188
  state.setSelection([executedOp.focus]);
179
189
  state.setFocusFieldComponentId(executedOp.focus);
190
+
191
+ // Clear insert mode immediately after selecting the newly added component
192
+ if (executedOp.type === "add-component") {
193
+ state.setInsertMode(false);
194
+ }
180
195
  }
181
196
  }
182
197
 
@@ -272,9 +287,9 @@ export function getOperationsContext(
272
287
  description: "Rename item",
273
288
  } as RenameItemOperation);
274
289
  state.requestRefresh("immediate");
275
- state.itemsRepository.refreshItems([item]);
290
+ itemsRepository.refreshItems([item]);
276
291
  },
277
- [state.itemsRepository, executeOp],
292
+ [itemsRepository, executeOp],
278
293
  );
279
294
 
280
295
  const lastEditField = useRef<FieldDescriptor | undefined>(undefined);
@@ -314,7 +329,7 @@ export function getOperationsContext(
314
329
  const op = await getOrMergeSuggestedEditOp(field, rawValue, value, {
315
330
  page: state.page,
316
331
  user: state.user,
317
- itemsRepository: state.itemsRepository,
332
+ itemsRepository: itemsRepository,
318
333
  suggestedEdits: state.suggestedEdits,
319
334
  setSuggestedEdits: state.setSuggestedEdits,
320
335
  });
@@ -344,7 +359,7 @@ export function getOperationsContext(
344
359
  return;
345
360
  }
346
361
 
347
- const saveSequence = await state.itemsRepository.updateFieldValue(
362
+ const saveSequence = await itemsRepository.updateFieldValue(
348
363
  field,
349
364
  state.user ?? { name: "unknown", ai: false },
350
365
  true,
@@ -373,7 +388,7 @@ export function getOperationsContext(
373
388
  return executeEditFieldDebounced({ field, value, rawValue, refresh });
374
389
  },
375
390
  [
376
- state.itemsRepository,
391
+ itemsRepository,
377
392
  getOrMergeSuggestedEditOp,
378
393
  state.suggestedEdits,
379
394
  state.page,
@@ -396,7 +411,7 @@ export function getOperationsContext(
396
411
  refresh?: "none" | "immediate" | "delayed" | "waitForQuietPeriod";
397
412
  saveSequence?: number;
398
413
  }): Promise<void> => {
399
- // state.itemsRepository.updateFieldValue(
414
+ // itemsRepository.updateFieldValue(
400
415
  // field,
401
416
  // state.user ?? { name: "unknown", ai: false },
402
417
  // true,
@@ -411,11 +426,11 @@ export function getOperationsContext(
411
426
 
412
427
  if (op) {
413
428
  await executeOp(op, { refresh });
414
- await state.itemsRepository.refreshItems([field.item]);
415
- state.itemsRepository.onFieldSaved(field, val, saveSequence || 0);
429
+ await itemsRepository.refreshItems([field.item]);
430
+ itemsRepository.onFieldSaved(field, val, saveSequence || 0);
416
431
  }
417
432
  },
418
- [state.itemsRepository, executeOp],
433
+ [itemsRepository, executeOp],
419
434
  );
420
435
 
421
436
  const executeEditFieldDebounced = useDebouncedCallback(
@@ -430,7 +445,7 @@ export function getOperationsContext(
430
445
  rawValue?: string | null;
431
446
  refresh?: "none" | "immediate" | "delayed" | "waitForQuietPeriod";
432
447
  }) => {
433
- const saveSequence = await state.itemsRepository.updateFieldValue(
448
+ const saveSequence = await itemsRepository.updateFieldValue(
434
449
  field,
435
450
  state.user ?? { name: "unknown", ai: false },
436
451
  true,
@@ -454,10 +469,10 @@ export function getOperationsContext(
454
469
  rawValue: string | null | undefined,
455
470
  value: string | undefined,
456
471
  ) {
457
- const item = await state.itemsRepository.getItem(field.item);
472
+ const item = await itemsRepository.getItem(field.item);
458
473
  if (!item) return;
459
474
 
460
- const fieldItem = await state.itemsRepository.getItem({
475
+ const fieldItem = await itemsRepository.getItem({
461
476
  id: field.fieldId,
462
477
  language: "en",
463
478
  version: 0,
@@ -557,7 +572,7 @@ export function getOperationsContext(
557
572
  (x: ItemDescriptor | undefined): x is ItemDescriptor => x != null,
558
573
  );
559
574
 
560
- await state.itemsRepository.refreshItems(itemsToRefresh);
575
+ await itemsRepository.refreshItems(itemsToRefresh);
561
576
  await state.refreshHistory(item);
562
577
  return true;
563
578
  } else {
@@ -613,7 +628,7 @@ export function getOperationsContext(
613
628
  (x: ItemDescriptor | undefined): x is ItemDescriptor => x != null,
614
629
  );
615
630
 
616
- await state.itemsRepository.refreshItems(itemsToRefresh);
631
+ await itemsRepository.refreshItems(itemsToRefresh);
617
632
  await state.refreshHistory(item);
618
633
  return true;
619
634
  } else {
@@ -628,9 +643,9 @@ export function getOperationsContext(
628
643
 
629
644
  const deleteItems = useCallback(
630
645
  async (items: ItemDescriptor[]) => {
631
- const itemsToDelete = await state.itemsRepository.getItems(items);
646
+ const itemsToDelete = await itemsRepository.getItems(items);
632
647
  await executeDeleteItems(items);
633
- state.itemsRepository.onItemsDeleted(
648
+ itemsRepository.onItemsDeleted(
634
649
  itemsToDelete.map((x) => ({
635
650
  item: x.descriptor,
636
651
  parentId: x.parentId,
@@ -638,14 +653,14 @@ export function getOperationsContext(
638
653
  );
639
654
  state.requestRefresh("immediate");
640
655
  },
641
- [state.itemsRepository],
656
+ [itemsRepository],
642
657
  );
643
658
 
644
659
  const moveItems = useCallback(
645
660
  async (items: ItemDescriptor[], target: ItemDescriptor, index: number) => {
646
661
  await executeMoveItems(items, target, index);
647
662
  },
648
- [state.itemsRepository],
663
+ [itemsRepository],
649
664
  );
650
665
 
651
666
  const createItem = useCallback(
@@ -653,10 +668,10 @@ export function getOperationsContext(
653
668
  const result = await executeCreateItem(parent, templateId, name);
654
669
  if (handleErrorResult(result as ExecutionResult<unknown>, ui, state))
655
670
  return;
656
- state.itemsRepository.refreshItems([parent]);
671
+ itemsRepository.refreshItems([parent]);
657
672
  return result.data as ItemDescriptor;
658
673
  },
659
- [state.itemsRepository],
674
+ [itemsRepository],
660
675
  );
661
676
 
662
677
  const createVersion = useCallback(
@@ -671,7 +686,7 @@ export function getOperationsContext(
671
686
  const result = await executeCopyItems(items, target, index);
672
687
  return result.data as ItemDescriptor[];
673
688
  },
674
- [state.itemsRepository],
689
+ [itemsRepository],
675
690
  );
676
691
 
677
692
  const duplicateItem = useCallback(
@@ -679,10 +694,10 @@ export function getOperationsContext(
679
694
  const result = await executeDuplicateItem(item, target, name);
680
695
  if (handleErrorResult(result as ExecutionResult<unknown>, ui, state))
681
696
  return;
682
- state.itemsRepository.refreshItems([target]);
697
+ itemsRepository.refreshItems([target]);
683
698
  return result.data as ItemDescriptor;
684
699
  },
685
- [state.itemsRepository],
700
+ [itemsRepository],
686
701
  );
687
702
 
688
703
  const onFieldBlur = useCallback(() => {
@@ -691,6 +706,10 @@ export function getOperationsContext(
691
706
  lastOp.current = undefined;
692
707
  }, []);
693
708
 
709
+ // Extract state values to avoid object reference changes
710
+ const mode = state.mode;
711
+ const page = state.page;
712
+
694
713
  const ops = useMemo(
695
714
  () => ({
696
715
  addComponent,
@@ -728,10 +747,10 @@ export function getOperationsContext(
728
747
  createItem,
729
748
  createVersion,
730
749
  duplicateComponents,
731
- undoing.current,
750
+
732
751
  moveItems,
733
- state.mode,
734
- state.page,
752
+ mode,
753
+ page,
735
754
  copyItems,
736
755
  duplicateItem,
737
756
  onFieldBlur,
@@ -746,7 +765,7 @@ export function getOperationsContext(
746
765
  executingEditOperations,
747
766
  },
748
767
  }),
749
- [executingEditOperations, editOperationExecuted, ops, undoing],
768
+ [executingEditOperations, editOperationExecuted, ops],
750
769
  );
751
770
  }
752
771
 
@@ -24,6 +24,7 @@ import {
24
24
  Trash2,
25
25
  TriangleAlert,
26
26
  } from "lucide-react";
27
+ import { AiPromptPopover } from "../ai/AiPromptPopover";
27
28
 
28
29
  export type ComponentCommandData = CommandData & {
29
30
  components: Component[];
@@ -134,13 +135,18 @@ function getAiCommand(
134
135
  ): ComponentCommand {
135
136
  return {
136
137
  id: "ai",
137
- icon: <Sparkles size={defaultIconSize} />,
138
+ icon: (
139
+ <AiPromptPopover components={components}>
140
+ <Sparkles size={defaultIconSize} />
141
+ </AiPromptPopover>
142
+ ),
138
143
  label: "AI",
139
- disabled: (c) => components.length !== 1,
144
+ disabled: (c) => components.length === 0,
140
145
 
141
146
  execute: async (context: CommandContext<any>) => {
147
+ // The AiPromptPopover handles the click interaction directly
148
+ // No need to execute anything here as the popover trigger handles it
142
149
  const event = context.event!;
143
- editContext.showAiPopup(event as any);
144
150
  event.preventDefault();
145
151
  event.stopPropagation();
146
152
  },
@@ -90,7 +90,6 @@ export const renameItemCommand: ItemCommand = {
90
90
  false,
91
91
  execute: async (context: ItemCommandContext) => {
92
92
  const item = context.data?.items[0];
93
-
94
93
  if (!item) return;
95
94
  const newName = await context.openDialog<string, ItemNameDialogProps>(
96
95
  ItemNameDialog,
@@ -9,8 +9,6 @@ export function createDesignerAiContext({
9
9
  editContext: EditContextType;
10
10
  }): AiContext {
11
11
  return {
12
- //configuration.services.editorService.baseUrl +
13
- endpoint: "/alpaca/ai/designerprompt",
14
12
  callback: () => {
15
13
  editContext.requestRefresh();
16
14
  },
@@ -56,7 +56,7 @@ export function DropLinkEditor({
56
56
  optionLabel="name"
57
57
  optionValue="id"
58
58
  placeholder="Select"
59
- className="md:w-14rem w-full"
59
+ className="md:w-14rem w-full bg-gray-5"
60
60
  virtualScrollerOptions={{
61
61
  lazy: true,
62
62
  onLazyLoad: onLazyLoad,
@@ -6,8 +6,9 @@ import {
6
6
  useEditContextRef,
7
7
  useModifiedFieldsContext,
8
8
  } from "../client/editContext";
9
+ import { useFieldModification } from "../client/fieldModificationStore";
9
10
 
10
- import { useEffect, useRef, useState } from "react";
11
+ import { useEffect, useRef, useState, useMemo } from "react";
11
12
 
12
13
  import { Field } from "../pageModel";
13
14
 
@@ -27,13 +28,12 @@ export function MultiLineText({
27
28
  const fieldItem = field.descriptor.item;
28
29
  const [value, setValue] = useState(field.value as string);
29
30
 
30
- const modifiedFieldsContext = useModifiedFieldsContext();
31
- const modifiedField = modifiedFieldsContext?.modifiedFields.find(
32
- (x) =>
33
- x.fieldId === field.id &&
34
- x.item.id === fieldItem.id &&
35
- x.item.language === fieldItem.language &&
36
- x.item.version === fieldItem.version,
31
+ // Field-specific subscription - only rerenders when THIS field changes
32
+ const { modifiedField } = useFieldModification(
33
+ field.id,
34
+ fieldItem.id,
35
+ fieldItem.language,
36
+ fieldItem.version
37
37
  );
38
38
 
39
39
  useEffect(() => {
@@ -41,7 +41,7 @@ export function MultiLineText({
41
41
  if (field.isHistoric) setValue(field.value as string);
42
42
  else setValue(modifiedField?.value ?? (field.value as string));
43
43
  }
44
- }, [field.value, modifiedFieldsContext?.modifiedFields]);
44
+ }, [field.value, field.isHistoric, modifiedField?.value]);
45
45
 
46
46
  if (!editContextRef.current) return;
47
47
 
@@ -4,6 +4,7 @@ import {
4
4
  useEditContextRef,
5
5
  useModifiedFieldsContext,
6
6
  } from "../client/editContext";
7
+ import { useFieldModification } from "../client/fieldModificationStore";
7
8
 
8
9
  import { useThrottledCallback } from "use-debounce";
9
10
  import { useEffect, useRef, useState, useMemo } from "react";
@@ -46,7 +47,6 @@ export function RichTextEditorComponent({
46
47
  updateFieldValue?: (value: string) => void;
47
48
  }) {
48
49
  const editContextRef = useEditContextRef();
49
- const modifiedFieldsContext = useModifiedFieldsContext();
50
50
  const [focused, setFocused] = useState(false);
51
51
  const [value, setValue] = useState(field.value as string);
52
52
  const editorRef = useRef<HTMLDivElement>(null);
@@ -56,12 +56,12 @@ export function RichTextEditorComponent({
56
56
  const fieldItem = field.descriptor.item;
57
57
  if (!fieldItem) return null;
58
58
 
59
- const modifiedField = modifiedFieldsContext?.modifiedFields.find(
60
- (x) =>
61
- x.fieldId === field.id &&
62
- x.item.id === fieldItem.id &&
63
- x.item.language === fieldItem.language &&
64
- x.item.version === fieldItem.version,
59
+ // Field-specific subscription - only rerenders when THIS field changes
60
+ const { modifiedField } = useFieldModification(
61
+ field.id,
62
+ fieldItem.id,
63
+ fieldItem.language,
64
+ fieldItem.version
65
65
  );
66
66
 
67
67
  useEffect(() => {
@@ -75,7 +75,7 @@ export function RichTextEditorComponent({
75
75
  if (!isEditorActive && newValue !== value) {
76
76
  setValue(newValue);
77
77
  }
78
- }, [field.value, modifiedFieldsContext?.modifiedFields, focused]);
78
+ }, [field.value, field.isHistoric, modifiedField?.value, focused, value]);
79
79
 
80
80
  const debouncedSetFieldvalue = useThrottledCallback((value) => {
81
81
  if (updateFieldValue) {
@@ -7,8 +7,9 @@ import {
7
7
  useModifiedFieldsContext,
8
8
  SelectionRange,
9
9
  } from "../client/editContext";
10
+ import { useFieldModification } from "../client/fieldModificationStore";
10
11
 
11
- import { useEffect, useRef, useState } from "react";
12
+ import { useEffect, useRef, useState, useMemo } from "react";
12
13
 
13
14
  import { Field } from "../pageModel";
14
15
 
@@ -23,7 +24,6 @@ export function SingleLineText({
23
24
  }) {
24
25
  const editContextRef = useEditContextRef();
25
26
  const editContext = useEditContext();
26
- const modifiedFieldsContext = useModifiedFieldsContext();
27
27
  const inputRef = useRef<HTMLInputElement>(null);
28
28
  // const [selectionStart, setSelectionStart] = useState(0);
29
29
  // const [selectionEnd, setSelectionEnd] = useState(0);
@@ -32,12 +32,12 @@ export function SingleLineText({
32
32
  const fieldItem = field.descriptor.item;
33
33
  const [value, setValue] = useState<string | undefined>(undefined);
34
34
 
35
- const modifiedField = modifiedFieldsContext?.modifiedFields.find(
36
- (x) =>
37
- x.fieldId === field.id &&
38
- x.item.id === fieldItem.id &&
39
- x.item.language === fieldItem.language &&
40
- x.item.version === fieldItem.version,
35
+ // Field-specific subscription - only rerenders when THIS field changes
36
+ const { modifiedField } = useFieldModification(
37
+ field.id,
38
+ fieldItem.id,
39
+ fieldItem.language,
40
+ fieldItem.version
41
41
  );
42
42
 
43
43
  useEffect(() => {
@@ -47,7 +47,7 @@ export function SingleLineText({
47
47
  setValue(modifiedField?.value ?? (field.value as string));
48
48
  }
49
49
  }
50
- }, [field.value, modifiedFieldsContext?.modifiedFields]);
50
+ }, [field.value, field.isHistoric, modifiedField?.value]);
51
51
 
52
52
  if (!fieldItem) return;
53
53
 
@@ -0,0 +1,68 @@
1
+ import { useEditContext } from "../client/editContext";
2
+ import { EditorSettings } from "../../types";
3
+
4
+ /**
5
+ * Hook to access general editor settings like Sitecore name validation
6
+ * @returns EditorSettings containing validation patterns and character restrictions
7
+ */
8
+ export function useEditorSettings(): EditorSettings | undefined {
9
+ const editContext = useEditContext();
10
+ return editContext?.editorSettings;
11
+ }
12
+
13
+ /**
14
+ * Hook to validate item names against Sitecore settings
15
+ * @returns Validation functions for item names
16
+ */
17
+ export function useItemNameValidation() {
18
+ const editorSettings = useEditorSettings();
19
+
20
+ const validateItemName = (name: string): { isValid: boolean; message?: string } => {
21
+ if (!editorSettings) {
22
+ return { isValid: true }; // No settings loaded, assume valid
23
+ }
24
+
25
+ // Check invalid characters
26
+ if (editorSettings.invalidItemNameChars) {
27
+ const invalidChars = editorSettings.invalidItemNameChars.replace(/\\/g, '');
28
+ for (const char of invalidChars) {
29
+ if (name.includes(char)) {
30
+ return {
31
+ isValid: false,
32
+ message: `Item name cannot contain the character: ${char}`
33
+ };
34
+ }
35
+ }
36
+ }
37
+
38
+ // Check name validation regex
39
+ if (editorSettings.itemNameValidation) {
40
+ try {
41
+ const regex = new RegExp(editorSettings.itemNameValidation);
42
+ if (!regex.test(name)) {
43
+ return {
44
+ isValid: false,
45
+ message: `Item name must satisfy the pattern: ${editorSettings.itemNameValidation}`
46
+ };
47
+ }
48
+ } catch (error) {
49
+ console.warn('Invalid regex pattern for item name validation:', error);
50
+ }
51
+ }
52
+
53
+ // Check maximum length
54
+ if (editorSettings.maxItemNameLength && name.length > editorSettings.maxItemNameLength) {
55
+ return {
56
+ isValid: false,
57
+ message: `Item name cannot exceed ${editorSettings.maxItemNameLength} characters`
58
+ };
59
+ }
60
+
61
+ return { isValid: true };
62
+ };
63
+
64
+ return {
65
+ validateItemName,
66
+ settings: editorSettings
67
+ };
68
+ }
@@ -53,11 +53,11 @@ export function ItemActionsMenu({ isMobile }: { isMobile: boolean }) {
53
53
  return (
54
54
  <DropdownMenu>
55
55
  <DropdownMenuTrigger asChild className="cursor-pointer">
56
- <div className="p-1">
56
+ <div className="p-1" data-testid="item-actions-menu-trigger">
57
57
  <VerticalDotsIcon />
58
58
  </div>
59
59
  </DropdownMenuTrigger>
60
- <DropdownMenuContent className="w-64" align="start">
60
+ <DropdownMenuContent className="w-64" align="start" data-testid="item-actions-menu-content">
61
61
  {itemGroups.map((group, groupIndex) => (
62
62
  <div key={`group-${group.id || groupIndex}`}>
63
63
  {group.items
@@ -69,6 +69,7 @@ export function ItemActionsMenu({ isMobile }: { isMobile: boolean }) {
69
69
  item.command?.({});
70
70
  }}
71
71
  className="flex items-center gap-2"
72
+ data-testid={`item-action-${item.id}`}
72
73
  >
73
74
  {typeof item.icon === "string" ? (
74
75
  <i className={item.icon} style={{ fontSize: "1rem" }} />
@@ -185,7 +185,7 @@ export function PageSelector({
185
185
  data-testid="page-selector-button"
186
186
  >
187
187
  <div className="flex flex-col">
188
- <div className="text-sm font-medium">
188
+ <div className="text-sm font-medium" data-testid="item-name">
189
189
  {item?.name || "unknown"}
190
190
  </div>
191
191
  <div className="text-xs text-gray-500">{item?.path}</div>
@@ -32,6 +32,7 @@ export function EditControls({
32
32
  icon={<Pencil className="h-6 w-6 p-1" strokeWidth={1} />}
33
33
  label="Edit"
34
34
  size="large"
35
+ data-testid="edit-mode-button"
35
36
  selected={editContext.mode === "edit"}
36
37
  onClick={() => editContext.setMode("edit")}
37
38
  />
@@ -15,6 +15,7 @@ export function InsertControls() {
15
15
 
16
16
  return (
17
17
  <SimpleIconButton
18
+ data-testid="insert-component-button"
18
19
  icon={<Plus className="h-6 w-6 p-1" strokeWidth={1} />}
19
20
  label="Add Component"
20
21
  size="large"
@@ -17,12 +17,14 @@ export function UtilityControls() {
17
17
  icon={<EnterFullScreenIcon className="h-6 w-6 p-1" />}
18
18
  label="Fullscreen"
19
19
  size="large"
20
+ data-testid="fullscreen-button"
20
21
  onClick={() => pageViewContext.setFullscreen(true)}
21
22
  />
22
23
  <SimpleIconButton
23
24
  icon={<Layers className="h-6 w-6 p-1" strokeWidth={1} />}
24
25
  label="Component Navigator"
25
26
  size="large"
27
+ data-testid="component-navigator-button"
26
28
  selected={editContext.showRightSidebar}
27
29
  onClick={() =>
28
30
  editContext.setShowRightSidebar(!editContext.showRightSidebar)