astro-tractstack 2.0.10 → 2.0.12

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.
@@ -387,12 +387,6 @@ export const Compositor = (props: CompositorProps) => {
387
387
  const ctx = getCtx(props);
388
388
  const range = $selection;
389
389
 
390
- if ($selection.pendingAction === 'style') {
391
- if (VERBOSE) console.log(LOG_PREFIX + 'useEffect acting on: style');
392
- await ctx.wrapRangeInSpan(range as SelectionStoreState, 'span');
393
- resetSelectionStore();
394
- }
395
-
396
390
  if ($selection.pendingAction === 'link') {
397
391
  if (VERBOSE) console.log(LOG_PREFIX + 'useEffect acting on: link');
398
392
  const newAnchorNodeId = await ctx.wrapRangeInAnchor(
@@ -402,6 +396,20 @@ export const Compositor = (props: CompositorProps) => {
402
396
  ctx.handleInsertSignal('a', newAnchorNodeId);
403
397
  }
404
398
  resetSelectionStore();
399
+ } else if ($selection.pendingAction === 'style') {
400
+ if (VERBOSE) console.log(LOG_PREFIX + 'useEffect acting on: style');
401
+ const newSpanNodeId = await ctx.wrapRangeInSpan(
402
+ range as SelectionStoreState,
403
+ 'span'
404
+ );
405
+ if (newSpanNodeId) {
406
+ settingsPanelStore.set({
407
+ action: 'style-element',
408
+ nodeId: newSpanNodeId,
409
+ expanded: true,
410
+ });
411
+ }
412
+ resetSelectionStore();
405
413
  }
406
414
  ctx.notifyNode('root');
407
415
  };
@@ -45,6 +45,8 @@ import type {
45
45
  BaseNode,
46
46
  FlatNode,
47
47
  } from '@/types/compositorTypes';
48
+ import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
49
+ import { selectionStore } from '@/stores/selection';
48
50
  import type { NodeProps, SelectionOrigin } from '@/types/nodeProps';
49
51
 
50
52
  const VERBOSE = false;
@@ -291,6 +293,31 @@ const getElement = (
291
293
  viewportKeyStore.get().value
292
294
  );
293
295
 
296
+ const handleElementClick = (e: MouseEvent<HTMLElement>) => {
297
+ // 1. ALWAYS stop the event from bubbling up to the Pane.
298
+ e.stopPropagation();
299
+
300
+ // 2. Check the selection store. The handleMouseUp in Compositor.tsx
301
+ // has already run by the time this 'click' event fires.
302
+ //
303
+ // - If it was a drag, Compositor.tsx set 'isActive: true' (line 267).
304
+ // - If it was a click, Compositor.tsx called 'resetSelectionStore'
305
+ // (line 242), so 'isActive: false'.
306
+ //
307
+ const { isActive } = selectionStore.get();
308
+
309
+ if (isActive) {
310
+ // A drag just finished. The user's intent was to select text.
311
+ // Do NOT open the panel.
312
+ return;
313
+ }
314
+
315
+ // 3. 'isActive' was false. This was a genuine click.
316
+ // We can safely open the settings panel.
317
+ // 'node' is already in scope from the getElement function.
318
+ handleClickEventDefault(node as FlatNode, true);
319
+ };
320
+
294
321
  const handleMouseDown = (e: MouseEvent<HTMLElement>) => {
295
322
  if (VERBOSE)
296
323
  console.log('[Node.tsx] handleMouseDown FIRED', { event: e });
@@ -346,6 +373,7 @@ const getElement = (
346
373
  {
347
374
  className: className,
348
375
  onMouseDown: handleMouseDown,
376
+ onClick: handleElementClick,
349
377
  'data-node-id': node.id,
350
378
  style: { userSelect: 'none' },
351
379
  },
@@ -294,11 +294,6 @@ const Pane = memo(
294
294
  (prevProps: NodeProps, nextProps: NodeProps) => {
295
295
  const isEqual =
296
296
  prevProps.nodeId === nextProps.nodeId && prevProps.ctx === nextProps.ctx;
297
- if (!isEqual) {
298
- console.log(
299
- ` !! Pane rerender triggered by props change: ${prevProps.nodeId} -> ${nextProps.nodeId}`
300
- );
301
- }
302
297
  return isEqual;
303
298
  }
304
299
  );
@@ -379,8 +379,9 @@ export const NodeBasicTag = (props: NodeTagProps) => {
379
379
  .filter(
380
380
  (childNode): childNode is FlatNode =>
381
381
  'tagName' in childNode &&
382
- ['a', 'button'].includes(childNode.tagName as string)
382
+ ['a', 'button', 'span'].includes(childNode.tagName as string)
383
383
  ) as FlatNode[];
384
+ if (VERBOSE) console.log('originalNodes to save:', originalNodes);
384
385
 
385
386
  const parsedNodes = processRichTextToNodes(
386
387
  currentContent,
@@ -24,13 +24,11 @@ import {
24
24
  hasAssemblyAIStore,
25
25
  } from '@/stores/storykeep';
26
26
  import { templateCategories } from '@/utils/compositor/templateMarkdownStyles';
27
- import { AddPanePanel_newAICopy } from './AddPanePanel_newAICopy';
28
- import { AddPaneNewCopyMode, type CopyMode } from './AddPanePanel_newCopyMode';
27
+ import { AiPaneGenerator } from './AiPaneGenerator';
29
28
  import { AddPaneNewCustomCopy } from './AddPanePanel_newCustomCopy';
30
- import { getTitleSlug } from '@/utils/aai/getTitleSlug';
31
- import { fullContentMapStore } from '@/stores/storykeep';
32
29
  import { themes, type Theme } from '@/types/tractstack';
33
- import { PaneAddMode } from '@/types/compositorTypes';
30
+ import { PaneAddMode, type TemplatePane } from '@/types/compositorTypes';
31
+ import { useStore } from '@nanostores/react';
34
32
 
35
33
  interface AddPaneNewPanelProps {
36
34
  nodeId: string;
@@ -58,47 +56,46 @@ interface TemplateCategory {
58
56
 
59
57
  const ITEMS_PER_PAGE = 8;
60
58
 
59
+ type Mode = 'template' | 'custom' | 'ai';
60
+
61
61
  const AddPaneNewPanel = ({
62
62
  nodeId,
63
63
  first,
64
- setMode,
64
+ setMode: setParentMode, // Renamed prop to avoid conflict with local state
65
65
  ctx,
66
66
  isStoryFragment = false,
67
67
  isContextPane = false,
68
68
  }: AddPaneNewPanelProps) => {
69
- const brand = brandColourStore.get();
70
- const hasAssemblyAI = hasAssemblyAIStore.get();
71
- const [copyMode, setCopyMode] = useState<CopyMode>('design');
69
+ const brand = useStore(brandColourStore);
70
+ const hasAssemblyAI = useStore(hasAssemblyAIStore);
71
+ const [mode, setMode] = useState<Mode>('template'); // Local mode state
72
72
  const [customMarkdown, setCustomMarkdown] = useState<string>(`...`);
73
73
  const [previews, setPreviews] = useState<PreviewPane[]>([]);
74
74
  const [currentPage, setCurrentPage] = useState(0);
75
75
  const [renderedPages, setRenderedPages] = useState<Set<number>>(new Set());
76
76
  const [selectedTheme, setSelectedTheme] = useState<Theme>(
77
- preferredThemeStore.get()
77
+ useStore(preferredThemeStore)
78
78
  );
79
79
  const [useOddVariant, setUseOddVariant] = useState(false);
80
80
  const [selectedCategory, setSelectedCategory] = useState<TemplateCategory>(
81
81
  templateCategories[isContextPane ? 1 : first ? 4 : 0]
82
82
  );
83
83
  const [isInserting, setIsInserting] = useState(false);
84
- const [aiContentGenerated, setAiContentGenerated] = useState(false);
85
84
  const [fragmentsToGenerate, setFragmentsToGenerate] = useState<
86
85
  PanePreviewRequest[]
87
86
  >([]);
88
- const shouldShowDesigns = copyMode !== 'ai' || aiContentGenerated;
89
87
 
90
88
  const categoryCollection = useMemo(() => {
91
- const categories =
92
- copyMode === `ai` || isContextPane
93
- ? [templateCategories[1]]
94
- : templateCategories;
89
+ const categories = isContextPane
90
+ ? [templateCategories[1]]
91
+ : templateCategories;
95
92
 
96
93
  return createListCollection({
97
94
  items: categories,
98
95
  itemToValue: (item) => item.id,
99
96
  itemToString: (item) => item.title,
100
97
  });
101
- }, [copyMode, isContextPane]);
98
+ }, [isContextPane]);
102
99
 
103
100
  const themesCollection = useMemo(() => {
104
101
  return createListCollection({
@@ -109,38 +106,25 @@ const AddPaneNewPanel = ({
109
106
  }, []);
110
107
 
111
108
  const filteredTemplates = useMemo(() => {
112
- if (copyMode === `ai` || isContextPane)
113
- return templateCategories[1].getTemplates(
114
- selectedTheme,
115
- brand,
116
- useOddVariant
117
- );
118
- if (isContextPane)
119
- return templateCategories[1].getTemplates(
120
- selectedTheme,
121
- brand,
122
- useOddVariant
123
- );
124
109
  return selectedCategory.getTemplates(selectedTheme, brand, useOddVariant);
125
- }, [selectedTheme, useOddVariant, selectedCategory, copyMode, isContextPane]);
110
+ }, [selectedTheme, useOddVariant, selectedCategory, brand]);
126
111
 
127
112
  useEffect(() => {
128
- if (copyMode !== 'ai') setAiContentGenerated(false);
129
- if (copyMode !== 'ai' || isContextPane)
130
- setSelectedCategory(templateCategories[first ? 4 : 0]);
131
- }, [copyMode, first, isContextPane]);
132
-
133
- const handleAiContentGenerated = (content: string) => {
134
- setCustomMarkdown(content);
135
- setAiContentGenerated(true);
136
- };
113
+ if (isContextPane) {
114
+ setSelectedCategory(templateCategories[1]);
115
+ } else if (first) {
116
+ setSelectedCategory(templateCategories[4]);
117
+ } else {
118
+ setSelectedCategory(templateCategories[0]);
119
+ }
120
+ }, [isContextPane, first]);
137
121
 
138
122
  useEffect(() => {
139
123
  const newPreviews = filteredTemplates.map((template, index: number) => {
140
- const ctx = new NodesContext();
141
- ctx.addNode(createEmptyStorykeep('tmp'));
124
+ const previewCtx = new NodesContext();
125
+ previewCtx.addNode(createEmptyStorykeep('tmp'));
142
126
  const thisTemplate =
143
- copyMode === 'custom' || (copyMode === 'ai' && aiContentGenerated)
127
+ mode === 'custom'
144
128
  ? {
145
129
  ...template,
146
130
  markdown: template.markdown && {
@@ -149,13 +133,13 @@ const AddPaneNewPanel = ({
149
133
  },
150
134
  }
151
135
  : template;
152
- ctx.addTemplatePane('tmp', thisTemplate);
153
- return { ctx, template: thisTemplate, index };
136
+ previewCtx.addTemplatePane('tmp', thisTemplate);
137
+ return { ctx: previewCtx, template: thisTemplate, index };
154
138
  });
155
139
  setPreviews(newPreviews);
156
140
  setCurrentPage(0);
157
141
  setRenderedPages(new Set());
158
- }, [filteredTemplates, customMarkdown, copyMode, aiContentGenerated]);
142
+ }, [filteredTemplates, customMarkdown, mode]);
159
143
 
160
144
  const totalPages = Math.ceil(previews.length / ITEMS_PER_PAGE);
161
145
 
@@ -223,81 +207,47 @@ const AddPaneNewPanel = ({
223
207
  });
224
208
  };
225
209
 
226
- const handleTemplateInsert = async (
227
- template: any,
228
- nodeId: string,
229
- first: boolean
230
- ) => {
231
- if (isInserting) return;
210
+ const handleApplyTemplate = async (template: any) => {
211
+ if (isInserting || !ctx) return;
232
212
  setIsInserting(true);
233
213
 
234
214
  try {
235
- if (ctx) {
236
- const hasMarkdownContent =
237
- template?.markdown?.markdownBody &&
238
- template.markdown.markdownBody.trim() !== '...' &&
239
- template.markdown.markdownBody.trim().length > 0;
240
-
241
- const insertTemplate = [`blank`, `custom`].includes(copyMode)
215
+ const insertTemplate =
216
+ mode === 'custom'
242
217
  ? {
243
218
  ...cloneDeep(template),
244
219
  markdown: template.markdown && {
245
220
  ...template.markdown,
246
- markdownBody: copyMode === `blank` ? `...` : customMarkdown,
221
+ markdownBody: customMarkdown,
247
222
  },
248
223
  }
249
224
  : cloneDeep(template);
250
225
 
251
- const markdownContent = [`blank`].includes(copyMode)
252
- ? null
253
- : copyMode === `custom`
254
- ? customMarkdown
255
- : insertTemplate?.markdown?.markdownBody;
256
-
257
- insertTemplate.title = '';
258
- insertTemplate.slug = '';
259
-
260
- if (
261
- copyMode === `ai` &&
262
- hasAssemblyAI &&
263
- markdownContent &&
264
- hasMarkdownContent
265
- ) {
266
- const existingSlugs = fullContentMapStore
267
- .get()
268
- .filter((item) => ['Pane', 'StoryFragment'].includes(item.type))
269
- .map((item) => item.slug);
270
-
271
- const titleSlugResult = await getTitleSlug(
272
- markdownContent,
273
- existingSlugs
274
- );
275
-
276
- if (titleSlugResult) {
277
- insertTemplate.title = titleSlugResult.title;
278
- insertTemplate.slug = titleSlugResult.slug;
279
- }
280
- }
281
-
282
- const ownerId =
283
- isStoryFragment || isContextPane
284
- ? nodeId
285
- : ctx.getClosestNodeTypeFromId(nodeId, 'StoryFragment');
286
-
287
- if (isContextPane) {
288
- insertTemplate.isContextPane = true;
289
- ctx.addContextTemplatePane(ownerId, insertTemplate);
290
- } else {
291
- const newPaneId = ctx.addTemplatePane(
226
+ insertTemplate.title = '';
227
+ insertTemplate.slug = '';
228
+
229
+ const ownerId =
230
+ isStoryFragment || isContextPane
231
+ ? nodeId
232
+ : ctx.getClosestNodeTypeFromId(nodeId, 'StoryFragment');
233
+
234
+ if (isContextPane) {
235
+ insertTemplate.isContextPane = true;
236
+ await ctx.applyAtomicUpdate(async (tmpCtx) => {
237
+ tmpCtx.addContextTemplatePane(ownerId, insertTemplate);
238
+ });
239
+ } else {
240
+ await ctx.applyAtomicUpdate(async (tmpCtx) => {
241
+ tmpCtx.addTemplatePane(
292
242
  ownerId,
293
243
  insertTemplate,
294
244
  nodeId,
295
245
  first ? 'before' : 'after'
296
246
  );
297
- if (newPaneId) ctx.notifyNode(`root`);
298
- }
299
- setMode(PaneAddMode.DEFAULT, false);
247
+ });
248
+ ctx.notifyNode(`root`);
300
249
  }
250
+ setParentMode(PaneAddMode.DEFAULT, false);
301
251
  } catch (error) {
302
252
  console.error('Error inserting template:', error);
303
253
  } finally {
@@ -305,6 +255,39 @@ const AddPaneNewPanel = ({
305
255
  }
306
256
  };
307
257
 
258
+ const handleApplyGeneratedPane = async (pane: TemplatePane) => {
259
+ if (isInserting || !ctx) return;
260
+ setIsInserting(true);
261
+ try {
262
+ const ownerId =
263
+ isStoryFragment || isContextPane
264
+ ? nodeId
265
+ : ctx.getClosestNodeTypeFromId(nodeId, 'StoryFragment');
266
+
267
+ if (isContextPane) {
268
+ pane.isContextPane = true;
269
+ await ctx.applyAtomicUpdate(async (tmpCtx) => {
270
+ tmpCtx.addContextTemplatePane(ownerId, pane);
271
+ });
272
+ } else {
273
+ await ctx.applyAtomicUpdate(async (tmpCtx) => {
274
+ tmpCtx.addTemplatePane(
275
+ ownerId,
276
+ pane,
277
+ nodeId,
278
+ first ? 'before' : 'after'
279
+ );
280
+ });
281
+ ctx.notifyNode(`root`);
282
+ }
283
+ setParentMode(PaneAddMode.DEFAULT, false);
284
+ } catch (error) {
285
+ console.error('Error applying generated pane:', error);
286
+ } finally {
287
+ setIsInserting(false);
288
+ }
289
+ };
290
+
308
291
  const handleThemeChange = (details: { value: string[] }) => {
309
292
  const newTheme = details.value[0] as Theme;
310
293
  if (newTheme) {
@@ -338,8 +321,9 @@ const AddPaneNewPanel = ({
338
321
  <style>{customStyles}</style>
339
322
  <div className="group flex w-full gap-1 rounded-md bg-white p-1.5">
340
323
  <button
341
- onClick={() => setMode(PaneAddMode.DEFAULT, first)}
324
+ onClick={() => setParentMode(PaneAddMode.DEFAULT, first)}
342
325
  className="w-fit rounded bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-gray-200 focus:bg-gray-200"
326
+ type="button"
343
327
  >
344
328
  ← Go Back
345
329
  </button>
@@ -348,11 +332,45 @@ const AddPaneNewPanel = ({
348
332
  <div className="font-action flex-none rounded px-2 py-2.5 text-sm font-bold text-cyan-700 shadow-sm">
349
333
  + Design New Pane
350
334
  </div>
335
+ <div className="flex items-center space-x-2 rounded-lg bg-gray-100 p-1">
336
+ <button
337
+ onClick={() => setMode('template')}
338
+ className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${
339
+ mode === 'template'
340
+ ? 'bg-white text-cyan-700 shadow'
341
+ : 'text-gray-600 hover:text-gray-800'
342
+ }`}
343
+ type="button"
344
+ >
345
+ Use Template
346
+ </button>
347
+ <button
348
+ onClick={() => setMode('custom')}
349
+ className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${
350
+ mode === 'custom'
351
+ ? 'bg-white text-cyan-700 shadow'
352
+ : 'text-gray-600 hover:text-gray-800'
353
+ }`}
354
+ type="button"
355
+ >
356
+ Paste Markdown
357
+ </button>
358
+ {hasAssemblyAI && (
359
+ <button
360
+ onClick={() => setMode('ai')}
361
+ className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${
362
+ mode === 'ai'
363
+ ? 'rounded-md border border-transparent bg-cyan-600 px-3 py-1.5 text-sm font-bold text-white shadow-sm hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2'
364
+ : 'text-gray-600 hover:text-gray-800'
365
+ }`}
366
+ type="button"
367
+ >
368
+ ✨ Generate with AI
369
+ </button>
370
+ )}
371
+ </div>
351
372
 
352
- {!(copyMode === 'ai' && aiContentGenerated) && (
353
- <AddPaneNewCopyMode selected={copyMode} onChange={setCopyMode} />
354
- )}
355
- {copyMode === 'custom' && (
373
+ {mode === 'custom' && (
356
374
  <div className="mt-4 w-full">
357
375
  <AddPaneNewCustomCopy
358
376
  value={customMarkdown}
@@ -360,18 +378,19 @@ const AddPaneNewPanel = ({
360
378
  />
361
379
  </div>
362
380
  )}
363
- {copyMode === 'ai' && !aiContentGenerated && (
381
+ {mode === 'ai' && (
364
382
  <div className="mt-4 w-full">
365
- <AddPanePanel_newAICopy
366
- onChange={handleAiContentGenerated}
367
- isContextPane={isContextPane}
383
+ <AiPaneGenerator
384
+ ownerId={nodeId}
385
+ onComplete={handleApplyGeneratedPane}
386
+ onCancel={() => setMode('template')}
368
387
  />
369
388
  </div>
370
389
  )}
371
390
  </div>
372
391
  </div>
373
392
 
374
- {shouldShowDesigns && (
393
+ {mode !== 'ai' && (
375
394
  <>
376
395
  <h3 className="font-action px-3.5 pb-1.5 pt-4 text-xl font-bold text-black">
377
396
  1. Template design settings
@@ -521,8 +540,7 @@ const AddPaneNewPanel = ({
521
540
  onClick={
522
541
  isInserting
523
542
  ? undefined
524
- : () =>
525
- handleTemplateInsert(preview.template, nodeId, first)
543
+ : () => handleApplyTemplate(preview.template)
526
544
  }
527
545
  className={`bg-mywhite group relative w-full rounded-sm shadow-inner ${
528
546
  isInserting
@@ -588,6 +606,7 @@ const AddPaneNewPanel = ({
588
606
  onClick={() => handlePageChange(currentPage - 1)}
589
607
  disabled={currentPage === 0}
590
608
  className="rounded bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-gray-200 focus:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50"
609
+ type="button"
591
610
  >
592
611
  Previous
593
612
  </button>
@@ -601,6 +620,7 @@ const AddPaneNewPanel = ({
601
620
  ? 'bg-cyan-700 text-white'
602
621
  : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
603
622
  }`}
623
+ type="button"
604
624
  >
605
625
  {index + 1}
606
626
  </button>
@@ -610,6 +630,7 @@ const AddPaneNewPanel = ({
610
630
  onClick={() => handlePageChange(currentPage + 1)}
611
631
  disabled={currentPage === totalPages - 1}
612
632
  className="rounded bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-gray-200 focus:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50"
633
+ type="button"
613
634
  >
614
635
  Next
615
636
  </button>