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
@@ -340,6 +340,18 @@ export class NodesContext {
340
340
  handleClickEventDefault(node, dblClick, this.clickedParentLayer.get());
341
341
  break;
342
342
  case `text`:
343
+ if (
344
+ node.nodeType === 'TagElement' &&
345
+ 'tagName' in node &&
346
+ (node.tagName === 'a' || node.tagName === 'button')
347
+ ) {
348
+ this.toolModeValStore.set({ value: 'styles' });
349
+ handleClickEventDefault(
350
+ node,
351
+ dblClick,
352
+ this.clickedParentLayer.get()
353
+ );
354
+ }
343
355
  if (dblClick && ![`Markdown`].includes(node.nodeType)) {
344
356
  this.toolModeValStore.set({ value: 'styles' });
345
357
  handleClickEventDefault(
@@ -544,7 +556,6 @@ export class NodesContext {
544
556
  tagNames.length > offset
545
557
  ? allowInsert(node, node.tagName as Tag, tagName, tagNames[offset + 1])
546
558
  : allowInsert(node, node.tagName as Tag, tagName);
547
-
548
559
  return { allowInsertBefore, allowInsertAfter };
549
560
  }
550
561
 
@@ -1063,56 +1074,6 @@ export class NodesContext {
1063
1074
  return '';
1064
1075
  }
1065
1076
 
1066
- //addPaneToStoryFragment(
1067
- // nodeId: string,
1068
- // pane: PaneNode,
1069
- // location: 'before' | 'after'
1070
- ///) {
1071
- // const node = this.allNodes.get().get(nodeId) as BaseNode;
1072
- // if (
1073
- // !node ||
1074
- // (node.nodeType !== 'StoryFragment' && node.nodeType !== 'Pane')
1075
- // ) {
1076
- // return;
1077
- // }
1078
-
1079
- // pane.id = ulid();
1080
- // this.addNode(pane);
1081
-
1082
- // if (node.nodeType === 'Pane') {
1083
- // const storyFragmentId = this.getClosestNodeTypeFromId(
1084
- // nodeId,
1085
- // 'StoryFragment'
1086
- // );
1087
- // const storyFragment = this.allNodes
1088
- // .get()
1089
- // .get(storyFragmentId) as StoryFragmentNode;
1090
- // if (storyFragment) {
1091
- // pane.parentId = storyFragmentId;
1092
- // const originalPaneIndex = storyFragment.paneIds.indexOf(pane.parentId);
1093
- // let insertIdx = -1;
1094
- // if (location === 'before')
1095
- // insertIdx = Math.max(0, originalPaneIndex - 1);
1096
- // else
1097
- // insertIdx = Math.min(
1098
- // storyFragment.paneIds.length - 1,
1099
- // originalPaneIndex + 1
1100
- // );
1101
- // storyFragment.paneIds.splice(insertIdx, 0, pane.id);
1102
- // }
1103
- // } else if (node.nodeType !== 'StoryFragment') {
1104
- // const storyFragment = node as StoryFragmentNode;
1105
- // if (storyFragment) {
1106
- // pane.parentId = node.id;
1107
- // if (location === 'after') {
1108
- // storyFragment.paneIds.push(pane.id);
1109
- // } else {
1110
- // storyFragment.paneIds.unshift(pane.id);
1111
- // }
1112
- // }
1113
- // }
1114
- //}
1115
-
1116
1077
  addContextTemplatePane(ownerId: string, pane: TemplatePane) {
1117
1078
  const ownerNode = this.allNodes.get().get(ownerId);
1118
1079
  if (ownerNode?.nodeType === 'Pane') {
@@ -2178,9 +2139,10 @@ export class NodesContext {
2178
2139
  getAllBunnyVideoInfo(): { url: string; title: string; videoId: string }[] {
2179
2140
  const results: { url: string; title: string; videoId: string }[] = [];
2180
2141
  const processedVideoIds = new Set<string>();
2181
-
2182
- // Find panes with bunny-video code hook
2183
2142
  const allNodes = Array.from(this.allNodes.get().values());
2143
+ const BUNNY_EMBED_BASE_URL = 'https://iframe.mediadelivery.net/embed/';
2144
+
2145
+ // Process pane-level bunny videos (which use full URLs)
2184
2146
  const paneNodes = allNodes.filter(
2185
2147
  (node) =>
2186
2148
  node.nodeType === 'Pane' &&
@@ -2188,7 +2150,6 @@ export class NodesContext {
2188
2150
  node.codeHookTarget === 'bunny-video'
2189
2151
  ) as PaneNode[];
2190
2152
 
2191
- // Process pane-level bunny videos
2192
2153
  for (const paneNode of paneNodes) {
2193
2154
  try {
2194
2155
  if (
@@ -2231,7 +2192,7 @@ export class NodesContext {
2231
2192
  }
2232
2193
  }
2233
2194
 
2234
- // Find inline bunny widgets
2195
+ // Process inline bunny widgets (which use ID/GUID fragments)
2235
2196
  const codeNodes = allNodes.filter(
2236
2197
  (node) =>
2237
2198
  node.nodeType === 'TagElement' &&
@@ -2243,47 +2204,26 @@ export class NodesContext {
2243
2204
  node.copy.includes('bunny(')
2244
2205
  ) as FlatNode[];
2245
2206
 
2246
- // Process inline widgets
2247
2207
  for (const codeNode of codeNodes) {
2248
2208
  if (
2249
2209
  Array.isArray(codeNode.codeHookParams) &&
2250
- codeNode.codeHookParams.length >= 2
2210
+ codeNode.codeHookParams.length > 0
2251
2211
  ) {
2252
- const urlParam = codeNode.codeHookParams[0];
2253
- const titleParam = codeNode.codeHookParams[1];
2254
-
2255
- const url = Array.isArray(urlParam)
2256
- ? urlParam[0]
2257
- : String(urlParam || '');
2258
- const title = Array.isArray(titleParam)
2259
- ? titleParam[0]
2260
- : String(titleParam || 'Untitled Video');
2261
-
2262
- if (url) {
2263
- let videoId = '';
2264
- try {
2265
- const urlObj = new URL(url);
2266
- if (
2267
- urlObj.hostname === 'iframe.mediadelivery.net' &&
2268
- urlObj.pathname.startsWith('/embed/')
2269
- ) {
2270
- const pathParts = urlObj.pathname.split('/');
2271
- if (pathParts.length >= 4) {
2272
- videoId = `${pathParts[2]}/${pathParts[3]}`;
2273
- }
2274
- }
2275
- } catch (error) {
2276
- console.error('Error extracting video ID from URL:', error);
2277
- }
2278
-
2279
- if (videoId && !processedVideoIds.has(videoId)) {
2280
- results.push({ url, title, videoId });
2212
+ const videoId = String(codeNode.codeHookParams[0] || '');
2213
+ const title = String(codeNode.codeHookParams[1] || 'Untitled Video');
2214
+
2215
+ if (videoId && /^\d+\/[a-f0-9\-]{36}$/.test(videoId)) {
2216
+ if (!processedVideoIds.has(videoId)) {
2217
+ results.push({
2218
+ url: `${BUNNY_EMBED_BASE_URL}${videoId}`,
2219
+ title,
2220
+ videoId,
2221
+ });
2281
2222
  processedVideoIds.add(videoId);
2282
2223
  }
2283
2224
  }
2284
2225
  }
2285
2226
  }
2286
-
2287
2227
  return results;
2288
2228
  }
2289
2229
 
@@ -2316,10 +2256,41 @@ export class NodesContext {
2316
2256
 
2317
2257
  getDirtyNodesClassData(): { dirtyPaneIds: string[]; classes: string[] } {
2318
2258
  const dirtyNodes = this.getDirtyNodes();
2259
+
2319
2260
  const dirtyPaneIds = dirtyNodes
2320
2261
  .filter((node) => node.nodeType === 'Pane')
2321
2262
  .map((node) => node.id);
2322
- const classes = extractClassesFromNodes(dirtyNodes);
2263
+
2264
+ // Collect all nodes that need class extraction
2265
+ const allNodesToExtract: BaseNode[] = [];
2266
+
2267
+ // Find root dirty nodes (dirty nodes whose parents are NOT dirty)
2268
+ const dirtyNodeIds = new Set(dirtyNodes.map((n) => n.id));
2269
+ const rootDirtyNodes = dirtyNodes.filter(
2270
+ (node) => !node.parentId || !dirtyNodeIds.has(node.parentId)
2271
+ );
2272
+
2273
+ // For each root dirty node, traverse all descendants
2274
+ rootDirtyNodes.forEach((rootNode) => {
2275
+ // Add the root node itself
2276
+ allNodesToExtract.push(rootNode);
2277
+
2278
+ // Traverse all descendants using breadth-first
2279
+ const queue = [...this.getChildNodeIDs(rootNode.id)];
2280
+ while (queue.length > 0) {
2281
+ const currentId = queue.shift();
2282
+ if (!currentId) continue;
2283
+
2284
+ const currentNode = this.allNodes.get().get(currentId);
2285
+ if (currentNode) {
2286
+ allNodesToExtract.push(currentNode);
2287
+ const childrenIds = this.getChildNodeIDs(currentId);
2288
+ queue.push(...childrenIds);
2289
+ }
2290
+ }
2291
+ });
2292
+
2293
+ const classes = extractClassesFromNodes(allNodesToExtract);
2323
2294
 
2324
2295
  return { dirtyPaneIds, classes };
2325
2296
  }
@@ -161,11 +161,10 @@ const pollingState = new Map<
161
161
  }
162
162
  >();
163
163
 
164
- // Constants for polling configuration
165
- const MAX_POLLING_ATTEMPTS = 10;
166
- const MAX_POLLING_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
167
- const BASE_POLLING_INTERVAL = 2000; // 2 seconds base interval
168
- const MAX_POLLING_INTERVAL = 32000; // 32 seconds max interval
164
+ const MAX_POLLING_ATTEMPTS = 25;
165
+ const MAX_POLLING_DURATION = 10 * 60 * 1000; // 10 minutes
166
+ const BASE_POLLING_INTERVAL = 10000; // 10 seconds
167
+ const MAX_POLLING_INTERVAL = 30000; // 30 seconds
169
168
 
170
169
  const fetchingStates = new Map<string, boolean>();
171
170
 
@@ -203,7 +202,7 @@ export async function loadOrphanAnalysis(): Promise<void> {
203
202
 
204
203
  updateTenantState(tenantId, {
205
204
  data,
206
- isLoading: false,
205
+ isLoading: data.status === 'loading', // Only stop loading if complete
207
206
  error: null,
208
207
  lastFetched: Date.now(),
209
208
  });
@@ -239,7 +238,7 @@ function startPolling(tenantId: string): void {
239
238
  lastAttemptTime: startTime,
240
239
  });
241
240
 
242
- // Start the first poll immediately
241
+ // Start the first poll
243
242
  scheduleNextPoll(tenantId);
244
243
  }
245
244
 
@@ -260,21 +259,21 @@ function scheduleNextPoll(tenantId: string): void {
260
259
  const elapsed = Date.now() - state.startTime;
261
260
  if (elapsed >= MAX_POLLING_DURATION) {
262
261
  console.warn(
263
- `Orphan analysis polling stopped: Maximum duration (${MAX_POLLING_DURATION}ms) exceeded for tenant ${tenantId}`
262
+ `Orphan analysis polling stopped: Maximum duration (${
263
+ MAX_POLLING_DURATION / 1000
264
+ }s) exceeded for tenant ${tenantId}`
264
265
  );
265
266
  handlePollingFailure(tenantId, 'Polling timeout exceeded');
266
267
  return;
267
268
  }
268
269
 
269
- // Calculate delay using exponential backoff for consecutive errors
270
- let delay = BASE_POLLING_INTERVAL;
271
- if (state.consecutiveErrors > 0) {
272
- // Exponential backoff: 2s → 4s → 8s → 16s → 32s (capped)
273
- delay = Math.min(
274
- BASE_POLLING_INTERVAL * Math.pow(2, state.consecutiveErrors),
275
- MAX_POLLING_INTERVAL
276
- );
277
- }
270
+ // This is more suitable for long-running jobs, as it spaces out requests
271
+ // even when the server responds successfully with a 'loading' status.
272
+ // Polling sequence: 10s → 20s → 30s → 30s...
273
+ const delay = Math.min(
274
+ BASE_POLLING_INTERVAL * Math.pow(2, state.attempts),
275
+ MAX_POLLING_INTERVAL
276
+ );
278
277
 
279
278
  // Schedule the next poll
280
279
  const timeoutId = setTimeout(() => executePoll(tenantId), delay);
@@ -304,6 +303,7 @@ async function executePoll(tenantId: string): Promise<void> {
304
303
 
305
304
  // Check if analysis is complete
306
305
  if (data.status === 'complete') {
306
+ updateTenantState(tenantId, { isLoading: false });
307
307
  stopPolling(tenantId);
308
308
  return;
309
309
  }
@@ -329,7 +329,6 @@ async function executePoll(tenantId: string): Promise<void> {
329
329
  error instanceof Error ? error.message : 'Unknown polling error';
330
330
  updateTenantState(tenantId, {
331
331
  error: `Polling error (attempt ${state.attempts}/${MAX_POLLING_ATTEMPTS}): ${errorMessage}`,
332
- lastFetched: Date.now(),
333
332
  });
334
333
 
335
334
  // Check if we should stop polling due to consecutive errors
@@ -344,7 +343,7 @@ async function executePoll(tenantId: string): Promise<void> {
344
343
  return;
345
344
  }
346
345
 
347
- // Schedule next poll with exponential backoff
346
+ // Schedule next poll (will use exponential backoff due to increased consecutiveErrors)
348
347
  scheduleNextPoll(tenantId);
349
348
  }
350
349
  }
@@ -357,8 +356,7 @@ function handlePollingFailure(tenantId: string, reason: string): void {
357
356
  // Update tenant state with final error
358
357
  updateTenantState(tenantId, {
359
358
  isLoading: false,
360
- error: `Orphan analysis polling failed: ${reason}. Please try refreshing the page or contact support if the issue persists.`,
361
- lastFetched: Date.now(),
359
+ error: `Orphan analysis failed: ${reason}. Please try refreshing the page.`,
362
360
  });
363
361
 
364
362
  // Clean up polling state
@@ -1,6 +1,7 @@
1
1
  import { atom, map } from 'nanostores';
2
2
  import { persistentAtom } from '@nanostores/persistent';
3
3
  import { handleSettingsPanelMobile } from '@/utils/layout';
4
+ import { getCtx, ROOT_NODE_NAME } from '@/stores/nodes';
4
5
  import type {
5
6
  FullContentMapItem,
6
7
  Theme,
@@ -28,6 +29,8 @@ export const preferredThemeStore = atom<Theme>('light');
28
29
  export const hasAssemblyAIStore = atom<boolean>(false);
29
30
  export const codehookMapStore = atom<string[]>([]);
30
31
 
32
+ export const pendingHomePageSlugStore = atom<string | null>(null);
33
+
31
34
  // Tool mode types
32
35
  export type ToolModeVal =
33
36
  | 'styles'
@@ -116,6 +119,10 @@ export const setViewportMode = (mode: ViewportKey) => {
116
119
  } else {
117
120
  viewportKeyStore.setKey('value', mode);
118
121
  }
122
+
123
+ // Notify root node to trigger coordinated re-render
124
+ const ctx = getCtx();
125
+ ctx.notifyNode(ROOT_NODE_NAME);
119
126
  };
120
127
 
121
128
  export const toggleShowAnalytics = () => {
@@ -1,5 +1,7 @@
1
1
  import type { BrandConfig } from './tractstack';
2
2
 
3
+ export type LispToken = string | number | LispToken[];
4
+
3
5
  export type ViewportKey = 'mobile' | 'tablet' | 'desktop' | 'auto';
4
6
  export type ViewportAuto = 'mobile' | 'tablet' | 'desktop';
5
7
  export type ToolModeVal =
@@ -21,6 +23,7 @@ export const toolAddModes = [
21
23
  'yt',
22
24
  'bunny',
23
25
  'belief',
26
+ 'interactiveDisclosure',
24
27
  'identify',
25
28
  'toggle',
26
29
  //"aside",
@@ -71,6 +74,7 @@ export type SettingsPanelSignal = {
71
74
  className?: string;
72
75
  minimized?: boolean;
73
76
  expanded?: boolean;
77
+ editLock?: number;
74
78
  };
75
79
 
76
80
  export interface OgImageParams {
@@ -135,6 +139,7 @@ export type Tag =
135
139
  | 'signup'
136
140
  | 'yt'
137
141
  | 'bunny'
142
+ | 'interactiveDisclosure'
138
143
  | 'belief'
139
144
  | 'identify'
140
145
  | 'toggle'
@@ -156,6 +161,7 @@ export const tagTitles: Record<Tag, string> = {
156
161
  signup: 'Email Signup Widget',
157
162
  yt: 'YouTube Widget',
158
163
  bunny: 'Bunny Video Widget',
164
+ interactiveDisclosure: 'Interactive Disclosure',
159
165
  belief: 'Belief Select Widget',
160
166
  toggle: 'Belief Toggle Widget',
161
167
  identify: 'Identify As Widget',
@@ -443,3 +443,20 @@ export interface BunnyPlayer {
443
443
  export interface PlayerJS {
444
444
  Player: new (elementId: string) => BunnyPlayer;
445
445
  }
446
+
447
+ export interface DiscoverySuggestion {
448
+ term: string;
449
+ type: 'EXACT' | 'TOPIC' | 'TEXT' | 'TITLE';
450
+ }
451
+
452
+ export interface FTSResult {
453
+ ID: string;
454
+ Relevance: number;
455
+ Term: string;
456
+ }
457
+
458
+ export interface CategorizedResults {
459
+ storyFragmentResults: FTSResult[];
460
+ contextPaneResults: FTSResult[];
461
+ resourceResults: FTSResult[];
462
+ }
@@ -1,3 +1,5 @@
1
+ import type { LispToken } from '@/types/compositorTypes';
2
+
1
3
  const DOUBLEQUOTE = [`"`];
2
4
  const BRACKETLEFT = `(`;
3
5
  const BRACKETRIGHT = `)`;
@@ -5,8 +7,6 @@ const SEMICOLON = `;`;
5
7
  const NEWLINE = `\n`;
6
8
  const WHITESPACE = [` `, `\n`, `\t`];
7
9
 
8
- type LispToken = string | number | LispToken[];
9
-
10
10
  export function lispLexer(
11
11
  payload: string = ``,
12
12
  inString: boolean = false
@@ -17,6 +17,9 @@ export const preParseAction = (
17
17
  //const parameterFour = (parameters && parameters[3]) || null;
18
18
 
19
19
  switch (command) {
20
+ case `declare`:
21
+ case `identifyAs`:
22
+ return ``;
20
23
  case `goto`:
21
24
  switch (parameterOne) {
22
25
  case `storykeep`:
@@ -4,9 +4,6 @@ import type {
4
4
  FieldErrors,
5
5
  } from '@/types/tractstack';
6
6
 
7
- /**
8
- * Convert backend BeliefNode to frontend BeliefNodeState
9
- */
10
7
  export function convertToLocalState(beliefNode: BeliefNode): BeliefNodeState {
11
8
  return {
12
9
  id: beliefNode.id,
@@ -17,9 +14,6 @@ export function convertToLocalState(beliefNode: BeliefNode): BeliefNodeState {
17
14
  };
18
15
  }
19
16
 
20
- /**
21
- * Convert frontend BeliefNodeState to backend BeliefNode format
22
- */
23
17
  export function convertToBackendFormat(state: BeliefNodeState): BeliefNode {
24
18
  return {
25
19
  id: state.id,
@@ -31,47 +25,45 @@ export function convertToBackendFormat(state: BeliefNodeState): BeliefNode {
31
25
  };
32
26
  }
33
27
 
34
- /**
35
- * Validate belief node state
36
- */
37
28
  export function validateBeliefNode(state: BeliefNodeState): FieldErrors {
38
29
  const errors: FieldErrors = {};
39
30
 
40
- // Validate title
41
31
  if (!state.title?.trim()) {
42
32
  errors.title = 'Title is required';
43
33
  }
44
34
 
45
- // Validate slug
46
35
  if (!state.slug?.trim()) {
47
36
  errors.slug = 'Slug is required';
37
+ } else {
38
+ const slugRegex = /^[a-zA-Z]+$/;
39
+ if (!slugRegex.test(state.slug)) {
40
+ errors.slug = 'Slug must contain only letters (a-z, A-Z)';
41
+ }
48
42
  }
49
43
 
50
- // Validate scale
51
44
  if (!state.scale?.trim()) {
52
45
  errors.scale = 'Scale is required';
53
46
  }
54
47
 
55
- // Validate custom values if scale is custom
56
48
  if (state.scale === 'custom') {
57
49
  if (!state.customValues || state.customValues.length === 0) {
58
50
  errors.customValues =
59
51
  'At least one custom value is required for custom scale';
60
52
  } else {
61
- state.customValues.forEach((value, index) => {
62
- if (!value?.trim()) {
63
- errors[`customValues.${index}`] = 'Custom value cannot be empty';
53
+ const valueRegex = /^[a-zA-Z]([a-zA-Z0-9?!]| (?=[a-zA-Z0-9?!]))*$/;
54
+ for (const value of state.customValues) {
55
+ if (value.trim() && !valueRegex.test(value)) {
56
+ errors.customValues =
57
+ 'Values must start with a letter, have no double or trailing spaces, and use valid characters.';
58
+ break;
64
59
  }
65
- });
60
+ }
66
61
  }
67
62
  }
68
63
 
69
64
  return errors;
70
65
  }
71
66
 
72
- /**
73
- * State interceptor for form state management
74
- */
75
67
  export function beliefStateIntercept(
76
68
  state: BeliefNodeState,
77
69
  field: keyof BeliefNodeState,
@@ -88,7 +80,6 @@ export function beliefStateIntercept(
88
80
  break;
89
81
  case 'scale':
90
82
  newState.scale = value || '';
91
- // Clear custom values when scale changes away from custom
92
83
  if (value !== 'custom') {
93
84
  newState.customValues = [];
94
85
  }
@@ -103,9 +94,6 @@ export function beliefStateIntercept(
103
94
  return newState;
104
95
  }
105
96
 
106
- /**
107
- * Add a new custom value to the state
108
- */
109
97
  export function addCustomValue(
110
98
  state: BeliefNodeState,
111
99
  value: string
@@ -118,9 +106,6 @@ export function addCustomValue(
118
106
  };
119
107
  }
120
108
 
121
- /**
122
- * Remove a custom value from the state
123
- */
124
109
  export function removeCustomValue(
125
110
  state: BeliefNodeState,
126
111
  index: number
@@ -131,9 +116,6 @@ export function removeCustomValue(
131
116
  };
132
117
  }
133
118
 
134
- /**
135
- * Update a specific custom value in the state
136
- */
137
119
  export function updateCustomValue(
138
120
  state: BeliefNodeState,
139
121
  index: number,
@@ -148,9 +130,6 @@ export function updateCustomValue(
148
130
  };
149
131
  }
150
132
 
151
- /**
152
- * Scale options for the belief form
153
- */
154
133
  export const SCALE_OPTIONS = [
155
134
  { value: 'likert', label: 'Likert Scale (1-5)' },
156
135
  { value: 'agreement', label: 'Agreement (Agree/Disagree)' },
@@ -160,9 +139,6 @@ export const SCALE_OPTIONS = [
160
139
  { value: 'custom', label: 'Custom Values' },
161
140
  ];
162
141
 
163
- /**
164
- * Get scale preview data for displaying scale options
165
- */
166
142
  export function getScalePreview(scale: string) {
167
143
  const scalePreviewData: {
168
144
  [key: string]: Array<{ id: number; name: string; color: string }>;
@@ -62,9 +62,9 @@ export function validateMenuNode(state: MenuNodeState): FieldErrors {
62
62
  errors[`menuLinks.${index}.actionLisp`] = 'Action is required';
63
63
  } else {
64
64
  // Basic ActionLisp validation
65
- if (!link.actionLisp.startsWith('(goto ')) {
65
+ if (!link.actionLisp.startsWith('(')) {
66
66
  errors[`menuLinks.${index}.actionLisp`] =
67
- 'Action must start with "(goto "';
67
+ 'Action must start with "("';
68
68
  }
69
69
  if (!link.actionLisp.endsWith('))')) {
70
70
  errors[`menuLinks.${index}.actionLisp`] = 'Action must end with "))"';
@@ -1,3 +1,9 @@
1
+ import type {
2
+ DiscoverySuggestion,
3
+ FTSResult,
4
+ CategorizedResults,
5
+ } from '@/types/tractstack';
6
+
1
7
  export interface APIResponse<T = any> {
2
8
  success: boolean;
3
9
  data?: T;
@@ -73,6 +79,11 @@ export class TractStackAPI {
73
79
  const defaultHeaders = {
74
80
  'Content-Type': 'application/json',
75
81
  'X-Tenant-ID': this.tenantId,
82
+ ...(typeof window !== 'undefined' &&
83
+ (window as any).TRACTSTACK_CONFIG?.sessionId && {
84
+ 'X-TractStack-Session-ID': (window as any).TRACTSTACK_CONFIG
85
+ .sessionId,
86
+ }),
76
87
  };
77
88
 
78
89
  try {
@@ -133,6 +144,21 @@ export class TractStackAPI {
133
144
  });
134
145
  }
135
146
 
147
+ async discover(
148
+ query: string
149
+ ): Promise<APIResponse<{ suggestions: DiscoverySuggestion[] }>> {
150
+ return this.get(`/api/v1/search/discover?q=${encodeURIComponent(query)}`);
151
+ }
152
+
153
+ async retrieve(
154
+ term: string,
155
+ isTopic: boolean = false
156
+ ): Promise<APIResponse<CategorizedResults>> {
157
+ return this.get(
158
+ `/api/v1/search/retrieve?term=${encodeURIComponent(term)}&topic=${isTopic}`
159
+ );
160
+ }
161
+
136
162
  async getContent(slug: string): Promise<APIResponse> {
137
163
  return this.get(`/api/v1/content/${slug}`);
138
164
  }
@@ -116,6 +116,13 @@ export const TemplateBeliefNode = {
116
116
  codeHookParams: ['BeliefTag', 'likert', 'prompt'],
117
117
  } as TemplateNode;
118
118
 
119
+ export const TemplateDisclosureNode = {
120
+ nodeType: 'TagElement',
121
+ tagName: 'code',
122
+ copy: 'interactiveDisclosure(BeliefTag)',
123
+ codeHookParams: ['BeliefTag'],
124
+ } as TemplateNode;
125
+
119
126
  export const TemplateBunnyNode = {
120
127
  nodeType: 'TagElement',
121
128
  tagName: 'code',
@@ -18,6 +18,7 @@ const allowInsert = (
18
18
  switch (tagNameNew) {
19
19
  case 'bunny':
20
20
  case 'yt':
21
+ case 'interactiveDisclosure':
21
22
  case 'belief':
22
23
  case 'toggle':
23
24
  case 'identify':
@@ -43,14 +44,14 @@ const allowInsert = (
43
44
 
44
45
  default:
45
46
  console.log(
46
- `1 miss on allowInsert: tagName:${tagName} tagNameNew:${tagNameNew} tagNameAdjacent:${tagNameAdjacent}`
47
+ `miss on allowInsert: tagName:${tagName} tagNameNew:${tagNameNew} tagNameAdjacent:${tagNameAdjacent}`
47
48
  );
48
49
  }
49
50
  break;
50
51
 
51
52
  default:
52
53
  console.log(
53
- `2 miss on allowInsert: tagName:${tagName} tagNameNew:${tagNameNew} tagNameAdjacent:${tagNameAdjacent}`
54
+ `miss on allowInsert: tagName:${tagName} tagNameNew:${tagNameNew} tagNameAdjacent:${tagNameAdjacent}`
54
55
  );
55
56
  }
56
57
  }
@@ -61,6 +62,7 @@ const allowInsert = (
61
62
  case 'bunny':
62
63
  case 'yt':
63
64
  case 'belief':
65
+ case 'interactiveDisclosure':
64
66
  case 'toggle':
65
67
  case 'identify':
66
68
  case 'signup':
@@ -83,7 +85,7 @@ const allowInsert = (
83
85
  return false;
84
86
  default:
85
87
  console.log(
86
- `3 miss on allowInsert: tagName:${tagName} tagNameNew:${tagNameNew} tagNameAdjacent:${tagNameAdjacent}`
88
+ `miss on allowInsert: tagName:${tagName} tagNameNew:${tagNameNew} tagNameAdjacent:${tagNameAdjacent}`
87
89
  );
88
90
  }
89
91
  break;