@alpaca-editor/core 1.0.4187 → 1.0.4190

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 (102) hide show
  1. package/dist/agents-view/AgentCard.js +1 -1
  2. package/dist/agents-view/AgentCard.js.map +1 -1
  3. package/dist/agents-view/AgentsView.js +7 -5
  4. package/dist/agents-view/AgentsView.js.map +1 -1
  5. package/dist/components/ui/PlaceholderInput.d.ts +41 -0
  6. package/dist/components/ui/PlaceholderInput.js +160 -0
  7. package/dist/components/ui/PlaceholderInput.js.map +1 -0
  8. package/dist/components/ui/PlaceholderInputTypes.d.ts +41 -0
  9. package/dist/components/ui/PlaceholderInputTypes.js +48 -0
  10. package/dist/components/ui/PlaceholderInputTypes.js.map +1 -0
  11. package/dist/components/ui/PlaceholderItemSelector.d.ts +7 -0
  12. package/dist/components/ui/PlaceholderItemSelector.js +154 -0
  13. package/dist/components/ui/PlaceholderItemSelector.js.map +1 -0
  14. package/dist/config/config.js +7 -14
  15. package/dist/config/config.js.map +1 -1
  16. package/dist/editor/ItemInfo.js +3 -3
  17. package/dist/editor/ItemInfo.js.map +1 -1
  18. package/dist/editor/QuickItemSwitcher.js +1 -1
  19. package/dist/editor/QuickItemSwitcher.js.map +1 -1
  20. package/dist/editor/ai/AgentTerminal.d.ts +7 -1
  21. package/dist/editor/ai/AgentTerminal.js +256 -382
  22. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  23. package/dist/editor/ai/Agents.js +198 -84
  24. package/dist/editor/ai/Agents.js.map +1 -1
  25. package/dist/editor/ai/AiResponseMessage.d.ts +3 -1
  26. package/dist/editor/ai/AiResponseMessage.js +63 -12
  27. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  28. package/dist/editor/ai/ToolCallDisplay.d.ts +2 -1
  29. package/dist/editor/ai/ToolCallDisplay.js +13 -5
  30. package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
  31. package/dist/editor/client/EditorShell.js +6 -5
  32. package/dist/editor/client/EditorShell.js.map +1 -1
  33. package/dist/editor/client/hooks/useSocketMessageHandler.js +6 -4
  34. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
  35. package/dist/editor/commands/componentCommands.js +2 -0
  36. package/dist/editor/commands/componentCommands.js.map +1 -1
  37. package/dist/editor/control-center/About.js +1 -1
  38. package/dist/editor/control-center/About.js.map +1 -1
  39. package/dist/editor/control-center/AllAgentsPanel.js +1 -1
  40. package/dist/editor/field-types/MultiLineText.js +1 -1
  41. package/dist/editor/field-types/MultiLineText.js.map +1 -1
  42. package/dist/editor/services/aiService.d.ts +1 -0
  43. package/dist/editor/services/aiService.js.map +1 -1
  44. package/dist/editor/sidebar/Validation.js +1 -1
  45. package/dist/editor/sidebar/Validation.js.map +1 -1
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.js +4 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/page-wizard/PageWizard.js +3 -3
  50. package/dist/page-wizard/PageWizard.js.map +1 -1
  51. package/dist/page-wizard/WizardSteps.js +1 -1
  52. package/dist/page-wizard/WizardSteps.js.map +1 -1
  53. package/dist/revision.d.ts +2 -2
  54. package/dist/revision.js +2 -2
  55. package/dist/splash-screen/ModernSplashScreen.d.ts +8 -0
  56. package/dist/splash-screen/ModernSplashScreen.js +36 -0
  57. package/dist/splash-screen/ModernSplashScreen.js.map +1 -0
  58. package/dist/splash-screen/OpenPage.js +10 -6
  59. package/dist/splash-screen/OpenPage.js.map +1 -1
  60. package/dist/splash-screen/ParheliaAssistantChat.d.ts +8 -0
  61. package/dist/splash-screen/ParheliaAssistantChat.js +155 -0
  62. package/dist/splash-screen/ParheliaAssistantChat.js.map +1 -0
  63. package/dist/splash-screen/RecentAgents.d.ts +7 -0
  64. package/dist/splash-screen/RecentAgents.js +76 -0
  65. package/dist/splash-screen/RecentAgents.js.map +1 -0
  66. package/dist/splash-screen/RecentPages.js +2 -2
  67. package/dist/splash-screen/RecentPages.js.map +1 -1
  68. package/dist/splash-screen/SplashScreen.js +1 -1
  69. package/dist/splash-screen/SplashScreen.js.map +1 -1
  70. package/dist/styles.css +241 -12
  71. package/package.json +1 -1
  72. package/src/agents-view/AgentCard.tsx +1 -6
  73. package/src/agents-view/AgentsView.tsx +18 -30
  74. package/src/components/ui/PlaceholderInput.tsx +290 -0
  75. package/src/components/ui/PlaceholderInputTypes.tsx +97 -0
  76. package/src/components/ui/PlaceholderItemSelector.tsx +253 -0
  77. package/src/config/config.tsx +8 -17
  78. package/src/editor/ItemInfo.tsx +3 -2
  79. package/src/editor/QuickItemSwitcher.tsx +1 -1
  80. package/src/editor/ai/AgentTerminal.tsx +544 -649
  81. package/src/editor/ai/Agents.tsx +464 -250
  82. package/src/editor/ai/AiResponseMessage.tsx +154 -29
  83. package/src/editor/ai/ToolCallDisplay.tsx +18 -4
  84. package/src/editor/client/EditorShell.tsx +9 -6
  85. package/src/editor/client/hooks/useSocketMessageHandler.ts +6 -7
  86. package/src/editor/commands/componentCommands.tsx +1 -0
  87. package/src/editor/control-center/About.tsx +2 -2
  88. package/src/editor/control-center/AllAgentsPanel.tsx +1 -1
  89. package/src/editor/field-types/MultiLineText.tsx +1 -1
  90. package/src/editor/services/aiService.ts +2 -0
  91. package/src/editor/sidebar/Validation.tsx +1 -1
  92. package/src/index.ts +5 -0
  93. package/src/page-wizard/PageWizard.tsx +3 -3
  94. package/src/page-wizard/WizardSteps.tsx +1 -1
  95. package/src/revision.ts +2 -2
  96. package/src/splash-screen/ModernSplashScreen.tsx +158 -0
  97. package/src/splash-screen/OpenPage.tsx +12 -4
  98. package/src/splash-screen/ParheliaAssistantChat.tsx +273 -0
  99. package/src/splash-screen/RecentAgents.tsx +151 -0
  100. package/src/splash-screen/RecentPages.tsx +58 -61
  101. package/src/splash-screen/SplashScreen.tsx +1 -1
  102. package/styles.css +20 -0
@@ -0,0 +1,290 @@
1
+ import * as React from "react";
2
+ import { cn } from "../../lib/utils";
3
+ import { Button } from "./button";
4
+ import { X } from "lucide-react";
5
+ import { placeholderInputTypeRegistry } from "./PlaceholderInputTypes";
6
+
7
+ /**
8
+ * PlaceholderInput component for AI Agent quick actions.
9
+ *
10
+ * Allows users to fill in placeholders in text templates with tab navigation.
11
+ *
12
+ * Example usage for AI Agents:
13
+ * ```json
14
+ * {
15
+ * "label": "Create Component",
16
+ * "prompt": "Create a {component_type} component named {component_name}",
17
+ * "behavior": "submit"
18
+ * }
19
+ * ```
20
+ *
21
+ * Supported placeholder formats:
22
+ * - Curly braces: {placeholder_name} or {0}, {1}, {2}
23
+ * - Angle brackets: <placeholder_name> or <0>, <1>, <2>
24
+ *
25
+ * Keyboard navigation:
26
+ * - Tab: Move to next placeholder (wraps to first)
27
+ * - Shift+Tab: Move to previous placeholder (wraps to last)
28
+ * - Enter: Submit with filled values
29
+ * - Escape: Cancel
30
+ */
31
+
32
+ export type Placeholder = {
33
+ name: string; // Display label
34
+ type: string; // "text", "item", "date", etc.
35
+ parameters: Record<string, string>; // Parsed key=value pairs
36
+ index: number;
37
+ startPos: number;
38
+ endPos: number;
39
+ };
40
+
41
+ type PlaceholderInputProps = {
42
+ text: string;
43
+ onComplete: (filledText: string) => void;
44
+ onCancel?: () => void;
45
+ className?: string;
46
+ showButtons?: boolean;
47
+ };
48
+
49
+ function parsePlaceholders(text: string): Placeholder[] {
50
+ const placeholders: Placeholder[] = [];
51
+ // Match both {placeholder} and <placeholder> formats
52
+ const regex = /\{([^}]+)\}|<([^>]+)>/g;
53
+ let match;
54
+ let index = 0;
55
+
56
+ while ((match = regex.exec(text)) !== null) {
57
+ // match[1] is for {placeholder}, match[2] is for <placeholder>
58
+ const placeholderContent = match[1] || match[2];
59
+ if (placeholderContent) {
60
+ // Parse the content: {label|type|param=value|param2=value2}
61
+ const parts = placeholderContent.split("|");
62
+ const label = parts[0]?.trim() || "";
63
+ const type = parts[1]?.trim() || "text"; // Default to "text" type
64
+
65
+ // Parse parameters from remaining parts (format: key=value)
66
+ const parameters: Record<string, string> = {};
67
+ for (let i = 2; i < parts.length; i++) {
68
+ const part = parts[i]?.trim() || "";
69
+ if (!part) continue;
70
+ const eqIndex = part.indexOf("=");
71
+ if (eqIndex !== -1) {
72
+ const key = part.substring(0, eqIndex).trim();
73
+ const value = part.substring(eqIndex + 1).trim();
74
+ if (key) {
75
+ parameters[key] = value;
76
+ }
77
+ }
78
+ }
79
+
80
+ placeholders.push({
81
+ name: label,
82
+ type,
83
+ parameters,
84
+ index: index++,
85
+ startPos: match.index,
86
+ endPos: match.index + match[0].length,
87
+ });
88
+ }
89
+ }
90
+
91
+ return placeholders;
92
+ }
93
+
94
+ export function PlaceholderInput({
95
+ text,
96
+ onComplete,
97
+ onCancel,
98
+ className,
99
+ showButtons = true,
100
+ }: PlaceholderInputProps) {
101
+ const placeholders = React.useMemo(() => parsePlaceholders(text), [text]);
102
+ const [values, setValues] = React.useState<{ [key: number]: string }>({});
103
+
104
+ // Create refs for each placeholder using useRef to avoid the null issue
105
+ const inputRefs = React.useRef<{ [key: number]: HTMLInputElement | HTMLTextAreaElement | null }>({});
106
+
107
+ // Auto-focus first placeholder on mount
108
+ React.useEffect(() => {
109
+ if (placeholders.length > 0) {
110
+ const firstInput = inputRefs.current[0];
111
+ if (firstInput) {
112
+ setTimeout(() => firstInput.focus(), 0);
113
+ }
114
+ }
115
+ }, [placeholders.length]);
116
+
117
+ const handleValueChange = (index: number, value: string) => {
118
+ setValues((prev) => ({ ...prev, [index]: value }));
119
+ };
120
+
121
+ const handleKeyDown = (
122
+ e: React.KeyboardEvent,
123
+ index: number,
124
+ ) => {
125
+ if (e.key === "Tab") {
126
+ e.preventDefault();
127
+ if (e.shiftKey) {
128
+ // Shift+Tab: go to previous input (wrap to last if at first)
129
+ const prevIndex = index - 1;
130
+ if (prevIndex >= 0) {
131
+ inputRefs.current[prevIndex]?.focus();
132
+ } else {
133
+ // Wrap to last placeholder
134
+ inputRefs.current[placeholders.length - 1]?.focus();
135
+ }
136
+ } else {
137
+ // Tab: go to next input (wrap to first if at last)
138
+ const nextIndex = index + 1;
139
+ if (nextIndex < placeholders.length) {
140
+ inputRefs.current[nextIndex]?.focus();
141
+ } else {
142
+ // Wrap to first placeholder
143
+ inputRefs.current[0]?.focus();
144
+ }
145
+ }
146
+ } else if (e.key === "Enter") {
147
+ e.preventDefault();
148
+ handleSubmit();
149
+ } else if (e.key === "Escape") {
150
+ e.preventDefault();
151
+ onCancel?.();
152
+ }
153
+ };
154
+
155
+ const handleSubmit = () => {
156
+ // Build the filled text by replacing placeholders with values
157
+ let filledText = text;
158
+ // Replace in reverse order to maintain correct positions
159
+ for (let i = placeholders.length - 1; i >= 0; i--) {
160
+ const placeholder = placeholders[i];
161
+ if (!placeholder) continue;
162
+ const value = values[i] || "";
163
+
164
+ // Format the value based on placeholder type and parameters
165
+ let formattedValue = value;
166
+
167
+ if (placeholder.type === "item" && value.includes("|")) {
168
+ // Parse item value format: "id|path"
169
+ const parts = value.split("|");
170
+ const id = parts[0] || "";
171
+ const path = parts[1] || "";
172
+ const name = path.split("/").pop() || "";
173
+ // For item placeholders, output both ID and name
174
+ formattedValue = `{${id}} (${name})`;
175
+ }
176
+
177
+ filledText =
178
+ filledText.substring(0, placeholder.startPos) +
179
+ formattedValue +
180
+ filledText.substring(placeholder.endPos);
181
+ }
182
+ onComplete(filledText);
183
+ };
184
+
185
+ if (placeholders.length === 0) {
186
+ // No placeholders, just display text
187
+ return <div className={cn("text-xs", className)}>{text}</div>;
188
+ }
189
+
190
+ // Build segments: text and placeholder inputs
191
+ const segments: React.ReactNode[] = [];
192
+ let lastPos = 0;
193
+
194
+ placeholders.forEach((placeholder, idx) => {
195
+ // Add text before this placeholder
196
+ if (placeholder.startPos > lastPos) {
197
+ segments.push(
198
+ <span key={`text-${idx}`} className="whitespace-pre-wrap">
199
+ {text.substring(lastPos, placeholder.startPos)}
200
+ </span>,
201
+ );
202
+ }
203
+
204
+ // Add input for this placeholder
205
+ // Look up the component for this placeholder type
206
+ const typeConfig = placeholderInputTypeRegistry.get(placeholder.type);
207
+ const InputComponent = typeConfig?.component;
208
+
209
+ if (InputComponent) {
210
+ segments.push(
211
+ <InputComponent
212
+ key={`input-${idx}`}
213
+ placeholder={placeholder}
214
+ value={values[idx] || ""}
215
+ onChange={(value) => handleValueChange(idx, value)}
216
+ onKeyDown={(e) => handleKeyDown(e, idx)}
217
+ inputRef={(el) => {
218
+ inputRefs.current[idx] = el;
219
+ }}
220
+ autoFocus={idx === 0}
221
+ />,
222
+ );
223
+ }
224
+
225
+ lastPos = placeholder.endPos;
226
+ });
227
+
228
+ // Add remaining text after last placeholder
229
+ if (lastPos < text.length) {
230
+ segments.push(
231
+ <span key="text-end" className="whitespace-pre-wrap">
232
+ {text.substring(lastPos)}
233
+ </span>,
234
+ );
235
+ }
236
+
237
+ const allFilled = placeholders.every((_, idx) => {
238
+ const value = values[idx];
239
+ return value !== undefined && value.trim() !== "";
240
+ });
241
+
242
+ return (
243
+ <div className={cn("space-y-2", className)}>
244
+ <div className={cn(
245
+ "relative flex flex-wrap items-center gap-1 rounded-lg border border-blue-200 bg-blue-50/50 p-3 text-xs h-[80px] overflow-y-auto",
246
+ onCancel && "pr-8"
247
+ )}>
248
+ {segments}
249
+ {onCancel && (
250
+ <button
251
+ onClick={onCancel}
252
+ className="absolute right-2 top-2 rounded p-0.5 text-gray-400 hover:bg-blue-100 hover:text-gray-600 transition-colors"
253
+ title="Cancel (Esc)"
254
+ aria-label="Cancel"
255
+ >
256
+ <X size={14} strokeWidth={2} />
257
+ </button>
258
+ )}
259
+ </div>
260
+ {showButtons && (
261
+ <div className="flex items-center gap-2">
262
+ <Button
263
+ size="sm"
264
+ onClick={handleSubmit}
265
+ disabled={!allFilled}
266
+ className="h-7"
267
+ >
268
+ Submit
269
+ </Button>
270
+ {onCancel && (
271
+ <Button
272
+ size="sm"
273
+ variant="ghost"
274
+ onClick={onCancel}
275
+ className="h-7"
276
+ >
277
+ Cancel
278
+ </Button>
279
+ )}
280
+ {!allFilled && (
281
+ <span className="text-xs text-gray-500">
282
+ Fill all fields (use Tab to navigate)
283
+ </span>
284
+ )}
285
+ </div>
286
+ )}
287
+ </div>
288
+ );
289
+ }
290
+
@@ -0,0 +1,97 @@
1
+ import * as React from "react";
2
+ import { Placeholder } from "./PlaceholderInput";
3
+
4
+ /**
5
+ * Props interface that all placeholder input type components must implement
6
+ */
7
+ export interface PlaceholderInputComponentProps {
8
+ placeholder: Placeholder;
9
+ value: string;
10
+ onChange: (value: string) => void;
11
+ onKeyDown: (e: React.KeyboardEvent) => void;
12
+ inputRef: React.Ref<HTMLInputElement | HTMLTextAreaElement>;
13
+ autoFocus?: boolean;
14
+ }
15
+
16
+ /**
17
+ * Configuration for a placeholder input type
18
+ */
19
+ export interface PlaceholderInputTypeConfig {
20
+ type: string;
21
+ component: React.ComponentType<PlaceholderInputComponentProps>;
22
+ validator?: (value: string) => boolean;
23
+ }
24
+
25
+ /**
26
+ * Registry for placeholder input types
27
+ */
28
+ class PlaceholderInputTypeRegistry {
29
+ private types: Map<string, PlaceholderInputTypeConfig> = new Map();
30
+
31
+ /**
32
+ * Register a new placeholder input type
33
+ */
34
+ register(config: PlaceholderInputTypeConfig) {
35
+ this.types.set(config.type.toLowerCase(), config);
36
+ }
37
+
38
+ /**
39
+ * Get a registered placeholder input type configuration
40
+ */
41
+ get(type: string): PlaceholderInputTypeConfig | undefined {
42
+ return this.types.get(type.toLowerCase());
43
+ }
44
+
45
+ /**
46
+ * Check if a type is registered
47
+ */
48
+ has(type: string): boolean {
49
+ return this.types.has(type.toLowerCase());
50
+ }
51
+ }
52
+
53
+ // Create singleton instance
54
+ export const placeholderInputTypeRegistry = new PlaceholderInputTypeRegistry();
55
+
56
+ /**
57
+ * Default text input component for placeholders
58
+ */
59
+ const TextInput: React.FC<PlaceholderInputComponentProps> = ({
60
+ placeholder,
61
+ value,
62
+ onChange,
63
+ onKeyDown,
64
+ inputRef,
65
+ autoFocus,
66
+ }) => {
67
+ return (
68
+ <input
69
+ ref={inputRef as React.Ref<HTMLInputElement>}
70
+ type="text"
71
+ value={value}
72
+ onChange={(e) => onChange(e.target.value)}
73
+ onKeyDown={onKeyDown}
74
+ placeholder={placeholder.name}
75
+ autoFocus={autoFocus}
76
+ className="inline-flex min-w-[80px] max-w-[200px] rounded border border-blue-400 bg-blue-50 px-2 py-0.5 text-xs outline-none transition-all focus:border-blue-600 focus:bg-blue-100 focus:ring-2 focus:ring-blue-500/30 placeholder:text-blue-400"
77
+ style={{
78
+ width: `${Math.max(80, (value?.length || placeholder.name.length) * 7 + 20)}px`,
79
+ }}
80
+ />
81
+ );
82
+ };
83
+
84
+ // Register default text type
85
+ placeholderInputTypeRegistry.register({
86
+ type: "text",
87
+ component: TextInput,
88
+ });
89
+
90
+ // Import and register item selector (dynamic import to avoid circular dependencies)
91
+ import("./PlaceholderItemSelector").then((module) => {
92
+ placeholderInputTypeRegistry.register({
93
+ type: "item",
94
+ component: module.PlaceholderItemSelector,
95
+ });
96
+ });
97
+
@@ -0,0 +1,253 @@
1
+ import * as React from "react";
2
+ import { cn } from "../../lib/utils";
3
+ import { PlaceholderInputComponentProps } from "./PlaceholderInputTypes";
4
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
5
+ import { ItemSearch, ResultItem } from "../../editor/ui/ItemSearch";
6
+ import ContentTree from "../../editor/ContentTree";
7
+ import { useEditContext } from "../../editor/client/editContext";
8
+ import { X, ChevronDown } from "lucide-react";
9
+ import { ItemTreeNodeData } from "../../editor/services/contentService";
10
+ import { ItemDescriptor } from "../../editor/pageModel";
11
+ import { Splitter } from "../../editor/ui/Splitter";
12
+
13
+ /**
14
+ * Item selector component for placeholder inputs
15
+ * Allows selecting a Sitecore item via search or tree browsing
16
+ */
17
+ export const PlaceholderItemSelector: React.FC<PlaceholderInputComponentProps> = ({
18
+ placeholder,
19
+ value,
20
+ onChange,
21
+ onKeyDown,
22
+ inputRef,
23
+ autoFocus,
24
+ }) => {
25
+ const [isOpen, setIsOpen] = React.useState(false);
26
+ const [selectedItem, setSelectedItem] = React.useState<ResultItem | null>(null);
27
+ const [rootItemId, setRootItemId] = React.useState<string | null>(null);
28
+ const [isLoadingRoot, setIsLoadingRoot] = React.useState(false);
29
+ const editContext = useEditContext();
30
+
31
+ // Parse root parameter from placeholder.parameters
32
+ const rootPathOrId = placeholder.parameters.root || "/sitecore/content";
33
+
34
+ // Helper to check if a string is a GUID
35
+ const isGuid = (str: string): boolean => {
36
+ const guidPattern = /^[{]?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}[}]?$/;
37
+ return guidPattern.test(str);
38
+ };
39
+
40
+ // Resolve root path/ID to an item ID on mount
41
+ React.useEffect(() => {
42
+ const resolveRootItem = async () => {
43
+ if (!editContext) return;
44
+
45
+ // If it's already a GUID, use it directly
46
+ if (isGuid(rootPathOrId)) {
47
+ setRootItemId(rootPathOrId.replace(/[{}]/g, ""));
48
+ return;
49
+ }
50
+
51
+ // Otherwise, resolve the path to an ID
52
+ setIsLoadingRoot(true);
53
+ try {
54
+ const item = await editContext.itemsRepository.getItem({
55
+ id: rootPathOrId, // Try as ID first
56
+ language: "en",
57
+ version: 0,
58
+ });
59
+
60
+ if (item?.id) {
61
+ setRootItemId(item.id);
62
+ } else {
63
+ // If not found by ID, it might be a path - we need to search for it
64
+ console.error("Could not resolve root item:", rootPathOrId);
65
+ // Fallback to Sitecore content root
66
+ setRootItemId("0DE95AE4-41AB-4D01-9EB0-67441B7C2450");
67
+ }
68
+ } catch (error) {
69
+ console.error("Error resolving root item:", error);
70
+ // Fallback to Sitecore content root
71
+ setRootItemId("0DE95AE4-41AB-4D01-9EB0-67441B7C2450");
72
+ } finally {
73
+ setIsLoadingRoot(false);
74
+ }
75
+ };
76
+
77
+ resolveRootItem();
78
+ }, [editContext, rootPathOrId]);
79
+
80
+ const loadItemByIdOrPath = React.useCallback(async (idOrPath: string) => {
81
+ if (!editContext) return;
82
+
83
+ try {
84
+ const item = await editContext.itemsRepository.getItem({
85
+ id: idOrPath,
86
+ language: "en",
87
+ version: 0,
88
+ });
89
+
90
+ if (item) {
91
+ setSelectedItem({
92
+ id: item.id,
93
+ path: item.path || item.name,
94
+ name: item.name,
95
+ language: "en",
96
+ icon: item.icon,
97
+ });
98
+
99
+ const newValue = `${item.id}|${item.path || item.name}`;
100
+ onChange(newValue);
101
+ }
102
+ } catch (error) {
103
+ console.error("Failed to load item:", error);
104
+ }
105
+ }, [editContext, onChange]);
106
+
107
+ // Parse existing value (format: "id|path")
108
+ React.useEffect(() => {
109
+ if (!value || !editContext) return;
110
+
111
+ if (value.includes("|")) {
112
+ const [id, path] = value.split("|");
113
+ const name = path?.split("/").pop() || "";
114
+ setSelectedItem({ id: id || "", path: path || "", name, language: "en" });
115
+ } else {
116
+ // Try to load item by ID or path
117
+ loadItemByIdOrPath(value);
118
+ }
119
+ }, [value, editContext, loadItemByIdOrPath]);
120
+
121
+ const handleItemSelected = (item: ItemDescriptor & { path?: string; idPath?: string; icon?: string }) => {
122
+ const resultItem: ResultItem = {
123
+ id: item.id,
124
+ name: item.name || "",
125
+ path: item.path || "",
126
+ language: item.language || "en",
127
+ icon: item.icon,
128
+ idPath: item.idPath,
129
+ };
130
+ setSelectedItem(resultItem);
131
+ const newValue = `${resultItem.id}|${resultItem.path}`;
132
+ onChange(newValue);
133
+ setIsOpen(false);
134
+ };
135
+
136
+ const handleClear = (e: React.MouseEvent) => {
137
+ e.stopPropagation();
138
+ setSelectedItem(null);
139
+ onChange("");
140
+ };
141
+
142
+ const displayValue = selectedItem ? selectedItem.path : "";
143
+
144
+ // Don't render until root is resolved
145
+ if (isLoadingRoot || !rootItemId) {
146
+ return (
147
+ <div
148
+ ref={inputRef as React.Ref<HTMLDivElement>}
149
+ className="inline-flex min-w-[150px] max-w-[300px] items-center gap-1 rounded border border-gray-300 bg-gray-50 px-2 py-0.5 text-xs"
150
+ >
151
+ <span className="flex-1 text-gray-400">Loading...</span>
152
+ </div>
153
+ );
154
+ }
155
+
156
+ return (
157
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
158
+ <PopoverTrigger asChild>
159
+ <div
160
+ ref={inputRef as React.Ref<HTMLDivElement>}
161
+ tabIndex={0}
162
+ onKeyDown={onKeyDown}
163
+ className={cn(
164
+ "inline-flex min-w-[150px] max-w-[300px] cursor-pointer items-center gap-1 rounded border border-blue-400 bg-blue-50 px-2 py-0.5 text-xs outline-none transition-all",
165
+ "hover:border-blue-500 hover:bg-blue-100",
166
+ "focus:border-blue-600 focus:bg-blue-100 focus:ring-2 focus:ring-blue-500/30",
167
+ )}
168
+ >
169
+ {selectedItem ? (
170
+ <>
171
+ {selectedItem.icon && (
172
+ <img
173
+ src={selectedItem.icon}
174
+ alt=""
175
+ className="h-3 w-3 flex-shrink-0"
176
+ />
177
+ )}
178
+ <span className="flex-1 truncate">{displayValue}</span>
179
+ <button
180
+ onClick={handleClear}
181
+ className="rounded p-0.5 hover:bg-blue-200"
182
+ title="Clear selection"
183
+ >
184
+ <X size={12} />
185
+ </button>
186
+ </>
187
+ ) : (
188
+ <>
189
+ <span className="flex-1 text-blue-400">{placeholder.name}</span>
190
+ <ChevronDown size={12} className="text-blue-400" />
191
+ </>
192
+ )}
193
+ </div>
194
+ </PopoverTrigger>
195
+ <PopoverContent
196
+ className="w-[700px] p-0"
197
+ align="start"
198
+ onOpenAutoFocus={(e) => e.preventDefault()}
199
+ >
200
+ <div className="flex h-[450px]">
201
+ <Splitter
202
+ direction="horizontal"
203
+ localStorageKey="placeholder-item-selector-split"
204
+ panels={[
205
+ {
206
+ name: "tree",
207
+ defaultSize: 300,
208
+ content: (
209
+ <div className="h-full overflow-auto border-r border-gray-200 p-2">
210
+ <ContentTree
211
+ rootItemIds={[rootItemId]}
212
+ selectionMode="single"
213
+ selectedItemIds={selectedItem ? [selectedItem.id] : []}
214
+ onSelectionChange={(selection) => {
215
+ const node = selection[0] as ItemTreeNodeData;
216
+ if (node) {
217
+ handleItemSelected({
218
+ id: node.id,
219
+ name: node.name,
220
+ path: node.path || node.name,
221
+ language: "en",
222
+ version: 0,
223
+ icon: node.icon,
224
+ });
225
+ }
226
+ }}
227
+ language="en"
228
+ />
229
+ </div>
230
+ ),
231
+ },
232
+ {
233
+ name: "search",
234
+ defaultSize: "auto",
235
+ content: (
236
+ <div className="flex h-full flex-col p-4">
237
+ <ItemSearch
238
+ rootItemIds={rootItemId ? [rootItemId] : undefined}
239
+ itemSelected={handleItemSelected}
240
+ language="en"
241
+ autoFocus={true}
242
+ />
243
+ </div>
244
+ ),
245
+ },
246
+ ]}
247
+ />
248
+ </div>
249
+ </PopoverContent>
250
+ </Popover>
251
+ );
252
+ };
253
+