astro-tractstack 2.0.0-rc.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +8 -97
  2. package/README.md +7 -5
  3. package/bin/create-tractstack.js +35 -11
  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 +1 -0
  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_Content.tsx +6 -0
  88. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
  89. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +37 -33
  90. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +55 -7
  91. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +17 -2
  92. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
  93. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
  94. package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
  95. package/templates/src/components/tenant/RegistrationForm.tsx +1 -1
  96. package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
  97. package/templates/src/constants/shapes.ts +9 -0
  98. package/templates/src/constants.ts +2121 -16
  99. package/templates/src/hooks/useSearch.ts +228 -0
  100. package/templates/src/layouts/Layout.astro +213 -104
  101. package/templates/src/lib/storyData.ts +4 -1
  102. package/templates/src/pages/[...slug]/edit.astro +14 -14
  103. package/templates/src/pages/[...slug].astro +82 -21
  104. package/templates/src/pages/api/orphan-analysis.ts +0 -1
  105. package/templates/src/pages/api/tailwind.ts +23 -21
  106. package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
  107. package/templates/src/pages/context/[...contextSlug].astro +7 -2
  108. package/templates/src/pages/storykeep/advanced.astro +5 -4
  109. package/templates/src/pages/storykeep/branding.astro +5 -4
  110. package/templates/src/pages/storykeep/content.astro +5 -4
  111. package/templates/src/pages/storykeep/init.astro +40 -1
  112. package/templates/src/pages/storykeep/login.astro +1 -1
  113. package/templates/src/pages/storykeep.astro +5 -4
  114. package/templates/src/stores/nodes.ts +59 -88
  115. package/templates/src/stores/orphanAnalysis.ts +19 -21
  116. package/templates/src/stores/storykeep.ts +7 -0
  117. package/templates/src/types/compositorTypes.ts +6 -0
  118. package/templates/src/types/tractstack.ts +17 -0
  119. package/templates/src/utils/actions/lispLexer.ts +2 -2
  120. package/templates/src/utils/actions/preParse_Action.ts +3 -0
  121. package/templates/src/utils/api/beliefHelpers.ts +12 -36
  122. package/templates/src/utils/api/menuHelpers.ts +2 -2
  123. package/templates/src/utils/api.ts +26 -0
  124. package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
  125. package/templates/src/utils/compositor/allowInsert.ts +5 -3
  126. package/templates/src/utils/compositor/nodesHelper.ts +4 -0
  127. package/templates/src/utils/compositor/processMarkdown.ts +16 -2
  128. package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
  129. package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
  130. package/templates/src/utils/compositor/typeGuards.ts +1 -0
  131. package/templates/src/utils/customHelpers.ts +38 -0
  132. package/templates/src/utils/helpers.ts +2 -2
  133. package/templates/src/utils/layout.ts +65 -144
  134. package/utils/inject-files.ts +95 -18
  135. package/templates/src/client/analytics-events.js +0 -207
  136. package/templates/src/client/belief-events.js +0 -191
  137. package/templates/src/client/sse.js +0 -613
  138. package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
  139. package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
  140. package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
  141. package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
@@ -9,24 +9,10 @@ import type { PaneNode } from '@/types/compositorTypes';
9
9
 
10
10
  const PER_PAGE = 20;
11
11
 
12
- // V2 Analytics Data Structure
13
- interface StoryfragmentAnalytics {
14
- id: string;
15
- total_actions: number;
16
- unique_visitors: number;
17
- last_24h_actions: number;
18
- last_7d_actions: number;
19
- last_28d_actions: number;
20
- last_24h_unique_visitors: number;
21
- last_7d_unique_visitors: number;
22
- last_28d_unique_visitors: number;
23
- total_leads: number;
24
- }
25
-
26
12
  interface ListContentSetupProps {
27
13
  params?: Record<string, string>;
28
14
  nodeId: string;
29
- config?: BrandConfig;
15
+ config: BrandConfig;
30
16
  }
31
17
 
32
18
  const ListContentSetup = ({
@@ -34,17 +20,8 @@ const ListContentSetup = ({
34
20
  nodeId,
35
21
  config,
36
22
  }: ListContentSetupProps) => {
37
- const [analyticsData, setAnalyticsData] = useState<
38
- Record<string, StoryfragmentAnalytics>
39
- >({});
40
- const [isAnalyticsLoading, setIsAnalyticsLoading] = useState(true);
41
23
  const $contentMap = useStore(fullContentMapStore);
42
-
43
24
  const [isPanelOpen, setIsPanelOpen] = useState(false);
44
-
45
- const [selectedMode, setSelectedMode] = useState(
46
- params?.defaultMode || 'recent'
47
- );
48
25
  const [excludedIds, setExcludedIds] = useState<string[]>(
49
26
  params?.excludedIds ? params.excludedIds.split(',') : []
50
27
  );
@@ -56,6 +33,7 @@ const ListContentSetup = ({
56
33
  );
57
34
  const [currentPage, setCurrentPage] = useState(1);
58
35
  const [bgColor, setBgColor] = useState(params?.bgColor || '');
36
+ const [title, setTitle] = useState(params?.title || '');
59
37
 
60
38
  const isInitialMount = useRef(true);
61
39
 
@@ -65,15 +43,15 @@ const ListContentSetup = ({
65
43
  selectedTopics.length > 0 ||
66
44
  excludedIds.length > 0 ||
67
45
  pageSize !== 10 ||
68
- bgColor !== '';
46
+ bgColor !== '' ||
47
+ title !== '';
69
48
 
70
49
  const validPages = $contentMap.filter(
71
50
  (item) =>
72
51
  item.type === 'StoryFragment' &&
73
52
  typeof item.description === 'string' &&
74
53
  typeof item.thumbSrc === 'string' &&
75
- typeof item.thumbSrcSet === 'string' &&
76
- typeof item.changed === 'string'
54
+ typeof item.thumbSrcSet === 'string'
77
55
  );
78
56
 
79
57
  // Build topic map for filtering
@@ -89,47 +67,6 @@ const ListContentSetup = ({
89
67
  }
90
68
  });
91
69
 
92
- const fetchAnalyticsData = async () => {
93
- try {
94
- setIsAnalyticsLoading(true);
95
- // Updated to use V2 API endpoint
96
- const response = await fetch('/api/v1/analytics/storyfragments', {
97
- headers: {
98
- 'X-Tenant-ID': window.TRACTSTACK_CONFIG?.tenantId || 'default',
99
- },
100
- });
101
-
102
- if (!response.ok) {
103
- throw new Error(`HTTP error! status: ${response.status}`);
104
- }
105
-
106
- const analyticsArray = await response.json();
107
-
108
- // Transform array to a map keyed by ID for easier lookup
109
- // V2 API returns array directly, not wrapped in a success/data structure
110
- const analyticsById = Array.isArray(analyticsArray)
111
- ? analyticsArray.reduce(
112
- (
113
- acc: Record<string, StoryfragmentAnalytics>,
114
- item: StoryfragmentAnalytics
115
- ) => {
116
- acc[item.id] = item;
117
- return acc;
118
- },
119
- {}
120
- )
121
- : {};
122
-
123
- setAnalyticsData(analyticsById);
124
- } catch (error) {
125
- console.error('Error fetching analytics data:', error);
126
- // Set empty analytics on error to prevent blocking the UI
127
- setAnalyticsData({});
128
- } finally {
129
- setIsAnalyticsLoading(false);
130
- }
131
- };
132
-
133
70
  const topics = Array.from(topicMap.entries())
134
71
  .map(([name, { count, pageIds }]) => ({
135
72
  name,
@@ -139,23 +76,16 @@ const ListContentSetup = ({
139
76
  .sort((a, b) => a.name.localeCompare(b.name));
140
77
 
141
78
  const filteredPages = validPages.filter((page) => {
142
- if (excludedIds.includes(page.id)) {
79
+ if (excludedIds.includes(page.id) || selectedTopics.length === 0) {
143
80
  return false;
144
81
  }
145
- if (selectedTopics.length === 0) {
146
- return true;
147
- }
148
82
  return (
149
83
  page.topics && page.topics.some((topic) => selectedTopics.includes(topic))
150
84
  );
151
85
  });
152
86
 
87
+ // Always sort by most recent
153
88
  const sortedPages = [...filteredPages].sort((a, b) => {
154
- if (selectedMode === 'popular') {
155
- const aViews = analyticsData[a.id]?.total_actions || 0;
156
- const bViews = analyticsData[b.id]?.total_actions || 0;
157
- return bViews - aViews;
158
- }
159
89
  const bDate = b.changed ? new Date(b.changed) : new Date(0);
160
90
  const aDate = a.changed ? new Date(a.changed) : new Date(0);
161
91
  return bDate.getTime() - aDate.getTime();
@@ -177,11 +107,11 @@ const ListContentSetup = ({
177
107
  codeHookTarget: 'list-content',
178
108
  codeHookPayload: {
179
109
  options: JSON.stringify({
180
- defaultMode: selectedMode,
181
110
  excludedIds: excludedIds.join(','),
182
111
  topics: selectedTopics.join(','),
183
112
  pageSize: pageSize,
184
113
  bgColor: bgColor,
114
+ title: title,
185
115
  }),
186
116
  },
187
117
  bgColour: bgColor || undefined,
@@ -198,10 +128,6 @@ const ListContentSetup = ({
198
128
  }
199
129
  };
200
130
 
201
- useEffect(() => {
202
- fetchAnalyticsData();
203
- }, []);
204
-
205
131
  useEffect(() => {
206
132
  if (isInitialMount.current) {
207
133
  isInitialMount.current = false;
@@ -213,7 +139,7 @@ const ListContentSetup = ({
213
139
  }, 500);
214
140
 
215
141
  return () => clearTimeout(timeoutId);
216
- }, [selectedMode, excludedIds, selectedTopics, pageSize, bgColor]);
142
+ }, [excludedIds, selectedTopics, pageSize, bgColor, title]);
217
143
 
218
144
  // Toggle a page's exclusion status
219
145
  const toggleExclude = (id: string) => {
@@ -339,7 +265,6 @@ const ListContentSetup = ({
339
265
  );
340
266
  }
341
267
 
342
- if (isAnalyticsLoading) return null;
343
268
  return (
344
269
  <div className="w-full space-y-6 bg-slate-50 p-6">
345
270
  <div className="flex items-center justify-between">
@@ -359,6 +284,23 @@ const ListContentSetup = ({
359
284
  <h3 className="text-lg font-bold text-gray-900">Content Settings</h3>
360
285
  </div>
361
286
  <div className="space-y-4 pt-4">
287
+ <div>
288
+ <label
289
+ htmlFor="list-title"
290
+ className="block text-sm font-bold text-gray-700"
291
+ >
292
+ Optional Title
293
+ </label>
294
+ <input
295
+ type="text"
296
+ id="list-title"
297
+ className="mt-1 block w-full rounded-md border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-600 focus:ring-cyan-600 sm:text-sm"
298
+ value={title}
299
+ onChange={(e) => setTitle(e.target.value)}
300
+ placeholder="e.g., Recent Articles"
301
+ />
302
+ </div>
303
+
362
304
  <div>
363
305
  <label
364
306
  htmlFor="page-size"
@@ -380,29 +322,6 @@ const ListContentSetup = ({
380
322
  </select>
381
323
  </div>
382
324
 
383
- <div>
384
- <label
385
- htmlFor="sort-mode"
386
- className="block text-sm font-bold text-gray-700"
387
- >
388
- Default sort order
389
- </label>
390
- <select
391
- id="sort-mode"
392
- name="sort-mode"
393
- className="mt-1 block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-cyan-600 focus:outline-none focus:ring-cyan-600 sm:text-sm"
394
- value={selectedMode}
395
- onChange={(e) => setSelectedMode(e.target.value)}
396
- >
397
- <option value="recent">Most Recent</option>
398
- <option value="popular">Most Popular</option>
399
- </select>
400
- <p className="mt-1 text-xs text-gray-500">
401
- Note: Users can toggle between views regardless of the default
402
- setting
403
- </p>
404
- </div>
405
-
406
325
  <div>
407
326
  <ColorPickerCombo
408
327
  title="Background Color"
@@ -536,7 +455,6 @@ const ListContentSetup = ({
536
455
  <div className="divide-y divide-gray-200">
537
456
  {paginatedPages.map((page) => {
538
457
  const isExcluded = excludedIds.includes(page.id);
539
- const analytics = analyticsData[page.id];
540
458
 
541
459
  return (
542
460
  <div key={page.id} className="flex items-center p-4">
@@ -549,23 +467,9 @@ const ListContentSetup = ({
549
467
  />
550
468
  </div>
551
469
  <div className="ml-4 min-w-0 flex-1">
552
- <div className="flex items-center justify-between">
553
- <p className="truncate text-sm font-bold text-gray-900">
554
- {page.title}
555
- </p>
556
- <div className="flex items-center space-x-4">
557
- <button
558
- onClick={() => toggleExclude(page.id)}
559
- className={`rounded px-2 py-1 text-xs font-bold ${
560
- isExcluded
561
- ? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
562
- : 'bg-red-100 text-red-600 hover:bg-red-200'
563
- }`}
564
- >
565
- {isExcluded ? 'Restore' : 'Exclude'}
566
- </button>
567
- </div>
568
- </div>
470
+ <p className="text-sm font-bold text-gray-900">
471
+ {page.title}
472
+ </p>
569
473
  <div className="mt-1">
570
474
  <p className="line-clamp-1 text-sm text-gray-500">
571
475
  {page.description}
@@ -586,22 +490,23 @@ const ListContentSetup = ({
586
490
  {topic}
587
491
  </span>
588
492
  ))}
493
+ <button
494
+ onClick={() => toggleExclude(page.id)}
495
+ className={`rounded px-2 py-1 text-xs font-bold ${
496
+ isExcluded
497
+ ? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
498
+ : 'bg-red-100 text-red-600 hover:bg-red-200'
499
+ }`}
500
+ >
501
+ {isExcluded ? 'Restore' : 'Exclude'}
502
+ </button>
589
503
  </div>
590
504
  )}
591
505
  <div className="mt-1 flex items-center text-xs text-gray-500">
592
- {analytics && (
593
- <>
594
- <span>{analytics.total_actions} views</span>
595
- {page.changed && (
596
- <>
597
- <span className="mx-2">•</span>
598
- <span>
599
- Updated{' '}
600
- {new Date(page.changed).toLocaleDateString()}
601
- </span>
602
- </>
603
- )}
604
- </>
506
+ {page.changed && (
507
+ <span>
508
+ Updated {new Date(page.changed).toLocaleDateString()}
509
+ </span>
605
510
  )}
606
511
  </div>
607
512
  </div>
@@ -0,0 +1,152 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { Combobox } from '@ark-ui/react/combobox';
3
+ import { Portal } from '@ark-ui/react/portal';
4
+ import { createListCollection } from '@ark-ui/react/collection';
5
+ import { useStore } from '@nanostores/react';
6
+ import { fullContentMapStore } from '@/stores/storykeep';
7
+ import { getCtx } from '@/stores/nodes';
8
+ import type { PaneNode } from '@/types/compositorTypes';
9
+ import type { BrandConfig } from '@/types/tractstack';
10
+
11
+ interface ProductCardSetupProps {
12
+ nodeId: string;
13
+ params: Record<string, any> | null;
14
+ config: BrandConfig;
15
+ }
16
+
17
+ export const ProductCardSetup = (props: ProductCardSetupProps) => {
18
+ const { nodeId, params } = props;
19
+ const ctx = getCtx();
20
+ const $contentMap = useStore(fullContentMapStore);
21
+
22
+ const [showSelector, setShowSelector] = useState(false);
23
+
24
+ const products = useMemo(() => {
25
+ return $contentMap
26
+ .filter(
27
+ (item) => item.type === 'Resource' && item.categorySlug === 'product'
28
+ )
29
+ .map((item) => ({ label: item.title, value: item.slug }));
30
+ }, [$contentMap]);
31
+
32
+ const productCollection = useMemo(() => {
33
+ return createListCollection({
34
+ items: products,
35
+ itemToValue: (item) => item.value,
36
+ itemToString: (item) => item.label,
37
+ });
38
+ }, [products]);
39
+
40
+ const [selectedItem, setSelectedItem] = useState<{
41
+ label: string;
42
+ value: string;
43
+ } | null>(() => {
44
+ const currentSlug = params?.slug;
45
+ if (currentSlug) {
46
+ return products.find((p) => p.value === currentSlug) || null;
47
+ }
48
+ return null;
49
+ });
50
+
51
+ const updatePayload = (newPayload: Record<string, any>) => {
52
+ const paneNode = ctx.allNodes.get().get(nodeId) as PaneNode;
53
+ if (!paneNode) return;
54
+
55
+ const updatedPaneNode = {
56
+ ...paneNode,
57
+ codeHookPayload: {
58
+ target: paneNode.codeHookPayload?.target,
59
+ options: JSON.stringify(newPayload),
60
+ },
61
+ isChanged: true,
62
+ };
63
+ ctx.modifyNodes([updatedPaneNode]);
64
+ };
65
+
66
+ const handleSelect = (details: { value: string[] }) => {
67
+ const slug = details.value[0];
68
+ const selected = products.find((p) => p.value === slug);
69
+ if (selected) {
70
+ setSelectedItem(selected);
71
+ updatePayload({ slug: selected.value });
72
+ setShowSelector(false);
73
+ }
74
+ };
75
+
76
+ const handleClear = () => {
77
+ setSelectedItem(null);
78
+ updatePayload({});
79
+ };
80
+
81
+ return (
82
+ <div className="space-y-4 p-2">
83
+ <h3 className="font-bold text-gray-800">Product Card Configuration</h3>
84
+
85
+ <div className="rounded-md border bg-gray-50 p-3">
86
+ <div className="flex items-center justify-between">
87
+ <div>
88
+ <p className="text-sm font-bold text-gray-600">Selected Product</p>
89
+ <p className="truncate font-bold text-gray-900">
90
+ {selectedItem ? selectedItem.label : 'None'}
91
+ </p>
92
+ </div>
93
+ <div className="flex gap-x-2">
94
+ <button
95
+ type="button"
96
+ onClick={() => setShowSelector(!showSelector)}
97
+ className="rounded bg-white px-3 py-1 text-sm font-bold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
98
+ >
99
+ {showSelector ? 'Cancel' : 'Change'}
100
+ </button>
101
+ <button
102
+ type="button"
103
+ onClick={handleClear}
104
+ className="rounded bg-white px-3 py-1 text-sm font-bold text-red-600 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
105
+ disabled={!selectedItem}
106
+ >
107
+ Clear
108
+ </button>
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ {showSelector && (
114
+ <div className="space-y-2 rounded-md border p-3">
115
+ <Combobox.Root
116
+ collection={productCollection}
117
+ onValueChange={handleSelect}
118
+ lazyMount
119
+ unmountOnExit
120
+ >
121
+ <Combobox.Label className="text-sm font-bold text-gray-700">
122
+ Find a product
123
+ </Combobox.Label>
124
+ <Combobox.Control>
125
+ <Combobox.Input
126
+ className="w-full rounded-md border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
127
+ placeholder="Search products..."
128
+ />
129
+ </Combobox.Control>
130
+ <Portal>
131
+ <Combobox.Positioner>
132
+ <Combobox.Content className="z-50 max-h-60 overflow-y-auto rounded-md border bg-white shadow-lg">
133
+ {products.map((item) => (
134
+ <Combobox.Item
135
+ key={item.value}
136
+ item={item}
137
+ className="relative cursor-pointer select-none px-4 py-2 text-gray-900 data-[highlighted]:bg-cyan-600 data-[highlighted]:text-white"
138
+ >
139
+ <Combobox.ItemText>{item.label}</Combobox.ItemText>
140
+ </Combobox.Item>
141
+ ))}
142
+ </Combobox.Content>
143
+ </Combobox.Positioner>
144
+ </Portal>
145
+ </Combobox.Root>
146
+ </div>
147
+ )}
148
+ </div>
149
+ );
150
+ };
151
+
152
+ export default ProductCardSetup;