astro-tractstack 2.0.23 → 2.0.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.23",
3
+ "version": "2.0.25",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -127,6 +127,7 @@ const getElement = (
127
127
  };
128
128
  const type = getType(node);
129
129
  const toolModeVal = getCtx(props).toolModeValStore.get().value;
130
+ const settingsPanel = useStore(settingsPanelStore);
130
131
 
131
132
  switch (type) {
132
133
  case 'Markdown': {
@@ -328,10 +329,14 @@ const getElement = (
328
329
  case 'aside':
329
330
  case 'p': {
330
331
  if (toolModeVal === 'styles') {
331
- const className = getCtx(props).getNodeClasses(
332
+ let className = getCtx(props).getNodeClasses(
332
333
  node.id,
333
334
  viewportKeyStore.get().value
334
335
  );
336
+ if (settingsPanel?.nodeId === node.id) {
337
+ className +=
338
+ ' outline-4 outline-dotted outline-orange-400 outline-offset-2';
339
+ }
335
340
 
336
341
  const handleElementClick = (e: MouseEvent<HTMLElement>) => {
337
342
  e.stopPropagation();
@@ -1,14 +1,16 @@
1
1
  import { useState, useEffect } from 'react';
2
+ import { useStore } from '@nanostores/react';
2
3
  import { getCtx } from '@/stores/nodes';
3
4
  import { RenderChildren } from './RenderChildren';
4
5
  import { isGridLayoutNode } from '@/utils/compositor/typeGuards';
5
6
  import type { NodeProps } from '@/types/nodeProps';
6
7
  import type { ParentClassesPayload } from '@/types/compositorTypes';
7
- import { viewportKeyStore } from '@/stores/storykeep';
8
+ import { viewportKeyStore, settingsPanelStore } from '@/stores/storykeep';
8
9
 
9
10
  export const GridLayout = (props: NodeProps) => {
10
11
  const ctx = getCtx(props);
11
12
  const node = ctx.allNodes.get().get(props.nodeId);
13
+ const settingsPanel = useStore(settingsPanelStore);
12
14
 
13
15
  const [currentViewport, setCurrentViewport] = useState(
14
16
  viewportKeyStore.get().value
@@ -65,11 +67,18 @@ export const GridLayout = (props: NodeProps) => {
65
67
  break;
66
68
  }
67
69
  const gridClassName = `grid grid-cols-${gridCols}`;
70
+ const activeOutline =
71
+ settingsPanel?.nodeId === props.nodeId
72
+ ? ' outline-4 outline-dotted outline-orange-400 outline-offset-2'
73
+ : '';
68
74
 
69
75
  const children = ctx.getChildNodeIDs(props.nodeId);
70
76
 
71
77
  let nodesToRender: JSX.Element = (
72
- <div className={gridClassName} style={{ position: 'relative', zIndex: 10 }}>
78
+ <div
79
+ className={`${gridClassName}${activeOutline}`}
80
+ style={{ position: 'relative', zIndex: 10 }}
81
+ >
73
82
  <RenderChildren children={children} nodeProps={props} />
74
83
  </div>
75
84
  );
@@ -3,7 +3,7 @@ import { getCtx } from '@/stores/nodes';
3
3
  import { viewportKeyStore } from '@/stores/storykeep';
4
4
  import { RenderChildren } from './RenderChildren';
5
5
  import { GhostInsertBlock } from './GhostInsertBlock';
6
- import { processGridClassesToString } from '@/utils/compositor/reduceNodesClassNames';
6
+ import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
7
7
  import type { NodeProps } from '@/types/nodeProps';
8
8
  import type {
9
9
  MarkdownPaneFragmentNode,
@@ -111,7 +111,29 @@ export const Markdown = (props: NodeProps) => {
111
111
  ? 'flex-row-reverse'
112
112
  : 'flex-row';
113
113
 
114
- const gridClassName = processGridClassesToString(node.gridClasses);
114
+ const [all, mobile, tablet, desktop] = processClassesForViewports(
115
+ node.gridClasses || { mobile: {}, tablet: {}, desktop: {} },
116
+ {},
117
+ 1
118
+ );
119
+
120
+ let gridClassName = '';
121
+ if (isPreview) gridClassName = desktop[0];
122
+ else {
123
+ switch (currentViewport) {
124
+ case 'desktop':
125
+ gridClassName = desktop[0];
126
+ break;
127
+ case 'tablet':
128
+ gridClassName = tablet[0];
129
+ break;
130
+ case 'mobile':
131
+ gridClassName = mobile[0];
132
+ break;
133
+ default:
134
+ gridClassName = all[0];
135
+ }
136
+ }
115
137
 
116
138
  let nodesToRender = (
117
139
  <>
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useRef, type RefObject, type MouseEvent } from 'react';
2
+ import { useStore } from '@nanostores/react';
2
3
  import { getCtx } from '@/stores/nodes';
3
- import { viewportKeyStore } from '@/stores/storykeep';
4
+ import { viewportKeyStore, settingsPanelStore } from '@/stores/storykeep';
4
5
  import { RenderChildren } from '../RenderChildren';
5
6
  import type { FlatNode } from '@/types/compositorTypes';
6
7
  import type { NodeProps } from '@/types/nodeProps';
@@ -12,6 +13,10 @@ export const NodeAnchorComponent = (props: NodeProps, tagName: string) => {
12
13
  const childNodeIDs = ctx.getChildNodeIDs(node?.parentId ?? '');
13
14
  const linkRef = useRef<HTMLAnchorElement | HTMLButtonElement>(null);
14
15
 
16
+ // Reactivity for outlines
17
+ const settingsPanel = useStore(settingsPanelStore);
18
+ const toolMode = useStore(ctx.toolModeValStore).value;
19
+
15
20
  // Get current position and next sibling for spacing logic
16
21
  const currentIndex = childNodeIDs.indexOf(nodeId);
17
22
 
@@ -189,7 +194,14 @@ export const NodeAnchorComponent = (props: NodeProps, tagName: string) => {
189
194
 
190
195
  // Create appropriate element based on tagName
191
196
  let baseClasses = ctx.getNodeClasses(nodeId, viewportKeyStore.get().value);
192
- baseClasses += ' outline outline-1 outline-dotted outline-gray-400/60';
197
+
198
+ if (toolMode === 'styles' && settingsPanel?.nodeId != nodeId) {
199
+ baseClasses += ' outline outline-1 outline-dotted outline-black';
200
+ } else if (settingsPanel?.nodeId === nodeId) {
201
+ baseClasses +=
202
+ ' outline-4 outline-dotted outline-orange-400 outline-offset-2';
203
+ }
204
+
193
205
  if (tagName === 'a') {
194
206
  return (
195
207
  <>
@@ -37,7 +37,8 @@ const VERBOSE = false;
37
37
  export const NodeBasicTag = (props: NodeTagProps) => {
38
38
  const nodeId = props.nodeId;
39
39
  const ctx = getCtx(props);
40
- //const Tag = ctx.showGuids.get() ? `div` : props.tagName;
40
+ const settingsPanel = useStore(settingsPanelStore);
41
+ const toolMode = useStore(ctx.toolModeValStore).value;
41
42
  const Tag =
42
43
  ctx.toolModeValStore.get().value === `debug` ? `div` : props.tagName;
43
44
 
@@ -256,7 +257,8 @@ export const NodeBasicTag = (props: NodeTagProps) => {
256
257
 
257
258
  // For formatting nodes <em> and <strong> and <span>
258
259
  if (['em', 'strong', 'span'].includes(props.tagName)) {
259
- const isEditorActive = toolModeVal === 'styles';
260
+ const isEditorActive = ['styles', 'text'].includes(toolModeVal);
261
+ const isEditorEnabled = toolModeVal === 'styles';
260
262
  const handleStyleClick = (e: MouseEvent<HTMLButtonElement>) => {
261
263
  e.preventDefault();
262
264
  e.stopPropagation();
@@ -273,7 +275,7 @@ export const NodeBasicTag = (props: NodeTagProps) => {
273
275
  };
274
276
 
275
277
  let baseClasses = ctx.getNodeClasses(nodeId, viewportKeyStore.get().value);
276
- baseClasses += ' outline outline-1 outline-dotted outline-gray-400/60';
278
+ baseClasses += ' outline outline-1 outline-dotted outline-black';
277
279
 
278
280
  return createElement(
279
281
  Tag,
@@ -296,7 +298,7 @@ export const NodeBasicTag = (props: NodeTagProps) => {
296
298
  },
297
299
  [
298
300
  <RenderChildren key="children" children={children} nodeProps={props} />,
299
- isEditorActive && (
301
+ isEditorEnabled && (
300
302
  <span
301
303
  key="chip"
302
304
  className="absolute z-10 flex select-none gap-x-1"
@@ -754,13 +756,22 @@ export const NodeBasicTag = (props: NodeTagProps) => {
754
756
  ctx.setClickedNodeId(nodeId, true);
755
757
  };
756
758
 
757
- // Determine classes
758
759
  const baseClasses = ctx.getNodeClasses(nodeId, viewportKeyStore.get().value);
760
+ let outlineClasses = '';
761
+ if (settingsPanel?.nodeId === nodeId) {
762
+ outlineClasses +=
763
+ ' outline-4 outline-dotted outline-orange-400 outline-offset-2';
764
+ } else if (toolMode === 'styles') {
765
+ outlineClasses += ' hover:outline hover:outline-2 hover:outline-black';
766
+ if (['span', 'strong', 'em'].includes(props.tagName)) {
767
+ outlineClasses += ' outline outline-1 outline-dotted outline-black';
768
+ }
769
+ }
759
770
  const editingClasses =
760
771
  editState === 'editing'
761
772
  ? 'outline-2 outline-cyan-500 outline-offset-2'
762
773
  : '';
763
- const className = `${baseClasses} ${editingClasses}`.trim();
774
+ const className = `${baseClasses} ${outlineClasses} ${editingClasses}`.trim();
764
775
 
765
776
  return (
766
777
  <>
@@ -30,7 +30,6 @@ export const NodeBasicTagEraser = (props: NodeTagProps) => {
30
30
  </div>
31
31
  <div className="flex items-center gap-1 rounded bg-white px-2 py-1 text-sm text-red-700 shadow-sm transition-colors group-focus-within:bg-red-700 group-focus-within:text-white group-hover:bg-red-700 group-hover:text-white">
32
32
  <TrashIcon className="h-4 w-4" />
33
- Click anywhere to delete
34
33
  </div>
35
34
  </div>
36
35
  );
@@ -73,19 +73,29 @@ const StyleParentPanel = ({
73
73
  return;
74
74
  }
75
75
 
76
+ // --- STABILIZATION FIX START ---
77
+ let effectiveNode = initialNode;
78
+ if (isMarkdownPaneFragmentNode(initialNode) && initialNode.parentId) {
79
+ const parent = ctx.allNodes.get().get(initialNode.parentId);
80
+ if (parent && isGridLayoutNode(parent)) {
81
+ effectiveNode = parent as GridLayoutNode;
82
+ }
83
+ }
84
+ // --- STABILIZATION FIX END ---
85
+
76
86
  const targets: StyleableTarget[] = [];
77
- const isGrid = isGridLayoutNode(initialNode);
87
+ const isGrid = isGridLayoutNode(effectiveNode);
78
88
 
79
89
  targets.push({
80
- id: initialNode.id,
90
+ id: effectiveNode.id,
81
91
  name: isGrid ? 'Outer Container' : 'Pane Styles',
82
- node: initialNode,
92
+ node: effectiveNode,
83
93
  targetProperty: 'parentClasses',
84
94
  });
85
95
 
86
96
  if (isGrid) {
87
97
  const columnNodes = ctx
88
- .getChildNodeIDs(initialNode.id)
98
+ .getChildNodeIDs(effectiveNode.id)
89
99
  .map((id) => ctx.allNodes.get().get(id) as BaseNode)
90
100
  .filter(isMarkdownPaneFragmentNode);
91
101
 
@@ -105,6 +115,11 @@ const StyleParentPanel = ({
105
115
 
106
116
  if (rememberedIndex != null && rememberedIndex < targets.length) {
107
117
  setSelectedTargetIndex(rememberedIndex);
118
+ } else if (initialNode.id !== effectiveNode.id) {
119
+ // If opened on a child column, find and select it in the list
120
+ const index = targets.findIndex((t) => t.id === initialNode.id);
121
+ if (index !== -1) setSelectedTargetIndex(index);
122
+ else setSelectedTargetIndex(0);
108
123
  } else {
109
124
  setSelectedTargetIndex(0);
110
125
  }
@@ -136,6 +151,30 @@ const StyleParentPanel = ({
136
151
  targetProperty,
137
152
  } = selectedTarget;
138
153
 
154
+ const handleNavigation = (direction: 'prev' | 'next') => {
155
+ const len = styleTargets.length;
156
+ if (len < 2) return;
157
+
158
+ const newIndex =
159
+ direction === 'next'
160
+ ? (selectedTargetIndex + 1) % len
161
+ : (selectedTargetIndex - 1 + len) % len;
162
+
163
+ setSelectedTargetIndex(newIndex);
164
+
165
+ // Dispatch Signal to move the Orange Outline
166
+ const newTarget = styleTargets[newIndex];
167
+ const currentSettings = settingsPanelStore.get();
168
+
169
+ if (currentSettings && newTarget) {
170
+ settingsPanelStore.set({
171
+ ...currentSettings,
172
+ nodeId: newTarget.id,
173
+ action: 'style-parent',
174
+ });
175
+ }
176
+ };
177
+
139
178
  const handleLayerAdd = (position: 'before' | 'after', layerNum: number) => {
140
179
  const targetNode = cloneDeep(selectedTargetNode);
141
180
 
@@ -245,11 +284,7 @@ const StyleParentPanel = ({
245
284
  const TargetNavigator = () => (
246
285
  <div className="mb-4 flex items-center justify-between rounded-md bg-slate-100 p-2">
247
286
  <button
248
- onClick={() =>
249
- setSelectedTargetIndex(
250
- (prev) => (prev - 1 + styleTargets.length) % styleTargets.length
251
- )
252
- }
287
+ onClick={() => handleNavigation('prev')}
253
288
  className="rounded-full p-1 text-gray-500 hover:bg-gray-200 hover:text-black"
254
289
  disabled={styleTargets.length < 2}
255
290
  >
@@ -259,9 +294,7 @@ const StyleParentPanel = ({
259
294
  {selectedTargetName}
260
295
  </span>
261
296
  <button
262
- onClick={() =>
263
- setSelectedTargetIndex((prev) => (prev + 1) % styleTargets.length)
264
- }
297
+ onClick={() => handleNavigation('next')}
265
298
  className="rounded-full p-1 text-gray-500 hover:bg-gray-200 hover:text-black"
266
299
  disabled={styleTargets.length < 2}
267
300
  >
@@ -41,6 +41,7 @@ const ViewportComboBox = ({
41
41
  }: ViewportComboBoxProps) => {
42
42
  const brandColors = brandConfigStore.get()?.BRAND_COLOURS || '';
43
43
  const [internalValue, setInternalValue] = useState(value);
44
+ const [displayValue, setDisplayValue] = useState(value);
44
45
  const [query, setQuery] = useState('');
45
46
  const [isNowNegative, setIsNowNegative] = useState(isNegative);
46
47
 
@@ -54,6 +55,7 @@ const ViewportComboBox = ({
54
55
  useEffect(() => {
55
56
  if (value !== internalValue) {
56
57
  setInternalValue(value);
58
+ setDisplayValue(value);
57
59
  setQuery('');
58
60
  }
59
61
  if (isNegative !== isNowNegative) {
@@ -61,7 +63,6 @@ const ViewportComboBox = ({
61
63
  }
62
64
  }, [value, isNegative]);
63
65
 
64
- // Create collection for combobox
65
66
  const collection = useMemo(() => {
66
67
  const filteredValues =
67
68
  query === ''
@@ -88,6 +89,7 @@ const ViewportComboBox = ({
88
89
  const handleInputChange = useCallback(
89
90
  (details: Combobox.InputValueChangeDetails) => {
90
91
  setQuery(details.inputValue);
92
+ setDisplayValue(details.inputValue);
91
93
  },
92
94
  []
93
95
  );
@@ -96,6 +98,7 @@ const ViewportComboBox = ({
96
98
  (details: { value: string[] }) => {
97
99
  const selectedValue = details.value[0] || '';
98
100
  setInternalValue(selectedValue);
101
+ setDisplayValue(selectedValue);
99
102
  setQuery('');
100
103
  const currentSignal = settingsPanelStore.get();
101
104
  if (currentSignal) {
@@ -114,13 +117,13 @@ const ViewportComboBox = ({
114
117
  onFinalChange(internalValue, viewport, isNowNegative);
115
118
  } else {
116
119
  setInternalValue(value);
120
+ setDisplayValue(value);
117
121
  }
118
122
  setQuery('');
119
123
  }, [internalValue, value, values, onFinalChange, viewport, isNowNegative]);
120
124
 
121
125
  const isColorValue = colorValues.includes(internalValue);
122
126
 
123
- // CSS to properly style the combobox items with hover and selection
124
127
  const comboboxItemStyles = `
125
128
  .viewport-item[data-highlighted] {
126
129
  background-color: #0891b2; /* bg-cyan-600 */
@@ -151,6 +154,7 @@ const ViewportComboBox = ({
151
154
  <div className="flex items-center">
152
155
  <div className="relative flex-grow">
153
156
  <Combobox.Root
157
+ inputValue={displayValue}
154
158
  collection={collection}
155
159
  value={[internalValue]}
156
160
  onValueChange={handleValueChange}
@@ -1489,24 +1489,19 @@ export class NodesContext {
1489
1489
  spanNode.overrideClasses || {},
1490
1490
  1
1491
1491
  );
1492
- const outlineClass =
1493
- this.toolModeValStore.get().value === 'styles'
1494
- ? ' outline outline-1 outline-dotted outline-gray-400/60'
1495
- : '';
1496
-
1497
1492
  const getClassString = (classes: string[]): string =>
1498
1493
  classes && classes.length > 0 ? classes[0] : '';
1499
1494
 
1500
- if (isPreview) return getClassString(desktop) + outlineClass;
1495
+ if (isPreview) return getClassString(desktop);
1501
1496
  switch (viewport) {
1502
1497
  case 'desktop':
1503
- return getClassString(desktop) + outlineClass;
1498
+ return getClassString(desktop);
1504
1499
  case 'tablet':
1505
- return getClassString(tablet) + outlineClass;
1500
+ return getClassString(tablet);
1506
1501
  case 'mobile':
1507
- return getClassString(mobile) + outlineClass;
1502
+ return getClassString(mobile);
1508
1503
  default:
1509
- return getClassString(all) + outlineClass;
1504
+ return getClassString(all);
1510
1505
  }
1511
1506
  }
1512
1507
 
@@ -516,6 +516,15 @@ export function extractClassesFromNodes(dirtyNodes: BaseNode[]): string[] {
516
516
  if (className.trim()) uniqueClasses.add(className.trim());
517
517
  });
518
518
  }
519
+
520
+ // Extract from gridCss arrays
521
+ if ('gridCss' in node && Array.isArray(node.gridCss)) {
522
+ node.gridCss.forEach((classString: string) => {
523
+ classString.split(' ').forEach((className: string) => {
524
+ if (className.trim()) uniqueClasses.add(className.trim());
525
+ });
526
+ });
527
+ }
519
528
  });
520
529
 
521
530
  return Array.from(uniqueClasses);
@@ -198,38 +198,3 @@ function getTailwindClassInfo(selector: string): {
198
198
  useKeyAsClass: classInfo.useKeyAsClass,
199
199
  };
200
200
  }
201
-
202
- export function processGridClassesToString(
203
- classes?: DefaultClassValue
204
- ): string {
205
- if (!classes) {
206
- return '';
207
- }
208
-
209
- const finalClasses: string[] = [];
210
- const mobileStyles = classes.mobile || {};
211
- const tabletStyles = classes.tablet || {};
212
- const desktopStyles = classes.desktop || {};
213
-
214
- for (const [selector, value] of Object.entries(mobileStyles)) {
215
- if (value) {
216
- finalClasses.push(reduceClassName(selector, value, 0));
217
- }
218
- }
219
-
220
- for (const [selector, value] of Object.entries(tabletStyles)) {
221
- if (value && mobileStyles[selector] !== value) {
222
- finalClasses.push(reduceClassName(selector, value, 1));
223
- }
224
- }
225
-
226
- for (const [selector, value] of Object.entries(desktopStyles)) {
227
- const tabletValue = tabletStyles[selector];
228
- const mobileValue = mobileStyles[selector];
229
- if (value && tabletValue !== value && mobileValue !== value) {
230
- finalClasses.push(reduceClassName(selector, value, 2));
231
- }
232
- }
233
-
234
- return finalClasses.filter(Boolean).join(' ');
235
- }