astro-tractstack 2.1.3 → 2.2.1

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 (128) hide show
  1. package/README.md +54 -266
  2. package/bin/create-tractstack.js +9 -6
  3. package/dist/index.js +109 -71
  4. package/package.json +4 -2
  5. package/templates/css/custom.css +5 -0
  6. package/templates/icons/code.svg +18 -0
  7. package/templates/icons/li.svg +4 -0
  8. package/templates/icons/link.svg +22 -0
  9. package/templates/icons/p.svg +3 -0
  10. package/templates/src/client/app.js +80 -1
  11. package/templates/src/components/Footer.astro +1 -1
  12. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +6 -6
  13. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +3 -3
  14. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +1 -1
  15. package/templates/src/components/codehooks/ListContentSetup.tsx +2 -2
  16. package/templates/src/components/codehooks/ProductCardSetup.tsx +1 -1
  17. package/templates/src/components/codehooks/ProductGridSetup.tsx +2 -2
  18. package/templates/src/components/codehooks/SandboxRegisterForm.tsx +3 -3
  19. package/templates/src/components/compositor/Compositor.tsx +25 -9
  20. package/templates/src/components/compositor/Node.tsx +168 -496
  21. package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +1 -0
  22. package/templates/src/components/compositor/elements/SignUp.tsx +1 -1
  23. package/templates/src/components/compositor/elements/YouTubeWrapper.tsx +2 -0
  24. package/templates/src/components/compositor/nodes/CreativePane.tsx +262 -0
  25. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +4 -6
  26. package/templates/src/components/compositor/nodes/GridLayout.tsx +4 -2
  27. package/templates/src/components/compositor/nodes/Markdown.tsx +18 -3
  28. package/templates/src/components/compositor/nodes/Pane.tsx +11 -5
  29. package/templates/src/components/compositor/nodes/RenderChildren.tsx +1 -1
  30. package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +5 -5
  31. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +90 -42
  32. package/templates/src/components/compositor/nodes/tagElements/NodeImg.tsx +2 -0
  33. package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +27 -1
  34. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +10 -8
  35. package/templates/src/components/compositor/tools/NodeOverlay.tsx +224 -0
  36. package/templates/src/components/compositor/tools/PaneOverlay.tsx +122 -0
  37. package/templates/src/components/edit/Header.tsx +68 -9
  38. package/templates/src/components/edit/PanelSwitch.tsx +42 -4
  39. package/templates/src/components/edit/SettingsPanel.tsx +2 -3
  40. package/templates/src/components/edit/ToolMode.tsx +1 -31
  41. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +2 -2
  42. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +1 -1
  43. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +193 -659
  44. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +15 -82
  45. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +95 -45
  46. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +137 -49
  47. package/templates/src/components/edit/pane/RestylePaneModal.tsx +1 -1
  48. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +375 -0
  49. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +1 -23
  50. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +327 -0
  51. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +267 -0
  52. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +371 -0
  53. package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +201 -76
  54. package/templates/src/components/edit/pane/steps/CreativeInjectStep.tsx +141 -0
  55. package/templates/src/components/edit/panels/CreativeImagePanel.tsx +435 -0
  56. package/templates/src/components/edit/panels/CreativeLinkPanel.tsx +110 -0
  57. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +1 -1
  58. package/templates/src/components/edit/panels/StyleParentPanel.tsx +118 -126
  59. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +3 -2
  60. package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +1 -0
  61. package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +3 -1
  62. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +3 -1
  63. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +1 -1
  64. package/templates/src/components/edit/state/SaveModal.tsx +19 -787
  65. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +2 -2
  66. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +1 -1
  67. package/templates/src/components/edit/widgets/BunnyWidget.tsx +5 -5
  68. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +1 -1
  69. package/templates/src/components/edit/widgets/SignupWidget.tsx +1 -1
  70. package/templates/src/components/fields/ActionBuilderTimeSelector.tsx +1 -1
  71. package/templates/src/components/fields/ArtpackImage.tsx +11 -3
  72. package/templates/src/components/fields/BackgroundImage.tsx +8 -0
  73. package/templates/src/components/fields/BackgroundImageWrapper.tsx +15 -9
  74. package/templates/src/components/fields/ImageUpload.tsx +6 -0
  75. package/templates/src/components/form/ActionBuilderField.tsx +15 -5
  76. package/templates/src/components/form/ActionBuilderSlugSelector.tsx +1 -1
  77. package/templates/src/components/form/ColorPicker.tsx +1 -1
  78. package/templates/src/components/form/EnumSelect.tsx +1 -1
  79. package/templates/src/components/form/NumberInput.tsx +1 -1
  80. package/templates/src/components/form/StringArrayInput.tsx +1 -1
  81. package/templates/src/components/form/StringInput.tsx +1 -1
  82. package/templates/src/components/form/UnsavedChangesBar.tsx +1 -1
  83. package/templates/src/components/form/advanced/APIConfigSection.tsx +2 -2
  84. package/templates/src/components/form/advanced/AuthConfigSection.tsx +2 -2
  85. package/templates/src/components/profile/ProfileCreate.tsx +1 -1
  86. package/templates/src/components/profile/ProfileEdit.tsx +1 -1
  87. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +2 -2
  88. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +1 -1
  89. package/templates/src/components/storykeep/controls/content/ContentSummary.tsx +2 -2
  90. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +1 -1
  91. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +6 -6
  92. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +1 -1
  93. package/templates/src/components/storykeep/controls/content/PaneTable.tsx +358 -0
  94. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -1
  95. package/templates/src/constants/prompts.json +18 -10
  96. package/templates/src/constants.ts +3 -0
  97. package/templates/src/hooks/usePaneFragments.ts +60 -0
  98. package/templates/src/lib/session.ts +71 -16
  99. package/templates/src/pages/[...slug].astro +4 -46
  100. package/templates/src/pages/api/css.ts +149 -0
  101. package/templates/src/pages/maint.astro +1 -1
  102. package/templates/src/pages/storykeep/login.astro +2 -2
  103. package/templates/src/stores/nodes.ts +162 -49
  104. package/templates/src/stores/orphanAnalysis.ts +6 -30
  105. package/templates/src/stores/previews.ts +7 -0
  106. package/templates/src/stores/storykeep.ts +0 -8
  107. package/templates/src/types/compositorTypes.ts +53 -10
  108. package/templates/src/utils/compositor/aiGeneration.ts +93 -0
  109. package/templates/src/utils/compositor/allowInsert.ts +2 -0
  110. package/templates/src/utils/compositor/htmlAst.ts +704 -0
  111. package/templates/src/utils/compositor/nodesHelper.ts +281 -102
  112. package/templates/src/utils/compositor/savePipeline.ts +893 -0
  113. package/templates/src/utils/etl/index.ts +3 -0
  114. package/templates/src/utils/etl/transformer.ts +10 -0
  115. package/templates/src/utils/helpers.ts +101 -0
  116. package/utils/inject-files.ts +100 -62
  117. package/templates/icons/text.svg +0 -6
  118. package/templates/src/components/compositor/NodeWithGuid.tsx +0 -69
  119. package/templates/src/components/compositor/nodes/GridLayout_eraser.tsx +0 -33
  120. package/templates/src/components/compositor/nodes/Markdown_eraser.tsx +0 -56
  121. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +0 -269
  122. package/templates/src/components/compositor/nodes/Pane_eraser.tsx +0 -186
  123. package/templates/src/components/compositor/nodes/Pane_layout.tsx +0 -79
  124. package/templates/src/components/compositor/nodes/tagElements/NodeA_eraser.tsx +0 -26
  125. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_eraser.tsx +0 -61
  126. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_insert.tsx +0 -120
  127. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_settings.tsx +0 -62
  128. package/templates/src/components/compositor/nodes/tagElements/NodeButton_eraser.tsx +0 -26
@@ -0,0 +1,358 @@
1
+ import { useState, useMemo, useEffect } from 'react';
2
+ import { useStore } from '@nanostores/react';
3
+ import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
4
+ import LockClosedIcon from '@heroicons/react/24/outline/LockClosedIcon';
5
+ import CheckCircleIcon from '@heroicons/react/24/outline/CheckCircleIcon';
6
+ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
7
+ import { Toggle } from '@ark-ui/react/toggle';
8
+ import { orphanAnalysisStore } from '@/stores/orphanAnalysis';
9
+ import {
10
+ PaneSnapshotGenerator,
11
+ type SnapshotData,
12
+ } from '@/components/compositor/preview/PaneSnapshotGenerator';
13
+ import { usePaneFragments } from '@/hooks/usePaneFragments';
14
+ import { TractStackAPI } from '@/utils/api';
15
+ import type { FullContentMapItem } from '@/types/tractstack';
16
+
17
+ interface PaneTableProps {
18
+ fullContentMap: FullContentMapItem[];
19
+ onRefresh: () => void;
20
+ }
21
+
22
+ interface PanePreviewItem {
23
+ pane: FullContentMapItem;
24
+ snapshot?: SnapshotData;
25
+ }
26
+
27
+ const ITEMS_PER_PAGE = 6;
28
+
29
+ const DeletingModal = ({ count }: { count: number }) => (
30
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
31
+ <div className="flex flex-col items-center gap-4 rounded-lg bg-white p-8 shadow-xl">
32
+ <div className="h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-red-600" />
33
+ <div className="text-center">
34
+ <h3 className="text-lg font-bold text-gray-900">Deleting Content</h3>
35
+ <p className="text-gray-500">Removing {count} pane(s)...</p>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ );
40
+
41
+ const PaneTable = ({ fullContentMap, onRefresh }: PaneTableProps) => {
42
+ const orphanState = useStore(orphanAnalysisStore);
43
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
44
+ const [currentPage, setCurrentPage] = useState(0);
45
+ const [previews, setPreviews] = useState<PanePreviewItem[]>([]);
46
+ const [isDeleting, setIsDeleting] = useState(false);
47
+ const [showOnlyUnused, setShowOnlyUnused] = useState(false);
48
+
49
+ const tenantId =
50
+ window.TRACTSTACK_CONFIG?.tenantId ||
51
+ import.meta.env.PUBLIC_TENANTID ||
52
+ 'default';
53
+
54
+ const isOrphan = (id: string) => {
55
+ if (!orphanState.data?.panes) return false;
56
+ const deps = orphanState.data.panes[id];
57
+ return deps && deps.length === 0;
58
+ };
59
+
60
+ const allPanes = useMemo(
61
+ () => fullContentMap.filter((item) => item.type === 'Pane'),
62
+ [fullContentMap]
63
+ );
64
+
65
+ const filteredPanes = useMemo(() => {
66
+ if (!showOnlyUnused) return allPanes;
67
+ return allPanes.filter((p) => isOrphan(p.id));
68
+ }, [allPanes, showOnlyUnused, orphanState.data]);
69
+
70
+ useEffect(() => {
71
+ setPreviews(filteredPanes.map((pane) => ({ pane })));
72
+ setCurrentPage(0);
73
+ setSelectedIds(new Set());
74
+ }, [filteredPanes]);
75
+
76
+ const totalPages = Math.ceil(previews.length / ITEMS_PER_PAGE);
77
+
78
+ const handlePageChange = (newPage: number) => {
79
+ if (newPage >= 0 && newPage < totalPages) {
80
+ setCurrentPage(newPage);
81
+ }
82
+ };
83
+
84
+ const visiblePreviews = useMemo(() => {
85
+ const startIndex = currentPage * ITEMS_PER_PAGE;
86
+ return previews.slice(startIndex, startIndex + ITEMS_PER_PAGE);
87
+ }, [previews, currentPage]);
88
+
89
+ const visiblePaneIds = useMemo(
90
+ () => visiblePreviews.map((p) => p.pane.id),
91
+ [visiblePreviews]
92
+ );
93
+
94
+ const {
95
+ fragments,
96
+ errors,
97
+ isLoading: fragmentsLoading,
98
+ } = usePaneFragments(visiblePaneIds);
99
+
100
+ const handleSnapshotComplete = (id: string, snapshot: SnapshotData) => {
101
+ const paneId = id.replace('table-', '');
102
+ setPreviews((prev) =>
103
+ prev.map((item) =>
104
+ item.pane.id === paneId ? { ...item, snapshot } : item
105
+ )
106
+ );
107
+ };
108
+
109
+ const toggleSelection = (id: string) => {
110
+ const newSet = new Set(selectedIds);
111
+ if (newSet.has(id)) {
112
+ newSet.delete(id);
113
+ } else {
114
+ newSet.add(id);
115
+ }
116
+ setSelectedIds(newSet);
117
+ };
118
+
119
+ const unusedCount = useMemo(() => {
120
+ return allPanes.filter((p) => isOrphan(p.id)).length;
121
+ }, [allPanes, orphanState.data]);
122
+
123
+ const handleSelectAllUnused = () => {
124
+ const newSet = new Set(selectedIds);
125
+ allPanes.forEach((p) => {
126
+ if (isOrphan(p.id)) {
127
+ newSet.add(p.id);
128
+ }
129
+ });
130
+ setSelectedIds(newSet);
131
+ };
132
+
133
+ const handleClearSelection = () => {
134
+ setSelectedIds(new Set());
135
+ };
136
+
137
+ const handleDelete = async () => {
138
+ if (selectedIds.size === 0) return;
139
+ if (
140
+ !confirm(
141
+ `Are you sure you want to delete ${selectedIds.size} pane(s)? This action cannot be undone.`
142
+ )
143
+ )
144
+ return;
145
+
146
+ setIsDeleting(true);
147
+ try {
148
+ const api = new TractStackAPI(tenantId);
149
+ const response = await api.request('/api/v1/nodes/panes/bulk', {
150
+ method: 'DELETE',
151
+ body: JSON.stringify({ paneIds: Array.from(selectedIds) }),
152
+ });
153
+
154
+ if (response.success) {
155
+ setSelectedIds(new Set());
156
+ onRefresh();
157
+ } else {
158
+ alert(`Failed to delete panes: ${response.error}`);
159
+ }
160
+ } catch (error) {
161
+ console.error('Delete failed:', error);
162
+ alert('An error occurred while deleting panes.');
163
+ } finally {
164
+ setIsDeleting(false);
165
+ }
166
+ };
167
+
168
+ return (
169
+ <div className="space-y-4">
170
+ {isDeleting && <DeletingModal count={selectedIds.size} />}
171
+
172
+ <div className="flex flex-wrap items-center justify-between gap-4 rounded-lg border border-gray-200 bg-gray-50 p-4">
173
+ <div className="flex items-center gap-6">
174
+ <div className="flex items-center gap-4">
175
+ <div className="text-sm text-gray-700">
176
+ {selectedIds.size} of {filteredPanes.length} Selected
177
+ </div>
178
+ {selectedIds.size > 0 && (
179
+ <button
180
+ onClick={handleClearSelection}
181
+ className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700"
182
+ >
183
+ <XMarkIcon className="h-3 w-3" />
184
+ Clear
185
+ </button>
186
+ )}
187
+ </div>
188
+
189
+ <div className="flex items-center gap-2 border-l border-gray-300 pl-6">
190
+ <span className="text-xs font-bold uppercase tracking-wider text-gray-500">
191
+ Filter:
192
+ </span>
193
+ <Toggle.Root
194
+ pressed={showOnlyUnused}
195
+ onPressedChange={setShowOnlyUnused}
196
+ className={`flex items-center gap-2 rounded-md border px-3 py-1.5 text-xs font-bold shadow-sm transition-all ${
197
+ showOnlyUnused
198
+ ? 'border-cyan-600 bg-cyan-600 text-white'
199
+ : 'border-gray-300 bg-white text-gray-600 hover:bg-gray-50'
200
+ }`}
201
+ >
202
+ Show Unused Only ({unusedCount})
203
+ </Toggle.Root>
204
+ </div>
205
+ </div>
206
+
207
+ <div className="flex items-center gap-3">
208
+ <button
209
+ onClick={handleSelectAllUnused}
210
+ className="flex items-center gap-1.5 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs text-gray-700 shadow-sm hover:bg-gray-50"
211
+ title={`Select all ${unusedCount} unused panes`}
212
+ >
213
+ <CheckCircleIcon className="h-4 w-4 text-green-600" />
214
+ Select All Unused
215
+ </button>
216
+
217
+ {selectedIds.size > 0 && (
218
+ <button
219
+ onClick={handleDelete}
220
+ disabled={isDeleting}
221
+ className="flex items-center gap-2 rounded-md bg-red-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-red-700 disabled:opacity-50"
222
+ >
223
+ <TrashIcon className="h-4 w-4" />
224
+ Delete ({selectedIds.size})
225
+ </button>
226
+ )}
227
+ </div>
228
+ </div>
229
+
230
+ {previews.length === 0 ? (
231
+ <div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-200 bg-white py-12">
232
+ <p className="text-gray-500">
233
+ No panes found matching the current filter.
234
+ </p>
235
+ </div>
236
+ ) : (
237
+ <div className="flex flex-wrap items-start justify-start gap-4 p-2">
238
+ {visiblePreviews.map((item) => {
239
+ const orphan = isOrphan(item.pane.id);
240
+ const isSelected = selectedIds.has(item.pane.id);
241
+
242
+ return (
243
+ <div
244
+ key={item.pane.id}
245
+ className={`relative flex min-w-72 flex-1 basis-1/4 flex-col rounded-lg border-2 bg-white shadow-sm transition-all ${
246
+ isSelected
247
+ ? 'border-cyan-600 ring-2 ring-cyan-100'
248
+ : 'border-transparent hover:border-gray-300'
249
+ }`}
250
+ >
251
+ <div className="absolute right-2 top-2 z-10">
252
+ {orphan ? (
253
+ <div className="flex h-7 w-7 items-center justify-center rounded-full bg-white shadow-md transition-transform hover:scale-110">
254
+ <input
255
+ type="checkbox"
256
+ checked={isSelected}
257
+ onChange={() => toggleSelection(item.pane.id)}
258
+ className="h-4 w-4 cursor-pointer rounded border-gray-300 text-cyan-600 focus:ring-cyan-500"
259
+ />
260
+ </div>
261
+ ) : (
262
+ <div
263
+ className="flex h-7 w-7 items-center justify-center rounded-full bg-gray-100 shadow-md"
264
+ title="This pane is in use and cannot be deleted"
265
+ >
266
+ <LockClosedIcon className="h-3.5 w-3.5 text-gray-400" />
267
+ </div>
268
+ )}
269
+ </div>
270
+
271
+ <div className="relative w-full overflow-hidden rounded-t-lg bg-gray-50">
272
+ {fragmentsLoading && !fragments[item.pane.id] && (
273
+ <div className="flex h-24 items-center justify-center text-gray-400">
274
+ <span className="text-xs">Loading...</span>
275
+ </div>
276
+ )}
277
+
278
+ {errors[item.pane.id] && (
279
+ <div className="flex h-24 items-center justify-center text-red-400">
280
+ <span className="text-xs">Preview Error</span>
281
+ </div>
282
+ )}
283
+
284
+ {fragments[item.pane.id] &&
285
+ !item.snapshot &&
286
+ !errors[item.pane.id] && (
287
+ <div className="max-h-32 overflow-hidden">
288
+ <PaneSnapshotGenerator
289
+ id={`table-${item.pane.id}`}
290
+ htmlString={fragments[item.pane.id]}
291
+ onComplete={handleSnapshotComplete}
292
+ outputWidth={400}
293
+ />
294
+ </div>
295
+ )}
296
+
297
+ {item.snapshot && (
298
+ <img
299
+ src={item.snapshot.imageData}
300
+ alt={item.pane.title}
301
+ className="max-h-32 w-full object-cover object-top"
302
+ />
303
+ )}
304
+ </div>
305
+
306
+ <div className="bg-gray-50 p-2">
307
+ <h4
308
+ className="truncate text-xs font-bold text-gray-900"
309
+ title={item.pane.title}
310
+ >
311
+ {item.pane.title}
312
+ </h4>
313
+ <div className="mt-0.5 flex items-center justify-between">
314
+ <span
315
+ className="truncate font-mono text-base text-gray-500"
316
+ title={item.pane.slug}
317
+ >
318
+ /{item.pane.slug}
319
+ </span>
320
+ {!orphan && (
321
+ <span className="rounded-full bg-green-100 px-1.5 py-0.5 text-base text-green-800">
322
+ In Use
323
+ </span>
324
+ )}
325
+ </div>
326
+ </div>
327
+ </div>
328
+ );
329
+ })}
330
+ </div>
331
+ )}
332
+
333
+ {totalPages > 1 && (
334
+ <div className="flex justify-center gap-2 pt-4">
335
+ <button
336
+ onClick={() => handlePageChange(currentPage - 1)}
337
+ disabled={currentPage === 0}
338
+ className="rounded border border-gray-300 bg-white px-3 py-1 text-sm disabled:opacity-50"
339
+ >
340
+ Previous
341
+ </button>
342
+ <span className="flex items-center text-sm text-gray-600">
343
+ Page {currentPage + 1} of {totalPages}
344
+ </span>
345
+ <button
346
+ onClick={() => handlePageChange(currentPage + 1)}
347
+ disabled={currentPage === totalPages - 1}
348
+ className="rounded border border-gray-300 bg-white px-3 py-1 text-sm disabled:opacity-50"
349
+ >
350
+ Next
351
+ </button>
352
+ </div>
353
+ )}
354
+ </div>
355
+ );
356
+ };
357
+
358
+ export default PaneTable;
@@ -162,7 +162,7 @@ export default function ResourceTable({
162
162
  <td className="px-6 py-4 text-sm text-gray-500">
163
163
  {(resource as any).oneliner || '-'}
164
164
  </td>
165
- <td className="sm:pr-6 relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-bold">
165
+ <td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-bold md:pr-6">
166
166
  <div className="flex items-center justify-end space-x-2">
167
167
  {/* Edit button */}
168
168
  <button
@@ -63,7 +63,7 @@
63
63
  ],
64
64
  "aiPaneStyleOnlyPrompt": {
65
65
  "system": "You are an expert **frontend developer**. Your task is to convert **raw Markdown** text into semantic HTML with Tailwind CSS classes based on a provided design theme. **CRITICAL: DO NOT REWRITE, SUMMARIZE, OR CHANGE THE TEXT CONTENT.** Your job is purely structural translation (Markdown -> HTML) and aesthetic formatting.",
66
- "user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must use. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nHere is the **RAW MARKDOWN** content you must style:\n\"{{COPY_INPUT}}\"\n\nCRITICAL RULES:\n1. **Parse the Markdown:** Convert headings (`##`, `###`) to tags (`<h2>`, `<h3>`), lists (`-`, `1.`) to (`<ul>`, `<ol>`), and formatting (`**`, `*`) to (`<strong>`, `<em>`). **DO NOT use `<h1>` tags.**\n2. **Apply Theme:** Add the Tailwind classes from the Shell's `defaultClasses` to the corresponding HTML tags.\n3. **Visual Rhythm:** You are responsible for the **inner layout and visual rhythm**. You MUST add appropriate vertical margins (e.g., `mt-4`, `mt-6`, `mt-8`) directly to any HTML block elements to ensure they do not touch.\n4. **Responsive Styles:** For responsive styles, you *must* only use `md:` and `xl:` prefixes.\n5. **Interactive Elements:** If the markdown contains a link `[text](url)` or a clear Call-to-Action phrase, style it as a `<button>` or styled `<a>` tag using the theme's button styles (if available) or high-contrast styling.\n6. **Block Wrapping:** **All text** must be wrapped in a block element like `<p>`, `<h2>`, `<h3>`, or `<li>`.\n7. **Contrast:** Verify that **all text elements** maintain **high contrast** against the `bgColour` provided in the `SHELL_JSON`. Prioritize readability.\n8. **OUTPUT ONLY THE RAW HTML.**"
66
+ "user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must use. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nHere is the **RAW MARKDOWN** content you must style:\n\"{{COPY_INPUT}}\"\n\nCRITICAL RULES:\n1. **Parse the Markdown:** Convert headings (`##`, `###`) to tags (`<h2>`, `<h3>`), lists (`-`, `1.`) to (`<ul>`, `<ol>`), and formatting (`**`, `*`) to (`<strong>`, `<em>`). **DO NOT use `<h1>` tags.**\n2. **Apply Theme:** Add the Tailwind classes from the Shell's `defaultClasses` to the corresponding HTML tags.\n3. **Visual Rhythm:** You are responsible for the **inner layout and visual rhythm**. You MUST add appropriate vertical margins (e.g., `mt-4`, `mt-6`, `mt-8`) directly to any HTML block elements to ensure they do not touch.\n4. **Responsive Styles:** For responsive styles, you *must* only use `md:` and `xl:` prefixes. DO NOT USE sm: or lg: modifiers. \n5. **Interactive Elements:** If the markdown contains a link `[text](url)` or a clear Call-to-Action phrase, style it as a `<button>` or styled `<a>` tag using the theme's button styles (if available) or high-contrast styling.\n6. **Block Wrapping:** **All text** must be wrapped in a block element like `<p>`, `<h2>`, `<h3>`, or `<li>`.\n7. **Contrast:** Verify that **all text elements** maintain **high contrast** against the `bgColour` provided in the `SHELL_JSON`. Prioritize readability.\n8. **OUTPUT ONLY THE RAW HTML.**"
67
67
  },
68
68
  "aiPaneShellPrompt": {
69
69
  "system": "You are an expert web designer. Your task is to generate the structural design for a component as a single JSON object. Respond *only* with the JSON.",
@@ -71,12 +71,12 @@
71
71
  },
72
72
  "aiPaneCopyPrompt": {
73
73
  "system": "You are an expert **web designer and copywriter**. Your task is to generate a single, visually compelling block of HTML content. You must ensure the content is well-written, engaging, **beautifully spaced**, and **highly readable**.",
74
- "user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must write your HTML for. Your HTML will be placed *inside* this shell. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nNow, generate the HTML content based on these inputs:\n\nContent Prompt: \"{{COPY_INPUT}}\"\nDesign Style: **strictly for visual reference** when choosing element styles **DO NOT** include any words or concepts from the `Design Style` input in the written copy text itself.\"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You are responsible for the **inner layout and visual rhythm**. You MUST add appropriate vertical margins (e.g., `mt-4`, `mt-6`, `mt-8`) directly to any HTML block elements that *deviate* from the default spacing. **Elements must not touch.**\n2. You **MUST NOT** use `<h1>` tags. You must use `<h2>`, `<h3>`, and `<p>` tags for all text content.\n3. For responsive styles, you *must* only use `md:` and `xl:` prefixes.\n4. To make headlines pop, you MUST wrap key words in `<span>` tags with creative classes (e.g., gradient text, different colors).\n5. **All text**, even short links or phrases (like 'Learn more →'), **must** be wrapped in a block element like `<p>` or `<button>`.\n6. You MUST include at least one `<button>` tag for the primary call-to-action.\n7. Verify that **all text elements**, including text within `<span>` tags, `<button>` elements, and any elements using override classes, maintain **high contrast** (meeting at least WCAG AA standards - 4.5:1 for normal text, 3:1 for large text) against the `bgColour` provided in the `SHELL_JSON`. **Prioritize readability above all else**.\n8. Respond *only* with the raw HTML.\n\nEXAMPLE of a good, well-spaced response:\n<h2 class=\"text-4xl font-bold tracking-tight text-white md:text-6xl\"><span class=\"bg-gradient-to-r from-purple-500 to-indigo-400 bg-clip-text text-transparent\">Own the Art.</span> Possess the Reality.</h2>\n<p class=\"mt-6 text-lg leading-8 text-gray-300 md:text-xl\">Every Sneaky Productions NFT is your key. This is where digital rarity meets tangible legacy.</p>\n<button class=\"mt-8 rounded-md bg-indigo-600 px-5 py-3 text-base font-bold text-white shadow-sm hover:bg-indigo-500\">Secure Your Drop</button>\n<p class=\"mt-4 text-sm text-gray-400\">Learn more <span>→</span></p>",
75
- "heroDefault": "A compelling hero section for a website about [topic]. It should have a strong, attention-grabbing headline, a brief paragraph explaining the core value proposition, and a clear call-to-action.",
76
- "contentDefault": "A content section that follows a hero. It should elaborate on a key feature or benefit related to [topic]. Include a sub-headline and a descriptive paragraph.",
77
- "articleIntro": "A powerful introductory paragraph (Lede) for an article about [topic]. Use large, legible typography (e.g. text-xl or text-2xl) to hook the reader immediately.",
78
- "articleBody": "A semantic article body section about [topic]. Use H3 subheadings, prose-style paragraphs, and unordered lists to structure the content for high readability.",
79
- "sectionHeader": "A distinct section header (sub-hero) for [topic]. Use a high-contrast background, a prominent H2 title, and a short descriptive lead paragraph to act as a visual divider."
74
+ "user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must write your HTML for. Your HTML will be placed *inside* this shell. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nNow, generate the HTML content based on these inputs:\n\nContent Prompt: \"{{COPY_INPUT}}\"\nDesign Style: **strictly for visual reference** when choosing element styles **DO NOT** include any words or concepts from the `Design Style` input in the written copy text itself.\"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You are responsible for the **inner layout and visual rhythm**. You MUST add appropriate vertical margins (e.g., `mt-4`, `mt-6`, `mt-8`) directly to any HTML block elements that *deviate* from the default spacing. **Elements must not touch.**\n2. You **MUST NOT** use `<h1>` tags. You must use `<h2>`, `<h3>`, and `<p>` tags for all text content.\n3. For responsive styles, you *must* only use `md:` and `xl:` prefixes. DO NOT USE sm: or lg: modifiers. \n4. To make headlines pop, you MUST wrap key words in `<span>` tags with creative classes (e.g., gradient text, different colors).\n5. **All text**, even short links or phrases (like 'Learn more →'), **must** be wrapped in a block element like `<p>` or `<button>`.\n6. You MUST include at least one `<button>` tag for the primary call-to-action.\n7. Verify that **all text elements**, including text within `<span>` tags, `<button>` elements, and any elements using override classes, maintain **high contrast** (meeting at least WCAG AA standards - 4.5:1 for normal text, 3:1 for large text) against the `bgColour` provided in the `SHELL_JSON`. **Prioritize readability above all else**.\n8. Respond *only* with the raw HTML.\n\nEXAMPLE of a good, well-spaced response:\n<h2 class=\"text-4xl font-bold tracking-tight text-white md:text-6xl\"><span class=\"bg-gradient-to-r from-purple-500 to-indigo-400 bg-clip-text text-transparent\">Own the Art.</span> Possess the Reality.</h2>\n<p class=\"mt-6 text-lg leading-8 text-gray-300 md:text-xl\">Every Sneaky Productions NFT is your key. This is where digital rarity meets tangible legacy.</p>\n<button class=\"mt-8 rounded-md bg-indigo-600 px-5 py-3 text-base font-bold text-white shadow-sm hover:bg-indigo-500\">Secure Your Drop</button>\n<p class=\"mt-4 text-sm text-gray-400\">Learn more <span>→</span></p>",
75
+ "heroDefault": "A compelling hero section for a website about {{TOPIC}}. It should have a strong, attention-grabbing headline, a brief paragraph explaining the core value proposition, and a clear call-to-action.",
76
+ "contentDefault": "A content section that follows a hero. It should elaborate on a key feature or benefit related to {{TOPIC}}. Include a sub-headline and a descriptive paragraph.",
77
+ "articleIntro": "A powerful introductory paragraph (Lede) for an article about {{TOPIC}}. Use large, legible typography (e.g. text-xl or text-2xl) to hook the reader immediately.",
78
+ "articleBody": "A semantic article body section about {{TOPIC}}. Use H3 subheadings, prose-style paragraphs, and unordered lists to structure the content for high readability.",
79
+ "sectionHeader": "A distinct section header (sub-hero) for {{TOPIC}}. Use a high-contrast background, a prominent H2 title, and a short descriptive lead paragraph to act as a visual divider."
80
80
  },
81
81
  "aiPaneShellPrompt_2cols": {
82
82
  "system": "You are an expert web designer. Your task is to generate the structural design for a component as a single JSON object. Respond *only* with the JSON.",
@@ -84,19 +84,27 @@
84
84
  },
85
85
  "aiPaneCopyPrompt_2cols": {
86
86
  "system": "You are an expert **web designer and copywriter**. Your task is to generate a single, visually compelling block of HTML content. You must ensure the content is well-written, engaging, **beautifully spaced**, and **highly readable**.",
87
- "user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must write your HTML for. Your HTML will be placed *inside* a column defined by this shell. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nNow, generate the HTML content based on these inputs. Your primary goal is to perform the following specific task for this single column. Use the \"Overall Component Brief\" only for context about the product or topic.\n\nSPECIFIC TASK: \"{{COLUMN_PROMPT}}\"\nOverall Component Brief (for context only): \"{{COPY_INPUT}}\"\nDesign Style: **strictly for visual reference** when choosing element styles **DO NOT** include any words or concepts from the `Design Style` input in the written copy text itself.\"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You are responsible for the **inner layout and visual rhythm**. Each element should be a separate block element.\n2. You **MUST NOT** use `<h1>` tags. You must use `<h2>`, `<h3>`, and `<p>` tags for all text content.\n3. For responsive styles, you *must* only use `md:` and `xl:` prefixes.\n4. To make headlines pop, you MUST wrap key words in `<span>` tags with creative classes (e.g., gradient text, different colors).\n5. **All text, including secondary links, must** be wrapped in a block element like `<p>` or `<button>`. Even when placing CTAs side-by-side in a `div`, this rule MUST be followed.\n6. Verify that **all text elements**, including text within `<span>` tags, `<button>` elements, and any elements using override classes, maintain **high contrast** (meeting at least WCAG AA standards - 4.5:1 for normal text, 3:1 for large text) against the `bgColour` provided in the `SHELL_JSON`. **Prioritize readability above all else**.\n7. Respond *only* with the raw HTML.\n\nEXAMPLE of a good response for THIS SPECIFIC TASK:\n{{COLUMN_EXAMPLE}}",
87
+ "user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must write your HTML for. Your HTML will be placed *inside* a column defined by this shell. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nNow, generate the HTML content based on these inputs. Your primary goal is to perform the following specific task for this single column. Use the \"Overall Component Brief\" only for context about the product or topic.\n\nSPECIFIC TASK: \"{{COLUMN_PROMPT}}\"\nOverall Component Brief (for context only): \"{{COPY_INPUT}}\"\nDesign Style: **strictly for visual reference** when choosing element styles **DO NOT** include any words or concepts from the `Design Style` input in the written copy text itself.\"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You are responsible for the **inner layout and visual rhythm**. Each element should be a separate block element.\n2. You **MUST NOT** use `<h1>` tags. You must use `<h2>`, `<h3>`, and `<p>` tags for all text content.\n3. For responsive styles, you *must* only use `md:` and `xl:` prefixes. DO NOT USE sm: or lg: modifiers. \n4. To make headlines pop, you MUST wrap key words in `<span>` tags with creative classes (e.g., gradient text, different colors).\n5. **All text, including secondary links, must** be wrapped in a block element like `<p>` or `<button>`. Even when placing CTAs side-by-side in a `div`, this rule MUST be followed.\n6. Verify that **all text elements**, including text within `<span>` tags, `<button>` elements, and any elements using override classes, maintain **high contrast** (meeting at least WCAG AA standards - 4.5:1 for normal text, 3:1 for large text) against the `bgColour` provided in the `SHELL_JSON`. **Prioritize readability above all else**.\n7. Respond *only* with the raw HTML.\n\nEXAMPLE of a good response for THIS SPECIFIC TASK:\n{{COLUMN_EXAMPLE}}",
88
88
  "presets": {
89
89
  "heroDefault": {
90
- "default": "A compelling hero section for a website about [topic]. It should have a strong, attention-grabbing headline, a brief paragraph explaining the core value proposition, and a clear call-to-action.",
90
+ "default": "A compelling hero section for a website about {{TOPIC}}. It should have a strong, attention-grabbing headline, a brief paragraph explaining the core value proposition, and a clear call-to-action.",
91
91
  "left": {
92
92
  "prompt": "Generate a slogan with very large typography. Just the slogan in this column.",
93
93
  "example": "<h2 class=\"text-4xl font-bold tracking-tight text-slate-100 md:text-5xl xl:text-6xl\">\n<span class=\"block\">Orchestrate Complexity.</span>\n<span class=\"block bg-gradient-to-r from-sky-400 to-cyan-300 bg-clip-text text-transparent md:mt-2\">Deploy with Certainty.</span>\n</h2>"
94
94
  },
95
95
  "right": {
96
96
  "prompt": "Generate a small sub-heading, a brief compelling paragraph of hook text, and two vertically stacked call-to-action buttons below it.",
97
- "example": "<h3 class=\"text-xl font-bold text-white\">Join us this week</h3>\n<p class=\"mt-4 text-lg leading-8 text-gray-300\">We're all about creating a place where anyone and everyone can know God & encounter Him.</p>\n<button class=\"mt-8 rounded-md bg-white px-4 py-3 text-base font-semibold text-gray-900 shadow-sm hover:bg-gray-100 w-full\">Plan a Visit</button>\n<button class=\"mt-4 rounded-md border border-white px-4 py-3 text-base font-semibold text-white shadow-sm hover:bg-white/10 w-full\">Get Involved</button>"
97
+ "example": "<h3 class=\"text-xl font-bold text-white\">Join us this week</h3>\n<p class=\"mt-4 text-lg leading-8 text-gray-300\">We're all about creating a place where anyone and everyone can know God & encounter Him.</p>\n<button class=\"mt-8 rounded-md bg-white px-4 py-3 text-base font-bold text-gray-900 shadow-sm hover:bg-gray-100 w-full\">Plan a Visit</button>\n<button class=\"mt-4 rounded-md border border-white px-4 py-3 text-base font-bold text-white shadow-sm hover:bg-white/10 w-full\">Get Involved</button>"
98
98
  }
99
99
  }
100
100
  }
101
+ },
102
+ "aiPaneCreativePrompt": {
103
+ "system": "You are an expert **Web Designer and Copywriter**. You specialize in building beautiful, high-converting page sections using Tailwind CSS. You combine striking visual layouts with engaging, persuasive copy. Your code is clean, semantic, and accessible.",
104
+ "user_template": "Design and code a **full-width, responsive page section** based on the brief below.\n\n**CONTENT BRIEF:** \"{{TOPIC}}\"\n**DESIGN DIRECTION:** \"{{DESIGN_NOTES}}\"\n\n**ARCHITECTURAL STANDARDS:**\n1. **Full-Width Root:** The outermost element MUST be a semantic tag (`<section>` or `<div>`) with the class `w-full` (and optional background colors). It must NOT have a max-width.\n2. **Inner Container:** All text and content MUST be wrapped in an inner container (e.g., `mx-auto max-w-7xl px-6 xl:px-8`) to ensuring readability on large screens while the background spans the full width.\n3. **Responsive Flow:** Design mobile-first. Use `md:` (tablet) and `xl:` (desktop) prefixes to switch from vertical stacks to grids or side-by-side layouts. DO NOT USE sm: or lg: modifiers. \n4. **Visual Rhythm:** Use generous vertical spacing (e.g., `py-24`, `gap-12`) to create a professional, airy feel. Elements should never feel cramped.\n\n**TECHNICAL CONSTRAINTS:**\n- **Output:** Return ONLY the raw HTML string. **NO Markdown backticks** (strictly forbidden).\n- **Scope:** No `<html>`, `<body>`, or `<script>` tags. Just the section markup.\n- **Assets:** Use `src=\"/static.jpg\"` for all images. Use `href=\"#\"` for all links.\n- **Interactivity:** Use `group` and `group-hover` classes for interactive states.\n\n**GENERATE HTML:**"
105
+ },
106
+ "aiPaneCreativeRefinePrompt": {
107
+ "system": "You are an expert **Web Designer**. Your task is to refine an existing web component based on the user's instructions. You must interpret the provided 'Current Context' (HTML+CSS) and output the updated version using **Standard HTML + Tailwind CSS**. CRITICAL: You MUST preserve the data-ast-id attribute on any element that contains it. Do not remove or alter this attribute.",
108
+ "user_template": "Refine the component below.\n\n**REFINEMENT REQUEST:** \"{{DESIGN_NOTES}}\"\n\n**CURRENT CONTEXT (For Reference):**\n<style>\n{{CSS_INPUT}}\n</style>\n{{HTML_INPUT}}\n\n**OUTPUT STANDARDS:**\n1. **Format:** Generate **standard HTML** tags. Do NOT use the hashed classes (e.g. `t8k-xxx`) from the context in your output.\n2. **Styling:** Re-implement the styles (with your refinements) using **Tailwind CSS utility classes**. Use `md:` and `xl:` prefixes for responsiveness. DO NOT USE sm: or lg: modifiers. \n3. **Custom Values:** For specific colors or values not available in standard Tailwind, use **inline `style=\"...\"` attributes**.\n4. **Result:** Return **ONLY** the raw HTML string. No backticks. No `<style>` blocks."
101
109
  }
102
110
  }
@@ -142,6 +142,7 @@ export const toolAddModes = [
142
142
  'identify',
143
143
  'toggle',
144
144
  'interactiveDisclosure',
145
+ 'span',
145
146
  //"aside",
146
147
  ] as const;
147
148
 
@@ -161,6 +162,7 @@ export const toolAddModeTitles: Record<ToolAddMode, string> = {
161
162
  identify: 'Identity As',
162
163
  toggle: 'Toggle Belief',
163
164
  interactiveDisclosure: 'Interactive Disclosure',
165
+ span: 'Creative Span',
164
166
  //aside: "Aside Text",
165
167
  };
166
168
 
@@ -177,6 +179,7 @@ export const toolAddModesIcons: Record<ToolAddMode, string> = {
177
179
  toggle: '',
178
180
  belief: '',
179
181
  interactiveDisclosure: '',
182
+ span: '',
180
183
  //aside: "",
181
184
  };
182
185
 
@@ -0,0 +1,60 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { TractStackAPI } from '@/utils/api';
3
+
4
+ export const usePaneFragments = (paneIds: string[]) => {
5
+ const [fragments, setFragments] = useState<Record<string, string>>({});
6
+ const [errors, setErrors] = useState<Record<string, string>>({});
7
+ const [isLoading, setIsLoading] = useState(false);
8
+ const loadedIds = useRef<Set<string>>(new Set());
9
+
10
+ const tenantId =
11
+ (typeof window !== 'undefined' && window.TRACTSTACK_CONFIG?.tenantId) ||
12
+ import.meta.env.PUBLIC_TENANTID ||
13
+ 'default';
14
+
15
+ useEffect(() => {
16
+ // Identify IDs that haven't been loaded yet
17
+ const neededIds = paneIds.filter((id) => !loadedIds.current.has(id));
18
+
19
+ if (neededIds.length === 0) return;
20
+
21
+ let isMounted = true;
22
+
23
+ const fetchFragments = async () => {
24
+ setIsLoading(true);
25
+ try {
26
+ const api = new TractStackAPI(tenantId);
27
+ const response = await api.post('/api/v1/fragments/panes', {
28
+ paneIds: neededIds,
29
+ });
30
+
31
+ if (!isMounted) return;
32
+
33
+ if (response.success && response.data) {
34
+ const newFragments = response.data.fragments || {};
35
+ const newErrors = response.data.errors || {};
36
+
37
+ // Update cache refs
38
+ Object.keys(newFragments).forEach((id) => loadedIds.current.add(id));
39
+ Object.keys(newErrors).forEach((id) => loadedIds.current.add(id));
40
+
41
+ setFragments((prev) => ({ ...prev, ...newFragments }));
42
+ setErrors((prev) => ({ ...prev, ...newErrors }));
43
+ }
44
+ } catch (error) {
45
+ console.error('Failed to fetch fragments:', error);
46
+ } finally {
47
+ if (isMounted) setIsLoading(false);
48
+ }
49
+ };
50
+
51
+ fetchFragments();
52
+
53
+ return () => {
54
+ isMounted = false;
55
+ };
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, [JSON.stringify(paneIds), tenantId]);
58
+
59
+ return { fragments, errors, isLoading };
60
+ };