astro-tractstack 2.0.0-rc.9 → 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 (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 +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_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
@@ -1,10 +1,11 @@
1
- import { useState, useCallback, useMemo } from 'react';
2
- import { Combobox } from '@ark-ui/react';
1
+ import { useState, useCallback, useMemo, useRef } from 'react';
2
+ import { Combobox, Portal } from '@ark-ui/react';
3
3
  import { createListCollection } from '@ark-ui/react/collection';
4
4
  import ChevronUpDownIcon from '@heroicons/react/24/outline/ChevronUpDownIcon';
5
5
  import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
6
6
  import { settingsPanelStore } from '@/stores/storykeep';
7
7
  import { tailwindClasses } from '@/utils/compositor/tailwindClasses';
8
+ import { useDropdownDirection } from '@/utils/helpers';
8
9
  import type { BasePanelProps } from '@/types/compositorTypes';
9
10
 
10
11
  // Recommended styles for button states
@@ -57,6 +58,8 @@ const StyleLinkPanelAdd = ({ node }: BasePanelProps) => {
57
58
  const [query, setQuery] = useState('');
58
59
  const [showAdvanced, setShowAdvanced] = useState(false);
59
60
  const [selectedStyle, setSelectedStyle] = useState<string | null>(null);
61
+ const comboboxRef = useRef<HTMLDivElement>(null);
62
+ const { openAbove } = useDropdownDirection(comboboxRef);
60
63
 
61
64
  if (!node?.tagName || (node.tagName !== 'a' && node.tagName !== 'button'))
62
65
  return null;
@@ -64,7 +67,6 @@ const StyleLinkPanelAdd = ({ node }: BasePanelProps) => {
64
67
  const currentClasses = new Set<string>();
65
68
  const isHoverMode = settingsPanelStore.get()?.action?.endsWith('-hover');
66
69
 
67
- // Get existing button or hover classes
68
70
  if (node.buttonPayload) {
69
71
  const classes = isHoverMode
70
72
  ? node.buttonPayload.buttonHoverClasses
@@ -76,7 +78,6 @@ const StyleLinkPanelAdd = ({ node }: BasePanelProps) => {
76
78
 
77
79
  const styles = getFilteredStyles(showAdvanced, currentClasses);
78
80
 
79
- // Create filtered collection for combobox
80
81
  const styleCollection = useMemo(() => {
81
82
  const filteredStyles =
82
83
  query === ''
@@ -94,7 +95,6 @@ const StyleLinkPanelAdd = ({ node }: BasePanelProps) => {
94
95
  });
95
96
  }, [styles, query]);
96
97
 
97
- // Get appropriate recommended styles
98
98
  const recommendedStyles = isHoverMode ? HOVER_STYLES : BUTTON_STYLES;
99
99
  const availableRecommendedStyles = recommendedStyles.filter(
100
100
  (style) => !currentClasses.has(style.key)
@@ -143,7 +143,6 @@ const StyleLinkPanelAdd = ({ node }: BasePanelProps) => {
143
143
  });
144
144
  };
145
145
 
146
- // CSS to properly style the combobox items with hover and selection
147
146
  const comboboxItemStyles = `
148
147
  .style-item[data-highlighted] {
149
148
  background-color: #0891b2; /* bg-cyan-600 */
@@ -164,7 +163,7 @@ const StyleLinkPanelAdd = ({ node }: BasePanelProps) => {
164
163
  `;
165
164
 
166
165
  return (
167
- <div className="min-h-[400px] max-w-md space-y-4">
166
+ <div className="max-w-md space-y-4">
168
167
  <style>{comboboxItemStyles}</style>
169
168
 
170
169
  <div className="flex flex-row flex-nowrap justify-between">
@@ -199,46 +198,57 @@ const StyleLinkPanelAdd = ({ node }: BasePanelProps) => {
199
198
  loopFocus={true}
200
199
  openOnKeyPress={true}
201
200
  composite={true}
201
+ positioning={{
202
+ placement: openAbove ? 'top' : 'bottom',
203
+ gutter: 4,
204
+ sameWidth: true,
205
+ }}
202
206
  >
203
- <div className="relative">
204
- <Combobox.Input
205
- className="border-mydarkgrey focus:border-myblue focus:ring-myblue w-full rounded-md py-2 pl-3 pr-10 text-xl shadow-sm"
206
- placeholder="Search styles..."
207
- autoComplete="off"
208
- />
209
- <Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
210
- <ChevronUpDownIcon
211
- className="text-mydarkgrey h-5 w-5"
212
- aria-hidden="true"
207
+ <Combobox.Control ref={comboboxRef}>
208
+ <div className="relative">
209
+ <Combobox.Input
210
+ className="border-mydarkgrey focus:border-myblue focus:ring-myblue w-full rounded-md py-2 pl-3 pr-10 text-xl shadow-sm"
211
+ placeholder="Search styles..."
212
+ autoComplete="off"
213
213
  />
214
- </Combobox.Trigger>
215
- </div>
214
+ <Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
215
+ <ChevronUpDownIcon
216
+ className="text-mydarkgrey h-5 w-5"
217
+ aria-hidden="true"
218
+ />
219
+ </Combobox.Trigger>
220
+ </div>
221
+ </Combobox.Control>
216
222
 
217
- <Combobox.Content className="absolute z-50 mt-1 max-h-64 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
218
- {styleCollection.items.length === 0 ? (
219
- <div className="text-mydarkgrey relative cursor-default select-none px-4 py-2">
220
- Nothing found.
221
- </div>
222
- ) : (
223
- styleCollection.items.map((style) => (
224
- <Combobox.Item
225
- key={style.key}
226
- item={style}
227
- className="style-item relative cursor-default select-none py-2 pl-10 pr-4 text-black"
228
- >
229
- <span className="block truncate">
230
- {style.title}
231
- <span className="ml-2 text-sm opacity-60">
232
- {style.className}
233
- </span>
234
- </span>
235
- <span className="style-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
236
- <CheckIcon className="h-5 w-5" aria-hidden="true" />
237
- </span>
238
- </Combobox.Item>
239
- ))
240
- )}
241
- </Combobox.Content>
223
+ <Portal>
224
+ <Combobox.Positioner style={{ zIndex: 1002 }}>
225
+ <Combobox.Content className="absolute z-50 mt-1 max-h-64 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
226
+ {styleCollection.items.length === 0 ? (
227
+ <div className="text-mydarkgrey relative cursor-default select-none px-4 py-2">
228
+ Nothing found.
229
+ </div>
230
+ ) : (
231
+ styleCollection.items.map((style) => (
232
+ <Combobox.Item
233
+ key={style.key}
234
+ item={style}
235
+ className="style-item relative cursor-default select-none py-2 pl-10 pr-4 text-black"
236
+ >
237
+ <span className="block truncate">
238
+ {style.title}
239
+ <span className="ml-2 text-sm opacity-60">
240
+ {style.className}
241
+ </span>
242
+ </span>
243
+ <span className="style-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
244
+ <CheckIcon className="h-5 w-5" aria-hidden="true" />
245
+ </span>
246
+ </Combobox.Item>
247
+ ))
248
+ )}
249
+ </Combobox.Content>
250
+ </Combobox.Positioner>
251
+ </Portal>
242
252
  </Combobox.Root>
243
253
  </div>
244
254
 
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, useCallback } from 'react';
2
2
  import { settingsPanelStore, fullContentMapStore } from '@/stores/storykeep';
3
3
  import { getCtx } from '@/stores/nodes';
4
4
  import { cloneDeep } from '@/utils/helpers';
@@ -6,10 +6,9 @@ import { lispLexer } from '@/utils/actions/lispLexer';
6
6
  import { preParseAction } from '@/utils/actions/preParse_Action';
7
7
  import { preParseBunny } from '@/utils/actions/preParse_Bunny';
8
8
  import ActionBuilderField from '@/components/form/ActionBuilderField';
9
- import BunnyMomentSelector from '@/components/fields/BunnyMomentSelector';
10
9
  import { GOTO_TARGETS } from '@/constants';
11
10
  import type { BrandConfig } from '@/types/tractstack';
12
- import type { FlatNode } from '@/types/compositorTypes';
11
+ import type { FlatNode, LispToken } from '@/types/compositorTypes';
13
12
 
14
13
  interface StyleLinkConfigPanelProps {
15
14
  node: FlatNode;
@@ -23,7 +22,6 @@ const StyleLinkConfigPanel = ({ node, config }: StyleLinkConfigPanelProps) => {
23
22
 
24
23
  const [isInitialized, setIsInitialized] = useState(false);
25
24
  const [callbackPayload, setCallbackPayload] = useState('');
26
- const [actionType, setActionType] = useState<'goto' | 'bunnyMoment'>('goto');
27
25
 
28
26
  const ctx = getCtx();
29
27
  const allNodes = ctx.allNodes.get();
@@ -42,122 +40,143 @@ const StyleLinkConfigPanel = ({ node, config }: StyleLinkConfigPanelProps) => {
42
40
  useEffect(() => {
43
41
  const currentPayload = node.buttonPayload?.callbackPayload || '';
44
42
  setCallbackPayload(currentPayload);
45
- setActionType(
46
- currentPayload.startsWith('(bunnyMoment') ? 'bunnyMoment' : 'goto'
47
- );
48
43
  setIsInitialized(true);
49
44
  }, [node]);
50
45
 
51
- const updateNode = (newCallbackPayload: string) => {
46
+ const updateNode = useCallback(() => {
52
47
  try {
53
48
  const linkNode = cloneDeep(allNodes.get(node.id)) as FlatNode;
54
- if (!linkNode || !markdownId) return;
55
-
56
- const lexedPayload = lispLexer(newCallbackPayload);
57
- let targetUrl = null;
58
- if (newCallbackPayload.startsWith('(goto')) {
59
- targetUrl =
60
- lexedPayload && preParseAction(lexedPayload, slug, isContext, config);
49
+ if (!linkNode || !markdownId) {
50
+ return;
61
51
  }
62
- const bunnyPayload = lexedPayload && preParseBunny(lexedPayload);
63
- const isExternalUrl =
64
- typeof targetUrl === 'string' && targetUrl.startsWith('https://');
65
- const existingButtonPayload = linkNode.buttonPayload || {
66
- buttonClasses: {},
67
- buttonHoverClasses: {},
68
- callbackPayload: '',
52
+
53
+ const existingButtonPayload = linkNode.buttonPayload;
54
+ const newButtonPayload: FlatNode['buttonPayload'] = {
55
+ callbackPayload: callbackPayload,
56
+ buttonClasses: existingButtonPayload?.buttonClasses || {},
57
+ buttonHoverClasses: existingButtonPayload?.buttonHoverClasses || {},
69
58
  };
70
59
 
71
- if (newCallbackPayload.startsWith('(bunnyMoment')) {
60
+ const [tokens] = lispLexer(callbackPayload);
61
+ delete linkNode.href;
62
+
63
+ if (
64
+ callbackPayload.startsWith('(declare') ||
65
+ callbackPayload.startsWith('(identifyAs')
66
+ ) {
72
67
  linkNode.tagName = 'button';
73
- linkNode.href = '#';
68
+ } else if (callbackPayload.startsWith('(bunnyMoment')) {
69
+ linkNode.tagName = 'button';
70
+ newButtonPayload.bunnyPayload =
71
+ (tokens && preParseBunny([tokens])) ?? undefined;
72
+ } else if (callbackPayload.startsWith('(goto')) {
73
+ const targetUrl =
74
+ tokens && preParseAction([tokens], slug, isContext, config);
75
+ const isExternalUrl =
76
+ typeof targetUrl === 'string' && targetUrl.startsWith('https://');
77
+
78
+ linkNode.href = targetUrl || '#';
79
+ linkNode.tagName = !targetUrl ? 'button' : 'a';
80
+ if (isExternalUrl) {
81
+ newButtonPayload.isExternalUrl = true;
82
+ }
74
83
  } else {
75
- linkNode.href = isExternalUrl ? targetUrl : targetUrl || '#';
76
- linkNode.tagName = !targetUrl || bunnyPayload ? 'button' : 'a';
84
+ linkNode.tagName = 'button';
77
85
  }
78
86
 
79
- linkNode.buttonPayload = {
80
- ...existingButtonPayload,
81
- callbackPayload: newCallbackPayload,
82
- buttonClasses: existingButtonPayload.buttonClasses || {},
83
- buttonHoverClasses: existingButtonPayload.buttonHoverClasses || {},
84
- ...(isExternalUrl ? { isExternalUrl: true } : {}),
85
- ...(bunnyPayload ? { bunnyPayload } : {}),
86
- };
87
+ linkNode.buttonPayload = newButtonPayload;
88
+
89
+ const currentSignal = settingsPanelStore.get();
90
+ if (currentSignal) {
91
+ settingsPanelStore.set({
92
+ ...currentSignal,
93
+ editLock: Date.now(),
94
+ });
95
+ }
87
96
 
88
97
  ctx.modifyNodes([{ ...linkNode, isChanged: true }]);
89
98
  } catch (error) {
90
- console.error('Error updating node:', error);
99
+ console.error('Error in updateNode:', error);
91
100
  }
92
- };
101
+ }, [
102
+ allNodes,
103
+ config,
104
+ ctx,
105
+ isContext,
106
+ markdownId,
107
+ node.id,
108
+ slug,
109
+ callbackPayload,
110
+ ]);
93
111
 
94
112
  useEffect(() => {
95
- if (!isInitialized) return;
96
-
97
- if (callbackPayload.startsWith('(bunnyMoment')) {
98
- const match = callbackPayload.match(
99
- /\(bunnyMoment\s+\(\s*([^\s]+)\s+(\d+)\s*\)\)/
100
- );
101
- if (match && match[1] && match[2]) {
102
- updateNode(callbackPayload);
103
- }
113
+ if (!isInitialized) {
104
114
  return;
105
115
  }
106
116
 
107
- const match = callbackPayload.match(/\(goto\s+\(([^)]+)\)/);
108
- if (!match) return;
109
-
110
- const parts = match[1].split(' ').filter(Boolean);
111
- if (parts.length === 0) return;
112
-
113
- const target = parts[0];
114
- const targetConfig = GOTO_TARGETS[target];
115
- if (!targetConfig) return;
116
-
117
117
  let isComplete = false;
118
118
 
119
- if (target === 'url') {
120
- isComplete = parts.length > 1;
121
- } else if (targetConfig.subcommands) {
122
- isComplete = parts.length > 1;
123
- } else if (targetConfig.requiresParam) {
124
- if (targetConfig.requiresSecondParam) {
125
- isComplete = parts.length > 2;
126
- } else {
127
- isComplete = parts.length > 1;
128
- }
129
- } else {
119
+ if (callbackPayload.trim() === '') {
130
120
  isComplete = true;
121
+ } else {
122
+ const [tokens] = lispLexer(callbackPayload);
123
+
124
+ if (
125
+ Array.isArray(tokens) &&
126
+ tokens.length > 0 &&
127
+ Array.isArray(tokens[0])
128
+ ) {
129
+ const mainExpression = tokens[0] as LispToken[];
130
+
131
+ if (mainExpression.length > 1) {
132
+ // Must have a command AND parameters
133
+ const command = mainExpression[0] as string;
134
+ const parameters = mainExpression[1] as LispToken[];
135
+
136
+ if (Array.isArray(parameters)) {
137
+ if (command === 'declare' || command === 'identifyAs') {
138
+ if (parameters.length === 2 && parameters[0] && parameters[1]) {
139
+ isComplete = true;
140
+ }
141
+ } else if (command === 'bunnyMoment') {
142
+ if (parameters.length === 2) {
143
+ isComplete = true;
144
+ }
145
+ } else if (command === 'goto') {
146
+ const target = parameters[0] as string;
147
+ const targetConfig = GOTO_TARGETS[target];
148
+ if (targetConfig) {
149
+ if (target === 'url') {
150
+ isComplete =
151
+ parameters.length > 1 &&
152
+ String(parameters[1]).includes('.');
153
+ } else if (targetConfig.subcommands) {
154
+ isComplete = parameters.length > 1;
155
+ } else if (targetConfig.requiresParam) {
156
+ if (targetConfig.requiresSecondParam) {
157
+ isComplete = parameters.length > 2;
158
+ } else {
159
+ isComplete = parameters.length > 1;
160
+ }
161
+ } else {
162
+ isComplete = true;
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
131
169
  }
132
170
 
133
171
  if (isComplete) {
134
- updateNode(callbackPayload);
172
+ updateNode();
135
173
  }
136
- }, [
137
- callbackPayload,
138
- isInitialized,
139
- node.id,
140
- config,
141
- allNodes,
142
- ctx,
143
- markdownId,
144
- slug,
145
- isContext,
146
- ]);
174
+ }, [callbackPayload, isInitialized, updateNode]);
147
175
 
148
176
  const handleChange = (value: string) => {
149
177
  setCallbackPayload(value);
150
178
  };
151
179
 
152
- const handleActionTypeChange = (type: 'goto' | 'bunnyMoment') => {
153
- setActionType(type);
154
- if (type === 'bunnyMoment') {
155
- setCallbackPayload('(bunnyMoment ( ))');
156
- } else {
157
- setCallbackPayload('');
158
- }
159
- };
160
-
161
180
  const handleCloseConfig = () => {
162
181
  settingsPanelStore.set({
163
182
  nodeId: node.id,
@@ -166,28 +185,6 @@ const StyleLinkConfigPanel = ({ node, config }: StyleLinkConfigPanelProps) => {
166
185
  });
167
186
  };
168
187
 
169
- const renderActionBuilder = () => {
170
- switch (actionType) {
171
- case 'bunnyMoment':
172
- return (
173
- <BunnyMomentSelector
174
- value={callbackPayload}
175
- onChange={handleChange}
176
- />
177
- );
178
- case 'goto':
179
- default:
180
- return (
181
- <ActionBuilderField
182
- slug={slug}
183
- value={callbackPayload}
184
- onChange={handleChange}
185
- contentMap={fullContentMapStore.get()}
186
- />
187
- );
188
- }
189
- };
190
-
191
188
  return (
192
189
  <div className="relative">
193
190
  <div className="w-full max-w-md space-y-4">
@@ -201,36 +198,14 @@ const StyleLinkConfigPanel = ({ node, config }: StyleLinkConfigPanelProps) => {
201
198
  </button>
202
199
  </div>
203
200
 
204
- <div className="mb-4 space-y-2">
205
- <label className="block text-sm text-gray-700">Action Type</label>
206
- <select
207
- value={actionType}
208
- onChange={(e) =>
209
- handleActionTypeChange(e.target.value as 'goto' | 'bunnyMoment')
210
- }
211
- className="mt-1 block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
212
- >
213
- <option value="goto">Navigation Action</option>
214
- <option value="bunnyMoment">Video Moment Action</option>
215
- </select>
216
- <p className="mt-1 text-sm text-gray-500">
217
- {actionType === 'goto'
218
- ? 'Create a link to navigate to another page or section'
219
- : 'Jump to a specific moment in a video on this page'}
220
- </p>
221
- </div>
222
-
223
201
  <div className="space-y-2">
224
- <div className="relative max-h-[60vh] min-h-[400px] overflow-y-auto">
225
- <div className="absolute inset-x-0">
226
- <label className="text-mydarkgrey mb-2 block text-sm">
227
- {actionType === 'goto'
228
- ? 'Callback Payload'
229
- : 'Video Moment Settings'}
230
- </label>
231
- {renderActionBuilder()}
232
- </div>
233
- </div>
202
+ <ActionBuilderField
203
+ slug={slug}
204
+ value={callbackPayload}
205
+ onChange={handleChange}
206
+ contentMap={fullContentMapStore.get()}
207
+ label="Action Configuration"
208
+ />
234
209
  </div>
235
210
  </div>
236
211
  </div>
@@ -1,11 +1,12 @@
1
- import { useState, useCallback, useMemo } from 'react';
2
- import { Combobox } from '@ark-ui/react';
1
+ import { useState, useCallback, useMemo, useRef } from 'react';
2
+ import { Combobox, Portal } from '@ark-ui/react';
3
3
  import { createListCollection } from '@ark-ui/react/collection';
4
4
  import ChevronUpDownIcon from '@heroicons/react/24/outline/ChevronUpDownIcon';
5
5
  import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
6
6
  import { settingsPanelStore } from '@/stores/storykeep';
7
7
  import { tailwindClasses } from '@/utils/compositor/tailwindClasses';
8
8
  import { isMarkdownPaneFragmentNode } from '@/utils/compositor/typeGuards';
9
+ import { useDropdownDirection } from '@/utils/helpers';
9
10
  import type { BasePanelProps, PaneFragmentNode } from '@/types/compositorTypes';
10
11
 
11
12
  const RECOMMENDED_STYLES = [
@@ -57,6 +58,8 @@ const StyleParentPanelAdd = ({ node, layer }: BasePanelProps) => {
57
58
  const [query, setQuery] = useState('');
58
59
  const [showAdvanced, setShowAdvanced] = useState(false);
59
60
  const [selectedStyle, setSelectedStyle] = useState<string | null>(null);
61
+ const comboboxRef = useRef<HTMLDivElement>(null);
62
+ const { openAbove } = useDropdownDirection(comboboxRef);
60
63
 
61
64
  const paneFragmentNode = node as PaneFragmentNode | null;
62
65
 
@@ -174,7 +177,7 @@ const StyleParentPanelAdd = ({ node, layer }: BasePanelProps) => {
174
177
  `;
175
178
 
176
179
  return (
177
- <div className="min-h-[400px] max-w-md space-y-4">
180
+ <div className="max-w-md space-y-4">
178
181
  <style>{comboboxItemStyles}</style>
179
182
 
180
183
  <div className="flex flex-row flex-nowrap justify-between">
@@ -209,46 +212,57 @@ const StyleParentPanelAdd = ({ node, layer }: BasePanelProps) => {
209
212
  loopFocus={true}
210
213
  openOnKeyPress={true}
211
214
  composite={true}
215
+ positioning={{
216
+ placement: openAbove ? 'top' : 'bottom',
217
+ gutter: 4,
218
+ sameWidth: true,
219
+ }}
212
220
  >
213
- <div className="relative">
214
- <Combobox.Input
215
- className="border-mydarkgrey focus:border-myblue focus:ring-myblue w-full rounded-md py-2 pl-3 pr-10 text-xl shadow-sm"
216
- placeholder="Search styles..."
217
- autoComplete="off"
218
- />
219
- <Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
220
- <ChevronUpDownIcon
221
- className="text-mydarkgrey h-5 w-5"
222
- aria-hidden="true"
221
+ <Combobox.Control ref={comboboxRef}>
222
+ <div className="relative">
223
+ <Combobox.Input
224
+ className="border-mydarkgrey focus:border-myblue focus:ring-myblue w-full rounded-md py-2 pl-3 pr-10 text-xl shadow-sm"
225
+ placeholder="Search styles..."
226
+ autoComplete="off"
223
227
  />
224
- </Combobox.Trigger>
225
- </div>
228
+ <Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
229
+ <ChevronUpDownIcon
230
+ className="text-mydarkgrey h-5 w-5"
231
+ aria-hidden="true"
232
+ />
233
+ </Combobox.Trigger>
234
+ </div>
235
+ </Combobox.Control>
226
236
 
227
- <Combobox.Content className="absolute z-50 mt-1 max-h-64 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
228
- {collection.items.length === 0 ? (
229
- <div className="text-mydarkgrey relative cursor-default select-none px-4 py-2">
230
- Nothing found.
231
- </div>
232
- ) : (
233
- collection.items.map((style) => (
234
- <Combobox.Item
235
- key={style.key}
236
- item={style}
237
- className="style-item relative cursor-default select-none py-2 pl-10 pr-4 text-black"
238
- >
239
- <span className="block truncate">
240
- {style.title}
241
- <span className="ml-2 text-sm opacity-60">
242
- {style.className}
243
- </span>
244
- </span>
245
- <span className="style-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
246
- <CheckIcon className="h-5 w-5" aria-hidden="true" />
247
- </span>
248
- </Combobox.Item>
249
- ))
250
- )}
251
- </Combobox.Content>
237
+ <Portal>
238
+ <Combobox.Positioner style={{ zIndex: 1002 }}>
239
+ <Combobox.Content className="max-h-64 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
240
+ {collection.items.length === 0 ? (
241
+ <div className="text-mydarkgrey relative cursor-default select-none px-4 py-2">
242
+ Nothing found.
243
+ </div>
244
+ ) : (
245
+ collection.items.map((style) => (
246
+ <Combobox.Item
247
+ key={style.key}
248
+ item={style}
249
+ className="style-item relative cursor-default select-none py-2 pl-10 pr-4 text-black"
250
+ >
251
+ <span className="block truncate">
252
+ {style.title}
253
+ <span className="ml-2 text-sm opacity-60">
254
+ {style.className}
255
+ </span>
256
+ </span>
257
+ <span className="style-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
258
+ <CheckIcon className="h-5 w-5" aria-hidden="true" />
259
+ </span>
260
+ </Combobox.Item>
261
+ ))
262
+ )}
263
+ </Combobox.Content>
264
+ </Combobox.Positioner>
265
+ </Portal>
252
266
  </Combobox.Root>
253
267
  </div>
254
268
 
@@ -4,7 +4,8 @@ import { settingsPanelStore } from '@/stores/storykeep';
4
4
  import { StylesMemory } from '@/components/edit/state/StylesMemory';
5
5
  import SelectedTailwindClass from '@/components/fields/SelectedTailwindClass';
6
6
  import { isMarkdownPaneFragmentNode } from '@/utils/compositor/typeGuards';
7
- import { widgetMeta } from '@/constants';
7
+ import { regexpHook, widgetMeta } from '@/constants';
8
+ import { getCtx } from '@/stores/nodes';
8
9
  import type {
9
10
  FlatNode,
10
11
  MarkdownPaneFragmentNode,
@@ -41,8 +42,6 @@ const StyleWidgetPanel = ({
41
42
  const outerOverrideClasses = outerContainerNode.overrideClasses;
42
43
 
43
44
  // Extract the widget type from the node's copy
44
- const regexpHook =
45
- /^(identifyAs|youtube|bunny|bunnyContext|toggle|resource|belief|signup)\((.*)\)$/;
46
45
  const hookMatch = node.copy?.match(regexpHook);
47
46
  const widgetId = hookMatch ? hookMatch[1] : 'unknown';
48
47
  const widgetName = widgetMeta[widgetId]?.title || `Widget`;
@@ -271,6 +270,7 @@ const StyleWidgetPanel = ({
271
270
  };
272
271
 
273
272
  const handleWidgetConfig = () => {
273
+ getCtx().toolModeValStore.set({ value: 'styles' });
274
274
  settingsPanelStore.set({
275
275
  action: `style-code-config`,
276
276
  nodeId: node.id,