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
@@ -0,0 +1,453 @@
1
+ import {
2
+ useState,
3
+ useMemo,
4
+ useEffect,
5
+ useRef,
6
+ type KeyboardEvent,
7
+ } from 'react';
8
+ import MagnifyingGlassIcon from '@heroicons/react/24/outline/MagnifyingGlassIcon';
9
+ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
10
+ import { useSearch } from '@/hooks/useSearch';
11
+ import type {
12
+ FullContentMapItem,
13
+ DiscoverySuggestion,
14
+ } from '@/types/tractstack';
15
+ import {
16
+ getResourceUrl,
17
+ getResourceImage,
18
+ getResourceDescription,
19
+ } from '@/utils/customHelpers';
20
+
21
+ // --- TYPES ---
22
+ interface SearchWidgetProps {
23
+ fullContentMap: FullContentMapItem[];
24
+ }
25
+
26
+ interface ResultItem {
27
+ id: string;
28
+ url: string;
29
+ title: string;
30
+ imageSrc: string;
31
+ thumbSrcSet?: string;
32
+ description?: string;
33
+ topics?: string[];
34
+ }
35
+
36
+ const ITEMS_PER_PAGE = 10;
37
+
38
+ function ResultItemCard({ item }: { item: ResultItem }) {
39
+ return (
40
+ <a href={item.url} className="group block">
41
+ <div className="flex items-start space-x-4 rounded-md p-2 group-hover:bg-slate-200/20">
42
+ <div
43
+ className="w-36 flex-shrink-0 rounded-md bg-gray-200 md:w-48 xl:w-72"
44
+ style={{ aspectRatio: '1200 / 630' }}
45
+ >
46
+ <img
47
+ src={item.imageSrc}
48
+ srcSet={item.thumbSrcSet}
49
+ alt={item.title}
50
+ style={{ aspectRatio: 1200 / 630 }}
51
+ className="w-36 flex-shrink-0 rounded-md md:w-48 xl:w-72"
52
+ />
53
+ </div>
54
+ <div className="flex-1">
55
+ <h3 className="text-lg font-bold text-black transition-colors group-hover:text-gray-900">
56
+ {item.title}
57
+ </h3>
58
+ {item.description && (
59
+ <p className="line-clamp-2 text-sm text-gray-800">
60
+ {item.description}
61
+ </p>
62
+ )}
63
+ {item.topics && item.topics.length > 0 && (
64
+ <div className="mt-4 flex flex-wrap gap-1">
65
+ {item.topics.slice(0, 3).map((topic: string) => (
66
+ <span
67
+ key={topic}
68
+ className="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800"
69
+ >
70
+ {topic}
71
+ </span>
72
+ ))}
73
+ </div>
74
+ )}
75
+ </div>
76
+ </div>
77
+ </a>
78
+ );
79
+ }
80
+
81
+ function CustomPagination({
82
+ currentPage,
83
+ totalPages,
84
+ onPageChange,
85
+ }: {
86
+ currentPage: number;
87
+ totalPages: number;
88
+ onPageChange: (page: number) => void;
89
+ }) {
90
+ return (
91
+ <div className="mt-8 flex items-center justify-center">
92
+ <div className="mr-4 text-sm text-gray-700">
93
+ Page {currentPage} of {totalPages}
94
+ </div>
95
+ <nav className="inline-flex rounded-md shadow-sm" aria-label="Pagination">
96
+ <button
97
+ onClick={() => onPageChange(currentPage - 1)}
98
+ disabled={currentPage === 1}
99
+ className="rounded-l-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-800 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
100
+ >
101
+ Previous
102
+ </button>
103
+ <button
104
+ onClick={() => onPageChange(currentPage + 1)}
105
+ disabled={currentPage >= totalPages}
106
+ className="rounded-r-md border border-cyan-600 bg-cyan-600 px-4 py-2 text-sm font-bold text-white hover:bg-cyan-700 disabled:cursor-not-allowed disabled:opacity-50"
107
+ >
108
+ Next
109
+ </button>
110
+ </nav>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ export default function SearchWidget({ fullContentMap }: SearchWidgetProps) {
116
+ const [query, setQuery] = useState('');
117
+ const [currentPage, setCurrentPage] = useState(1);
118
+ const [activeSearch, setActiveSearch] = useState<DiscoverySuggestion | null>(
119
+ null
120
+ );
121
+ const inputRef = useRef<HTMLInputElement>(null);
122
+
123
+ const {
124
+ suggestions,
125
+ isDiscovering,
126
+ searchResults,
127
+ isRetrieving,
128
+ retrieveError,
129
+ discoverTerms,
130
+ selectSuggestion,
131
+ selectExactMatch,
132
+ clearAll,
133
+ } = useSearch();
134
+
135
+ useEffect(() => {
136
+ if (query.trim().length >= 3) {
137
+ discoverTerms(query);
138
+ }
139
+ }, [query]);
140
+
141
+ useEffect(() => {
142
+ setCurrentPage(1);
143
+ }, [searchResults]);
144
+
145
+ const handleSuggestionSelect = (suggestion: DiscoverySuggestion) => {
146
+ setActiveSearch(suggestion);
147
+ selectSuggestion(suggestion);
148
+ setQuery('');
149
+ };
150
+
151
+ const handleExactMatch = (term: string) => {
152
+ const searchObj = { term, type: 'EXACT' as const };
153
+ setActiveSearch(searchObj);
154
+ selectExactMatch(term);
155
+ setQuery('');
156
+ };
157
+
158
+ const handleSuggestionClick = (suggestion: DiscoverySuggestion) => {
159
+ handleSuggestionSelect(suggestion);
160
+ };
161
+
162
+ const handleNewSearch = () => {
163
+ setActiveSearch(null);
164
+ clearAll();
165
+ };
166
+
167
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
168
+ if (e.key === 'Enter' && query.trim()) {
169
+ e.preventDefault();
170
+ if (query.trim().length < 3) return;
171
+
172
+ if (suggestions.length > 0) {
173
+ const exactMatch = suggestions.find(
174
+ (s) => s.term.toLowerCase() === query.trim().toLowerCase()
175
+ );
176
+ if (exactMatch) {
177
+ handleSuggestionSelect(exactMatch);
178
+ } else {
179
+ handleSuggestionSelect(suggestions[0]);
180
+ }
181
+ } else {
182
+ handleExactMatch(query.trim());
183
+ }
184
+ }
185
+ };
186
+
187
+ const getTypeColor = (type: string) => {
188
+ switch (type) {
189
+ case 'TITLE':
190
+ return 'bg-blue-100 text-blue-800 border-blue-200';
191
+ case 'EXACT':
192
+ return 'bg-orange-100 text-orange-800 border-orange-200';
193
+ case 'TOPIC':
194
+ return 'bg-purple-100 text-purple-800 border-purple-200';
195
+ case 'TEXT':
196
+ return 'bg-green-100 text-green-800 border-green-200';
197
+ default:
198
+ return 'bg-gray-100 text-gray-800 border-gray-200';
199
+ }
200
+ };
201
+
202
+ const suggestionForDisplay = useMemo(() => {
203
+ if (query.length < 3 || suggestions.length === 0) {
204
+ return null;
205
+ }
206
+ const exactMatch = suggestions.find(
207
+ (s) => s.term.toLowerCase() === query.trim().toLowerCase()
208
+ );
209
+ return exactMatch || suggestions[0];
210
+ }, [suggestions, query]);
211
+
212
+ const bestCompletion = suggestionForDisplay ? suggestionForDisplay.term : '';
213
+
214
+ const showCompletion =
215
+ bestCompletion.toLowerCase().startsWith(query.toLowerCase()) &&
216
+ query.length >= 3 &&
217
+ bestCompletion.length > query.length;
218
+
219
+ let preservedCompletion = '';
220
+ if (showCompletion) {
221
+ const completionText = bestCompletion.slice(query.length);
222
+ preservedCompletion = completionText.startsWith(' ')
223
+ ? '\u00A0' + completionText.slice(1)
224
+ : completionText;
225
+ }
226
+
227
+ const allResultItems = useMemo((): ResultItem[] => {
228
+ if (!searchResults) return [];
229
+ const items: ResultItem[] = [];
230
+ const combined = [
231
+ ...searchResults.storyFragmentResults,
232
+ ...searchResults.contextPaneResults,
233
+ ...searchResults.resourceResults,
234
+ ];
235
+ combined.forEach((ftsResult) => {
236
+ const item = fullContentMap.find(
237
+ (mapItem) => mapItem.id === ftsResult.ID
238
+ );
239
+ if (item) {
240
+ if (item.type === 'StoryFragment') {
241
+ items.push({
242
+ id: item.id,
243
+ title: item.title,
244
+ url: `/${item.slug}`,
245
+ imageSrc: item.thumbSrc || '/static.jpg',
246
+ thumbSrcSet: item.thumbSrcSet,
247
+ description: item.description,
248
+ topics: item.topics,
249
+ });
250
+ } else if (item.type === 'Pane' && item.isContext) {
251
+ items.push({
252
+ id: item.id,
253
+ title: item.title,
254
+ url: `/context/${item.slug}`,
255
+ imageSrc: '/static.jpg',
256
+ description: 'Contextual information page.',
257
+ });
258
+ } else if (item.type === 'Resource') {
259
+ items.push({
260
+ id: item.id,
261
+ title: item.title,
262
+ url: getResourceUrl(item.categorySlug || '', item.slug),
263
+ imageSrc:
264
+ getResourceImage(item.id, item.slug, item.categorySlug || '') ||
265
+ '/static.jpg',
266
+ description:
267
+ getResourceDescription(
268
+ item.id,
269
+ item.slug,
270
+ item.categorySlug || ''
271
+ ) || undefined,
272
+ });
273
+ }
274
+ }
275
+ });
276
+ return items;
277
+ }, [searchResults, fullContentMap]);
278
+
279
+ const totalResults = allResultItems.length;
280
+ const totalPages = Math.ceil(totalResults / ITEMS_PER_PAGE);
281
+ const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
282
+ const paginatedItems = allResultItems.slice(
283
+ startIndex,
284
+ startIndex + ITEMS_PER_PAGE
285
+ );
286
+ const leftColItems = paginatedItems.slice(
287
+ 0,
288
+ Math.ceil(paginatedItems.length / 2)
289
+ );
290
+ const rightColItems = paginatedItems.slice(
291
+ Math.ceil(paginatedItems.length / 2)
292
+ );
293
+
294
+ return (
295
+ <div className="mx-auto max-w-7xl px-4 py-2">
296
+ <div className={searchResults ? `rounded-xl border-2 p-6 md:p-12` : ``}>
297
+ <div
298
+ className={`relative mx-auto mb-8 ${!searchResults ? `max-w-5xl` : ``}`}
299
+ >
300
+ {/* PHASE 1: DISCOVERY (WHEN THERE ARE NO RESULTS) */}
301
+ {!searchResults && (
302
+ <div>
303
+ <div className="relative">
304
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-5">
305
+ <MagnifyingGlassIcon className="h-8 w-8 text-gray-400" />
306
+ </div>
307
+
308
+ {showCompletion && (
309
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex h-full w-full items-center py-5 pl-16 pr-5 text-3xl text-gray-400">
310
+ <span
311
+ className="font-bold"
312
+ style={{ visibility: 'hidden', whiteSpace: 'pre' }}
313
+ >
314
+ {query}
315
+ </span>
316
+ <span>{preservedCompletion}</span>
317
+ </div>
318
+ )}
319
+
320
+ <input
321
+ ref={inputRef}
322
+ type="text"
323
+ value={query}
324
+ onChange={(e) => setQuery(e.target.value)}
325
+ onKeyDown={handleKeyDown}
326
+ placeholder="Search for content..."
327
+ className="relative z-10 w-full rounded-xl border-2 border-gray-300 bg-transparent py-5 pl-16 pr-5 text-3xl font-bold text-gray-800 placeholder-gray-400 shadow-sm transition-shadow focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
328
+ />
329
+ </div>
330
+
331
+ {isDiscovering && (
332
+ <div className="mx-auto mt-2 max-w-5xl rounded-xl border border-gray-200 bg-white p-4 text-center text-gray-500 shadow-lg">
333
+ Discovering...
334
+ </div>
335
+ )}
336
+ {suggestions.length > 0 && !isDiscovering && (
337
+ <div className="mx-auto mt-2 max-w-5xl rounded-xl border border-gray-200 bg-white p-4 shadow-lg">
338
+ <p className="mb-3 text-sm font-bold text-gray-600">
339
+ Suggestions
340
+ </p>
341
+ <div className="flex flex-wrap gap-2">
342
+ {suggestions.map((suggestion, index) => (
343
+ <button
344
+ key={index}
345
+ onClick={() => handleSuggestionClick(suggestion)}
346
+ className={`inline-flex items-center rounded-lg border px-3 py-1.5 text-sm font-bold transition-all hover:shadow-md ${getTypeColor(
347
+ suggestion.type
348
+ )}`}
349
+ >
350
+ {suggestion.term}
351
+ </button>
352
+ ))}
353
+ </div>
354
+ </div>
355
+ )}
356
+ {/* Show this block only when discovery is finished, the user has typed, and there are no suggestions. */}
357
+ {!isDiscovering &&
358
+ suggestions.length === 0 &&
359
+ query.trim().length >= 3 && (
360
+ <div className="mx-auto mt-2 max-w-5xl rounded-xl border border-gray-200 bg-white p-4 text-center shadow-lg">
361
+ <p className="text-gray-600">
362
+ No suggestions found for "
363
+ <span className="font-bold">{query}</span>".
364
+ </p>
365
+ </div>
366
+ )}
367
+ </div>
368
+ )}
369
+
370
+ {/* PHASE 2: RESULTS (WHEN A SEARCH HAS BEEN MADE) */}
371
+ {searchResults && activeSearch && (
372
+ <div
373
+ className={`inline-flex items-center gap-2 rounded-lg border-2 px-4 py-2 text-lg font-bold ${getTypeColor(activeSearch.type)}`}
374
+ >
375
+ <span>{activeSearch.term}</span>
376
+ <button
377
+ onClick={handleNewSearch}
378
+ className="flex items-center justify-center rounded-full text-gray-600 hover:text-gray-800"
379
+ aria-label={`Remove ${activeSearch.term}`}
380
+ >
381
+ <XMarkIcon className="h-5 w-5" />
382
+ </button>
383
+ </div>
384
+ )}
385
+ </div>
386
+
387
+ <div className="mt-8">
388
+ {isRetrieving && (
389
+ <div className="flex justify-center p-8">
390
+ <div className="h-10 w-10 animate-spin rounded-full border-b-2 border-blue-600" />
391
+ </div>
392
+ )}
393
+ {retrieveError && (
394
+ <div className="rounded-md bg-red-50 p-4 text-red-700">
395
+ <p>Error: {retrieveError}</p>
396
+ </div>
397
+ )}
398
+
399
+ {searchResults && !isRetrieving && (
400
+ <div>
401
+ <div className="mb-4">
402
+ <h2 className="text-lg font-bold text-black">
403
+ {totalResults} result{totalResults !== 1 ? 's' : ''} found
404
+ </h2>
405
+ {totalResults > 0 && (
406
+ <p className="mt-1 text-sm text-gray-800">
407
+ Showing {startIndex + 1}-
408
+ {Math.min(startIndex + ITEMS_PER_PAGE, totalResults)} of{' '}
409
+ {totalResults}
410
+ </p>
411
+ )}
412
+ </div>
413
+ {totalResults > 0 ? (
414
+ <>
415
+ <div className="block space-y-6 md:hidden">
416
+ {paginatedItems.map((item) => (
417
+ <ResultItemCard key={item.id} item={item} />
418
+ ))}
419
+ </div>
420
+ <div className="hidden md:flex md:space-x-6">
421
+ <div className="space-y-6 md:w-1/2">
422
+ {leftColItems.map((item) => (
423
+ <ResultItemCard key={item.id} item={item} />
424
+ ))}
425
+ </div>
426
+ <div className="space-y-6 md:w-1/2">
427
+ {rightColItems.map((item) => (
428
+ <ResultItemCard key={item.id} item={item} />
429
+ ))}
430
+ </div>
431
+ </div>
432
+ {totalPages > 1 && (
433
+ <CustomPagination
434
+ currentPage={currentPage}
435
+ totalPages={totalPages}
436
+ onPageChange={setCurrentPage}
437
+ />
438
+ )}
439
+ </>
440
+ ) : (
441
+ <div className="rounded-lg bg-gray-50 px-4 py-12 text-center">
442
+ <p className="text-lg italic text-gray-600">
443
+ No results found.
444
+ </p>
445
+ </div>
446
+ )}
447
+ </div>
448
+ )}
449
+ </div>
450
+ </div>
451
+ </div>
452
+ );
453
+ }
@@ -30,6 +30,7 @@ import StoryFragmentConfigPanel from '@/components/edit/storyfragment/StoryFragm
30
30
  import StoryFragmentTitlePanel from '@/components/edit/storyfragment/StoryFragmentPanel_title';
31
31
  import ContextPanePanel from '@/components/edit/context/ContextPaneConfig';
32
32
  import ContextPaneTitlePanel from '@/components/edit/context/ContextPaneConfig_title';
33
+ import { regexpHook } from '@/constants';
33
34
  import type {
34
35
  StoryFragmentNode,
35
36
  PaneNode,
@@ -40,8 +41,6 @@ import type { NodeProps } from '@/types/nodeProps';
40
41
 
41
42
  function parseCodeHook(node: BaseNode | FlatNode) {
42
43
  if ('codeHookParams' in node && Array.isArray(node.codeHookParams)) {
43
- const regexpHook =
44
- /^(identifyAs|youtube|bunny|bunnyContext|toggle|resource|belief|signup)\((.*)\)$/;
45
44
  const hookMatch = node.copy?.match(regexpHook);
46
45
 
47
46
  if (!hookMatch) return null;
@@ -58,8 +57,6 @@ function parseCodeHook(node: BaseNode | FlatNode) {
58
57
  const firstChild = node.children[0];
59
58
  if (!firstChild?.value) return null;
60
59
 
61
- const regexpHook =
62
- /(identifyAs|youtube|bunny|bunnyContext|toggle|resource|belief|signup)\((.*?)\)/;
63
60
  const regexpValues = /((?:[^\\|]+|\\\|?)+)/g;
64
61
  const hookMatch = firstChild.value.match(regexpHook);
65
62
 
@@ -87,7 +84,7 @@ const getElement = (
87
84
  const isPreview = getCtx(props).rootNodeId.get() === `tmp`;
88
85
  const hasPanes = useStore(getCtx(props).hasPanes);
89
86
  const isTemplate = useStore(getCtx(props).isTemplate);
90
- const sharedProps = { nodeId: node.id, ctx: props.ctx };
87
+ const sharedProps = { nodeId: node.id, ctx: props.ctx, config: props.config };
91
88
  const type = getType(node);
92
89
 
93
90
  switch (type) {
@@ -312,7 +309,7 @@ const getElement = (
312
309
  case 'impression':
313
310
  return <></>;
314
311
  default:
315
- console.warn(`Node.tsx miss on ${type}`);
312
+ console.warn(`Node.tsx miss on ${type}`, node);
316
313
  return <></>;
317
314
  }
318
315
  };
@@ -51,26 +51,36 @@ const PanelVisibilityWrapper = ({
51
51
  const currentWrapper = wrapperRef.current;
52
52
  if (!currentWrapper) return;
53
53
 
54
- // Always observe the panel, regardless of active state
54
+ // Skip intersection observer for 'add' panels - they behave differently
55
+ if (panelType === 'add') {
56
+ return;
57
+ }
58
+
55
59
  const observer = new IntersectionObserver(
56
60
  (entries) => {
57
- // Only take action if this panel is currently active and not intersecting
58
- if (!entries[0].isIntersecting && isActive) {
59
- nodesCtx.closeAllPanels();
61
+ // Add delay to prevent immediate closing during panel activation
62
+ if (isActive) {
63
+ setTimeout(() => {
64
+ // Double-check the panel is still active before closing
65
+ const currentActiveMode = nodesCtx.activePaneMode.get();
66
+ const stillActive =
67
+ currentActiveMode.panel === panelType &&
68
+ currentActiveMode.paneId === nodeId;
69
+ if (!entries[0].isIntersecting && stillActive) {
70
+ nodesCtx.closeAllPanels();
71
+ }
72
+ }, 100); // Small delay to allow panel to render
60
73
  }
61
74
  },
62
75
  {
63
- threshold: 0.1, // Close when 90% of the panel is out of view
64
- rootMargin: '-10px', // Small margin to make detection a bit more forgiving
76
+ threshold: 0.1,
77
+ rootMargin: '-10px',
65
78
  }
66
79
  );
67
80
 
68
81
  observer.observe(currentWrapper);
69
-
70
- return () => {
71
- observer.disconnect();
72
- };
73
- }, [nodeId, panelType, nodesCtx, isActive]); // Include isActive in dependencies
82
+ return () => observer.disconnect();
83
+ }, [nodeId, panelType, nodesCtx, isActive]);
74
84
 
75
85
  return (
76
86
  <div
@@ -1,49 +1,50 @@
1
1
  interface BunnyVideoProps {
2
- embedUrl: string;
2
+ videoId: string;
3
3
  title: string;
4
4
  className?: string;
5
5
  }
6
6
 
7
- const BunnyVideo = ({ embedUrl, title, className = '' }: BunnyVideoProps) => {
8
- // Check if a string is a valid URL
9
- const isValidUrl = (url: string): boolean => {
10
- try {
11
- new URL(url);
12
- return true;
13
- } catch (e) {
14
- return false;
15
- }
16
- };
7
+ const BUNNY_EMBED_BASE_URL = 'https://iframe.mediadelivery.net/embed/';
17
8
 
18
- // Render placeholder when URL is invalid
19
- if (!isValidUrl(embedUrl)) {
9
+ const isValidBunnyVideoId = (id: string): boolean => {
10
+ if (!id) return false;
11
+ const videoIdRegex = /^\d+\/[a-f0-9\-]{36}$/;
12
+ return videoIdRegex.test(id);
13
+ };
14
+
15
+ const BunnyVideo = ({ videoId, title, className = '' }: BunnyVideoProps) => {
16
+ // If the videoId is missing or has an invalid format, render the placeholder.
17
+ if (!isValidBunnyVideoId(videoId)) {
20
18
  return (
21
19
  <div
22
20
  className={`flex aspect-video w-full items-center justify-center bg-gray-100 ${className}`}
23
21
  >
24
22
  <div className="p-4 text-center">
25
- <div className="text-mydarkgrey mb-2">Video URL not set</div>
23
+ <div className="text-mydarkgrey mb-2">Video ID not set</div>
26
24
  <div className="text-mygrey text-sm">
27
- Configure this widget with a valid Bunny Stream URL
25
+ Configure this widget with a valid Bunny Video ID
28
26
  </div>
29
27
  </div>
30
28
  </div>
31
29
  );
32
30
  }
33
31
 
34
- // Build URL with default parameters for preview
35
- let videoUrl;
32
+ // Build the full, final URL from the videoId.
33
+ let finalVideoUrl;
36
34
  try {
37
- videoUrl = new URL(embedUrl);
35
+ const baseURL = `${BUNNY_EMBED_BASE_URL}${videoId}`;
36
+ const videoUrl = new URL(baseURL);
38
37
  videoUrl.searchParams.set('autoplay', '0');
39
38
  videoUrl.searchParams.set('preload', 'false');
40
39
  videoUrl.searchParams.set('responsive', 'true');
40
+ finalVideoUrl = videoUrl.toString();
41
41
  } catch (e) {
42
+ // This handles cases where a malformed videoId might be passed.
42
43
  return (
43
44
  <div
44
45
  className={`flex aspect-video w-full items-center justify-center bg-gray-100 ${className}`}
45
46
  >
46
- <div className="text-mydarkgrey text-center">Invalid video URL</div>
47
+ <div className="text-mydarkgrey text-center">Invalid Video ID</div>
47
48
  </div>
48
49
  );
49
50
  }
@@ -51,7 +52,7 @@ const BunnyVideo = ({ embedUrl, title, className = '' }: BunnyVideoProps) => {
51
52
  return (
52
53
  <div className={`relative w-full ${className}`}>
53
54
  <iframe
54
- src={videoUrl.toString()}
55
+ src={finalVideoUrl}
55
56
  className="aspect-video w-full"
56
57
  title={title}
57
58
  allow="autoplay; fullscreen"