astro-tractstack 2.0.13 → 2.0.15

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 (28) hide show
  1. package/dist/index.js +40 -0
  2. package/package.json +1 -1
  3. package/templates/src/client/view.js +5 -0
  4. package/templates/src/components/compositor/Compositor.tsx +3 -2
  5. package/templates/src/components/compositor/Node.tsx +25 -8
  6. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +105 -0
  7. package/templates/src/components/edit/ToolMode.tsx +7 -0
  8. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +459 -561
  9. package/templates/src/components/edit/pane/AiPaneGenerator.tsx +19 -82
  10. package/templates/src/components/edit/pane/RestylePaneModal.tsx +573 -0
  11. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +140 -0
  12. package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +105 -0
  13. package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +395 -0
  14. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +205 -0
  15. package/templates/src/constants/prompts.json +3 -1
  16. package/templates/src/stores/selection.ts +4 -0
  17. package/templates/src/types/compositorTypes.ts +51 -1
  18. package/templates/src/types/tractstack.ts +36 -31
  19. package/templates/src/utils/aai/getTitleSlug.ts +1 -1
  20. package/templates/src/utils/api/brandConfig.ts +8 -2
  21. package/templates/src/utils/api/brandHelpers.ts +4 -0
  22. package/templates/src/utils/compositor/aiPaneParser.ts +32 -84
  23. package/templates/src/utils/compositor/designLibraryHelper.ts +416 -0
  24. package/templates/src/utils/compositor/processMarkdown.ts +1 -1
  25. package/utils/inject-files.ts +40 -0
  26. package/templates/src/components/edit/pane/PageGen.tsx +0 -485
  27. package/templates/src/components/edit/pane/PageGenSelector.tsx +0 -245
  28. package/templates/src/components/edit/pane/PageGenSpecial.tsx +0 -339
@@ -1,245 +0,0 @@
1
- import { useState, useMemo } from 'react';
2
- import { useStore } from '@nanostores/react';
3
- import { RadioGroup } from '@ark-ui/react/radio-group';
4
- import CheckCircleIcon from '@heroicons/react/20/solid/CheckCircleIcon';
5
- //import CubeTransparentIcon from '@heroicons/react/24/outline/CubeTransparentIcon';
6
- import DocumentIcon from '@heroicons/react/24/outline/DocumentIcon';
7
- import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
8
- import AddPanePanel from './AddPanePanel';
9
- import PageCreationGen from './PageGen';
10
- import PageCreationSpecial from './PageGenSpecial';
11
- import {
12
- /* hasAssemblyAIStore,*/ fullContentMapStore,
13
- } from '@/stores/storykeep';
14
- import type { NodesContext } from '@/stores/nodes';
15
- import type { BrandConfig } from '@/types/tractstack';
16
-
17
- interface PageCreationSelectorProps {
18
- nodeId: string;
19
- ctx: NodesContext;
20
- isTemplate?: boolean;
21
- config?: BrandConfig;
22
- }
23
-
24
- type CreationMode = {
25
- id: 'design' | 'generate' | 'featured';
26
- name: string;
27
- description: string;
28
- icon: typeof DocumentIcon;
29
- active: boolean;
30
- disabled?: boolean;
31
- disabledReason?: string;
32
- };
33
-
34
- export const PageCreationSelector = ({
35
- nodeId,
36
- ctx,
37
- isTemplate = false,
38
- config,
39
- }: PageCreationSelectorProps) => {
40
- const [selectedMode, setSelectedMode] =
41
- useState<CreationMode['id']>('design');
42
- const [showTemplates, setShowTemplates] = useState(false);
43
- const [showGen, setShowGen] = useState(false);
44
- const [showFeatured, setShowFeatured] = useState(false);
45
- const $contentMap = useStore(fullContentMapStore);
46
-
47
- const validPagesCount = useMemo(() => {
48
- return $contentMap.filter(
49
- (item) =>
50
- item.type === 'StoryFragment' &&
51
- typeof item.description === 'string' &&
52
- typeof item.thumbSrc === 'string' &&
53
- typeof item.thumbSrcSet === 'string' &&
54
- typeof item.changed === 'string'
55
- ).length;
56
- }, [$contentMap]);
57
-
58
- const modes = useMemo(() => {
59
- const baseModesWithoutFeature = [
60
- {
61
- id: 'design',
62
- name: 'Design from scratch',
63
- description:
64
- 'Build your page section by section using our design system',
65
- icon: DocumentIcon,
66
- active: true,
67
- },
68
- /*
69
- ...(hasAssemblyAIStore.get()
70
- ? [
71
- {
72
- id: 'generate',
73
- name: 'Generate with AI',
74
- description:
75
- 'Tell us what kind of page you want and AI will generate a first draft',
76
- icon: CubeTransparentIcon,
77
- active: hasAssemblyAIStore.get(),
78
- },
79
- ]
80
- : []),
81
- */
82
- ];
83
-
84
- //const featuredMode = {
85
- // id: 'featured',
86
- // name: 'Featured Content home page',
87
- // description:
88
- // 'A layout with a prominent hero section showcasing a featured article and grid of additional top articles',
89
- // icon: NewspaperIcon,
90
- // active: true,
91
- // disabled: validPagesCount < 3,
92
- // disabledReason:
93
- // validPagesCount === 0
94
- // ? 'Not yet available; no pages with SEO metadata found.'
95
- // : `Not yet available; requires at least 3 pages with SEO metadata (currently ${validPagesCount}).`,
96
- //};
97
-
98
- return [...baseModesWithoutFeature /*, featuredMode */] as CreationMode[];
99
- }, [validPagesCount]);
100
-
101
- const handleContinue = () => {
102
- if (!selectedMode) return;
103
-
104
- const selectedModeObj = modes.find((m) => m.id === selectedMode);
105
- if (selectedModeObj?.disabled) return;
106
-
107
- if (selectedMode === 'design') {
108
- setShowTemplates(true);
109
- } else if (selectedMode === 'generate') {
110
- setShowGen(true);
111
- } else if (selectedMode === 'featured') {
112
- setShowFeatured(true);
113
- }
114
- };
115
-
116
- const radioGroupStyles = `
117
- .radio-control[data-state="unchecked"] .radio-dot {
118
- background-color: #d1d5db; /* gray-300 */
119
- }
120
- .radio-control[data-state="checked"] .radio-dot {
121
- background-color: #0891b2; /* cyan-600 */
122
- }
123
- .radio-control[data-state="checked"] {
124
- border-color: #0891b2; /* cyan-600 */
125
- }
126
- .radio-item[data-state="checked"] {
127
- background-color: #f9f9f9;
128
- color: white;
129
- }
130
- .radio-item[data-disabled="true"] {
131
- background-color: #f9fafb;
132
- cursor: not-allowed;
133
- }
134
- `;
135
-
136
- if (showTemplates || isTemplate)
137
- return (
138
- <AddPanePanel
139
- nodeId={nodeId}
140
- first={true}
141
- ctx={ctx}
142
- isStoryFragment={true}
143
- config={config!}
144
- />
145
- );
146
- else if (showGen) return <PageCreationGen nodeId={nodeId} ctx={ctx} />;
147
- else if (showFeatured)
148
- return <PageCreationSpecial nodeId={nodeId} ctx={ctx} />;
149
-
150
- return (
151
- <div className="p-0.5 shadow-inner">
152
- <style>{radioGroupStyles}</style>
153
- <div className="w-full rounded-md bg-white p-6">
154
- <h2 className="font-action mb-6 text-2xl font-bold text-gray-900">
155
- How would you like to create your page?
156
- </h2>
157
-
158
- <div className="w-full max-w-3xl">
159
- <RadioGroup.Root
160
- defaultValue="design"
161
- onValueChange={(details) => {
162
- if (details.value) {
163
- setSelectedMode(details.value as CreationMode['id']);
164
- }
165
- }}
166
- >
167
- <RadioGroup.Label className="sr-only">
168
- Page Creation Mode
169
- </RadioGroup.Label>
170
- <div className="space-y-4">
171
- {modes.map((mode) => (
172
- <RadioGroup.Item
173
- key={mode.id}
174
- value={mode.id}
175
- disabled={mode.disabled}
176
- className={`radio-item relative flex cursor-pointer rounded-lg px-5 py-6 shadow-md focus:outline-none ${
177
- mode.disabled
178
- ? 'bg-gray-50'
179
- : 'bg-white hover:ring-2 hover:ring-cyan-600 hover:ring-offset-2'
180
- }`}
181
- >
182
- <div className="flex w-full items-center justify-between">
183
- <div className="flex items-center">
184
- <div className="flex-shrink-0">
185
- {mode.disabled ? (
186
- <ExclamationTriangleIcon
187
- className="h-8 w-8 text-amber-500"
188
- aria-hidden="true"
189
- />
190
- ) : (
191
- <mode.icon
192
- className="h-8 w-8 text-cyan-700 data-[state=checked]:text-white"
193
- aria-hidden="true"
194
- />
195
- )}
196
- </div>
197
- <div className="ml-4">
198
- <RadioGroup.ItemText>
199
- <p
200
- className={`font-bold ${mode.disabled ? 'text-gray-400' : 'text-gray-900 data-[state=checked]:text-white'}`}
201
- >
202
- {mode.name}
203
- </p>
204
- <span
205
- className={`inline ${mode.disabled ? 'text-gray-400' : 'text-gray-500 data-[state=checked]:text-cyan-100'}`}
206
- >
207
- {mode.description}
208
- {mode.disabled && mode.disabledReason && (
209
- <span className="mt-1 block font-bold text-amber-500">
210
- {mode.disabledReason}
211
- </span>
212
- )}
213
- </span>
214
- </RadioGroup.ItemText>
215
- </div>
216
- </div>
217
- <div className="hidden shrink-0 text-white data-[state=checked]:block">
218
- <CheckCircleIcon className="h-6 w-6" />
219
- </div>
220
- </div>
221
- <RadioGroup.ItemControl className="radio-control mr-2 flex h-4 w-4 items-center justify-center rounded-full border border-gray-300">
222
- <div className="radio-dot h-2 w-2 rounded-full" />
223
- </RadioGroup.ItemControl>
224
- <RadioGroup.ItemHiddenInput />
225
- </RadioGroup.Item>
226
- ))}
227
- </div>
228
- </RadioGroup.Root>
229
- </div>
230
-
231
- <div className="mt-8 flex justify-end">
232
- <button
233
- type="button"
234
- onClick={handleContinue}
235
- className="inline-flex justify-center rounded-md bg-cyan-700 px-6 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-600"
236
- >
237
- Continue
238
- </button>
239
- </div>
240
- </div>
241
- </div>
242
- );
243
- };
244
-
245
- export default PageCreationSelector;
@@ -1,339 +0,0 @@
1
- import { useState } from 'react';
2
- import type { ReactNode } from 'react';
3
- import { RadioGroup } from '@ark-ui/react/radio-group';
4
- import { ulid } from 'ulid';
5
- import VisualBreakPreview from '@/components/compositor/preview/VisualBreakPreview';
6
- import { getTemplateVisualBreakPane } from '@/utils/compositor/TemplatePanes';
7
- import { fullContentMapStore } from '@/stores/storykeep';
8
- import type { NodesContext } from '@/stores/nodes';
9
- import { findUniqueSlug } from '@/utils/helpers';
10
- import { tailwindToHex } from '@/utils/compositor/tailwindColors';
11
- import { SvgBreaks } from '@/constants/shapes';
12
- import type { StoryFragmentNode, TemplatePane } from '@/types/compositorTypes';
13
-
14
- // Layout options with IDs, labels, and descriptions
15
- const layoutOptions = [
16
- {
17
- id: 'featured-only',
18
- label: 'Featured Content Only',
19
- description: 'A hero section highlighting your most important content',
20
- },
21
- {
22
- id: 'featured-list',
23
- label: 'Featured with List',
24
- description: 'Hero section with a supporting content grid below',
25
- },
26
- {
27
- id: 'complete-home',
28
- label: 'Complete Home Layout',
29
- description: 'Hero section, visual break, and supporting content grid',
30
- },
31
- ];
32
-
33
- // Visual break variants for selection
34
- const breakVariants = [
35
- { id: 'cutwide2', label: 'Wave Cut', odd: true },
36
- { id: 'cutwide1', label: 'Diagonal Cut', odd: true },
37
- { id: 'burstwide2', label: 'Burst', odd: false },
38
- { id: 'crookedwide', label: 'Crooked', odd: false },
39
- ];
40
-
41
- interface PageCreationSpecialProps {
42
- nodeId: string;
43
- ctx: NodesContext;
44
- }
45
-
46
- const PageCreationSpecial = ({
47
- nodeId,
48
- ctx,
49
- }: PageCreationSpecialProps): ReactNode => {
50
- // State for layout and visual break selection
51
- const [selectedLayout, setSelectedLayout] = useState(layoutOptions[0].id);
52
- const [selectedBreak, setSelectedBreak] = useState(breakVariants[0].id);
53
- const [isCreating, setIsCreating] = useState(false);
54
-
55
- const existingSlugs = fullContentMapStore
56
- .get()
57
- .filter((item) => ['Pane', 'StoryFragment'].includes(item.type))
58
- .map((item) => item.slug);
59
-
60
- // CSS for RadioGroup styling
61
- const radioGroupStyles = `
62
- .radio-control[data-state="unchecked"] .radio-dot {
63
- background-color: #d1d5db; /* gray-300 */
64
- }
65
- .radio-control[data-state="checked"] .radio-dot {
66
- background-color: #0891b2; /* cyan-600 */
67
- }
68
- .radio-control[data-state="checked"] {
69
- border-color: #0891b2;
70
- }
71
- .radio-item[data-state="checked"] {
72
- border-color: #0891b2;
73
- background-color: #ecfeff; /* cyan-50 */
74
- }
75
- `;
76
-
77
- // Function to handle continue/apply button
78
- const handleApply = async () => {
79
- if (!selectedLayout) return; // Null check
80
-
81
- try {
82
- setIsCreating(true);
83
-
84
- // Get the storyfragment node
85
- const storyfragment = ctx.allNodes.get().get(nodeId) as StoryFragmentNode;
86
- if (!storyfragment) {
87
- console.error('Story fragment not found');
88
- return;
89
- }
90
-
91
- // Create panes array to hold the IDs of all panes we'll create
92
- const paneIds: string[] = [];
93
-
94
- // 1. Create Featured Content pane
95
- const featuredContentPane: TemplatePane = {
96
- id: ulid(),
97
- nodeType: 'Pane',
98
- title: 'Featured Article',
99
- slug: findUniqueSlug(`featured-article`, existingSlugs),
100
- isDecorative: false,
101
- parentId: nodeId,
102
- codeHookTarget: 'featured-article',
103
- codeHookPayload: {
104
- options: JSON.stringify({
105
- title: 'Featured Article',
106
- }),
107
- },
108
- };
109
-
110
- // Add the featured content pane
111
- const featuredContentId = ctx.addTemplatePane(
112
- nodeId,
113
- featuredContentPane
114
- );
115
- if (featuredContentId) {
116
- paneIds.push(featuredContentId);
117
- }
118
-
119
- // If layout includes visual break + list content
120
- if (selectedLayout === 'complete-home') {
121
- // Get the selected break variant
122
- const breakVariant = breakVariants.find((b) => b.id === selectedBreak);
123
- const bgColor = breakVariant?.odd ? 'white' : 'gray-50';
124
- const fillColor = breakVariant?.odd ? 'gray-50' : 'white';
125
-
126
- const shapeName = `kCz${selectedBreak}`;
127
- const isFlipped = SvgBreaks[shapeName]?.flipped || false;
128
-
129
- const finalBgColor = tailwindToHex(
130
- isFlipped ? fillColor : bgColor,
131
- null
132
- );
133
- const finalFillColor = tailwindToHex(
134
- isFlipped ? bgColor : fillColor,
135
- null
136
- );
137
-
138
- // 2. Create Visual Break pane
139
- const visualBreakTemplate = getTemplateVisualBreakPane(selectedBreak);
140
- visualBreakTemplate.id = ulid();
141
- visualBreakTemplate.title = 'Visual Break';
142
- visualBreakTemplate.slug = `${storyfragment.slug}-visual-break`;
143
- visualBreakTemplate.bgColour = finalBgColor;
144
-
145
- // Configure the SVG fill color
146
- if (visualBreakTemplate.bgPane) {
147
- if (visualBreakTemplate.bgPane.type === 'visual-break') {
148
- if (visualBreakTemplate.bgPane.breakDesktop) {
149
- visualBreakTemplate.bgPane.breakDesktop.svgFill = finalFillColor;
150
- }
151
- if (visualBreakTemplate.bgPane.breakTablet) {
152
- visualBreakTemplate.bgPane.breakTablet.svgFill = finalFillColor;
153
- }
154
- if (visualBreakTemplate.bgPane.breakMobile) {
155
- visualBreakTemplate.bgPane.breakMobile.svgFill = finalFillColor;
156
- }
157
- }
158
- }
159
-
160
- // Add the visual break pane
161
- const visualBreakId = ctx.addTemplatePane(nodeId, visualBreakTemplate);
162
- if (visualBreakId) {
163
- paneIds.push(visualBreakId);
164
- }
165
- }
166
-
167
- // If layout includes list content
168
- if (
169
- selectedLayout === 'featured-list' ||
170
- selectedLayout === 'complete-home'
171
- ) {
172
- // 3. Create List Content pane
173
- const listContentPane: TemplatePane = {
174
- id: ulid(),
175
- nodeType: 'Pane',
176
- title: 'Content List',
177
- slug: `${storyfragment.slug}-content-list`,
178
- isDecorative: false,
179
- parentId: nodeId,
180
- // For complete-home layout, match the background color with the visual break
181
- bgColour: tailwindToHex(
182
- selectedLayout === 'complete-home' ? 'gray-50' : 'white',
183
- null
184
- ),
185
- codeHookTarget: 'list-content',
186
- codeHookPayload: {
187
- options: JSON.stringify({
188
- title: 'More Articles',
189
- sortByPopular: 'true',
190
- showTopics: 'true',
191
- showDate: 'true',
192
- limit: '10',
193
- category: '',
194
- }),
195
- },
196
- };
197
-
198
- // Add the list content pane
199
- const listContentId = ctx.addTemplatePane(nodeId, listContentPane);
200
- if (listContentId) {
201
- paneIds.push(listContentId);
202
- }
203
- }
204
-
205
- // Update the storyfragment with the new panes
206
- if (paneIds.length > 0) {
207
- storyfragment.paneIds = paneIds;
208
- storyfragment.isChanged = true;
209
- ctx.modifyNodes([storyfragment]);
210
- ctx.notifyNode('root');
211
- }
212
-
213
- // Set title and slug if they're not set
214
- if (!storyfragment.title || !storyfragment.slug) {
215
- const updatedFragment = {
216
- ...storyfragment,
217
- title: storyfragment.title || 'Home Page',
218
- slug: storyfragment.slug || 'home',
219
- isChanged: true,
220
- };
221
- ctx.modifyNodes([updatedFragment]);
222
- }
223
- } catch (error) {
224
- console.error('Error creating special layout:', error);
225
- } finally {
226
- setIsCreating(false);
227
- }
228
- };
229
-
230
- return (
231
- <div className="rounded-md bg-white p-6">
232
- <style>{radioGroupStyles}</style>
233
- <div className="text-mydarkgrey mb-6 space-y-6 italic">
234
- <strong>Note:</strong> when editing web pages (story fragments) be sure
235
- to click on Topics &amp; Details for each page; (if you see no articles,
236
- that's why!)
237
- </div>
238
- <div className="mb-6 space-y-6">
239
- <div>
240
- <RadioGroup.Root
241
- defaultValue={selectedLayout}
242
- onValueChange={(details) => {
243
- if (details.value) {
244
- setSelectedLayout(details.value);
245
- }
246
- }}
247
- >
248
- <RadioGroup.Label className="text-lg font-bold">
249
- Select Layout
250
- </RadioGroup.Label>
251
- <div className="mt-2 space-y-4">
252
- {layoutOptions.map((option) => (
253
- <RadioGroup.Item
254
- key={option.id}
255
- value={option.id}
256
- className="radio-item flex items-center space-x-3 rounded-lg border p-4"
257
- >
258
- <div className="flex items-center">
259
- <RadioGroup.ItemControl className="radio-control mr-2 flex h-4 w-4 items-center justify-center rounded-full border border-gray-300">
260
- <div className="radio-dot h-2 w-2 rounded-full" />
261
- </RadioGroup.ItemControl>
262
- <RadioGroup.ItemText>
263
- <div>
264
- <div className="font-bold">{option.label}</div>
265
- <div className="text-sm text-gray-500">
266
- {option.description}
267
- </div>
268
- </div>
269
- </RadioGroup.ItemText>
270
- </div>
271
- <RadioGroup.ItemHiddenInput />
272
- </RadioGroup.Item>
273
- ))}
274
- </div>
275
- </RadioGroup.Root>
276
- </div>
277
-
278
- {selectedLayout === 'complete-home' && (
279
- <div>
280
- <div className="mb-2 text-lg font-bold">
281
- Select Visual Break Style
282
- </div>
283
- <div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-5">
284
- {breakVariants.map((breakVar) => (
285
- <div
286
- key={breakVar.id}
287
- className={`cursor-pointer rounded-lg border p-2 ${
288
- selectedBreak === breakVar.id
289
- ? 'border-cyan-600 ring-2 ring-cyan-600 ring-opacity-50'
290
- : 'border-gray-300'
291
- }`}
292
- onClick={() => setSelectedBreak(breakVar.id)}
293
- >
294
- <div className="h-16 overflow-hidden rounded">
295
- <VisualBreakPreview
296
- bgColour="#ffffff"
297
- fillColour="#000000"
298
- variant={breakVar.id}
299
- height={60}
300
- />
301
- </div>
302
- <div className="mt-1 text-center text-sm font-bold">
303
- {breakVar.label}
304
- </div>
305
- </div>
306
- ))}
307
- </div>
308
- </div>
309
- )}
310
- </div>
311
-
312
- <div className="mt-6 flex justify-end gap-3">
313
- <button
314
- onClick={() => {
315
- ctx.setPanelMode(nodeId, 'add', 'DEFAULT');
316
- ctx.notifyNode('root');
317
- }}
318
- className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 hover:bg-gray-50"
319
- disabled={isCreating}
320
- >
321
- Back
322
- </button>
323
- <button
324
- onClick={handleApply}
325
- disabled={isCreating || !selectedLayout}
326
- className={`rounded-md px-6 py-2 text-sm font-bold text-white transition-colors ${
327
- isCreating || !selectedLayout
328
- ? 'bg-gray-400'
329
- : 'bg-cyan-600 hover:bg-cyan-700'
330
- }`}
331
- >
332
- {isCreating ? 'Creating...' : 'Create Layout'}
333
- </button>
334
- </div>
335
- </div>
336
- );
337
- };
338
-
339
- export default PageCreationSpecial;