astro-tractstack 2.0.0-rc.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE +8 -97
  2. package/README.md +7 -5
  3. package/bin/create-tractstack.js +31 -8
  4. package/dist/index.js +106 -29
  5. package/package.json +10 -5
  6. package/templates/css/frontend.css +1 -1
  7. package/templates/custom/minimal/CodeHook.astro +13 -12
  8. package/templates/custom/minimal/CustomRoutes.astro +25 -31
  9. package/templates/custom/with-examples/CodeHook.astro +22 -11
  10. package/templates/custom/with-examples/CustomRoutes.astro +4 -8
  11. package/templates/custom/with-examples/ProductCard.astro +29 -0
  12. package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
  13. package/templates/custom/with-examples/ProductGrid.astro +64 -0
  14. package/templates/custom/with-examples/pages/Collections.astro +58 -98
  15. package/templates/gitignore +42 -0
  16. package/templates/prettierignore +5 -0
  17. package/templates/prettierrc +19 -0
  18. package/templates/src/client/app.js +127 -0
  19. package/templates/src/client/htmx.min.js +3519 -0
  20. package/templates/src/client/view.js +429 -0
  21. package/templates/src/components/Footer.astro +4 -9
  22. package/templates/src/components/Header.astro +67 -60
  23. package/templates/src/components/Menu.tsx +188 -52
  24. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
  25. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
  26. package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
  27. package/templates/src/components/codehooks/EpinetWrapper.tsx +10 -9
  28. package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
  29. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
  30. package/templates/src/components/codehooks/ListContent.astro +32 -162
  31. package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
  32. package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
  33. package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
  34. package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
  35. package/templates/src/components/compositor/Node.tsx +3 -6
  36. package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
  37. package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
  38. package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
  39. package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
  40. package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
  41. package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
  42. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
  43. package/templates/src/components/edit/Header.tsx +10 -4
  44. package/templates/src/components/edit/PanelSwitch.tsx +11 -7
  45. package/templates/src/components/edit/SettingsPanel.tsx +29 -18
  46. package/templates/src/components/edit/ToolBar.tsx +1 -28
  47. package/templates/src/components/edit/ToolMode.tsx +45 -32
  48. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
  49. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
  50. package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
  51. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
  52. package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
  53. package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
  54. package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
  55. package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
  56. package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
  57. package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
  58. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
  59. package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
  60. package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
  61. package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
  62. package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
  63. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
  64. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
  65. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
  66. package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
  67. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
  68. package/templates/src/components/edit/state/SaveModal.tsx +316 -169
  69. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
  70. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
  71. package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
  72. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
  73. package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
  74. package/templates/src/components/fields/ArtpackImage.tsx +4 -1
  75. package/templates/src/components/fields/BackgroundImage.tsx +1 -1
  76. package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
  77. package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
  78. package/templates/src/components/fields/ImageUpload.tsx +1 -1
  79. package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
  80. package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
  81. package/templates/src/components/form/ActionBuilderField.tsx +306 -87
  82. package/templates/src/components/search/SearchModal.tsx +420 -0
  83. package/templates/src/components/search/SearchResults.tsx +367 -0
  84. package/templates/src/components/search/SearchWrapper.tsx +46 -0
  85. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
  86. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
  87. package/templates/src/components/storykeep/Dashboard_Branding.tsx +1 -1
  88. package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
  89. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
  90. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +38 -34
  91. package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +1 -1
  92. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +56 -8
  93. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +18 -3
  94. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
  95. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
  96. package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
  97. package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
  98. package/templates/src/constants/shapes.ts +9 -0
  99. package/templates/src/constants.ts +2121 -16
  100. package/templates/src/hooks/useSearch.ts +228 -0
  101. package/templates/src/layouts/Layout.astro +213 -104
  102. package/templates/src/lib/storyData.ts +4 -1
  103. package/templates/src/pages/[...slug]/edit.astro +14 -14
  104. package/templates/src/pages/[...slug].astro +82 -21
  105. package/templates/src/pages/api/orphan-analysis.ts +0 -1
  106. package/templates/src/pages/api/tailwind.ts +23 -21
  107. package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
  108. package/templates/src/pages/context/[...contextSlug].astro +7 -2
  109. package/templates/src/pages/storykeep/advanced.astro +5 -4
  110. package/templates/src/pages/storykeep/branding.astro +5 -4
  111. package/templates/src/pages/storykeep/content.astro +5 -4
  112. package/templates/src/pages/storykeep/init.astro +40 -1
  113. package/templates/src/pages/storykeep/login.astro +1 -1
  114. package/templates/src/pages/storykeep.astro +5 -4
  115. package/templates/src/stores/nodes.ts +59 -88
  116. package/templates/src/stores/orphanAnalysis.ts +19 -21
  117. package/templates/src/stores/storykeep.ts +7 -0
  118. package/templates/src/types/compositorTypes.ts +6 -0
  119. package/templates/src/types/tractstack.ts +17 -0
  120. package/templates/src/utils/actions/lispLexer.ts +2 -2
  121. package/templates/src/utils/actions/preParse_Action.ts +3 -0
  122. package/templates/src/utils/api/beliefHelpers.ts +12 -36
  123. package/templates/src/utils/api/menuHelpers.ts +2 -2
  124. package/templates/src/utils/api.ts +26 -0
  125. package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
  126. package/templates/src/utils/compositor/allowInsert.ts +5 -3
  127. package/templates/src/utils/compositor/nodesHelper.ts +4 -0
  128. package/templates/src/utils/compositor/processMarkdown.ts +16 -2
  129. package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
  130. package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
  131. package/templates/src/utils/compositor/typeGuards.ts +1 -0
  132. package/templates/src/utils/customHelpers.ts +38 -0
  133. package/templates/src/utils/helpers.ts +2 -2
  134. package/templates/src/utils/layout.ts +65 -144
  135. package/utils/inject-files.ts +95 -18
  136. package/templates/src/client/analytics-events.js +0 -207
  137. package/templates/src/client/belief-events.js +0 -191
  138. package/templates/src/client/sse.js +0 -613
  139. package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
  140. package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
  141. package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
  142. package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
@@ -0,0 +1,117 @@
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import type { BeliefNode } from '@/types/compositorTypes';
3
+ import { heldBeliefsScales } from '@/constants/beliefs';
4
+
5
+ interface ActionBuilderBeliefSelectorProps {
6
+ value: string;
7
+ onChange: (value: string) => void;
8
+ beliefs: BeliefNode[];
9
+ }
10
+
11
+ const parseValue = (value: string): [string, string] => {
12
+ try {
13
+ const match = value.match(/\(([^)]+)\)/);
14
+ if (match) {
15
+ const parts = match[1].split(' ').filter(Boolean);
16
+ if (parts.length >= 2) {
17
+ return [parts[0], parts.slice(1).join(' ')];
18
+ }
19
+ if (parts.length === 1) {
20
+ return [parts[0], ''];
21
+ }
22
+ }
23
+ } catch (e) {
24
+ console.error('Error parsing belief selector value:', e);
25
+ }
26
+ return ['', ''];
27
+ };
28
+
29
+ export default function ActionBuilderBeliefSelector({
30
+ value,
31
+ onChange,
32
+ beliefs,
33
+ }: ActionBuilderBeliefSelectorProps) {
34
+ const [selectedBeliefSlug, setSelectedBeliefSlug] = useState('');
35
+ const [selectedValue, setSelectedValue] = useState('');
36
+
37
+ useEffect(() => {
38
+ const [slug, val] = parseValue(value);
39
+ setSelectedBeliefSlug(slug);
40
+ setSelectedValue(val);
41
+ }, [value]);
42
+
43
+ useEffect(() => {
44
+ if (selectedBeliefSlug && selectedValue) {
45
+ const newValue = `(${selectedBeliefSlug} ${selectedValue})`;
46
+ if (value !== newValue) {
47
+ onChange(newValue);
48
+ }
49
+ }
50
+ }, [selectedBeliefSlug, selectedValue, onChange, value]);
51
+
52
+ const selectedBelief = useMemo(
53
+ () => beliefs.find((b) => b.slug === selectedBeliefSlug),
54
+ [selectedBeliefSlug, beliefs]
55
+ );
56
+
57
+ const valueOptions = useMemo(() => {
58
+ if (!selectedBelief) return [];
59
+ if (selectedBelief.scale === 'custom' && selectedBelief.customValues) {
60
+ return selectedBelief.customValues.map((v) => ({ label: v, value: v }));
61
+ }
62
+ const scale =
63
+ heldBeliefsScales[selectedBelief.scale as keyof typeof heldBeliefsScales];
64
+ if (scale) {
65
+ return scale.map(({ slug, name }) => ({ label: name, value: slug }));
66
+ }
67
+ return [];
68
+ }, [selectedBelief]);
69
+
70
+ const handleBeliefChange = (slug: string) => {
71
+ setSelectedBeliefSlug(slug);
72
+ setSelectedValue('');
73
+ onChange(slug ? `(${slug} )` : '');
74
+ };
75
+
76
+ const commonSelectClass =
77
+ 'w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700';
78
+
79
+ return (
80
+ <div className="space-y-4">
81
+ <div className="space-y-2">
82
+ <label className="block text-sm text-gray-700">Belief</label>
83
+ <select
84
+ value={selectedBeliefSlug}
85
+ onChange={(e) => handleBeliefChange(e.target.value)}
86
+ className={commonSelectClass}
87
+ >
88
+ <option value="">Select a belief...</option>
89
+ {beliefs.map((belief) => (
90
+ <option key={belief.id} value={belief.slug}>
91
+ {belief.title} ({belief.scale})
92
+ </option>
93
+ ))}
94
+ </select>
95
+ </div>
96
+
97
+ {selectedBelief && (
98
+ <div className="space-y-2">
99
+ <label className="block text-sm text-gray-700">Value</label>
100
+ <select
101
+ value={selectedValue}
102
+ onChange={(e) => setSelectedValue(e.target.value)}
103
+ className={commonSelectClass}
104
+ disabled={!selectedBelief || valueOptions.length === 0}
105
+ >
106
+ <option value="">Select a value...</option>
107
+ {valueOptions.map((option) => (
108
+ <option key={option.value} value={option.value}>
109
+ {option.label}
110
+ </option>
111
+ ))}
112
+ </select>
113
+ </div>
114
+ )}
115
+ </div>
116
+ );
117
+ }
@@ -1,7 +1,12 @@
1
1
  import { useState, useEffect, useMemo } from 'react';
2
- import { GOTO_TARGETS } from '@/constants';
2
+ import { TractStackAPI } from '@/utils/api';
3
+ import { GOTO_TARGETS, ACTION_COMMANDS, type ActionCommand } from '@/constants';
4
+ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
3
5
  import ActionBuilderSlugSelector from './ActionBuilderSlugSelector';
6
+ import ActionBuilderBeliefSelector from './ActionBuilderBeliefSelector';
7
+ import BunnyMomentSelector from '@/components/fields/BunnyMomentSelector';
4
8
  import type { FullContentMapItem } from '@/types/tractstack';
9
+ import type { BeliefNode } from '@/types/compositorTypes';
5
10
 
6
11
  interface ActionBuilderFieldProps {
7
12
  value: string;
@@ -12,86 +17,248 @@ interface ActionBuilderFieldProps {
12
17
  slug?: string;
13
18
  }
14
19
 
20
+ const parseActionLisp = (
21
+ value: string
22
+ ): { command: ActionCommand; params: string } => {
23
+ try {
24
+ const match = value.match(/^\s*\(([\w-]+)\s+(.*)\)\s*$/);
25
+ if (match) {
26
+ const cmd = match[1] as ActionCommand;
27
+ const params = match[2];
28
+ if (ACTION_COMMANDS[cmd]) {
29
+ return { command: cmd, params };
30
+ }
31
+ }
32
+ } catch (e) {
33
+ console.error('Error parsing actionLisp value:', e);
34
+ }
35
+ return { command: 'goto', params: '' };
36
+ };
37
+
15
38
  export default function ActionBuilderField({
16
39
  value,
17
40
  onChange,
18
41
  contentMap,
19
- label = 'Navigation Action',
42
+ label = 'Action',
20
43
  error,
21
44
  slug,
22
45
  }: ActionBuilderFieldProps) {
46
+ const [command, setCommand] = useState<ActionCommand>('goto');
47
+ const [params, setParams] = useState('');
48
+ const [beliefs, setBeliefs] = useState<BeliefNode[]>([]);
49
+
50
+ useEffect(() => {
51
+ const fetchData = async () => {
52
+ try {
53
+ const api = new TractStackAPI();
54
+ const {
55
+ data: { beliefIds },
56
+ } = await api.get('/api/v1/nodes/beliefs');
57
+ if (!beliefIds?.length) return;
58
+ const {
59
+ data: { beliefs },
60
+ } = await api.post('/api/v1/nodes/beliefs', { beliefIds });
61
+ setBeliefs(beliefs || []);
62
+ } catch (error) {
63
+ console.error('Error fetching beliefs for ActionBuilder:', error);
64
+ }
65
+ };
66
+ fetchData();
67
+ }, []);
68
+
69
+ useEffect(() => {
70
+ if (value) {
71
+ const { command: parsedCommand, params: parsedParams } =
72
+ parseActionLisp(value);
73
+ setCommand(parsedCommand);
74
+ setParams(parsedParams);
75
+ } else {
76
+ setCommand('goto');
77
+ setParams('');
78
+ }
79
+ }, [value]);
80
+
81
+ const handleParamChange = (newParams: string) => {
82
+ setParams(newParams);
83
+ const trimmedParams = newParams.trim();
84
+
85
+ if (!trimmedParams || trimmedParams === '()') {
86
+ onChange('');
87
+ return;
88
+ }
89
+
90
+ if (command === 'identifyAs') {
91
+ const firstSpaceIndex = trimmedParams.indexOf(' ');
92
+ if (firstSpaceIndex === -1) {
93
+ // Handle case with only beliefId and no value
94
+ onChange(`(${command} ${trimmedParams})`);
95
+ } else {
96
+ const beliefId = trimmedParams.substring(0, firstSpaceIndex);
97
+ const value = trimmedParams.substring(firstSpaceIndex + 1);
98
+ const finalValue = value.includes(' ') ? `"${value}"` : value;
99
+ onChange(`(${command} ${beliefId} ${finalValue})`);
100
+ }
101
+ } else {
102
+ // Original behavior for all other commands
103
+ onChange(`(${command} ${trimmedParams})`);
104
+ }
105
+ };
106
+
107
+ const handleCommandChange = (newCommand: ActionCommand) => {
108
+ setCommand(newCommand);
109
+ setParams('');
110
+ };
111
+
112
+ const handleClearAction = () => {
113
+ setCommand('goto');
114
+ setParams('');
115
+ onChange('');
116
+ };
117
+
118
+ const hasAction = value && value.trim() !== '';
119
+
120
+ const renderBuilderForCommand = () => {
121
+ switch (command) {
122
+ case 'declare':
123
+ return (
124
+ <ActionBuilderBeliefSelector
125
+ value={params}
126
+ onChange={handleParamChange}
127
+ beliefs={beliefs.filter((b) => b.scale !== 'custom')}
128
+ />
129
+ );
130
+ case 'identifyAs':
131
+ return (
132
+ <ActionBuilderBeliefSelector
133
+ value={params}
134
+ onChange={handleParamChange}
135
+ beliefs={beliefs.filter((b) => b.scale === 'custom')}
136
+ />
137
+ );
138
+ case 'bunnyMoment':
139
+ return (
140
+ <BunnyMomentSelector value={params} onChange={handleParamChange} />
141
+ );
142
+ case 'goto':
143
+ default:
144
+ return (
145
+ <GotoBuilder
146
+ value={params}
147
+ onChange={handleParamChange}
148
+ contentMap={contentMap}
149
+ slug={slug}
150
+ />
151
+ );
152
+ }
153
+ };
154
+
155
+ return (
156
+ <div className="space-y-4">
157
+ {label && (
158
+ <div className="flex items-center justify-between">
159
+ <label className="block text-sm font-bold text-gray-700">
160
+ {label}
161
+ </label>
162
+ {hasAction && (
163
+ <button
164
+ type="button"
165
+ onClick={handleClearAction}
166
+ className="rounded p-1 text-gray-500 hover:bg-gray-100 hover:text-red-600"
167
+ title="Clear action"
168
+ >
169
+ <XMarkIcon className="h-4 w-4" />
170
+ </button>
171
+ )}
172
+ </div>
173
+ )}
174
+
175
+ <div className="space-y-2">
176
+ <label className="block text-sm text-gray-700">Action Type</label>
177
+ <select
178
+ value={command}
179
+ onChange={(e) => handleCommandChange(e.target.value as ActionCommand)}
180
+ className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700"
181
+ >
182
+ {Object.entries(ACTION_COMMANDS).map(([key, data]) => (
183
+ <option key={key} value={key}>
184
+ {data.name}
185
+ </option>
186
+ ))}
187
+ </select>
188
+ <p className="mt-1 text-sm text-gray-500">
189
+ {ACTION_COMMANDS[command]?.description}
190
+ </p>
191
+ </div>
192
+
193
+ <div className="mt-4 border-t border-gray-200 pt-4">
194
+ {renderBuilderForCommand()}
195
+ </div>
196
+
197
+ {error && <p className="text-sm text-red-600">{error}</p>}
198
+ </div>
199
+ );
200
+ }
201
+
202
+ function GotoBuilder({
203
+ value,
204
+ onChange,
205
+ contentMap,
206
+ slug,
207
+ }: Omit<ActionBuilderFieldProps, 'label' | 'error' | 'value' | 'onChange'> & {
208
+ value: string;
209
+ onChange: (v: string) => void;
210
+ }) {
23
211
  const [selectedTarget, setSelectedTarget] = useState<string>('');
24
212
  const [selectedSubcommand, setSelectedSubcommand] = useState<string>('');
25
213
  const [param1, setParam1] = useState<string>('');
26
214
  const [param2, setParam2] = useState<string>('');
27
- //const [param3, setParam3] = useState<string>('');
28
215
 
29
- // Parse existing value on mount/change
30
216
  useEffect(() => {
31
- if (value) {
32
- try {
33
- const match = value.match(/\(goto\s+\(([^)]+)\)/);
34
- if (match) {
35
- const parts = match[1].split(' ').filter(Boolean);
36
- if (parts.length > 0) {
37
- setSelectedTarget(parts[0]);
38
- if (GOTO_TARGETS[parts[0]]?.subcommands) {
39
- if (parts.length > 1) setSelectedSubcommand(parts[1]);
40
- } else {
41
- if (parts.length > 1) setParam1(parts[1]);
42
- if (parts.length > 2) setParam2(parts[2]);
43
- //if (parts.length > 3) setParam3(parts[3]);
44
- }
217
+ try {
218
+ const match = value.match(/\(([^)]+)\)/);
219
+ if (match) {
220
+ const parts = match[1].split(' ').filter(Boolean);
221
+ if (parts.length > 0) {
222
+ setSelectedTarget(parts[0]);
223
+ if (GOTO_TARGETS[parts[0]]?.subcommands) {
224
+ if (parts.length > 1) setSelectedSubcommand(parts[1]);
225
+ } else {
226
+ if (parts.length > 1) setParam1(parts[1]);
227
+ if (parts.length > 2) setParam2(parts[2]);
45
228
  }
46
229
  }
47
- } catch (e) {
48
- console.error('Error parsing action value:', e);
230
+ } else {
231
+ setSelectedTarget('');
232
+ setSelectedSubcommand('');
233
+ setParam1('');
234
+ setParam2('');
49
235
  }
236
+ } catch (e) {
237
+ console.error('Error parsing goto value:', e);
50
238
  }
51
239
  }, [value]);
52
240
 
53
- const updateValue = (
54
- target: string,
55
- sub: string = '',
56
- p1: string = '',
57
- p2: string = '',
58
- p3: string = ''
59
- ) => {
60
- let newValue = `(goto (${target}`;
241
+ const updateValue = (target: string, sub = '', p1 = '', p2 = '') => {
242
+ if (!target) {
243
+ onChange('');
244
+ return;
245
+ }
246
+ let newValue = `(${target}`;
61
247
  if (GOTO_TARGETS[target]?.subcommands) {
62
248
  if (sub) newValue += ` ${sub}`;
63
249
  } else {
64
250
  if (p1) newValue += ` ${p1}`;
65
251
  if (p2) newValue += ` ${p2}`;
66
- if (p3) newValue += ` ${p3}`;
67
252
  }
68
- newValue += '))';
253
+ newValue += ')';
69
254
  onChange(newValue);
70
255
  };
71
256
 
72
- const targetOptions = useMemo(() => {
73
- return Object.entries(GOTO_TARGETS).map(([key, data]) => ({
74
- value: key,
75
- label: data.name,
76
- }));
77
- }, []);
78
-
79
- const subcommandOptions = useMemo(() => {
80
- if (!selectedTarget || !GOTO_TARGETS[selectedTarget]?.subcommands) {
81
- return [];
82
- }
83
- return GOTO_TARGETS[selectedTarget].subcommands!.map((cmd) => ({
84
- value: cmd,
85
- label: cmd,
86
- }));
87
- }, [selectedTarget]);
88
-
89
257
  const handleTargetChange = (newTarget: string) => {
90
258
  setSelectedTarget(newTarget);
91
259
  setSelectedSubcommand('');
92
260
  setParam1('');
93
261
  setParam2('');
94
- //setParam3('');
95
262
  updateValue(newTarget);
96
263
  };
97
264
 
@@ -100,14 +267,62 @@ export default function ActionBuilderField({
100
267
  updateValue(selectedTarget, newSubcommand);
101
268
  };
102
269
 
270
+ const handleClearParam = (param: 'param1' | 'param2') => {
271
+ if (param === 'param1') {
272
+ setParam1('');
273
+ setParam2(''); // Cascade unset
274
+ updateValue(selectedTarget, selectedSubcommand, '', '');
275
+ } else {
276
+ setParam2('');
277
+ updateValue(selectedTarget, selectedSubcommand, param1, '');
278
+ }
279
+ };
280
+
281
+ const targetOptions = useMemo(
282
+ () =>
283
+ Object.entries(GOTO_TARGETS).map(([key, data]) => ({
284
+ value: key,
285
+ label: data.name,
286
+ })),
287
+ []
288
+ );
289
+
290
+ const subcommandOptions = useMemo(() => {
291
+ if (!selectedTarget || !GOTO_TARGETS[selectedTarget]?.subcommands) {
292
+ return [];
293
+ }
294
+ return GOTO_TARGETS[selectedTarget].subcommands!.map((cmd) => ({
295
+ value: cmd,
296
+ label: cmd,
297
+ }));
298
+ }, [selectedTarget]);
299
+
103
300
  const renderParamInput = (paramType: 'param1' | 'param2') => {
104
301
  const isParam1 = paramType === 'param1';
105
302
  const currentValue = isParam1 ? param1 : param2;
303
+ const hasValue = currentValue && currentValue.trim() !== '';
304
+
305
+ const inputWithClear = (inputEl: JSX.Element) => (
306
+ <div className="flex items-center gap-2">
307
+ <div className="flex-grow">{inputEl}</div>
308
+ {hasValue && (
309
+ <button
310
+ type="button"
311
+ onClick={() => handleClearParam(paramType)}
312
+ className="flex-shrink-0 rounded p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-800"
313
+ title="Clear selection"
314
+ >
315
+ <XMarkIcon className="h-4 w-4" />
316
+ </button>
317
+ )}
318
+ </div>
319
+ );
106
320
 
107
321
  switch (selectedTarget) {
108
322
  case 'context':
109
323
  return (
110
- isParam1 && (
324
+ isParam1 &&
325
+ inputWithClear(
111
326
  <ActionBuilderSlugSelector
112
327
  type="context"
113
328
  value={currentValue}
@@ -123,7 +338,8 @@ export default function ActionBuilderField({
123
338
 
124
339
  case 'storyFragment':
125
340
  return (
126
- isParam1 && (
341
+ isParam1 &&
342
+ inputWithClear(
127
343
  <ActionBuilderSlugSelector
128
344
  type="storyFragment"
129
345
  value={currentValue}
@@ -136,16 +352,15 @@ export default function ActionBuilderField({
136
352
  />
137
353
  )
138
354
  );
139
-
140
355
  case 'storyFragmentPane':
141
356
  if (isParam1) {
142
- return (
357
+ return inputWithClear(
143
358
  <ActionBuilderSlugSelector
144
359
  type="storyFragment"
145
360
  value={currentValue}
146
361
  onSelect={(newValue) => {
147
362
  setParam1(newValue);
148
- setParam2(''); // Reset pane selection
363
+ setParam2('');
149
364
  updateValue(selectedTarget, '', newValue, '');
150
365
  }}
151
366
  label="Select Story Fragment"
@@ -155,7 +370,8 @@ export default function ActionBuilderField({
155
370
  }
156
371
  return (
157
372
  !isParam1 &&
158
- param1 && (
373
+ param1 &&
374
+ inputWithClear(
159
375
  <ActionBuilderSlugSelector
160
376
  type="pane"
161
377
  value={currentValue}
@@ -169,17 +385,16 @@ export default function ActionBuilderField({
169
385
  />
170
386
  )
171
387
  );
172
-
173
388
  case 'bunny':
174
389
  if (isParam1 && slug) {
175
390
  if (!currentValue) setParam1(slug);
176
- return (
391
+ return inputWithClear(
177
392
  <ActionBuilderSlugSelector
178
393
  type="storyFragment"
179
394
  value={currentValue || slug}
180
395
  onSelect={(newValue) => {
181
396
  setParam1(newValue);
182
- setParam2(''); // Reset time selection when story fragment changes
397
+ setParam2('');
183
398
  updateValue(selectedTarget, '', newValue, '');
184
399
  }}
185
400
  label="Select Story Fragment"
@@ -188,47 +403,57 @@ export default function ActionBuilderField({
188
403
  );
189
404
  }
190
405
  return null;
191
-
192
406
  case 'url':
193
407
  return (
194
408
  isParam1 && (
195
- <div className="space-y-2">
196
- <label className="block text-sm font-bold text-gray-700">
197
- External URL
198
- </label>
199
- <input
200
- type="text"
201
- value={currentValue}
202
- onChange={(e) => {
203
- const newValue = e.target.value;
204
- setParam1(newValue);
205
- updateValue(selectedTarget, '', newValue);
206
- }}
207
- placeholder="https://..."
208
- className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700"
209
- />
409
+ <div className="flex items-center gap-2">
410
+ <div className="flex-grow space-y-2">
411
+ <label className="block text-sm font-bold text-gray-700">
412
+ External URL
413
+ </label>
414
+ <input
415
+ type="text"
416
+ value={currentValue}
417
+ onChange={(e) => {
418
+ const newValue = e.target.value;
419
+ setParam1(newValue);
420
+ }}
421
+ onBlur={(e) => {
422
+ updateValue(selectedTarget, '', e.target.value);
423
+ }}
424
+ placeholder="https://..."
425
+ className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700"
426
+ />
427
+ </div>
428
+ {hasValue && (
429
+ <button
430
+ type="button"
431
+ onClick={() => handleClearParam('param1')}
432
+ className="mt-6 flex-shrink-0 rounded p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-800"
433
+ title="Clear URL"
434
+ >
435
+ <XMarkIcon className="h-4 w-4" />
436
+ </button>
437
+ )}
210
438
  </div>
211
439
  )
212
440
  );
213
-
214
441
  default:
215
442
  return null;
216
443
  }
217
444
  };
218
445
 
446
+ const commonSelectClass =
447
+ 'w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700';
448
+
219
449
  return (
220
450
  <div className="space-y-4">
221
- {label && (
222
- <label className="block text-sm font-bold text-gray-700">{label}</label>
223
- )}
224
-
225
- {/* Target Selection */}
226
451
  <div className="space-y-2">
227
452
  <label className="block text-sm text-gray-700">Navigation Target</label>
228
453
  <select
229
454
  value={selectedTarget}
230
455
  onChange={(e) => handleTargetChange(e.target.value)}
231
- className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700"
456
+ className={commonSelectClass}
232
457
  >
233
458
  <option value="">Select a target...</option>
234
459
  {targetOptions.map((option) => (
@@ -237,7 +462,6 @@ export default function ActionBuilderField({
237
462
  </option>
238
463
  ))}
239
464
  </select>
240
-
241
465
  {GOTO_TARGETS[selectedTarget] && (
242
466
  <p className="mt-1 text-sm text-gray-500">
243
467
  {GOTO_TARGETS[selectedTarget].description}
@@ -245,14 +469,13 @@ export default function ActionBuilderField({
245
469
  )}
246
470
  </div>
247
471
 
248
- {/* Subcommand Selection */}
249
472
  {selectedTarget && GOTO_TARGETS[selectedTarget]?.subcommands && (
250
473
  <div className="space-y-2">
251
474
  <label className="block text-sm text-gray-700">Section</label>
252
475
  <select
253
476
  value={selectedSubcommand}
254
477
  onChange={(e) => handleSubcommandChange(e.target.value)}
255
- className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700"
478
+ className={commonSelectClass}
256
479
  >
257
480
  <option value="">Select a section...</option>
258
481
  {subcommandOptions.map((option) => (
@@ -264,19 +487,15 @@ export default function ActionBuilderField({
264
487
  </div>
265
488
  )}
266
489
 
267
- {/* Parameter 1 */}
268
490
  {selectedTarget &&
269
491
  GOTO_TARGETS[selectedTarget]?.requiresParam &&
270
492
  !GOTO_TARGETS[selectedTarget].subcommands &&
271
493
  renderParamInput('param1')}
272
494
 
273
- {/* Parameter 2 */}
274
495
  {selectedTarget &&
275
496
  GOTO_TARGETS[selectedTarget]?.requiresSecondParam &&
276
497
  param1 &&
277
498
  renderParamInput('param2')}
278
-
279
- {error && <p className="text-sm text-red-600">{error}</p>}
280
499
  </div>
281
500
  );
282
501
  }