astro-tractstack 2.0.0-rc.61 → 2.0.0-rc.63

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 (28) hide show
  1. package/dist/index.js +12 -0
  2. package/package.json +1 -1
  3. package/templates/src/client/sse.js +34 -16
  4. package/templates/src/components/Menu.tsx +192 -52
  5. package/templates/src/components/compositor/Node.tsx +1 -4
  6. package/templates/src/components/compositor/nodes/Widget.tsx +15 -1
  7. package/templates/src/components/edit/PanelSwitch.tsx +2 -1
  8. package/templates/src/components/edit/SettingsPanel.tsx +1 -1
  9. package/templates/src/components/edit/ToolBar.tsx +1 -28
  10. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
  11. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +1 -3
  12. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +12 -5
  13. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +604 -0
  14. package/templates/src/components/fields/ColorPickerCombo.tsx +4 -14
  15. package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
  16. package/templates/src/components/form/ActionBuilderField.tsx +289 -89
  17. package/templates/src/constants.ts +2120 -15
  18. package/templates/src/layouts/Layout.astro +5 -0
  19. package/templates/src/stores/nodes.ts +0 -51
  20. package/templates/src/types/compositorTypes.ts +5 -0
  21. package/templates/src/utils/actions/lispLexer.ts +2 -2
  22. package/templates/src/utils/actions/preParse_Action.ts +3 -0
  23. package/templates/src/utils/api/menuHelpers.ts +2 -2
  24. package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
  25. package/templates/src/utils/compositor/allowInsert.ts +5 -3
  26. package/templates/src/utils/compositor/nodesHelper.ts +4 -0
  27. package/templates/src/utils/compositor/typeGuards.ts +1 -0
  28. package/utils/inject-files.ts +12 -0
package/dist/index.js CHANGED
@@ -229,6 +229,12 @@ async function w(t, e, c) {
229
229
  src: t("../templates/src/components/edit/widgets/BeliefWidget.tsx"),
230
230
  dest: "src/components/edit/widgets/BeliefWidget.tsx"
231
231
  },
232
+ {
233
+ src: t(
234
+ "../templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx"
235
+ ),
236
+ dest: "src/components/edit/widgets/InteractiveDisclosureWidget.tsx"
237
+ },
232
238
  {
233
239
  src: t(
234
240
  "../templates/src/components/edit/widgets/IdentifyAsWidget.tsx"
@@ -988,6 +994,12 @@ async function w(t, e, c) {
988
994
  src: t("../templates/src/components/form/ActionBuilderField.tsx"),
989
995
  dest: "src/components/form/ActionBuilderField.tsx"
990
996
  },
997
+ {
998
+ src: t(
999
+ "../templates/src/components/form/ActionBuilderBeliefSelector.tsx"
1000
+ ),
1001
+ dest: "src/components/form/ActionBuilderBeliefSelector.tsx"
1002
+ },
991
1003
  {
992
1004
  src: t("../templates/src/components/form/MagicPathBuilder.tsx"),
993
1005
  dest: "src/components/form/MagicPathBuilder.tsx"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.0-rc.61",
3
+ "version": "2.0.0-rc.63",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -481,23 +481,41 @@ function processStoryfragmentUpdate(update) {
481
481
  log(`📊 Refresh summary: ${refreshedCount} successful, ${errorCount} failed`);
482
482
 
483
483
  if (update.gotoPaneId) {
484
- const targetElement = document.getElementById(`pane-${update.gotoPaneId}`);
485
- if (targetElement) {
486
- log(`🔍 Scrolling to target pane: ${update.gotoPaneId}`);
487
- try {
488
- targetElement.scrollIntoView({ behavior: 'smooth' });
489
- log('✅ Scroll completed successfully');
490
- } catch (error) {
491
- log('❌ Scroll failed:', error);
484
+ // Wait a brief moment for the DOM to update and the element to become visible.
485
+ setTimeout(() => {
486
+ const targetElement = document.getElementById(
487
+ `pane-${update.gotoPaneId}`
488
+ );
489
+ if (targetElement) {
490
+ log(`🔍 Smart scrolling to target pane: ${update.gotoPaneId}`);
491
+ try {
492
+ const elementRect = targetElement.getBoundingClientRect();
493
+ const viewportHeight = window.innerHeight;
494
+
495
+ // If the element is taller than the viewport, just scroll to the top of it.
496
+ if (elementRect.height > viewportHeight) {
497
+ targetElement.scrollIntoView({
498
+ behavior: 'smooth',
499
+ block: 'start',
500
+ });
501
+ log('✅ Scroll completed (long element - align to top).');
502
+ } else {
503
+ // Otherwise, center it in the viewport.
504
+ targetElement.scrollIntoView({
505
+ behavior: 'smooth',
506
+ block: 'center',
507
+ });
508
+ log('✅ Scroll completed (short element - align to center).');
509
+ }
510
+ } catch (error) {
511
+ log('❌ Smart scroll failed:', error);
512
+ }
513
+ } else {
514
+ log(
515
+ `⚠️ Target pane element not found after delay: pane-${update.gotoPaneId}`
516
+ );
492
517
  }
493
- } else {
494
- log(`⚠️ Target pane element not found: pane-${update.gotoPaneId}`, {
495
- expectedId: `pane-${update.gotoPaneId}`,
496
- availablePaneElements: Array.from(
497
- document.querySelectorAll('[id^="pane-"]')
498
- ).map((el) => el.id),
499
- });
500
- }
518
+ }, 100);
501
519
  }
502
520
 
503
521
  log('🔄 === UPDATE PROCESSING COMPLETE ===');
@@ -2,7 +2,8 @@ import { Menu } from '@ark-ui/react';
2
2
  import { Portal } from '@ark-ui/react/portal';
3
3
  import ChevronDownIcon from '@heroicons/react/20/solid/ChevronDownIcon';
4
4
  import { lispLexer } from '@/utils/actions/lispLexer';
5
- import { preParseAction } from '@/utils/actions//preParse_Action';
5
+ import { preParseAction } from '@/utils/actions/preParse_Action';
6
+ import type { LispToken } from '@/types/compositorTypes';
6
7
 
7
8
  // CSS to style the menu items with hover and selection states
8
9
  const menuStyles = `
@@ -46,9 +47,10 @@ interface MenuDatum {
46
47
  optionsPayload: MenuLink[];
47
48
  }
48
49
 
49
- interface MenuLinkDatum extends MenuLink {
50
- to: string;
51
- internal: boolean;
50
+ interface ProcessedMenuLinkDatum extends MenuLink {
51
+ renderAs: 'a' | 'button' | 'span';
52
+ href?: string;
53
+ htmxVals?: string;
52
54
  }
53
55
 
54
56
  interface MenuProps {
@@ -62,7 +64,77 @@ const MenuComponent = (props: MenuProps) => {
62
64
  const { payload, slug, isContext, brandConfig } = props;
63
65
  const thisPayload = payload.optionsPayload;
64
66
 
65
- // Process featured and additional links
67
+ // Helper function to process menu links - MODIFIED to build the correct hx-vals payload
68
+ function processMenuLink(e: MenuLink): ProcessedMenuLinkDatum {
69
+ const item = { ...e } as ProcessedMenuLinkDatum;
70
+ const actionLisp = item.actionLisp?.trim();
71
+
72
+ if (!actionLisp) {
73
+ item.renderAs = 'span';
74
+ return item;
75
+ }
76
+
77
+ try {
78
+ if (actionLisp.startsWith('(goto')) {
79
+ const tokens = lispLexer(actionLisp);
80
+ const to = preParseAction(tokens, slug, isContext, brandConfig);
81
+ item.renderAs = 'a';
82
+ item.href = to || '#';
83
+ return item;
84
+ }
85
+
86
+ if (
87
+ actionLisp.startsWith('(declare') ||
88
+ actionLisp.startsWith('(identifyAs')
89
+ ) {
90
+ const tokens = lispLexer(actionLisp);
91
+ const commandExpression = (
92
+ tokens?.[0] as LispToken[]
93
+ )?.[0] as LispToken[];
94
+ const command = commandExpression?.[0] as string;
95
+ const parameters = commandExpression?.[1] as (string | number)[];
96
+ const beliefId = parameters?.[0];
97
+ const value = parameters?.[1];
98
+
99
+ if (command && beliefId !== undefined && value !== undefined) {
100
+ let hxValsMap: { [key: string]: string } = {};
101
+
102
+ // CORRECTED: Build the hx-vals payload to match server expectations.
103
+ if (command === 'declare') {
104
+ hxValsMap = {
105
+ beliefId: String(beliefId),
106
+ beliefType: 'Belief', // This was the missing required field.
107
+ beliefValue: String(value), // Key changed from beliefVerb to beliefValue.
108
+ };
109
+ } else if (command === 'identifyAs') {
110
+ hxValsMap = {
111
+ beliefId: String(beliefId),
112
+ beliefType: 'Belief', // This was the missing required field.
113
+ beliefVerb: 'IDENTIFY_AS', // This is specific to identifyAs.
114
+ beliefObject: String(value),
115
+ };
116
+ }
117
+
118
+ if (Object.keys(hxValsMap).length > 0) {
119
+ item.renderAs = 'button';
120
+ item.htmxVals = JSON.stringify(hxValsMap);
121
+ return item;
122
+ }
123
+ }
124
+ }
125
+ } catch (error) {
126
+ console.error(
127
+ `Failed to process menu item for action: ${actionLisp}`,
128
+ error
129
+ );
130
+ }
131
+
132
+ // Fallback for unknown commands or parsing failures
133
+ item.renderAs = 'span';
134
+ return item;
135
+ }
136
+
137
+ // Process featured and additional links using the modified helper
66
138
  const featuredLinks = thisPayload
67
139
  .filter((e: MenuLink) => e.featured)
68
140
  .map(processMenuLink);
@@ -70,19 +142,48 @@ const MenuComponent = (props: MenuProps) => {
70
142
  .filter((e: MenuLink) => !e.featured)
71
143
  .map(processMenuLink);
72
144
 
73
- // Helper function to process menu links
74
- function processMenuLink(e: MenuLink): MenuLinkDatum {
75
- const item = { ...e } as MenuLinkDatum;
76
- const thisPayload = lispLexer(e.actionLisp);
77
- const to = preParseAction(thisPayload, slug, isContext, brandConfig);
78
- if (typeof to === `string`) {
79
- item.to = to;
80
- item.internal = true;
81
- } else if (typeof to === `object`) {
82
- item.to = to[0];
145
+ // Helper component to render either a link or a button, avoiding repetition.
146
+ const InteractiveMenuItem = ({ item }: { item: ProcessedMenuLinkDatum }) => {
147
+ if (item.renderAs === 'button') {
148
+ return (
149
+ <button
150
+ type="button"
151
+ className="text-mydarkgrey focus:ring-myblue block text-2xl font-bold leading-6 hover:text-black hover:underline hover:decoration-dashed hover:decoration-4 hover:underline-offset-4 focus:text-black focus:outline-none focus:ring-2"
152
+ title={item.description}
153
+ aria-label={`${item.name} - ${item.description}`}
154
+ hx-post="/api/v1/state"
155
+ hx-swap="none"
156
+ hx-vals={item.htmxVals}
157
+ >
158
+ {item.name}
159
+ </button>
160
+ );
161
+ }
162
+
163
+ if (item.renderAs === 'a') {
164
+ return (
165
+ <a
166
+ href={item.href}
167
+ className="text-mydarkgrey focus:ring-myblue block text-2xl font-bold leading-6 hover:text-black hover:underline hover:decoration-dashed hover:decoration-4 hover:underline-offset-4 focus:text-black focus:outline-none focus:ring-2"
168
+ title={item.description}
169
+ aria-label={`${item.name} - ${item.description}`}
170
+ >
171
+ {item.name}
172
+ </a>
173
+ );
83
174
  }
84
- return item;
85
- }
175
+
176
+ // Fallback for 'span'
177
+ return (
178
+ <span
179
+ className="text-mydarkgrey block text-2xl font-bold leading-6 opacity-50"
180
+ title={item.description}
181
+ aria-label={`${item.name} - ${item.description}`}
182
+ >
183
+ {item.name}
184
+ </span>
185
+ );
186
+ };
86
187
 
87
188
  return (
88
189
  <>
@@ -90,16 +191,9 @@ const MenuComponent = (props: MenuProps) => {
90
191
 
91
192
  {/* Desktop Navigation */}
92
193
  <nav className="font-action ml-6 hidden flex-wrap items-center justify-end space-x-3 md:flex md:space-x-6">
93
- {featuredLinks.map((item: MenuLinkDatum) => (
194
+ {featuredLinks.map((item: ProcessedMenuLinkDatum) => (
94
195
  <div key={item.name} className="relative py-1.5">
95
- <a
96
- href={item.to}
97
- className="text-mydarkgrey focus:ring-myblue block text-2xl font-bold leading-6 hover:text-black hover:underline hover:decoration-dashed hover:decoration-4 hover:underline-offset-4 focus:text-black focus:outline-none focus:ring-2"
98
- title={item.description}
99
- aria-label={`${item.name} - ${item.description}`}
100
- >
101
- {item.name}
102
- </a>
196
+ <InteractiveMenuItem item={item} />
103
197
  </div>
104
198
  ))}
105
199
  </nav>
@@ -122,21 +216,42 @@ const MenuComponent = (props: MenuProps) => {
122
216
  <div className="text-md ring-mydarkgrey/5 flex-auto overflow-hidden rounded-3xl bg-white p-4 leading-6 shadow-lg ring-1">
123
217
  {/* Featured Links Section */}
124
218
  <div className="px-8">
125
- {featuredLinks.map((item: MenuLinkDatum) => (
219
+ {featuredLinks.map((item: ProcessedMenuLinkDatum) => (
126
220
  <Menu.Item
127
221
  key={item.name}
128
222
  value={item.name}
129
223
  className="menu-item hover:bg-mygreen/20 group relative flex gap-x-6 rounded-lg p-4"
130
224
  >
131
225
  <div>
132
- <a
133
- href={item.to}
134
- className="font-action text-myblack text-xl hover:text-black focus:text-black focus:outline-none"
135
- aria-label={`${item.name} - ${item.description}`}
136
- >
137
- {item.name}
138
- <span className="absolute inset-0" />
139
- </a>
226
+ {item.renderAs === 'button' ? (
227
+ <button
228
+ type="button"
229
+ className="font-action text-myblack text-xl hover:text-black focus:text-black focus:outline-none"
230
+ aria-label={`${item.name} - ${item.description}`}
231
+ hx-post="/api/v1/state"
232
+ hx-swap="none"
233
+ hx-vals={item.htmxVals}
234
+ >
235
+ {item.name}
236
+ <span className="absolute inset-0" />
237
+ </button>
238
+ ) : item.renderAs === 'a' ? (
239
+ <a
240
+ href={item.href}
241
+ className="font-action text-myblack text-xl hover:text-black focus:text-black focus:outline-none"
242
+ aria-label={`${item.name} - ${item.description}`}
243
+ >
244
+ {item.name}
245
+ <span className="absolute inset-0" />
246
+ </a>
247
+ ) : (
248
+ <span
249
+ className="font-action text-myblack text-xl opacity-50"
250
+ aria-label={`${item.name} - ${item.description}`}
251
+ >
252
+ {item.name}
253
+ </span>
254
+ )}
140
255
  <p className="text-mydarkgrey mt-1">
141
256
  {item.description}
142
257
  </p>
@@ -161,24 +276,49 @@ const MenuComponent = (props: MenuProps) => {
161
276
  className="mt-6 space-y-6"
162
277
  aria-labelledby="additional-links-heading"
163
278
  >
164
- {additionalLinks.map((item: MenuLinkDatum) => (
165
- <li key={item.name} className="relative">
166
- <Menu.Item
167
- value={item.name}
168
- className="menu-item block w-full text-left"
169
- >
170
- <a
171
- href={item.to}
172
- className="text-mydarkgrey block truncate rounded p-2 text-sm font-bold leading-6 hover:text-black focus:text-black focus:underline focus:outline-none"
173
- title={item.description}
174
- aria-label={`${item.name} - ${item.description}`}
279
+ {additionalLinks.map(
280
+ (item: ProcessedMenuLinkDatum) => (
281
+ <li key={item.name} className="relative">
282
+ <Menu.Item
283
+ value={item.name}
284
+ className="menu-item block w-full text-left"
175
285
  >
176
- {item.name}
177
- <span className="absolute inset-0" />
178
- </a>
179
- </Menu.Item>
180
- </li>
181
- ))}
286
+ {item.renderAs === 'button' ? (
287
+ <button
288
+ type="button"
289
+ className="text-mydarkgrey block truncate rounded p-2 text-sm font-bold leading-6 hover:text-black focus:text-black focus:underline focus:outline-none"
290
+ title={item.description}
291
+ aria-label={`${item.name} - ${item.description}`}
292
+ hx-post="/api/v1/state"
293
+ hx-swap="none"
294
+ hx-vals={item.htmxVals}
295
+ >
296
+ {item.name}
297
+ <span className="absolute inset-0" />
298
+ </button>
299
+ ) : item.renderAs === 'a' ? (
300
+ <a
301
+ href={item.href}
302
+ className="text-mydarkgrey block truncate rounded p-2 text-sm font-bold leading-6 hover:text-black focus:text-black focus:underline focus:outline-none"
303
+ title={item.description}
304
+ aria-label={`${item.name} - ${item.description}`}
305
+ >
306
+ {item.name}
307
+ <span className="absolute inset-0" />
308
+ </a>
309
+ ) : (
310
+ <span
311
+ className="text-mydarkgrey block truncate rounded p-2 text-sm font-bold leading-6 opacity-50"
312
+ title={item.description}
313
+ aria-label={`${item.name} - ${item.description}`}
314
+ >
315
+ {item.name}
316
+ </span>
317
+ )}
318
+ </Menu.Item>
319
+ </li>
320
+ )
321
+ )}
182
322
  </ul>
183
323
  </div>
184
324
  )}
@@ -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
 
@@ -83,8 +83,22 @@ const getWidgetElement = (
83
83
  </div>
84
84
  ) : null;
85
85
 
86
+ case 'interactiveDisclosure':
87
+ return (
88
+ <div className={`${classNames} pointer-events-none`}>
89
+ <div className="rounded-md border-2 border-dashed border-gray-300 bg-gray-50 p-4 text-center">
90
+ <p className="text-sm font-bold text-gray-700">
91
+ Interactive Disclosure
92
+ </p>
93
+ <p className="mt-1 text-xs text-gray-500">
94
+ Belief Trigger: <code className="font-bold">{value1}</code>
95
+ </p>
96
+ </div>
97
+ </div>
98
+ );
99
+
86
100
  default:
87
- return null;
101
+ return <div>Widget {hook} not found.</div>;
88
102
  }
89
103
  };
90
104
 
@@ -313,7 +313,8 @@ const PanelSwitch = ({
313
313
  break;
314
314
 
315
315
  case 'style-code-config':
316
- if (clickedNode) return <StyleWidgetConfigPanel node={clickedNode} />;
316
+ if (clickedNode)
317
+ return <StyleWidgetConfigPanel node={clickedNode} config={config} />;
317
318
  break;
318
319
 
319
320
  case 'style-code-add':
@@ -33,7 +33,7 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
33
33
  animation: window.matchMedia(
34
34
  '(prefers-reduced-motion: no-preference)'
35
35
  ).matches
36
- ? 'fadeInFromHalf 150ms ease-in'
36
+ ? 'fadeInFromHalf 450ms ease-in'
37
37
  : 'none',
38
38
  '--fade-start': '0.5',
39
39
  '--fade-end': '1',
@@ -2,37 +2,10 @@ import { useStore } from '@nanostores/react';
2
2
  import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
3
3
  import { getCtx } from '@/stores/nodes';
4
4
  import { toggleSettingsPanel } from '@/stores/storykeep';
5
+ import { toolAddModeTitles, toolAddModes } from '@/constants';
5
6
 
6
7
  import type { ToolAddMode } from '@/types/compositorTypes';
7
8
 
8
- const toolAddModeTitles: Record<ToolAddMode, string> = {
9
- p: 'Paragraph',
10
- h2: 'Heading 2',
11
- h3: 'Heading 3',
12
- h4: 'Heading 4',
13
- img: 'Image',
14
- signup: 'Email Sign-up Widget',
15
- yt: 'YouTube Video',
16
- bunny: 'Bunny Video',
17
- belief: 'Belief Select',
18
- identify: 'Identity As',
19
- toggle: 'Toggle Belief',
20
- };
21
-
22
- const toolAddModes: ToolAddMode[] = [
23
- 'p',
24
- 'h2',
25
- 'h3',
26
- 'h4',
27
- 'img',
28
- 'signup',
29
- 'yt',
30
- 'bunny',
31
- 'belief',
32
- 'identify',
33
- 'toggle',
34
- ];
35
-
36
9
  const AddElementsPanel = ({
37
10
  currentToolAddMode,
38
11
  }: {