astro-tractstack 2.0.0-rc.62 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.0-rc.62",
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 ===');
@@ -18,22 +18,30 @@ import ArrowUturnLeftIcon from '@heroicons/react/24/outline/ArrowUturnLeftIcon';
18
18
  import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
19
19
  import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
20
20
  import ChevronUpDownIcon from '@heroicons/react/24/outline/ChevronUpDownIcon';
21
+ import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
22
+ import ArrowUpIcon from '@heroicons/react/24/outline/ArrowUpIcon';
23
+ import ArrowDownIcon from '@heroicons/react/24/outline/ArrowDownIcon';
21
24
 
22
25
  interface DisclosureItem {
23
26
  id: string;
24
27
  beliefValue: string;
28
+ isCustom: boolean;
25
29
  title: string;
26
30
  description?: string;
27
31
  icon: string;
28
32
  actionLisp: string;
29
33
  isDisabled?: boolean;
30
34
  }
35
+
31
36
  interface WidgetStyles {
32
37
  textColor: string;
33
38
  bgColor: string;
34
39
  bgOpacity: number;
35
40
  }
36
- type StoredDisclosureItem = Omit<DisclosureItem, 'id' | 'isDisabled'>;
41
+ type StoredDisclosureItem = Omit<
42
+ DisclosureItem,
43
+ 'id' | 'isDisabled' | 'isCustom'
44
+ >;
37
45
  interface InteractiveDisclosureWidgetProps {
38
46
  node: FlatNode;
39
47
  onUpdate: (params: string[]) => void;
@@ -61,12 +69,7 @@ const IconSelector = ({
61
69
  () => createListCollection({ items: filteredIcons }),
62
70
  [filteredIcons]
63
71
  );
64
-
65
- const iconSelectorStyles = `
66
- .icon-item .icon-indicator { display: none; }
67
- .icon-item[data-state="checked"] .icon-indicator { display: flex; }
68
- `;
69
-
72
+ const iconSelectorStyles = `.icon-item .icon-indicator { display: none; } .icon-item[data-state="checked"] .icon-indicator { display: flex; }`;
70
73
  return (
71
74
  <div>
72
75
  <style>{iconSelectorStyles}</style>
@@ -88,8 +91,8 @@ const IconSelector = ({
88
91
  </Combobox.Trigger>
89
92
  </Combobox.Control>
90
93
  <Portal>
91
- <Combobox.Positioner style={{ zIndex: 9010 }}>
92
- <Combobox.Content className="max-h-60 w-[--reference-width] overflow-y-auto rounded-md bg-white shadow-lg">
94
+ <Combobox.Positioner style={{ zIndex: 9010, minWidth: '250px' }}>
95
+ <Combobox.Content className="max-h-60 w-full overflow-y-auto rounded-md bg-white shadow-lg">
93
96
  {filteredIcons.map((icon) => (
94
97
  <Combobox.Item
95
98
  key={icon}
@@ -117,23 +120,51 @@ const DisclosureItemEditor = ({
117
120
  onUpdate,
118
121
  onToggle,
119
122
  config,
123
+ onMoveUp,
124
+ onMoveDown,
125
+ isFirst,
126
+ isLast,
120
127
  }: {
121
128
  item: DisclosureItem;
122
129
  onUpdate: (updates: Partial<DisclosureItem>) => void;
123
130
  onToggle: () => void;
124
131
  config: BrandConfig;
132
+ onMoveUp: () => void;
133
+ onMoveDown: () => void;
134
+ isFirst: boolean;
135
+ isLast: boolean;
125
136
  }) => {
126
137
  return (
127
138
  <div
128
139
  className={`space-y-4 rounded-lg border bg-white p-4 shadow-sm transition-opacity ${item.isDisabled ? 'border-gray-100 opacity-40' : 'border-gray-200'}`}
129
140
  >
130
141
  <div className="flex items-center justify-between">
131
- <h4 className="font-bold text-gray-800">
132
- {item.title}{' '}
133
- <span className="text-xs font-normal text-gray-500">
134
- (for value: {item.beliefValue})
135
- </span>
136
- </h4>
142
+ <div className="flex items-center gap-2">
143
+ <div className="flex flex-col">
144
+ <button
145
+ type="button"
146
+ onClick={onMoveUp}
147
+ disabled={isFirst}
148
+ className="rounded p-0.5 text-gray-500 hover:bg-gray-100 disabled:opacity-25"
149
+ >
150
+ <ArrowUpIcon className="h-4 w-4" />
151
+ </button>
152
+ <button
153
+ type="button"
154
+ onClick={onMoveDown}
155
+ disabled={isLast}
156
+ className="rounded p-0.5 text-gray-500 hover:bg-gray-100 disabled:opacity-25"
157
+ >
158
+ <ArrowDownIcon className="h-4 w-4" />
159
+ </button>
160
+ </div>
161
+ <h4 className="font-bold text-gray-800">
162
+ {item.title}{' '}
163
+ <span className="text-xs font-normal text-gray-500">
164
+ (Key: {item.beliefValue})
165
+ </span>
166
+ </h4>
167
+ </div>
137
168
  <button
138
169
  type="button"
139
170
  onClick={onToggle}
@@ -147,6 +178,13 @@ const DisclosureItemEditor = ({
147
178
  </button>
148
179
  </div>
149
180
  <fieldset disabled={item.isDisabled} className="space-y-4">
181
+ {item.isCustom && (
182
+ <SingleParam
183
+ label="Key / Value"
184
+ value={item.beliefValue}
185
+ onChange={(value) => onUpdate({ beliefValue: value })}
186
+ />
187
+ )}
150
188
  <SingleParam
151
189
  label="Display Title"
152
190
  value={item.title}
@@ -161,13 +199,25 @@ const DisclosureItemEditor = ({
161
199
  value={item.icon}
162
200
  onChange={(value) => onUpdate({ icon: value })}
163
201
  />
164
- <div className="relative rounded-md border p-3">
165
- <ActionBuilderField
166
- value={item.actionLisp}
167
- onChange={(value) => onUpdate({ actionLisp: value })}
168
- contentMap={fullContentMapStore.get()}
169
- />
170
- </div>
202
+
203
+ {item.isCustom ? (
204
+ <div className="relative rounded-md border p-3">
205
+ <ActionBuilderField
206
+ value={item.actionLisp}
207
+ onChange={(value) => onUpdate({ actionLisp: value })}
208
+ contentMap={fullContentMapStore.get()}
209
+ />
210
+ </div>
211
+ ) : (
212
+ <div>
213
+ <label className="block text-xs font-bold text-gray-600">
214
+ Action (Locked)
215
+ </label>
216
+ <div className="mt-1 rounded-md border border-gray-200 bg-gray-50 p-2 font-mono text-xs text-gray-500">
217
+ {item.actionLisp}
218
+ </div>
219
+ </div>
220
+ )}
171
221
  </fieldset>
172
222
  </div>
173
223
  );
@@ -187,6 +237,7 @@ export default function InteractiveDisclosureWidget({
187
237
  bgOpacity: 100,
188
238
  });
189
239
  const [isModalOpen, setIsModalOpen] = useState(false);
240
+ const [isDataLoaded, setIsDataLoaded] = useState(false);
190
241
 
191
242
  const selectedBelief = beliefs.find((b) => b.slug === selectedBeliefTag);
192
243
  const hasRealSelection = !!selectedBelief;
@@ -194,8 +245,12 @@ export default function InteractiveDisclosureWidget({
194
245
  useEffect(() => {
195
246
  const beliefTag = String(node.codeHookParams?.[0] || '');
196
247
  const payloadJson = String(node.codeHookParams?.[1] || '');
197
- setSelectedBeliefTag(beliefTag && beliefTag !== 'BELIEF' ? beliefTag : '');
198
248
 
249
+ if (beliefs.length === 0 && beliefTag && beliefTag !== 'BELIEF') {
250
+ return;
251
+ }
252
+
253
+ setSelectedBeliefTag(beliefTag && beliefTag !== 'BELIEF' ? beliefTag : '');
199
254
  const currentBelief = beliefs.find((b) => b.slug === beliefTag);
200
255
 
201
256
  if (payloadJson && currentBelief) {
@@ -204,9 +259,10 @@ export default function InteractiveDisclosureWidget({
204
259
  setWidgetStyles(
205
260
  parsed.styles || { textColor: '', bgColor: '', bgOpacity: 100 }
206
261
  );
207
- const loadedDisclosures = parsed.disclosures || {};
262
+ const loadedDisclosures =
263
+ (parsed.disclosures as StoredDisclosureItem[]) || [];
208
264
 
209
- const possibleKeys =
265
+ const scaleKeys =
210
266
  currentBelief.scale === 'custom'
211
267
  ? (currentBelief.customValues || []).map((v) => ({
212
268
  slug: v,
@@ -216,34 +272,50 @@ export default function InteractiveDisclosureWidget({
216
272
  currentBelief.scale as keyof typeof heldBeliefsScales
217
273
  ] || [];
218
274
 
219
- const allDisclosures = possibleKeys.map(({ slug, name }) => {
220
- if (loadedDisclosures[slug]) {
275
+ const actionCommand =
276
+ currentBelief.scale === 'custom' ? 'identifyAs' : 'declare';
277
+
278
+ const finalDisclosures: DisclosureItem[] = loadedDisclosures.map(
279
+ (loadedItem) => {
280
+ const isFromScale = scaleKeys.some(
281
+ (sk) => sk.slug === loadedItem.beliefValue
282
+ );
221
283
  return {
222
- ...(loadedDisclosures[slug] as StoredDisclosureItem),
284
+ ...loadedItem,
223
285
  id: generateId(),
224
- beliefValue: slug,
286
+ isCustom: !isFromScale,
287
+ actionLisp: isFromScale
288
+ ? `(${actionCommand} ${beliefTag} ${loadedItem.beliefValue})`
289
+ : loadedItem.actionLisp,
225
290
  isDisabled: false,
226
291
  };
227
292
  }
228
- return {
229
- id: generateId(),
230
- beliefValue: slug,
231
- title: name,
232
- description: '',
233
- icon: 'app',
234
- actionLisp: '',
235
- isDisabled: true,
236
- };
293
+ );
294
+
295
+ scaleKeys.forEach(({ slug, name }) => {
296
+ if (!finalDisclosures.some((d) => d.beliefValue === slug)) {
297
+ finalDisclosures.push({
298
+ id: generateId(),
299
+ beliefValue: slug,
300
+ title: name,
301
+ description: '',
302
+ icon: 'app',
303
+ actionLisp: `(${actionCommand} ${beliefTag} ${slug})`,
304
+ isCustom: false,
305
+ isDisabled: true,
306
+ });
307
+ }
237
308
  });
238
- setDisclosures(allDisclosures);
309
+
310
+ setDisclosures(finalDisclosures);
239
311
  } catch (e) {
240
- setDisclosures([]);
241
- setWidgetStyles({ textColor: '', bgColor: '', bgOpacity: 100 });
312
+ console.error('Error parsing disclosure payload:', e);
242
313
  }
243
314
  } else {
244
315
  setDisclosures([]);
245
316
  setWidgetStyles({ textColor: '', bgColor: '', bgOpacity: 100 });
246
317
  }
318
+ setIsDataLoaded(true);
247
319
  }, [node, beliefs]);
248
320
 
249
321
  useEffect(() => {
@@ -253,30 +325,27 @@ export default function InteractiveDisclosureWidget({
253
325
  const {
254
326
  data: { beliefIds },
255
327
  } = await api.get('/api/v1/nodes/beliefs');
256
- if (!beliefIds?.length) return;
328
+ if (!beliefIds?.length) {
329
+ setBeliefs([]);
330
+ return;
331
+ }
257
332
  const {
258
333
  data: { beliefs },
259
334
  } = await api.post('/api/v1/nodes/beliefs', { beliefIds });
260
335
  setBeliefs(beliefs || []);
261
336
  } catch (error) {
262
337
  console.error('Error fetching beliefs:', error);
338
+ setBeliefs([]);
263
339
  }
264
340
  };
265
341
  fetchData();
266
- }, []);
342
+ }, [node]);
267
343
 
268
344
  const handleUpdate = () => {
269
- const disclosuresToStore: Record<
270
- string,
271
- Omit<StoredDisclosureItem, 'beliefValue'>
272
- > = {};
273
- disclosures
345
+ const disclosuresToStore: StoredDisclosureItem[] = disclosures
274
346
  .filter((d) => !d.isDisabled)
275
- .forEach(({ id, beliefValue, isDisabled, ...rest }) => {
276
- if (beliefValue) {
277
- disclosuresToStore[beliefValue] = rest;
278
- }
279
- });
347
+ .map(({ id, isCustom, isDisabled, ...rest }) => rest);
348
+
280
349
  const payload = { styles: widgetStyles, disclosures: disclosuresToStore };
281
350
  onUpdate([selectedBeliefTag, JSON.stringify(payload)]);
282
351
  };
@@ -286,24 +355,54 @@ export default function InteractiveDisclosureWidget({
286
355
  const belief = beliefs.find((b) => b.slug === tag);
287
356
  let newDisclosures: DisclosureItem[] = [];
288
357
  if (belief) {
358
+ const actionCommand =
359
+ belief.scale === 'custom' ? 'identifyAs' : 'declare';
289
360
  const keys =
290
361
  belief.scale === 'custom'
291
362
  ? (belief.customValues || []).map((v) => ({ slug: v, name: v }))
292
363
  : heldBeliefsScales[belief.scale as keyof typeof heldBeliefsScales] ||
293
364
  [];
365
+
294
366
  newDisclosures = keys.map(({ slug, name }) => ({
295
367
  id: generateId(),
296
368
  beliefValue: slug,
297
369
  title: name,
298
370
  description: '',
299
371
  icon: 'app',
300
- actionLisp: '',
372
+ actionLisp: `(${actionCommand} ${tag} ${slug})`,
373
+ isCustom: false,
301
374
  isDisabled: false,
302
375
  }));
303
376
  }
304
377
  setDisclosures(newDisclosures);
305
378
  };
306
379
 
380
+ const moveDisclosure = (id: string, direction: 'up' | 'down') => {
381
+ const index = disclosures.findIndex((d) => d.id === id);
382
+ if (index === -1) return;
383
+ const newIndex = direction === 'up' ? index - 1 : index + 1;
384
+ if (newIndex < 0 || newIndex >= disclosures.length) return;
385
+
386
+ const newDisclosures = [...disclosures];
387
+ const [movedItem] = newDisclosures.splice(index, 1);
388
+ newDisclosures.splice(newIndex, 0, movedItem);
389
+ setDisclosures(newDisclosures);
390
+ };
391
+
392
+ const addCustomDisclosure = () => {
393
+ const newItem: DisclosureItem = {
394
+ id: generateId(),
395
+ beliefValue: `custom-key-${disclosures.length + 1}`,
396
+ title: 'New Custom Item',
397
+ description: '',
398
+ icon: 'plus-circle',
399
+ actionLisp: '',
400
+ isCustom: true,
401
+ isDisabled: false,
402
+ };
403
+ setDisclosures([...disclosures, newItem]);
404
+ };
405
+
307
406
  const updateDisclosure = (id: string, updates: Partial<DisclosureItem>) =>
308
407
  setDisclosures(
309
408
  disclosures.map((d) => (d.id === id ? { ...d, ...updates } : d))
@@ -317,6 +416,13 @@ export default function InteractiveDisclosureWidget({
317
416
  )
318
417
  );
319
418
 
419
+ const handleColorChange = (
420
+ key: 'textColor' | 'bgColor',
421
+ hex: string | null
422
+ ) => {
423
+ updateWidgetStyles({ [key]: hex || '' });
424
+ };
425
+
320
426
  return (
321
427
  <div className="space-y-4">
322
428
  <div className="flex items-center gap-2">
@@ -347,7 +453,6 @@ export default function InteractiveDisclosureWidget({
347
453
  </button>
348
454
  )}
349
455
  </div>
350
-
351
456
  {hasRealSelection && (
352
457
  <div className="mt-4 border-t border-gray-200 pt-4">
353
458
  <button
@@ -393,64 +498,94 @@ export default function InteractiveDisclosureWidget({
393
498
  </Dialog.Title>
394
499
  </div>
395
500
  <div className="flex-1 space-y-6 overflow-y-auto p-4">
396
- <div className="space-y-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
397
- <h3 className="font-bold text-gray-800">Widget Styles</h3>
398
- <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
399
- <div>
400
- <ColorPickerCombo
401
- title="Background Color"
402
- defaultColor={widgetStyles.bgColor}
403
- onColorChange={(hex) =>
404
- updateWidgetStyles({ bgColor: hex })
405
- }
406
- config={config}
407
- allowNull={true}
408
- />
501
+ {isDataLoaded ? (
502
+ <>
503
+ <div className="space-y-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
504
+ <h3 className="font-bold text-gray-800">
505
+ Widget Styles
506
+ </h3>
507
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
508
+ <div>
509
+ <ColorPickerCombo
510
+ title="Background Color"
511
+ defaultColor={widgetStyles.bgColor}
512
+ onColorChange={(hex) =>
513
+ handleColorChange('bgColor', hex)
514
+ }
515
+ config={config}
516
+ allowNull={true}
517
+ skipTailwind={false}
518
+ />
519
+ </div>
520
+ <div>
521
+ <ColorPickerCombo
522
+ title="Text Color"
523
+ defaultColor={widgetStyles.textColor}
524
+ onColorChange={(hex) =>
525
+ handleColorChange('textColor', hex)
526
+ }
527
+ config={config}
528
+ allowNull={true}
529
+ skipTailwind={false}
530
+ />
531
+ </div>
532
+ <div>
533
+ <label className="block text-xs font-bold text-gray-600">
534
+ BG Opacity (%)
535
+ </label>
536
+ <div className="mt-1 flex items-center gap-2">
537
+ <input
538
+ type="range"
539
+ min="0"
540
+ max="100"
541
+ value={widgetStyles.bgOpacity}
542
+ onChange={(e) =>
543
+ updateWidgetStyles({
544
+ bgOpacity: parseInt(e.target.value),
545
+ })
546
+ }
547
+ className="w-full"
548
+ />
549
+ <span className="w-12 text-center font-mono text-sm">
550
+ {widgetStyles.bgOpacity}%
551
+ </span>
552
+ </div>
553
+ </div>
554
+ </div>
409
555
  </div>
410
- <div>
411
- <ColorPickerCombo
412
- title="Text Color"
413
- defaultColor={widgetStyles.textColor}
414
- onColorChange={(hex) =>
415
- updateWidgetStyles({ textColor: hex })
556
+
557
+ {disclosures.map((item, index) => (
558
+ <DisclosureItemEditor
559
+ key={item.id}
560
+ item={item}
561
+ onUpdate={(updates) =>
562
+ updateDisclosure(item.id, updates)
416
563
  }
564
+ onToggle={() => toggleDisclosure(item.id)}
417
565
  config={config}
418
- allowNull={true}
566
+ onMoveUp={() => moveDisclosure(item.id, 'up')}
567
+ onMoveDown={() => moveDisclosure(item.id, 'down')}
568
+ isFirst={index === 0}
569
+ isLast={index === disclosures.length - 1}
419
570
  />
571
+ ))}
572
+
573
+ <div className="pt-4">
574
+ <button
575
+ type="button"
576
+ onClick={addCustomDisclosure}
577
+ className="flex w-full items-center justify-center rounded-md border-2 border-dashed border-gray-300 bg-white px-3 py-2 text-sm font-bold text-gray-500 hover:border-cyan-600 hover:text-cyan-600"
578
+ >
579
+ <PlusIcon className="mr-2 h-5 w-5" />
580
+ Add Custom Disclosure
581
+ </button>
420
582
  </div>
421
- <div>
422
- <label className="block text-xs font-bold text-gray-600">
423
- BG Opacity (%)
424
- </label>
425
- <div className="mt-1 flex items-center gap-2">
426
- <input
427
- type="range"
428
- min="0"
429
- max="100"
430
- value={widgetStyles.bgOpacity}
431
- onChange={(e) =>
432
- updateWidgetStyles({
433
- bgOpacity: parseInt(e.target.value),
434
- })
435
- }
436
- className="w-full"
437
- />
438
- <span className="w-12 text-center font-mono text-sm">
439
- {widgetStyles.bgOpacity}%
440
- </span>
441
- </div>
442
- </div>
583
+ </>
584
+ ) : (
585
+ <div className="p-8 text-center">
586
+ Loading configuration...
443
587
  </div>
444
- </div>
445
- {disclosures.map((item) => (
446
- <DisclosureItemEditor
447
- key={item.id}
448
- item={item}
449
- onUpdate={(updates) => updateDisclosure(item.id, updates)}
450
- onToggle={() => toggleDisclosure(item.id)}
451
- config={config}
452
- />
453
- ))}
588
+ )}
454
589
  </div>
455
590
  <div className="flex-shrink-0 justify-end border-t border-gray-200 bg-white px-6 py-3">
456
591
  <Dialog.CloseTrigger asChild>
@@ -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`: