astro-tractstack 2.0.17 → 2.0.18

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 (60) hide show
  1. package/dist/index.js +18 -0
  2. package/package.json +1 -1
  3. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +1 -1
  4. package/templates/src/components/codehooks/ListContentSetup.tsx +1 -1
  5. package/templates/src/components/compositor/Compositor.tsx +1 -0
  6. package/templates/src/components/compositor/Node.tsx +41 -17
  7. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +9 -6
  8. package/templates/src/components/compositor/nodes/GridLayout.tsx +124 -0
  9. package/templates/src/components/compositor/nodes/GridLayout_eraser.tsx +33 -0
  10. package/templates/src/components/compositor/nodes/Markdown.tsx +67 -37
  11. package/templates/src/components/compositor/nodes/Markdown_eraser.tsx +56 -0
  12. package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +8 -2
  13. package/templates/src/components/edit/PanelSwitch.tsx +232 -75
  14. package/templates/src/components/edit/SettingsPanel.tsx +0 -1
  15. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +3 -3
  16. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +167 -145
  17. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +2 -2
  18. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -7
  19. package/templates/src/components/edit/pane/PanePanel_impression.tsx +1 -1
  20. package/templates/src/components/edit/pane/RestylePaneModal.tsx +8 -5
  21. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +6 -6
  22. package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +3 -3
  23. package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +4 -4
  24. package/templates/src/components/edit/panels/StyleElementPanel.tsx +11 -4
  25. package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +8 -8
  26. package/templates/src/components/edit/panels/StyleElementPanel_remove.tsx +14 -4
  27. package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +16 -4
  28. package/templates/src/components/edit/panels/StyleImagePanel.tsx +8 -3
  29. package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +9 -2
  30. package/templates/src/components/edit/panels/StyleImagePanel_remove.tsx +5 -2
  31. package/templates/src/components/edit/panels/StyleImagePanel_update.tsx +5 -2
  32. package/templates/src/components/edit/panels/StyleLiElementPanel.tsx +7 -3
  33. package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +9 -2
  34. package/templates/src/components/edit/panels/StyleLiElementPanel_remove.tsx +5 -2
  35. package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +5 -2
  36. package/templates/src/components/edit/panels/StyleParentPanel.tsx +530 -171
  37. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +77 -42
  38. package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +38 -22
  39. package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +171 -66
  40. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +166 -98
  41. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +7 -3
  42. package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +9 -2
  43. package/templates/src/components/edit/panels/StyleWidgetPanel_remove.tsx +5 -2
  44. package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +6 -2
  45. package/templates/src/components/edit/state/SaveModal.tsx +10 -2
  46. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +6 -6
  47. package/templates/src/components/fields/PaneBreakShapeSelector.tsx +1 -1
  48. package/templates/src/components/widgets/ImpressionWrapper.tsx +4 -1
  49. package/templates/src/constants/prompts.json +1 -1
  50. package/templates/src/stores/nodes.ts +110 -33
  51. package/templates/src/stores/storykeep.ts +3 -1
  52. package/templates/src/types/compositorTypes.ts +37 -2
  53. package/templates/src/utils/compositor/TemplateNodes.ts +8 -0
  54. package/templates/src/utils/compositor/nodesHelper.ts +229 -0
  55. package/templates/src/utils/compositor/reduceNodesClassNames.ts +40 -1
  56. package/templates/src/utils/compositor/typeGuards.ts +7 -0
  57. package/templates/src/utils/etl/extractor.ts +1 -5
  58. package/templates/src/utils/etl/index.ts +1 -0
  59. package/templates/src/utils/etl/transformer.ts +70 -25
  60. package/utils/inject-files.ts +18 -0
@@ -1,85 +1,149 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
3
- import { settingsPanelStore } from '@/stores/storykeep';
3
+ import ArrowUturnLeftIcon from '@heroicons/react/24/outline/ArrowUturnLeftIcon';
4
+ import ChevronLeftIcon from '@heroicons/react/24/outline/ChevronLeftIcon';
5
+ import ChevronRightIcon from '@heroicons/react/24/outline/ChevronRightIcon';
6
+ import {
7
+ settingsPanelStore,
8
+ stylePanelTargetMemoryStore,
9
+ } from '@/stores/storykeep';
4
10
  import { getCtx } from '@/stores/nodes';
5
11
  import {
6
12
  isMarkdownPaneFragmentNode,
13
+ isArtpackImageNode,
14
+ isBgImageNode,
15
+ isGridLayoutNode,
7
16
  isPaneNode,
8
17
  } from '@/utils/compositor/typeGuards';
9
- import { StylesMemory } from '@/components/edit/state/StylesMemory';
10
18
  import SelectedTailwindClass from '@/components/fields/SelectedTailwindClass';
11
19
  import BackgroundImageWrapper from '@/components/fields/BackgroundImageWrapper';
20
+ import ColorPickerCombo from '@/components/fields/ColorPickerCombo';
12
21
  import { cloneDeep } from '@/utils/helpers';
22
+ import {
23
+ convertToGrid,
24
+ revertFromGrid,
25
+ addColumn,
26
+ } from '@/utils/compositor/nodesHelper';
13
27
  import type {
14
28
  MarkdownPaneFragmentNode,
15
- BasePanelProps,
29
+ ParentBasePanelProps,
30
+ ArtpackImageNode,
31
+ BgImageNode,
32
+ GridLayoutNode,
33
+ BaseNode,
34
+ DefaultClassValue,
16
35
  } from '@/types/compositorTypes';
17
36
 
18
- interface ParentStyles {
19
- bgColor: string;
20
- parentClasses: {
21
- mobile: Record<string, string>;
22
- tablet: Record<string, string>;
23
- desktop: Record<string, string>;
24
- }[];
25
- }
37
+ type VisibilityKey =
38
+ | 'hiddenViewportMobile'
39
+ | 'hiddenViewportTablet'
40
+ | 'hiddenViewportDesktop';
41
+ type PanelView = 'summary' | 'wrapperStyles' | 'backgroundImage';
42
+
43
+ type StyleableNode = MarkdownPaneFragmentNode | GridLayoutNode;
44
+
45
+ type StyleableTarget = {
46
+ id: string;
47
+ name: string;
48
+ node: StyleableNode;
49
+ targetProperty: 'parentClasses' | 'gridClasses';
50
+ };
26
51
 
27
52
  const StyleParentPanel = ({
28
- node,
29
- parentNode,
53
+ node: initialNode,
54
+ parentNode: paneNode,
30
55
  layer,
31
56
  config,
32
- }: BasePanelProps) => {
33
- if (
34
- !parentNode ||
35
- !node ||
36
- !isMarkdownPaneFragmentNode(node) ||
37
- !isPaneNode(parentNode) ||
38
- !isMarkdownPaneFragmentNode(node)
39
- ) {
40
- return null;
41
- }
42
-
43
- const [layerCount, setLayerCount] = useState(node.parentClasses?.length || 0);
57
+ }: ParentBasePanelProps) => {
58
+ const [currentView, setCurrentView] = useState<PanelView>('summary');
44
59
  const [currentLayer, setCurrentLayer] = useState<number>(layer || 1);
45
- const [settings, setSettings] = useState<ParentStyles>({
46
- bgColor: parentNode.bgColour || '',
47
- parentClasses: node.parentClasses || [],
48
- });
60
+ const [styleTargets, setStyleTargets] = useState<StyleableTarget[]>([]);
61
+ const [selectedTargetIndex, setSelectedTargetIndex] = useState(0);
62
+
63
+ const ctx = getCtx();
49
64
 
50
- // Update state when node changes
51
65
  useEffect(() => {
52
- setLayerCount(node.parentClasses?.length || 0);
53
- setSettings({
54
- bgColor: parentNode.bgColour || '',
55
- parentClasses: node.parentClasses || [],
66
+ if (
67
+ !initialNode ||
68
+ !(
69
+ isMarkdownPaneFragmentNode(initialNode) || isGridLayoutNode(initialNode)
70
+ ) ||
71
+ !paneNode ||
72
+ !isPaneNode(paneNode)
73
+ ) {
74
+ return;
75
+ }
76
+
77
+ const targets: StyleableTarget[] = [];
78
+ const isGrid = isGridLayoutNode(initialNode);
79
+
80
+ targets.push({
81
+ id: initialNode.id,
82
+ name: isGrid ? 'Outer Container' : 'Pane Styles',
83
+ node: initialNode,
84
+ targetProperty: 'parentClasses',
56
85
  });
57
- }, [node, parentNode.bgColour]);
86
+
87
+ if (isGrid) {
88
+ const columnNodes = ctx
89
+ .getChildNodeIDs(initialNode.id)
90
+ .map((id) => ctx.allNodes.get().get(id) as BaseNode)
91
+ .filter(isMarkdownPaneFragmentNode);
92
+
93
+ columnNodes.forEach((colNode, index) => {
94
+ targets.push({
95
+ id: colNode.id,
96
+ name: `Column ${index + 1}`,
97
+ node: colNode,
98
+ targetProperty: 'gridClasses',
99
+ });
100
+ });
101
+ }
102
+
103
+ setStyleTargets(targets);
104
+
105
+ const rememberedIndex = stylePanelTargetMemoryStore.get().get(paneNode.id);
106
+
107
+ if (rememberedIndex != null && rememberedIndex < targets.length) {
108
+ setSelectedTargetIndex(rememberedIndex);
109
+ } else {
110
+ setSelectedTargetIndex(0);
111
+ }
112
+
113
+ setCurrentView('summary');
114
+ }, [initialNode, ctx, paneNode]);
58
115
 
59
116
  useEffect(() => {
60
117
  setCurrentLayer(layer || 1);
61
118
  }, [layer]);
62
119
 
63
- const handleLayerAdd = (position: 'before' | 'after', layerNum: number) => {
64
- const ctx = getCtx();
65
- const allNodes = ctx.allNodes.get();
66
- const markdownNode = cloneDeep(allNodes.get(node.id));
67
- if (!markdownNode || !isMarkdownPaneFragmentNode(markdownNode)) return;
120
+ useEffect(() => {
121
+ if (paneNode?.id) {
122
+ const newMemory = new Map(stylePanelTargetMemoryStore.get());
123
+ newMemory.set(paneNode.id, selectedTargetIndex);
124
+ stylePanelTargetMemoryStore.set(newMemory);
125
+ }
126
+ }, [selectedTargetIndex, paneNode?.id]);
68
127
 
69
- // Create an empty layer
70
- const emptyLayer = {
71
- mobile: {},
72
- tablet: {},
73
- desktop: {},
74
- };
128
+ const selectedTarget = styleTargets[selectedTargetIndex];
129
+ if (!selectedTarget || !paneNode || !isPaneNode(paneNode)) {
130
+ return null;
131
+ }
75
132
 
76
- // Create new arrays for both parentClasses
77
- let newParentClasses = [...(markdownNode.parentClasses || [])];
133
+ const {
134
+ id: selectedTargetId,
135
+ name: selectedTargetName,
136
+ node: selectedTargetNode,
137
+ targetProperty,
138
+ } = selectedTarget;
78
139
 
79
- // Calculate the insert index based on position and layerNum
140
+ const handleLayerAdd = (position: 'before' | 'after', layerNum: number) => {
141
+ const targetNode = cloneDeep(selectedTargetNode);
142
+
143
+ const emptyLayer = { mobile: {}, tablet: {}, desktop: {} };
144
+ let newParentClasses = [...(targetNode.parentClasses || [])];
80
145
  const insertIndex = position === 'before' ? layerNum - 1 : layerNum;
81
146
 
82
- // Insert the empty layer at the calculated index
83
147
  newParentClasses = [
84
148
  ...newParentClasses.slice(0, insertIndex),
85
149
  emptyLayer,
@@ -88,176 +152,471 @@ const StyleParentPanel = ({
88
152
 
89
153
  ctx.modifyNodes([
90
154
  {
91
- ...markdownNode,
155
+ ...targetNode,
92
156
  parentClasses: newParentClasses,
93
157
  isChanged: true,
94
- } as MarkdownPaneFragmentNode,
158
+ } as StyleableNode,
95
159
  ]);
96
160
 
97
- // Update local state
98
- setSettings((prev) => ({
99
- ...prev,
100
- parentClasses: newParentClasses,
101
- }));
102
- setLayerCount(newParentClasses.length);
103
-
104
- // Set the current layer to the newly added layer
105
161
  const newLayer = position === 'before' ? layerNum : layerNum + 1;
106
162
  setCurrentLayer(newLayer);
107
163
  };
108
164
 
109
- const handleClickDeleteLayer = () => {
165
+ const dispatchToSubPanel = (
166
+ action: string,
167
+ extraProps: Record<string, any> = {}
168
+ ) => {
110
169
  settingsPanelStore.set({
111
- nodeId: node.id,
112
- layer: currentLayer,
113
- action: `style-parent-delete-layer`,
170
+ nodeId: selectedTargetId,
171
+ action,
172
+ ...extraProps,
173
+ targetProperty: targetProperty,
114
174
  expanded: true,
115
175
  });
116
176
  };
117
177
 
178
+ const handleGridColumnChange = (
179
+ viewport: 'mobile' | 'tablet' | 'desktop',
180
+ value: string
181
+ ) => {
182
+ const count = parseInt(value, 10);
183
+ if (isNaN(count) || !isGridLayoutNode(selectedTargetNode)) return;
184
+
185
+ const updatedNode = cloneDeep(selectedTargetNode);
186
+ updatedNode.gridColumns[viewport] = count;
187
+ updatedNode.isChanged = true;
188
+ ctx.modifyNodes([updatedNode]);
189
+ ctx.notifyNode('root');
190
+ };
191
+
192
+ const handleClickDeleteLayer = () => {
193
+ dispatchToSubPanel('style-parent-delete-layer', { layer: currentLayer });
194
+ };
118
195
  const handleClickRemove = (name: string) => {
119
- settingsPanelStore.set({
120
- nodeId: node.id,
196
+ dispatchToSubPanel('style-parent-remove', {
121
197
  layer: currentLayer,
122
198
  className: name,
123
- action: `style-parent-remove`,
124
- expanded: true,
125
199
  });
126
200
  };
127
-
128
201
  const handleClickUpdate = (name: string) => {
129
- settingsPanelStore.set({
130
- nodeId: node.id,
202
+ dispatchToSubPanel('style-parent-update', {
131
203
  layer: currentLayer,
132
204
  className: name,
133
- action: `style-parent-update`,
134
- expanded: true,
135
205
  });
136
206
  };
137
-
138
207
  const handleClickAdd = () => {
139
- settingsPanelStore.set({
140
- nodeId: node.id,
141
- layer: currentLayer,
142
- action: `style-parent-add`,
143
- expanded: true,
144
- });
208
+ dispatchToSubPanel('style-parent-add', { layer: currentLayer });
209
+ };
210
+
211
+ const handleColorChange = (color: string) => {
212
+ const updatedPaneNode = cloneDeep(paneNode);
213
+ if (color) {
214
+ updatedPaneNode.bgColour = color;
215
+ } else if (typeof updatedPaneNode.bgColour === 'string' && !color) {
216
+ delete updatedPaneNode.bgColour;
217
+ }
218
+ updatedPaneNode.isChanged = true;
219
+ ctx.modifyNodes([updatedPaneNode]);
220
+ ctx.notifyNode('root');
145
221
  };
146
222
 
147
- // Safely get current classes
148
- const currentClasses = settings.parentClasses?.[currentLayer - 1] || {
149
- mobile: {},
150
- tablet: {},
151
- desktop: {},
223
+ const handleVisibilityChange = (
224
+ viewport: 'mobile' | 'tablet' | 'desktop'
225
+ ) => {
226
+ const updatedNode = cloneDeep(selectedTargetNode);
227
+ const key: VisibilityKey = `hiddenViewport${
228
+ viewport.charAt(0).toUpperCase() + viewport.slice(1)
229
+ }` as VisibilityKey;
230
+ updatedNode[key] = !updatedNode[key];
231
+ updatedNode.isChanged = true;
232
+ ctx.modifyNodes([updatedNode]);
233
+ ctx.notifyNode('root');
152
234
  };
153
- const hasNoClasses = !Object.values(currentClasses).some(
154
- (breakpoint) => Object.keys(breakpoint).length > 0
235
+
236
+ const BackButton = () => (
237
+ <button
238
+ onClick={() => setCurrentView('summary')}
239
+ className="mb-4 flex items-center gap-2 text-sm font-bold text-gray-600 hover:text-black"
240
+ >
241
+ <ArrowUturnLeftIcon className="h-4 w-4" />
242
+ Back to Summary
243
+ </button>
155
244
  );
156
245
 
157
- return (
158
- <div className="space-y-4">
246
+ const TargetNavigator = () => (
247
+ <div className="mb-4 flex items-center justify-between rounded-md bg-slate-100 p-2">
248
+ <button
249
+ onClick={() =>
250
+ setSelectedTargetIndex(
251
+ (prev) => (prev - 1 + styleTargets.length) % styleTargets.length
252
+ )
253
+ }
254
+ className="rounded-full p-1 text-gray-500 hover:bg-gray-200 hover:text-black"
255
+ disabled={styleTargets.length < 2}
256
+ >
257
+ <ChevronLeftIcon className="h-5 w-5" />
258
+ </button>
259
+ <span className="text-sm font-bold uppercase tracking-wider text-gray-700">
260
+ {selectedTargetName}
261
+ </span>
262
+ <button
263
+ onClick={() =>
264
+ setSelectedTargetIndex((prev) => (prev + 1) % styleTargets.length)
265
+ }
266
+ className="rounded-full p-1 text-gray-500 hover:bg-gray-200 hover:text-black"
267
+ disabled={styleTargets.length < 2}
268
+ >
269
+ <ChevronRightIcon className="h-5 w-5" />
270
+ </button>
271
+ </div>
272
+ );
273
+
274
+ const renderSummaryView = () => {
275
+ if (!initialNode) return null;
276
+ const isGrid = isGridLayoutNode(initialNode);
277
+ const childNodeIds = ctx.getChildNodeIDs(paneNode.id);
278
+ const bgNode = childNodeIds
279
+ .map((id) => ctx.allNodes.get().get(id))
280
+ .find(
281
+ (n) =>
282
+ n?.nodeType === 'BgPane' &&
283
+ 'type' in n &&
284
+ (n.type === 'background-image' || n.type === 'artpack-image')
285
+ ) as (BgImageNode | ArtpackImageNode) | undefined;
286
+ let bgSummary = 'None';
287
+ if (bgNode) {
288
+ if (isArtpackImageNode(bgNode)) bgSummary = `Artpack: ${bgNode.image}`;
289
+ else if (isBgImageNode(bgNode)) bgSummary = `Custom Image`;
290
+ }
291
+
292
+ const wrapperSummary = `${selectedTargetNode.parentClasses?.length || 0} layers`;
293
+
294
+ let columnClasses: DefaultClassValue = {
295
+ mobile: {},
296
+ tablet: {},
297
+ desktop: {},
298
+ };
299
+ let columnHasNoClasses = true;
300
+
301
+ if (
302
+ selectedTargetIndex > 0 &&
303
+ isMarkdownPaneFragmentNode(selectedTargetNode)
304
+ ) {
305
+ columnClasses = selectedTargetNode.gridClasses || {
306
+ mobile: {},
307
+ tablet: {},
308
+ desktop: {},
309
+ };
310
+ columnHasNoClasses = !Object.values(columnClasses).some(
311
+ (breakpoint) => Object.keys(breakpoint).length > 0
312
+ );
313
+ }
314
+
315
+ return (
159
316
  <div className="space-y-4">
160
- <BackgroundImageWrapper
161
- paneId={parentNode.id}
162
- config={config || undefined}
163
- />
164
- </div>
317
+ {styleTargets.length > 1 && <TargetNavigator />}
165
318
 
166
- <div className="mb-4 flex items-center gap-3 rounded-md bg-slate-50 p-3">
167
- <span className="text-mydarkgrey text-sm font-bold">Layer:</span>
168
- <div className="flex items-center gap-2">
169
- <button
170
- key="first-add"
171
- className="border-mydarkgrey/30 text-mydarkgrey rounded border border-dashed p-1 text-xs transition-colors hover:bg-white/50 hover:text-black"
172
- title="Add Layer Here"
173
- onClick={() => handleLayerAdd('before', 1)}
174
- >
175
- <PlusIcon className="h-3 w-3" />
176
- </button>
177
- {[...Array(layerCount).keys()]
178
- .map((i) => i + 1)
179
- .map((num, index) => (
180
- <div
181
- key={`layer-group-${num}`}
182
- className="flex items-center gap-1"
183
- >
319
+ {selectedTargetIndex === 0 && (
320
+ <div className="space-y-3">
321
+ <ColorPickerCombo
322
+ title="Pane Background Color"
323
+ defaultColor={paneNode.bgColour || ''}
324
+ onColorChange={handleColorChange}
325
+ config={config!}
326
+ allowNull={true}
327
+ />
328
+ <div className="flex items-center justify-between border-t border-gray-200 pt-3">
329
+ <span>Pane Styles:</span>
330
+ <div className="flex items-center gap-2">
331
+ <span className="text-sm text-gray-600">{wrapperSummary}</span>
184
332
  <button
185
- className={`min-w-[32px] rounded-md px-3 py-1.5 text-sm font-bold transition-colors ${
186
- currentLayer === num
187
- ? 'bg-myblue text-white shadow-sm'
188
- : 'text-mydarkgrey hover:bg-mydarkgrey/10 bg-white hover:text-black'
189
- }`}
190
- onClick={() => setCurrentLayer(num)}
333
+ onClick={() => setCurrentView('wrapperStyles')}
334
+ className="rounded bg-gray-100 px-3 py-1 text-sm font-bold text-gray-700 hover:bg-gray-200"
191
335
  >
192
- {num}
336
+ Edit
193
337
  </button>
338
+ </div>
339
+ </div>
340
+ <div className="flex items-center justify-between">
341
+ <span>Background Image:</span>
342
+ <div className="flex items-center gap-2">
343
+ <span className="max-w-36 truncate text-right text-sm text-gray-600">
344
+ {bgSummary}
345
+ </span>
194
346
  <button
195
- className="border-mydarkgrey/30 text-mydarkgrey rounded border border-dashed p-1 text-xs transition-colors hover:bg-white/50 hover:text-black"
196
- title="Add Layer Here"
197
- onClick={() =>
198
- handleLayerAdd(
199
- index === layerCount - 1 ? 'after' : 'before',
200
- index === layerCount - 1 ? num : num + 1
201
- )
202
- }
347
+ onClick={() => setCurrentView('backgroundImage')}
348
+ className="rounded bg-gray-100 px-3 py-1 text-sm font-bold text-gray-700 hover:bg-gray-200"
203
349
  >
204
- <PlusIcon className="h-3 w-3" />
350
+ Edit
205
351
  </button>
206
352
  </div>
207
- ))}
353
+ </div>
354
+ </div>
355
+ )}
356
+
357
+ {selectedTargetIndex > 0 &&
358
+ isMarkdownPaneFragmentNode(selectedTargetNode) && (
359
+ <div className="space-y-4">
360
+ <h3 className="mb-3 text-sm font-bold uppercase text-gray-500">
361
+ Column Styles
362
+ </h3>
363
+ {columnHasNoClasses ? (
364
+ <div>
365
+ <em>No styles for this column.</em>
366
+ </div>
367
+ ) : (
368
+ <div className="flex flex-wrap gap-2">
369
+ {Object.entries(columnClasses.mobile || {}).map(
370
+ ([className]) => (
371
+ <SelectedTailwindClass
372
+ key={className}
373
+ name={className}
374
+ values={{
375
+ mobile: columnClasses.mobile[className],
376
+ tablet: columnClasses.tablet?.[className],
377
+ desktop: columnClasses.desktop?.[className],
378
+ }}
379
+ onRemove={handleClickRemove}
380
+ onUpdate={handleClickUpdate}
381
+ />
382
+ )
383
+ )}
384
+ </div>
385
+ )}
386
+ <div>
387
+ <ul className="text-mydarkgrey flex flex-wrap gap-x-4 gap-y-1">
388
+ <li>
389
+ <em>Actions:</em>
390
+ </li>
391
+ <li>
392
+ <button
393
+ onClick={handleClickAdd}
394
+ className="text-myblue font-bold underline hover:text-black"
395
+ >
396
+ Add Style
397
+ </button>
398
+ </li>
399
+ </ul>
400
+ </div>
401
+ </div>
402
+ )}
403
+
404
+ {selectedTargetIndex === 0 && (
405
+ <div className="space-y-3 border-t border-gray-200 pt-4">
406
+ <h3 className="text-sm font-bold uppercase text-gray-500">
407
+ Layout
408
+ </h3>
409
+ {!isGrid ? (
410
+ <button
411
+ onClick={() => convertToGrid(initialNode.id)}
412
+ className="w-full rounded bg-cyan-600 px-4 py-2 text-sm font-bold text-white hover:bg-cyan-700"
413
+ >
414
+ Convert to Grid Layout
415
+ </button>
416
+ ) : (
417
+ <>
418
+ {isGridLayoutNode(selectedTargetNode) && (
419
+ <div className="space-y-3 rounded-md border border-gray-200 bg-gray-50 p-3">
420
+ <h4 className="text-xs font-bold uppercase text-gray-500">
421
+ Grid Columns
422
+ </h4>
423
+ <div className="grid grid-cols-3 gap-3">
424
+ {(['mobile', 'tablet', 'desktop'] as const).map(
425
+ (viewport) => (
426
+ <div key={viewport}>
427
+ <label className="block text-center text-xs capitalize text-gray-600">
428
+ {viewport}
429
+ </label>
430
+ <select
431
+ value={selectedTargetNode.gridColumns[viewport]}
432
+ onChange={(e) =>
433
+ handleGridColumnChange(viewport, e.target.value)
434
+ }
435
+ className="mt-1 block w-full rounded-md border-gray-300 py-1 pl-2 pr-7 text-sm focus:border-cyan-500 focus:outline-none focus:ring-cyan-500"
436
+ >
437
+ {Array.from({ length: 12 }, (_, i) => i + 1).map(
438
+ (n) => (
439
+ <option key={n} value={n}>
440
+ {n}
441
+ </option>
442
+ )
443
+ )}
444
+ </select>
445
+ </div>
446
+ )
447
+ )}
448
+ </div>
449
+ </div>
450
+ )}
451
+ <button
452
+ onClick={() => revertFromGrid(initialNode.id)}
453
+ className="w-full rounded bg-gray-200 px-4 py-2 text-sm font-bold text-gray-700 hover:bg-gray-300"
454
+ >
455
+ Revert to Standard Pane
456
+ </button>
457
+ <button
458
+ onClick={() => addColumn(initialNode.id)}
459
+ className="w-full rounded border border-dashed border-gray-400 bg-transparent px-4 py-2 text-sm font-bold text-gray-700 hover:border-gray-600 hover:bg-gray-50"
460
+ >
461
+ Add Column
462
+ </button>
463
+ </>
464
+ )}
465
+ </div>
466
+ )}
467
+
468
+ <div className="space-y-3 border-t border-gray-200 pt-4">
469
+ <h3 className="text-sm font-bold uppercase text-gray-500">
470
+ Hide on Viewport
471
+ </h3>
472
+ <div className="flex justify-around">
473
+ {(['mobile', 'tablet', 'desktop'] as const).map((viewport) => {
474
+ const key: VisibilityKey =
475
+ `hiddenViewport${viewport.charAt(0).toUpperCase() + viewport.slice(1)}` as VisibilityKey;
476
+ return (
477
+ <label key={viewport} className="flex items-center space-x-2">
478
+ <input
479
+ type="checkbox"
480
+ className="h-4 w-4 rounded border-gray-300 text-cyan-600 focus:ring-cyan-500"
481
+ checked={!!selectedTargetNode[key]}
482
+ onChange={() => handleVisibilityChange(viewport)}
483
+ />
484
+ <span className="text-sm capitalize text-gray-700">
485
+ {viewport}
486
+ </span>
487
+ </label>
488
+ );
489
+ })}
490
+ </div>
208
491
  </div>
209
492
  </div>
493
+ );
494
+ };
210
495
 
211
- {hasNoClasses ? (
212
- <div className="space-y-4">
213
- <em>No styles.</em>
214
- </div>
215
- ) : currentClasses ? (
216
- <div className="flex flex-wrap gap-2">
217
- {Object.entries(currentClasses.mobile).map(([className]) => (
218
- <SelectedTailwindClass
219
- key={className}
220
- name={className}
221
- values={{
222
- mobile: currentClasses.mobile[className],
223
- tablet: currentClasses.tablet?.[className],
224
- desktop: currentClasses.desktop?.[className],
225
- }}
226
- onRemove={handleClickRemove}
227
- onUpdate={handleClickUpdate}
228
- />
229
- ))}
230
- </div>
231
- ) : null}
496
+ const renderWrapperStylesView = () => {
497
+ const layerCount = selectedTargetNode.parentClasses?.length || 0;
498
+ const currentClasses = selectedTargetNode.parentClasses?.[
499
+ currentLayer - 1
500
+ ] || { mobile: {}, tablet: {}, desktop: {} };
501
+ const hasNoClasses = !Object.values(currentClasses).some(
502
+ (breakpoint) => Object.keys(breakpoint).length > 0
503
+ );
232
504
 
505
+ return (
233
506
  <div className="space-y-4">
234
- <ul className="text-mydarkgrey flex flex-wrap gap-x-4 gap-y-1">
235
- <li>
236
- <em>Actions:</em>
237
- </li>
238
- <li>
239
- <button
240
- onClick={() => handleClickAdd()}
241
- className="text-myblue font-bold underline hover:text-black"
242
- >
243
- Add Style
244
- </button>
245
- </li>
246
- <li>
507
+ <BackButton />
508
+ <div className="mb-4 flex items-center gap-3 rounded-md bg-slate-50 p-3">
509
+ <span className="text-mydarkgrey text-sm font-bold">Layer:</span>
510
+ <div className="flex flex-wrap items-center gap-2">
247
511
  <button
248
- onClick={() => handleClickDeleteLayer()}
249
- className="text-myblue font-bold underline hover:text-black"
512
+ className="border-mydarkgrey/30 text-mydarkgrey rounded border border-dashed p-1 text-xs transition-colors hover:bg-white/50 hover:text-black"
513
+ title="Add Layer Here"
514
+ onClick={() => handleLayerAdd('before', 1)}
250
515
  >
251
- Delete Layer
516
+ <PlusIcon className="h-3 w-3" />
252
517
  </button>
253
- </li>
254
- <li>
255
- <StylesMemory node={node} parentNode={parentNode} />
256
- </li>
257
- </ul>
518
+ {[...Array(layerCount).keys()]
519
+ .map((i) => i + 1)
520
+ .map((num, index) => (
521
+ <div
522
+ key={`layer-group-${num}`}
523
+ className="flex items-center gap-1"
524
+ >
525
+ <button
526
+ className={`min-w-8 rounded-md px-3 py-1.5 text-sm font-bold transition-colors ${
527
+ currentLayer === num
528
+ ? 'bg-myblue text-white shadow-sm'
529
+ : 'text-mydarkgrey hover:bg-mydarkgrey/10 bg-white hover:text-black'
530
+ }`}
531
+ onClick={() => setCurrentLayer(num)}
532
+ >
533
+ {num}
534
+ </button>
535
+ <button
536
+ className="border-mydarkgrey/30 text-mydarkgrey rounded border border-dashed p-1 text-xs transition-colors hover:bg-white/50 hover:text-black"
537
+ title="Add Layer Here"
538
+ onClick={() =>
539
+ handleLayerAdd(
540
+ index === layerCount - 1 ? 'after' : 'before',
541
+ index === layerCount - 1 ? num : num + 1
542
+ )
543
+ }
544
+ >
545
+ <PlusIcon className="h-3 w-3" />
546
+ </button>
547
+ </div>
548
+ ))}
549
+ </div>
550
+ </div>
551
+ {hasNoClasses ? (
552
+ <div>
553
+ <em>No styles for this layer.</em>
554
+ </div>
555
+ ) : (
556
+ <div className="flex flex-wrap gap-2">
557
+ {Object.entries(currentClasses.mobile).map(([className]) => (
558
+ <SelectedTailwindClass
559
+ key={className}
560
+ name={className}
561
+ values={{
562
+ mobile: currentClasses.mobile[className],
563
+ tablet: currentClasses.tablet?.[className],
564
+ desktop: currentClasses.desktop?.[className],
565
+ }}
566
+ onRemove={handleClickRemove}
567
+ onUpdate={handleClickUpdate}
568
+ />
569
+ ))}
570
+ </div>
571
+ )}
572
+ <div>
573
+ <ul className="text-mydarkgrey flex flex-wrap gap-x-4 gap-y-1">
574
+ <li>
575
+ <em>Actions:</em>
576
+ </li>
577
+ <li>
578
+ <button
579
+ onClick={handleClickAdd}
580
+ className="text-myblue font-bold underline hover:text-black"
581
+ >
582
+ Add Style
583
+ </button>
584
+ </li>
585
+ <li>
586
+ <button
587
+ onClick={handleClickDeleteLayer}
588
+ className="text-myblue font-bold underline hover:text-black"
589
+ >
590
+ Delete Layer
591
+ </button>
592
+ </li>
593
+ </ul>
594
+ </div>
258
595
  </div>
596
+ );
597
+ };
598
+
599
+ const renderBackgroundImageVIew = () => (
600
+ <div className="space-y-4">
601
+ <BackButton />
602
+ <BackgroundImageWrapper paneId={paneNode.id} config={config!} />
259
603
  </div>
260
604
  );
605
+
606
+ const renderContent = () => {
607
+ switch (currentView) {
608
+ case 'summary':
609
+ return renderSummaryView();
610
+ case 'wrapperStyles':
611
+ return renderWrapperStylesView();
612
+ case 'backgroundImage':
613
+ return renderBackgroundImageVIew();
614
+ default:
615
+ return renderSummaryView();
616
+ }
617
+ };
618
+
619
+ return <div className="space-y-4">{renderContent()}</div>;
261
620
  };
262
621
 
263
622
  export default StyleParentPanel;