astro-tractstack 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) 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/custom/minimal/CodeHook.astro +1 -0
  7. package/templates/custom/with-examples/CodeHook.astro +1 -0
  8. package/templates/icons/code.svg +18 -0
  9. package/templates/icons/li.svg +4 -0
  10. package/templates/icons/link.svg +22 -0
  11. package/templates/icons/p.svg +3 -0
  12. package/templates/src/client/app.js +80 -1
  13. package/templates/src/components/Footer.astro +1 -1
  14. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +6 -6
  15. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +3 -3
  16. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +1 -1
  17. package/templates/src/components/codehooks/ListContentSetup.tsx +2 -2
  18. package/templates/src/components/codehooks/ProductCardSetup.tsx +1 -1
  19. package/templates/src/components/codehooks/ProductGridSetup.tsx +2 -2
  20. package/templates/src/components/codehooks/SandboxRegisterForm.tsx +3 -3
  21. package/templates/src/components/compositor/Compositor.tsx +25 -9
  22. package/templates/src/components/compositor/Node.tsx +168 -496
  23. package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +1 -0
  24. package/templates/src/components/compositor/elements/SignUp.tsx +1 -1
  25. package/templates/src/components/compositor/elements/YouTubeWrapper.tsx +2 -0
  26. package/templates/src/components/compositor/nodes/CreativePane.tsx +262 -0
  27. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +4 -6
  28. package/templates/src/components/compositor/nodes/GridLayout.tsx +4 -2
  29. package/templates/src/components/compositor/nodes/Markdown.tsx +18 -3
  30. package/templates/src/components/compositor/nodes/Pane.tsx +11 -5
  31. package/templates/src/components/compositor/nodes/RenderChildren.tsx +1 -1
  32. package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +5 -5
  33. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +90 -42
  34. package/templates/src/components/compositor/nodes/tagElements/NodeImg.tsx +2 -0
  35. package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +27 -1
  36. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +10 -8
  37. package/templates/src/components/compositor/tools/NodeOverlay.tsx +224 -0
  38. package/templates/src/components/compositor/tools/PaneOverlay.tsx +122 -0
  39. package/templates/src/components/edit/Header.tsx +68 -9
  40. package/templates/src/components/edit/PanelSwitch.tsx +42 -4
  41. package/templates/src/components/edit/SettingsPanel.tsx +2 -3
  42. package/templates/src/components/edit/ToolMode.tsx +1 -31
  43. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +2 -2
  44. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +1 -1
  45. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +193 -659
  46. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +15 -82
  47. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +95 -45
  48. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +137 -49
  49. package/templates/src/components/edit/pane/RestylePaneModal.tsx +1 -1
  50. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +375 -0
  51. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +1 -23
  52. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +327 -0
  53. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +267 -0
  54. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +371 -0
  55. package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +201 -76
  56. package/templates/src/components/edit/pane/steps/CreativeInjectStep.tsx +141 -0
  57. package/templates/src/components/edit/panels/CreativeImagePanel.tsx +435 -0
  58. package/templates/src/components/edit/panels/CreativeLinkPanel.tsx +110 -0
  59. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +1 -1
  60. package/templates/src/components/edit/panels/StyleParentPanel.tsx +118 -126
  61. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +3 -2
  62. package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +1 -0
  63. package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +3 -1
  64. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +3 -1
  65. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +1 -1
  66. package/templates/src/components/edit/state/SaveModal.tsx +19 -787
  67. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +2 -2
  68. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +1 -1
  69. package/templates/src/components/edit/widgets/BunnyWidget.tsx +5 -5
  70. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +1 -1
  71. package/templates/src/components/edit/widgets/SignupWidget.tsx +1 -1
  72. package/templates/src/components/fields/ActionBuilderTimeSelector.tsx +1 -1
  73. package/templates/src/components/fields/ArtpackImage.tsx +11 -3
  74. package/templates/src/components/fields/BackgroundImage.tsx +8 -0
  75. package/templates/src/components/fields/BackgroundImageWrapper.tsx +15 -9
  76. package/templates/src/components/fields/ImageUpload.tsx +6 -0
  77. package/templates/src/components/form/ActionBuilderField.tsx +15 -5
  78. package/templates/src/components/form/ActionBuilderSlugSelector.tsx +1 -1
  79. package/templates/src/components/form/ColorPicker.tsx +1 -1
  80. package/templates/src/components/form/EnumSelect.tsx +1 -1
  81. package/templates/src/components/form/NumberInput.tsx +1 -1
  82. package/templates/src/components/form/StringArrayInput.tsx +1 -1
  83. package/templates/src/components/form/StringInput.tsx +1 -1
  84. package/templates/src/components/form/UnsavedChangesBar.tsx +1 -1
  85. package/templates/src/components/form/advanced/APIConfigSection.tsx +2 -2
  86. package/templates/src/components/form/advanced/AuthConfigSection.tsx +2 -2
  87. package/templates/src/components/profile/ProfileCreate.tsx +1 -1
  88. package/templates/src/components/profile/ProfileEdit.tsx +1 -1
  89. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +2 -2
  90. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +1 -1
  91. package/templates/src/components/storykeep/controls/content/ContentSummary.tsx +2 -2
  92. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +1 -1
  93. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +6 -6
  94. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +1 -1
  95. package/templates/src/components/storykeep/controls/content/PaneTable.tsx +358 -0
  96. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -1
  97. package/templates/src/constants/prompts.json +18 -10
  98. package/templates/src/constants.ts +3 -0
  99. package/templates/src/hooks/usePaneFragments.ts +60 -0
  100. package/templates/src/lib/session.ts +71 -16
  101. package/templates/src/pages/[...slug].astro +5 -46
  102. package/templates/src/pages/api/css.ts +149 -0
  103. package/templates/src/pages/context/[...contextSlug].astro +1 -0
  104. package/templates/src/pages/maint.astro +1 -1
  105. package/templates/src/pages/storykeep/login.astro +2 -2
  106. package/templates/src/stores/nodes.ts +162 -49
  107. package/templates/src/stores/orphanAnalysis.ts +6 -30
  108. package/templates/src/stores/previews.ts +7 -0
  109. package/templates/src/stores/storykeep.ts +0 -8
  110. package/templates/src/types/compositorTypes.ts +53 -10
  111. package/templates/src/utils/compositor/aiGeneration.ts +93 -0
  112. package/templates/src/utils/compositor/allowInsert.ts +2 -0
  113. package/templates/src/utils/compositor/htmlAst.ts +704 -0
  114. package/templates/src/utils/compositor/nodesHelper.ts +281 -102
  115. package/templates/src/utils/compositor/savePipeline.ts +893 -0
  116. package/templates/src/utils/etl/index.ts +3 -0
  117. package/templates/src/utils/etl/transformer.ts +10 -0
  118. package/templates/src/utils/helpers.ts +101 -0
  119. package/utils/inject-files.ts +100 -62
  120. package/templates/icons/text.svg +0 -6
  121. package/templates/src/components/compositor/NodeWithGuid.tsx +0 -69
  122. package/templates/src/components/compositor/nodes/GridLayout_eraser.tsx +0 -33
  123. package/templates/src/components/compositor/nodes/Markdown_eraser.tsx +0 -56
  124. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +0 -269
  125. package/templates/src/components/compositor/nodes/Pane_eraser.tsx +0 -186
  126. package/templates/src/components/compositor/nodes/Pane_layout.tsx +0 -79
  127. package/templates/src/components/compositor/nodes/tagElements/NodeA_eraser.tsx +0 -26
  128. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_eraser.tsx +0 -61
  129. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_insert.tsx +0 -120
  130. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_settings.tsx +0 -62
  131. package/templates/src/components/compositor/nodes/tagElements/NodeButton_eraser.tsx +0 -26
@@ -0,0 +1,435 @@
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import { Combobox } from '@ark-ui/react';
3
+ import { createListCollection } from '@ark-ui/react/collection';
4
+ import { Dialog } from '@ark-ui/react/dialog';
5
+ import { Portal } from '@ark-ui/react/portal';
6
+ import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
7
+ import SwatchIcon from '@heroicons/react/24/outline/SwatchIcon';
8
+ import ArrowUpTrayIcon from '@heroicons/react/24/outline/ArrowUpTrayIcon';
9
+ import ChevronUpDownIcon from '@heroicons/react/24/outline/ChevronUpDownIcon';
10
+ import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
11
+ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
12
+ import { getCtx } from '@/stores/nodes';
13
+ import { hasArtpacksStore } from '@/stores/storykeep';
14
+ import ImageUpload, { type ImageParams } from '@/components/fields/ImageUpload';
15
+ import type { BasePanelProps, PaneNode } from '@/types/compositorTypes';
16
+
17
+ interface CreativeImagePanelProps extends BasePanelProps {
18
+ mode: 'img' | 'bg';
19
+ }
20
+
21
+ const CreativeImagePanel = ({
22
+ node,
23
+ childId,
24
+ mode,
25
+ }: CreativeImagePanelProps) => {
26
+ if (!node) return null;
27
+ const ctx = getCtx();
28
+ const paneNode = node as unknown as PaneNode;
29
+ const assetMeta = paneNode?.htmlAst?.editableElements?.[childId || ''];
30
+ const $artpacks = hasArtpacksStore.get();
31
+
32
+ const [activeTab, setActiveTab] = useState<'upload' | 'artpack'>('upload');
33
+ const [altDescription, setAltDescription] = useState('');
34
+ const [isExternal, setIsExternal] = useState(false);
35
+
36
+ const [isArtpackModalOpen, setIsArtpackModalOpen] = useState(false);
37
+ const [selectedCollection, setSelectedCollection] = useState<string>('t8k');
38
+ const [availableImages, setAvailableImages] = useState<string[]>([]);
39
+ const [isLoading, setIsLoading] = useState(false);
40
+ const [query, setQuery] = useState('');
41
+
42
+ useEffect(() => {
43
+ if (assetMeta) {
44
+ setAltDescription(assetMeta.alt || '');
45
+ const src = assetMeta.src || '';
46
+ setIsExternal(
47
+ src.startsWith('http') && !src.includes(window.location.origin)
48
+ );
49
+
50
+ if (src.includes('/artpacks/')) {
51
+ setActiveTab('artpack');
52
+ }
53
+ }
54
+ }, [assetMeta]);
55
+
56
+ useEffect(() => {
57
+ if (selectedCollection && $artpacks && $artpacks[selectedCollection]) {
58
+ setIsLoading(true);
59
+ const images = $artpacks[selectedCollection];
60
+ setAvailableImages(images);
61
+ setTimeout(() => setIsLoading(false), 0);
62
+ } else {
63
+ setAvailableImages([]);
64
+ setIsLoading(false);
65
+ }
66
+ }, [selectedCollection, $artpacks]);
67
+
68
+ const collectionList = useMemo(() => {
69
+ const filteredCollections =
70
+ query === ''
71
+ ? Object.keys($artpacks || {})
72
+ : Object.keys($artpacks || {}).filter((collection) =>
73
+ collection.toLowerCase().includes(query.toLowerCase())
74
+ );
75
+
76
+ return createListCollection({
77
+ items: filteredCollections,
78
+ itemToValue: (item) => item,
79
+ itemToString: (item) => item,
80
+ });
81
+ }, [$artpacks, query]);
82
+
83
+ if (!paneNode || !assetMeta) return null;
84
+
85
+ const handleUpdate = (params: Partial<ImageParams>) => {
86
+ const newSrc = params.src || '';
87
+ const isNowExternal =
88
+ newSrc.startsWith('http') && !newSrc.includes(window.location.origin);
89
+ setIsExternal(isNowExternal);
90
+
91
+ const el = document.querySelector(
92
+ `[data-ast-id="${childId}"]`
93
+ ) as HTMLElement;
94
+ if (el) {
95
+ if (mode === 'img') {
96
+ const img = el as HTMLImageElement;
97
+ img.src = newSrc;
98
+ if (params.srcSet) img.srcset = params.srcSet;
99
+ } else if (mode === 'bg') {
100
+ el.style.backgroundImage = `url('${newSrc}')`;
101
+ }
102
+ }
103
+
104
+ (ctx as any).updateCreativeAsset(paneNode.id, childId, {
105
+ src: params.src,
106
+ srcSet: params.srcSet,
107
+ fileId: params.fileId,
108
+ base64Data: params.base64Data,
109
+ alt: params.altDescription || altDescription,
110
+ isCssBackground: mode === 'bg',
111
+ tagName: mode === 'img' ? 'img' : assetMeta.tagName,
112
+ collection: params.collection,
113
+ image: params.image,
114
+ });
115
+ };
116
+
117
+ const handleRemove = () => {
118
+ setIsExternal(false);
119
+
120
+ const el = document.querySelector(
121
+ `[data-ast-id="${childId}"]`
122
+ ) as HTMLElement;
123
+ if (el) {
124
+ if (mode === 'img') {
125
+ (el as HTMLImageElement).src = '';
126
+ (el as HTMLImageElement).srcset = '';
127
+ } else if (mode === 'bg') {
128
+ el.style.backgroundImage = 'none';
129
+ }
130
+ }
131
+
132
+ (ctx as any).updateCreativeAsset(paneNode.id, childId, {
133
+ src: '',
134
+ fileId: undefined,
135
+ base64Data: undefined,
136
+ srcSet: undefined,
137
+ });
138
+ };
139
+
140
+ const handleAltBlur = (val: string) => {
141
+ if (val !== assetMeta.alt) {
142
+ const el = document.querySelector(
143
+ `[data-ast-id="${childId}"]`
144
+ ) as HTMLImageElement;
145
+ if (el && mode === 'img') {
146
+ el.alt = val;
147
+ }
148
+
149
+ (ctx as any).updateCreativeAsset(paneNode.id, childId, {
150
+ alt: val,
151
+ });
152
+ }
153
+ };
154
+
155
+ const buildImageSrcSet = (collection: string, image: string): string => {
156
+ return [
157
+ `/artpacks/${collection}/${image}_1920px.webp 1920w`,
158
+ `/artpacks/${collection}/${image}_1080px.webp 1080w`,
159
+ `/artpacks/${collection}/${image}_600px.webp 600w`,
160
+ ].join(', ');
161
+ };
162
+
163
+ const handleSelectArtpackImage = (collection: string, image: string) => {
164
+ const src = `/artpacks/${collection}/${image}_1920px.webp`;
165
+ const srcSet = buildImageSrcSet(collection, image);
166
+ const alt = `Artpack image from ${collection} collection`;
167
+
168
+ handleUpdate({
169
+ src,
170
+ srcSet,
171
+ fileId: undefined,
172
+ altDescription: alt,
173
+ collection: collection,
174
+ image: image,
175
+ });
176
+ setAltDescription(alt);
177
+ setIsArtpackModalOpen(false);
178
+ };
179
+
180
+ const handleCollectionSelect = (details: { value: string[] }) => {
181
+ const newCollection = details.value[0] || '';
182
+ if (newCollection) {
183
+ setIsLoading(true);
184
+ setSelectedCollection(newCollection);
185
+ }
186
+ };
187
+
188
+ const comboboxItemStyles = `
189
+ .collection-item[data-highlighted] {
190
+ background-color: #0891b2;
191
+ color: white;
192
+ }
193
+ .collection-item[data-highlighted] .collection-indicator {
194
+ color: white;
195
+ }
196
+ .collection-item[data-state="checked"] .collection-indicator {
197
+ display: flex;
198
+ }
199
+ .collection-item .collection-indicator {
200
+ display: none;
201
+ }
202
+ .collection-item[data-state="checked"] {
203
+ font-weight: bold;
204
+ }
205
+ `;
206
+
207
+ return (
208
+ <div className="space-y-6">
209
+ <style>{comboboxItemStyles}</style>
210
+ <div className="space-y-2">
211
+ <h3 className="text-lg font-bold text-mydarkgrey">
212
+ {mode === 'bg' ? 'Background Image' : 'Image Asset'}
213
+ </h3>
214
+ <p className="text-xs text-gray-500">
215
+ {mode === 'bg'
216
+ ? 'Updates the CSS background rule for this element.'
217
+ : 'Updates the source attribute of this image tag.'}
218
+ </p>
219
+ </div>
220
+
221
+ <div className="flex gap-2 border-b border-gray-200">
222
+ <button
223
+ onClick={() => setActiveTab('upload')}
224
+ className={`flex items-center gap-2 border-b-2 px-3 py-2 text-sm ${
225
+ activeTab === 'upload'
226
+ ? 'border-cyan-600 text-cyan-700'
227
+ : 'border-transparent text-gray-500 hover:text-gray-700'
228
+ }`}
229
+ >
230
+ <ArrowUpTrayIcon className="h-4 w-4" />
231
+ Upload / File
232
+ </button>
233
+ <button
234
+ onClick={() => setActiveTab('artpack')}
235
+ className={`flex items-center gap-2 border-b-2 px-3 py-2 text-sm ${
236
+ activeTab === 'artpack'
237
+ ? 'border-cyan-600 text-cyan-700'
238
+ : 'border-transparent text-gray-500 hover:text-gray-700'
239
+ }`}
240
+ >
241
+ <SwatchIcon className="h-4 w-4" />
242
+ Artpack
243
+ </button>
244
+ </div>
245
+
246
+ <div className="space-y-4">
247
+ {activeTab === 'upload' ? (
248
+ <ImageUpload
249
+ currentFileId={assetMeta.fileId}
250
+ nodeId={paneNode.id}
251
+ onUpdate={(params) => handleUpdate(params)}
252
+ onRemove={handleRemove}
253
+ />
254
+ ) : (
255
+ <div className="space-y-4">
256
+ <div className="rounded-md border border-gray-200 p-4 text-center">
257
+ <p className="mb-3 text-sm text-gray-600">
258
+ Choose a high-quality, optimized image from the library.
259
+ </p>
260
+ <button
261
+ onClick={() => setIsArtpackModalOpen(true)}
262
+ className="inline-flex items-center rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white hover:bg-cyan-700"
263
+ >
264
+ <SwatchIcon className="mr-2 h-4 w-4" />
265
+ Browse Artpacks
266
+ </button>
267
+ </div>
268
+ {assetMeta.src && assetMeta.src.includes('/artpacks/') && (
269
+ <div className="text-xs text-gray-500">
270
+ Current: {assetMeta.src.split('/').pop()}
271
+ </div>
272
+ )}
273
+ </div>
274
+ )}
275
+
276
+ {isExternal && (
277
+ <div className="flex items-start gap-2 rounded-md bg-yellow-50 p-3 text-xs text-yellow-800">
278
+ <ExclamationTriangleIcon className="mt-0.5 h-4 w-4 flex-shrink-0 text-yellow-600" />
279
+ <p>
280
+ You are using an external image URL. This asset will not be
281
+ optimized for performance or responsive sizing.
282
+ </p>
283
+ </div>
284
+ )}
285
+
286
+ {mode === 'img' && (
287
+ <div>
288
+ <label className="mb-1 block text-sm font-bold text-mydarkgrey">
289
+ Alt Description
290
+ </label>
291
+ <input
292
+ type="text"
293
+ value={altDescription}
294
+ onChange={(e) => setAltDescription(e.target.value)}
295
+ onBlur={(e) => handleAltBlur(e.target.value)}
296
+ onKeyDown={(e) => {
297
+ if (e.key === 'Enter') e.currentTarget.blur();
298
+ }}
299
+ className="w-full rounded-md border-gray-300 py-2 pl-3 text-sm shadow-sm focus:border-cyan-700 focus:ring-cyan-700"
300
+ placeholder="Describe the image..."
301
+ />
302
+ </div>
303
+ )}
304
+ </div>
305
+
306
+ <Dialog.Root
307
+ open={isArtpackModalOpen}
308
+ onOpenChange={(details) => setIsArtpackModalOpen(details.open)}
309
+ modal={true}
310
+ >
311
+ <Portal>
312
+ <Dialog.Backdrop className="fixed inset-0 bg-black/30 backdrop-blur-sm" />
313
+ <Dialog.Positioner
314
+ className="fixed inset-0 flex items-center justify-center p-4"
315
+ style={{ zIndex: 10010 }}
316
+ >
317
+ <Dialog.Content
318
+ className="w-full max-w-4xl overflow-y-auto rounded-lg bg-white p-6 shadow-xl"
319
+ style={{ maxHeight: '80vh' }}
320
+ >
321
+ <div className="mb-4 flex items-center justify-between">
322
+ <Dialog.Title className="text-lg font-bold">
323
+ Select Artpack Image
324
+ </Dialog.Title>
325
+ <Dialog.CloseTrigger className="rounded-full p-1 hover:bg-gray-100">
326
+ <XMarkIcon className="h-5 w-5 text-gray-500" />
327
+ </Dialog.CloseTrigger>
328
+ </div>
329
+
330
+ {Object.keys($artpacks || {}).length === 0 ? (
331
+ <div className="py-8 text-center text-gray-500">
332
+ No artpack collections available.
333
+ </div>
334
+ ) : (
335
+ <div className="space-y-6">
336
+ <div>
337
+ <label className="mb-2 block text-sm font-bold text-mydarkgrey">
338
+ Select Collection
339
+ </label>
340
+ <Combobox.Root
341
+ collection={collectionList}
342
+ value={selectedCollection ? [selectedCollection] : []}
343
+ onValueChange={handleCollectionSelect}
344
+ onInputValueChange={(details) =>
345
+ setQuery(details.inputValue)
346
+ }
347
+ loopFocus={true}
348
+ openOnKeyPress={true}
349
+ composite={true}
350
+ >
351
+ <div className="relative">
352
+ <Combobox.Control className="relative w-full cursor-default overflow-hidden rounded-lg border border-gray-300 bg-white text-left shadow-sm focus-within:border-myblue focus-within:ring-1 focus-within:ring-myblue">
353
+ <Combobox.Input
354
+ className="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 text-mydarkgrey focus:ring-0"
355
+ placeholder="Select a collection..."
356
+ autoComplete="off"
357
+ />
358
+ <Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
359
+ <ChevronUpDownIcon
360
+ className="h-5 w-5 text-mydarkgrey"
361
+ aria-hidden="true"
362
+ />
363
+ </Combobox.Trigger>
364
+ </Combobox.Control>
365
+ <Combobox.Content className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none md:text-sm">
366
+ {collectionList.items.map((item) => (
367
+ <Combobox.Item
368
+ key={item}
369
+ item={item}
370
+ className="collection-item relative cursor-default select-none py-2 pl-10 pr-4 text-mydarkgrey"
371
+ >
372
+ <span className="block truncate">{item}</span>
373
+ <span className="collection-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
374
+ <CheckIcon
375
+ className="h-5 w-5"
376
+ aria-hidden="true"
377
+ />
378
+ </span>
379
+ </Combobox.Item>
380
+ ))}
381
+ </Combobox.Content>
382
+ </div>
383
+ </Combobox.Root>
384
+ </div>
385
+
386
+ {!isLoading &&
387
+ selectedCollection &&
388
+ availableImages.length > 0 ? (
389
+ <div>
390
+ <div className="grid grid-cols-2 gap-4 p-2 md:grid-cols-3 xl:grid-cols-4">
391
+ {availableImages.map((image) => (
392
+ <div
393
+ key={image}
394
+ className="group relative cursor-pointer overflow-hidden rounded border border-gray-200 transition-all hover:border-cyan-600 hover:shadow-md"
395
+ onClick={() =>
396
+ handleSelectArtpackImage(
397
+ selectedCollection,
398
+ image
399
+ )
400
+ }
401
+ >
402
+ <img
403
+ src={`/artpacks/${selectedCollection}/${image}_600px.webp`}
404
+ alt={image}
405
+ className="aspect-video w-full object-cover transition-transform duration-300 group-hover:scale-105"
406
+ loading="lazy"
407
+ />
408
+ <div className="absolute inset-0 flex items-end bg-gradient-to-t from-black/60 to-transparent opacity-0 transition-opacity group-hover:opacity-100">
409
+ <span className="w-full truncate p-2 text-center text-xs text-white">
410
+ {image}
411
+ </span>
412
+ </div>
413
+ </div>
414
+ ))}
415
+ </div>
416
+ </div>
417
+ ) : isLoading ? (
418
+ <div className="py-12 text-center">
419
+ <div className="mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
420
+ <p className="mt-2 text-sm text-gray-500">
421
+ Loading collection...
422
+ </p>
423
+ </div>
424
+ ) : null}
425
+ </div>
426
+ )}
427
+ </Dialog.Content>
428
+ </Dialog.Positioner>
429
+ </Portal>
430
+ </Dialog.Root>
431
+ </div>
432
+ );
433
+ };
434
+
435
+ export default CreativeImagePanel;
@@ -0,0 +1,110 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useStore } from '@nanostores/react';
3
+ import InformationCircleIcon from '@heroicons/react/24/outline/InformationCircleIcon';
4
+ import { getCtx } from '@/stores/nodes';
5
+ import { fullContentMapStore } from '@/stores/storykeep';
6
+ import ActionBuilderField from '@/components/form/ActionBuilderField';
7
+ import { lispLexer } from '@/utils/actions/lispLexer';
8
+ import { preParseAction } from '@/utils/actions/preParse_Action';
9
+ import type { BasePanelProps, PaneNode } from '@/types/compositorTypes';
10
+
11
+ const CreativeLinkPanel = ({ node, childId }: BasePanelProps) => {
12
+ if (!node) return null;
13
+ const pane = node as unknown as PaneNode;
14
+ const assetMeta = pane?.htmlAst?.editableElements?.[childId || ''];
15
+ const contentMap = useStore(fullContentMapStore);
16
+ const ctx = getCtx();
17
+
18
+ const [actionLisp, setActionLisp] = useState('');
19
+
20
+ const isAnchor = assetMeta?.tagName === 'a';
21
+ const restriction = isAnchor ? 'navigation' : 'action';
22
+
23
+ useEffect(() => {
24
+ if (assetMeta) {
25
+ let initialValue = assetMeta.buttonPayload?.callbackPayload || '';
26
+
27
+ if (!initialValue && isAnchor && assetMeta.href) {
28
+ initialValue = `(goto url ${assetMeta.href})`;
29
+ }
30
+
31
+ setActionLisp(initialValue);
32
+ }
33
+ }, [assetMeta, isAnchor]);
34
+
35
+ if (!pane || !assetMeta) return null;
36
+
37
+ const handleUpdate = (newValue: string) => {
38
+ setActionLisp(newValue);
39
+
40
+ const el = document.querySelector(
41
+ `[data-ast-id="${childId}"]`
42
+ ) as HTMLElement;
43
+ if (el) {
44
+ if (isAnchor) {
45
+ try {
46
+ const config = (window as any).TRACTSTACK_CONFIG || {};
47
+ const lexed = lispLexer(newValue);
48
+ const resolvedHref = preParseAction(
49
+ lexed,
50
+ pane.slug,
51
+ !!pane.isContextPane,
52
+ config
53
+ );
54
+ if (resolvedHref) {
55
+ (el as HTMLAnchorElement).href = resolvedHref;
56
+ }
57
+ } catch (e) {
58
+ console.warn('Live DOM href resolution failed:', e);
59
+ }
60
+ } else {
61
+ el.setAttribute('data-callback', newValue);
62
+ }
63
+ }
64
+
65
+ (ctx as any).updateCreativeAsset(node.id, childId, {
66
+ tagName: assetMeta.tagName,
67
+ buttonPayload: {
68
+ ...assetMeta.buttonPayload,
69
+ callbackPayload: newValue,
70
+ },
71
+ });
72
+ };
73
+
74
+ return (
75
+ <div className="space-y-6">
76
+ <div className="space-y-2">
77
+ <h3 className="text-lg font-bold text-mydarkgrey">
78
+ {isAnchor ? 'Link Destination' : 'Button Action'}
79
+ </h3>
80
+ <p className="text-xs text-gray-500">
81
+ {isAnchor
82
+ ? 'Configure where this link takes the user.'
83
+ : 'Configure what happens when this button is clicked.'}
84
+ </p>
85
+ </div>
86
+
87
+ <div className="space-y-4">
88
+ <ActionBuilderField
89
+ value={actionLisp}
90
+ onChange={handleUpdate}
91
+ contentMap={contentMap}
92
+ label={isAnchor ? 'Destination' : 'Interaction'}
93
+ restriction={restriction}
94
+ />
95
+
96
+ <div className="flex items-start gap-2 rounded-md bg-blue-50 p-3 text-xs text-blue-800">
97
+ <InformationCircleIcon className="mt-0.5 h-4 w-4 flex-shrink-0 text-blue-600" />
98
+ <p>
99
+ This element is semantically locked as a{' '}
100
+ <strong>{isAnchor ? 'Link (<a>)' : 'Button (<button>)'}</strong>. To
101
+ change its type (e.g. turn a link into a button), use the "Refine
102
+ Design" tool.
103
+ </p>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ );
108
+ };
109
+
110
+ export default CreativeLinkPanel;
@@ -345,7 +345,7 @@ const StyleCodeHookPanel = ({
345
345
 
346
346
  <Portal>
347
347
  <Combobox.Positioner style={{ zIndex: 1002 }}>
348
- <Combobox.Content className="sm:text-sm z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
348
+ <Combobox.Content className="z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none md:text-sm">
349
349
  {collection.items.length === 0 ? (
350
350
  <div className="relative cursor-default select-none px-4 py-2 text-mydarkgrey">
351
351
  Nothing found.