astro-tractstack 2.0.12 → 2.0.14
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/dist/index.js +22 -0
- package/package.json +1 -1
- package/templates/src/client/view.js +5 -0
- package/templates/src/components/compositor/Compositor.tsx +3 -2
- package/templates/src/components/compositor/Node.tsx +18 -2
- package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +105 -0
- package/templates/src/components/edit/ToolMode.tsx +7 -0
- package/templates/src/components/edit/pane/AddPanePanel.tsx +5 -1
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +4 -1
- package/templates/src/components/edit/pane/AiPaneGenerator.tsx +264 -94
- package/templates/src/components/edit/pane/AiPanePreview.tsx +60 -210
- package/templates/src/components/edit/pane/PageGen.tsx +1 -1
- package/templates/src/components/edit/pane/PageGenSelector.tsx +4 -0
- package/templates/src/components/edit/pane/RestylePaneModal.tsx +573 -0
- package/templates/src/components/edit/state/SaveToLibraryModal.tsx +205 -0
- package/templates/src/constants/prompts.json +3 -3
- package/templates/src/stores/selection.ts +4 -0
- package/templates/src/types/compositorTypes.ts +51 -1
- package/templates/src/types/tractstack.ts +36 -31
- package/templates/src/utils/aai/getTitleSlug.ts +1 -1
- package/templates/src/utils/api/brandConfig.ts +8 -2
- package/templates/src/utils/api/brandHelpers.ts +4 -0
- package/templates/src/utils/compositor/aiPaneParser.ts +39 -13
- package/templates/src/utils/compositor/designLibraryHelper.ts +331 -0
- package/templates/src/utils/compositor/processMarkdown.ts +1 -1
- package/utils/inject-files.ts +22 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react';
|
|
2
|
+
import { useStore } from '@nanostores/react';
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
Select,
|
|
6
|
+
Combobox,
|
|
7
|
+
Pagination,
|
|
8
|
+
Portal,
|
|
9
|
+
type SelectValueChangeDetails,
|
|
10
|
+
type ComboboxInputValueChangeDetails,
|
|
11
|
+
type PaginationPageChangeDetails,
|
|
12
|
+
} from '@ark-ui/react';
|
|
13
|
+
import { createListCollection } from '@ark-ui/react/collection';
|
|
14
|
+
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
|
15
|
+
import { selectionStore } from '@/stores/selection';
|
|
16
|
+
import { getCtx, NodesContext } from '@/stores/nodes';
|
|
17
|
+
import { createEmptyStorykeep } from '@/utils/compositor/nodesHelper';
|
|
18
|
+
import {
|
|
19
|
+
extractPaneCopy,
|
|
20
|
+
mergeCopyIntoTemplate,
|
|
21
|
+
convertStorageToLiveTemplate,
|
|
22
|
+
} from '@/utils/compositor/designLibraryHelper';
|
|
23
|
+
import type {
|
|
24
|
+
PaneNode,
|
|
25
|
+
StoragePane,
|
|
26
|
+
TemplatePane,
|
|
27
|
+
TemplateMarkdown, // Added import
|
|
28
|
+
BaseNode, // Added import
|
|
29
|
+
} from '@/types/compositorTypes';
|
|
30
|
+
import type { BrandConfig, DesignLibraryEntry } from '@/types/tractstack';
|
|
31
|
+
import {
|
|
32
|
+
PaneSnapshotGenerator,
|
|
33
|
+
type SnapshotData,
|
|
34
|
+
} from '@/components/compositor/preview/PaneSnapshotGenerator';
|
|
35
|
+
import {
|
|
36
|
+
PanesPreviewGenerator,
|
|
37
|
+
type PanePreviewRequest,
|
|
38
|
+
type PaneFragmentResult,
|
|
39
|
+
} from '@/components/compositor/preview/PanesPreviewGenerator';
|
|
40
|
+
import { classNames } from '@/utils/helpers';
|
|
41
|
+
|
|
42
|
+
const PAGE_SIZE = 6;
|
|
43
|
+
const VERBOSE = false;
|
|
44
|
+
|
|
45
|
+
interface TemplatePreviewItemProps {
|
|
46
|
+
template: TemplatePane;
|
|
47
|
+
config: BrandConfig;
|
|
48
|
+
onClick: () => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const TemplatePreviewItem = ({
|
|
52
|
+
template,
|
|
53
|
+
config,
|
|
54
|
+
onClick,
|
|
55
|
+
}: TemplatePreviewItemProps) => {
|
|
56
|
+
const [previewState, setPreviewState] = useState<{
|
|
57
|
+
htmlFragment?: string;
|
|
58
|
+
snapshot?: SnapshotData;
|
|
59
|
+
error?: string;
|
|
60
|
+
} | null>(null);
|
|
61
|
+
|
|
62
|
+
const fragmentRequest = useMemo((): PanePreviewRequest[] => {
|
|
63
|
+
// This preview logic is correct: it creates a *temporary* context.
|
|
64
|
+
const ctx = new NodesContext();
|
|
65
|
+
ctx.addNode(createEmptyStorykeep('tmp'));
|
|
66
|
+
ctx.addTemplatePane('tmp', template);
|
|
67
|
+
return [{ id: template.id, ctx }];
|
|
68
|
+
}, [template]);
|
|
69
|
+
|
|
70
|
+
const handleFragmentComplete = (results: PaneFragmentResult[]) => {
|
|
71
|
+
const result = results[0];
|
|
72
|
+
if (result?.htmlString) {
|
|
73
|
+
setPreviewState({ htmlFragment: result.htmlString });
|
|
74
|
+
} else {
|
|
75
|
+
setPreviewState({
|
|
76
|
+
error: result?.error || 'Failed to generate HTML fragment.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleSnapshotComplete = (data: SnapshotData) => {
|
|
82
|
+
setPreviewState((prev) => (prev ? { ...prev, snapshot: data } : null));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
className="cursor-pointer rounded-lg border bg-white shadow-sm transition-all hover:shadow-lg"
|
|
88
|
+
onClick={onClick}
|
|
89
|
+
>
|
|
90
|
+
<div className="h-64 overflow-hidden rounded-t-lg border-b bg-gray-50">
|
|
91
|
+
{!previewState && (
|
|
92
|
+
<div className="h-full w-full animate-pulse bg-gray-200" />
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
{previewState?.error && (
|
|
96
|
+
<div className="flex h-full items-center justify-center p-4">
|
|
97
|
+
<p className="text-xs text-red-500">{previewState.error}</p>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{fragmentRequest.length > 0 && !previewState?.htmlFragment && (
|
|
102
|
+
<PanesPreviewGenerator
|
|
103
|
+
requests={fragmentRequest}
|
|
104
|
+
onComplete={handleFragmentComplete}
|
|
105
|
+
onError={(err) => setPreviewState({ error: err })}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{previewState?.htmlFragment && !previewState.snapshot && (
|
|
110
|
+
<PaneSnapshotGenerator
|
|
111
|
+
id={template.id}
|
|
112
|
+
htmlString={previewState.htmlFragment}
|
|
113
|
+
outputWidth={800}
|
|
114
|
+
config={config}
|
|
115
|
+
onComplete={(_id, data) => handleSnapshotComplete(data)}
|
|
116
|
+
onError={(_id, err) =>
|
|
117
|
+
setPreviewState((prev) =>
|
|
118
|
+
prev ? { ...prev, error: err } : { error: err }
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{previewState?.snapshot && (
|
|
125
|
+
<img
|
|
126
|
+
src={previewState.snapshot.imageData}
|
|
127
|
+
alt={`Preview for ${template.title}`}
|
|
128
|
+
className="block w-full"
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
<div className="p-3">
|
|
133
|
+
<h3 className="truncate font-semibold" title={template.title}>
|
|
134
|
+
{template.title}
|
|
135
|
+
</h3>
|
|
136
|
+
<p className="text-sm text-gray-600">
|
|
137
|
+
{(template as any).category || 'Uncategorized'}
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
interface RestylePaneModalProps {
|
|
145
|
+
config: BrandConfig;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const RestylePaneModal = ({ config }: RestylePaneModalProps) => {
|
|
149
|
+
const ctx = getCtx();
|
|
150
|
+
const { isRestyleModalOpen, paneToRestyleId } = useStore(selectionStore, {
|
|
151
|
+
keys: ['isRestyleModalOpen', 'paneToRestyleId'],
|
|
152
|
+
});
|
|
153
|
+
const designLibrary = config?.DESIGN_LIBRARY || [];
|
|
154
|
+
|
|
155
|
+
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
|
156
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
157
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
158
|
+
|
|
159
|
+
const categories = useMemo(() => {
|
|
160
|
+
const allCategories = new Set(
|
|
161
|
+
designLibrary.map((entry: DesignLibraryEntry) => entry.category)
|
|
162
|
+
);
|
|
163
|
+
return ['all', ...Array.from(allCategories)];
|
|
164
|
+
}, [designLibrary]);
|
|
165
|
+
|
|
166
|
+
const originalPaneData = useMemo(() => {
|
|
167
|
+
if (!paneToRestyleId) return null;
|
|
168
|
+
const paneNode = ctx.allNodes.get().get(paneToRestyleId) as PaneNode;
|
|
169
|
+
if (!paneNode) {
|
|
170
|
+
console.error(
|
|
171
|
+
'DEBUG: originalPaneData - FAILED. PaneNode not found for id:',
|
|
172
|
+
paneToRestyleId
|
|
173
|
+
);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const copy = extractPaneCopy(paneNode);
|
|
178
|
+
|
|
179
|
+
if (VERBOSE)
|
|
180
|
+
console.log('DEBUG: originalPaneData (SUCCESS)', {
|
|
181
|
+
paneId: paneNode.id,
|
|
182
|
+
extractedCopy: copy,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return { paneNode, copy };
|
|
186
|
+
}, [paneToRestyleId, isRestyleModalOpen]);
|
|
187
|
+
|
|
188
|
+
const filteredEntries = useMemo(() => {
|
|
189
|
+
return designLibrary.filter(
|
|
190
|
+
(entry: DesignLibraryEntry) =>
|
|
191
|
+
(selectedCategory === 'all' || entry.category === selectedCategory) &&
|
|
192
|
+
entry.title.toLowerCase().includes(searchTerm.toLowerCase())
|
|
193
|
+
);
|
|
194
|
+
}, [designLibrary, selectedCategory, searchTerm]);
|
|
195
|
+
|
|
196
|
+
const paginatedEntries = useMemo(() => {
|
|
197
|
+
const start = (currentPage - 1) * PAGE_SIZE;
|
|
198
|
+
const end = start + PAGE_SIZE;
|
|
199
|
+
return filteredEntries.slice(start, end);
|
|
200
|
+
}, [filteredEntries, currentPage]);
|
|
201
|
+
|
|
202
|
+
const totalPages = Math.ceil(filteredEntries.length / PAGE_SIZE);
|
|
203
|
+
|
|
204
|
+
const mergedTemplates = useMemo<
|
|
205
|
+
{ entry: DesignLibraryEntry; template: TemplatePane }[]
|
|
206
|
+
>(() => {
|
|
207
|
+
if (!originalPaneData) return [];
|
|
208
|
+
|
|
209
|
+
return paginatedEntries.map((entry: DesignLibraryEntry) => {
|
|
210
|
+
const mergedStoragePane: StoragePane = mergeCopyIntoTemplate(
|
|
211
|
+
entry.template,
|
|
212
|
+
originalPaneData.copy
|
|
213
|
+
);
|
|
214
|
+
const liveTemplatePane: TemplatePane =
|
|
215
|
+
convertStorageToLiveTemplate(mergedStoragePane);
|
|
216
|
+
liveTemplatePane.title = entry.title;
|
|
217
|
+
(liveTemplatePane as any).category = entry.category;
|
|
218
|
+
return { entry, template: liveTemplatePane };
|
|
219
|
+
});
|
|
220
|
+
}, [paginatedEntries, originalPaneData]);
|
|
221
|
+
|
|
222
|
+
if (VERBOSE)
|
|
223
|
+
console.log('DEBUG: Final mergedTemplates array:', mergedTemplates);
|
|
224
|
+
|
|
225
|
+
const handleClose = () => {
|
|
226
|
+
selectionStore.setKey('isRestyleModalOpen', false);
|
|
227
|
+
selectionStore.setKey('paneToRestyleId', null);
|
|
228
|
+
setCurrentPage(1);
|
|
229
|
+
setSearchTerm('');
|
|
230
|
+
setSelectedCategory('all');
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const handleSelectTemplate = (template: TemplatePane) => {
|
|
234
|
+
if (VERBOSE)
|
|
235
|
+
console.log(
|
|
236
|
+
'%cDEBUG: handleSelectTemplate CLICKED (Hollow & Replace)',
|
|
237
|
+
'color: #00A; font-weight: bold;',
|
|
238
|
+
{
|
|
239
|
+
templateToApply: template,
|
|
240
|
+
originalPaneData: originalPaneData,
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (!originalPaneData) {
|
|
245
|
+
console.error(
|
|
246
|
+
'%cDEBUG: handleSelectTemplate FAILED: originalPaneData is null.',
|
|
247
|
+
'color: red; font-weight: bold;'
|
|
248
|
+
);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const originalPane = originalPaneData.paneNode;
|
|
253
|
+
const originalPaneId = originalPane.id;
|
|
254
|
+
|
|
255
|
+
if (VERBOSE)
|
|
256
|
+
console.log(
|
|
257
|
+
`%cDEBUG: STEP 1 - HOLLOWING OUT original pane ${originalPaneId}`,
|
|
258
|
+
'color: #A0A; font-weight: bold;'
|
|
259
|
+
);
|
|
260
|
+
const oldChildrenNodes = ctx
|
|
261
|
+
.getChildNodeIDs(originalPaneId)
|
|
262
|
+
.map((id) => ctx.allNodes.get().get(id));
|
|
263
|
+
if (VERBOSE)
|
|
264
|
+
console.log(
|
|
265
|
+
'%cDEBUG: Original pane children BEFORE delete:',
|
|
266
|
+
oldChildrenNodes
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const deletedChildren = ctx.deleteChildren(originalPaneId); // This deletes *children*, not the pane itself
|
|
270
|
+
|
|
271
|
+
const childrenAfterDelete = ctx.getChildNodeIDs(originalPaneId);
|
|
272
|
+
if (VERBOSE) {
|
|
273
|
+
console.log(
|
|
274
|
+
`%cDEBUG: Deleted ${deletedChildren.length} old child nodes.`,
|
|
275
|
+
'color: #A0A;'
|
|
276
|
+
);
|
|
277
|
+
console.log(
|
|
278
|
+
`%cDEBUG: Original pane children IDs AFTER delete: [${childrenAfterDelete.join(', ')}]`,
|
|
279
|
+
'color: #A0A;'
|
|
280
|
+
);
|
|
281
|
+
console.log(
|
|
282
|
+
`%cDEBUG: STEP 2 - REFILLING pane with new nodes...`,
|
|
283
|
+
'color: #0A0; font-weight: bold;'
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const newNodesToAdd: BaseNode[] = [];
|
|
288
|
+
const newMarkdown = template.markdown as TemplateMarkdown | undefined;
|
|
289
|
+
const newBgPane = template.bgPane;
|
|
290
|
+
|
|
291
|
+
if (newMarkdown) {
|
|
292
|
+
// Re-parent the new Markdown node to the original pane
|
|
293
|
+
newMarkdown.parentId = originalPaneId;
|
|
294
|
+
newNodesToAdd.push(newMarkdown);
|
|
295
|
+
|
|
296
|
+
// The markdown.nodes are already parented to the newMarkdown.id, which is correct.
|
|
297
|
+
// We just need to add them to the context.
|
|
298
|
+
if (newMarkdown.nodes) {
|
|
299
|
+
newNodesToAdd.push(...newMarkdown.nodes);
|
|
300
|
+
}
|
|
301
|
+
if (VERBOSE) {
|
|
302
|
+
console.log(`%cDEBUG: Prepared new Markdown node:`, newMarkdown);
|
|
303
|
+
console.log(
|
|
304
|
+
`%cDEBUG: Prepared ${newMarkdown.nodes?.length || 0} new markdown sub-nodes:`,
|
|
305
|
+
newMarkdown.nodes
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (newBgPane) {
|
|
311
|
+
// Re-parent the new BgPane node to the original pane
|
|
312
|
+
newBgPane.parentId = originalPaneId;
|
|
313
|
+
newNodesToAdd.push(newBgPane);
|
|
314
|
+
if (VERBOSE) console.log(`%cDEBUG: Prepared new BgPane:`, newBgPane);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
ctx.addNodes(newNodesToAdd); // This adds all nodes AND links them in parentNodes map
|
|
318
|
+
|
|
319
|
+
const childrenAfterAdd = ctx.getChildNodeIDs(originalPaneId);
|
|
320
|
+
const childrenNodesAfterAdd = childrenAfterAdd.map((id) =>
|
|
321
|
+
ctx.allNodes.get().get(id)
|
|
322
|
+
);
|
|
323
|
+
if (VERBOSE) {
|
|
324
|
+
console.log(
|
|
325
|
+
`%cDEBUG: Original pane children IDs AFTER add: [${childrenAfterAdd.join(', ')}]`,
|
|
326
|
+
'color: #0A0;'
|
|
327
|
+
);
|
|
328
|
+
console.log(
|
|
329
|
+
`%cDEBUG: Original pane children nodes AFTER add:`,
|
|
330
|
+
childrenNodesAfterAdd
|
|
331
|
+
);
|
|
332
|
+
console.log(
|
|
333
|
+
`%cDEBUG: STEP 3 - UPDATING original pane properties...`,
|
|
334
|
+
'color: #00F; font-weight: bold;'
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// We must get a fresh reference from the store to modify
|
|
339
|
+
const paneToUpdate = ctx.allNodes.get().get(originalPaneId) as PaneNode;
|
|
340
|
+
|
|
341
|
+
if (!paneToUpdate) {
|
|
342
|
+
console.error(
|
|
343
|
+
`%cDEBUG: FAILED TO FIND PANE ${originalPaneId} IN STORE FOR FINAL UPDATE.`,
|
|
344
|
+
'color: red; font-weight: bold;'
|
|
345
|
+
);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Copy all style/config properties from the template, but keep the original ID, parentId, slug, title
|
|
350
|
+
paneToUpdate.bgColour = template.bgColour;
|
|
351
|
+
paneToUpdate.isDecorative = template.isDecorative;
|
|
352
|
+
paneToUpdate.heightOffsetDesktop = template.heightOffsetDesktop;
|
|
353
|
+
paneToUpdate.heightOffsetMobile = template.heightOffsetMobile;
|
|
354
|
+
paneToUpdate.heightOffsetTablet = template.heightOffsetTablet;
|
|
355
|
+
paneToUpdate.heightRatioDesktop = template.heightRatioDesktop;
|
|
356
|
+
paneToUpdate.heightRatioMobile = template.heightRatioMobile;
|
|
357
|
+
paneToUpdate.heightRatioTablet = template.heightRatioTablet;
|
|
358
|
+
paneToUpdate.isChanged = true; // Mark as dirty
|
|
359
|
+
|
|
360
|
+
if (VERBOSE)
|
|
361
|
+
console.log(
|
|
362
|
+
`%cDEBUG: Calling modifyNodes with this pane object:`,
|
|
363
|
+
paneToUpdate
|
|
364
|
+
);
|
|
365
|
+
ctx.modifyNodes([paneToUpdate]); // This will save the changes and notify the UI
|
|
366
|
+
|
|
367
|
+
if (VERBOSE) {
|
|
368
|
+
console.log(
|
|
369
|
+
'%cDEBUG: handleSelectTemplate FINISHED.',
|
|
370
|
+
'color: #00A; font-weight: bold;'
|
|
371
|
+
);
|
|
372
|
+
console.log(
|
|
373
|
+
'%cDEBUG: Notifying ROOT_NODE to force re-render.',
|
|
374
|
+
'color: green; font-weight: bold;'
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
ctx.notifyNode('root');
|
|
378
|
+
|
|
379
|
+
handleClose();
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const comboboxCollection = useMemo(
|
|
383
|
+
() =>
|
|
384
|
+
createListCollection({
|
|
385
|
+
items: filteredEntries,
|
|
386
|
+
itemToValue: (item) => item.title,
|
|
387
|
+
itemToString: (item) => item.title,
|
|
388
|
+
}),
|
|
389
|
+
[filteredEntries]
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const selectCollection = useMemo(
|
|
393
|
+
() =>
|
|
394
|
+
createListCollection({
|
|
395
|
+
items: categories.map((c) => ({ label: c, value: c })),
|
|
396
|
+
itemToValue: (item) => item.value,
|
|
397
|
+
itemToString: (item) => item.label,
|
|
398
|
+
}),
|
|
399
|
+
[categories]
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<Dialog.Root open={isRestyleModalOpen} onOpenChange={handleClose} modal>
|
|
404
|
+
<Portal>
|
|
405
|
+
<Dialog.Backdrop className="z-103 fixed inset-0 bg-black/70" />
|
|
406
|
+
<Dialog.Positioner className="z-104 fixed inset-0 flex items-center justify-center">
|
|
407
|
+
<Dialog.Content className="flex h-[90vh] w-[90vw] flex-col rounded-lg bg-white shadow-2xl">
|
|
408
|
+
<header className="flex items-center justify-between border-b p-4">
|
|
409
|
+
<Dialog.Title className="text-xl font-semibold">
|
|
410
|
+
Restyle Pane from Design Library
|
|
411
|
+
</Dialog.Title>
|
|
412
|
+
<Dialog.CloseTrigger
|
|
413
|
+
type="button"
|
|
414
|
+
className="rounded-full p-1 text-gray-600 hover:bg-gray-100"
|
|
415
|
+
>
|
|
416
|
+
<XMarkIcon className="h-6 w-6" />
|
|
417
|
+
</Dialog.CloseTrigger>
|
|
418
|
+
</header>
|
|
419
|
+
|
|
420
|
+
<nav className="flex items-center gap-x-4 border-b bg-gray-50 p-4">
|
|
421
|
+
<Select.Root
|
|
422
|
+
collection={selectCollection}
|
|
423
|
+
value={[selectedCategory]}
|
|
424
|
+
onValueChange={(details: SelectValueChangeDetails) =>
|
|
425
|
+
setSelectedCategory(details.value[0])
|
|
426
|
+
}
|
|
427
|
+
className="w-48"
|
|
428
|
+
positioning={{ gutter: 4 }}
|
|
429
|
+
>
|
|
430
|
+
<Select.Label className="mb-1 text-sm font-medium">
|
|
431
|
+
Category
|
|
432
|
+
</Select.Label>
|
|
433
|
+
<Select.Control>
|
|
434
|
+
<Select.Trigger className="flex w-full items-center justify-between rounded border bg-white p-2 text-left">
|
|
435
|
+
<Select.ValueText />
|
|
436
|
+
<Select.Indicator>▼</Select.Indicator>
|
|
437
|
+
</Select.Trigger>
|
|
438
|
+
</Select.Control>
|
|
439
|
+
<Portal>
|
|
440
|
+
<Select.Positioner>
|
|
441
|
+
<Select.Content className="z-105 rounded border bg-white shadow-lg">
|
|
442
|
+
{categories.map((c) => (
|
|
443
|
+
<Select.Item
|
|
444
|
+
key={c}
|
|
445
|
+
item={{ label: c, value: c }}
|
|
446
|
+
className="cursor-pointer p-2 hover:bg-gray-100"
|
|
447
|
+
>
|
|
448
|
+
<Select.ItemText>{c}</Select.ItemText>
|
|
449
|
+
</Select.Item>
|
|
450
|
+
))}
|
|
451
|
+
</Select.Content>
|
|
452
|
+
</Select.Positioner>
|
|
453
|
+
</Portal>
|
|
454
|
+
</Select.Root>
|
|
455
|
+
|
|
456
|
+
<Combobox.Root
|
|
457
|
+
collection={comboboxCollection}
|
|
458
|
+
onInputValueChange={(e: ComboboxInputValueChangeDetails) =>
|
|
459
|
+
setSearchTerm(e.inputValue)
|
|
460
|
+
}
|
|
461
|
+
className="flex-1"
|
|
462
|
+
positioning={{ gutter: 4 }}
|
|
463
|
+
>
|
|
464
|
+
<Combobox.Label className="mb-1 text-sm font-medium">
|
|
465
|
+
Filter by Title
|
|
466
|
+
</Combobox.Label>
|
|
467
|
+
<Combobox.Control>
|
|
468
|
+
<Combobox.Input
|
|
469
|
+
placeholder="Search by title..."
|
|
470
|
+
className="w-full rounded border p-2"
|
|
471
|
+
/>
|
|
472
|
+
</Combobox.Control>
|
|
473
|
+
<Portal>
|
|
474
|
+
<Combobox.Positioner>
|
|
475
|
+
<Combobox.Content className="z-105 rounded border bg-white shadow-lg">
|
|
476
|
+
{filteredEntries.map((entry: DesignLibraryEntry) => (
|
|
477
|
+
<Combobox.Item
|
|
478
|
+
key={entry.title}
|
|
479
|
+
item={entry}
|
|
480
|
+
className="cursor-pointer p-2 hover:bg-gray-100"
|
|
481
|
+
>
|
|
482
|
+
<Combobox.ItemText>{entry.title}</Combobox.ItemText>
|
|
483
|
+
</Combobox.Item>
|
|
484
|
+
))}
|
|
485
|
+
</Combobox.Content>
|
|
486
|
+
</Combobox.Positioner>
|
|
487
|
+
</Portal>
|
|
488
|
+
</Combobox.Root>
|
|
489
|
+
</nav>
|
|
490
|
+
|
|
491
|
+
<main className="flex-1 overflow-y-auto bg-gray-100 p-6">
|
|
492
|
+
{mergedTemplates.length === 0 ? (
|
|
493
|
+
<div className="flex h-full items-center justify-center">
|
|
494
|
+
<p className="text-gray-500">No designs found.</p>
|
|
495
|
+
</div>
|
|
496
|
+
) : (
|
|
497
|
+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
498
|
+
{mergedTemplates.map(({ template }) => (
|
|
499
|
+
<TemplatePreviewItem
|
|
500
|
+
key={template.id}
|
|
501
|
+
template={template}
|
|
502
|
+
config={config}
|
|
503
|
+
onClick={() => handleSelectTemplate(template)}
|
|
504
|
+
/>
|
|
505
|
+
))}
|
|
506
|
+
</div>
|
|
507
|
+
)}
|
|
508
|
+
</main>
|
|
509
|
+
|
|
510
|
+
{totalPages > 1 && (
|
|
511
|
+
<footer className="flex items-center justify-center border-t p-4">
|
|
512
|
+
<Pagination.Root
|
|
513
|
+
count={totalPages * PAGE_SIZE}
|
|
514
|
+
pageSize={PAGE_SIZE}
|
|
515
|
+
siblingCount={1}
|
|
516
|
+
page={currentPage}
|
|
517
|
+
onPageChange={(details: PaginationPageChangeDetails) =>
|
|
518
|
+
setCurrentPage(details.page)
|
|
519
|
+
}
|
|
520
|
+
className="flex items-center gap-x-2"
|
|
521
|
+
>
|
|
522
|
+
<Pagination.PrevTrigger
|
|
523
|
+
type="button"
|
|
524
|
+
className="rounded p-2 text-sm hover:bg-gray-100 disabled:text-gray-400"
|
|
525
|
+
disabled={currentPage === 1}
|
|
526
|
+
>
|
|
527
|
+
Previous
|
|
528
|
+
</Pagination.PrevTrigger>
|
|
529
|
+
<Pagination.Context>
|
|
530
|
+
{(pagination) =>
|
|
531
|
+
pagination.pages.map((page, index: number) =>
|
|
532
|
+
page.type === 'page' ? (
|
|
533
|
+
<Pagination.Item
|
|
534
|
+
key={index}
|
|
535
|
+
{...page}
|
|
536
|
+
type="page"
|
|
537
|
+
className={classNames(
|
|
538
|
+
'flex h-9 w-9 items-center justify-center rounded text-sm',
|
|
539
|
+
page.value === currentPage
|
|
540
|
+
? 'bg-blue-600 font-bold text-white'
|
|
541
|
+
: 'hover:bg-gray-100'
|
|
542
|
+
)}
|
|
543
|
+
>
|
|
544
|
+
{page.value}
|
|
545
|
+
</Pagination.Item>
|
|
546
|
+
) : (
|
|
547
|
+
<Pagination.Ellipsis
|
|
548
|
+
key={index}
|
|
549
|
+
index={index}
|
|
550
|
+
className="px-2 text-sm"
|
|
551
|
+
>
|
|
552
|
+
...
|
|
553
|
+
</Pagination.Ellipsis>
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
</Pagination.Context>
|
|
558
|
+
<Pagination.NextTrigger
|
|
559
|
+
type="button"
|
|
560
|
+
className="rounded p-2 text-sm hover:bg-gray-100 disabled:text-gray-400"
|
|
561
|
+
disabled={currentPage === totalPages}
|
|
562
|
+
>
|
|
563
|
+
Next
|
|
564
|
+
</Pagination.NextTrigger>
|
|
565
|
+
</Pagination.Root>
|
|
566
|
+
</footer>
|
|
567
|
+
)}
|
|
568
|
+
</Dialog.Content>
|
|
569
|
+
</Dialog.Positioner>
|
|
570
|
+
</Portal>
|
|
571
|
+
</Dialog.Root>
|
|
572
|
+
);
|
|
573
|
+
};
|