astro-tractstack 2.0.27 → 2.0.29

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.27",
3
+ "version": "2.0.29",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -5,22 +5,48 @@ import XMarkIcon from '@heroicons/react/24/solid/XMarkIcon';
5
5
  import { ProfileStorage } from '@/utils/profileStorage';
6
6
  import SandboxRegisterForm from '@/components/codehooks/SandboxRegisterForm';
7
7
 
8
- export default function SandboxAuthWrapper() {
8
+ interface SandboxAuthWrapperProps {
9
+ isServerSideAuthenticated: boolean;
10
+ }
11
+
12
+ export default function SandboxAuthWrapper({
13
+ isServerSideAuthenticated,
14
+ }: SandboxAuthWrapperProps) {
9
15
  const [profileExists, setProfileExists] = useState<boolean | null>(null);
10
16
 
11
17
  useEffect(() => {
12
- setProfileExists(ProfileStorage.hasProfile());
13
- }, []);
18
+ const hasLocalProfile = ProfileStorage.hasProfile();
19
+
20
+ if (hasLocalProfile && !isServerSideAuthenticated) {
21
+ const token = localStorage.getItem('tractstack_profile_token');
22
+
23
+ if (token) {
24
+ ProfileStorage.storeProfileToken(token);
25
+ window.location.reload();
26
+ return;
27
+ } else {
28
+ ProfileStorage.clearProfile();
29
+ setProfileExists(false);
30
+ }
31
+ } else {
32
+ setProfileExists(hasLocalProfile);
33
+ }
34
+ }, [isServerSideAuthenticated]);
14
35
 
15
36
  const handleRegistrationSuccess = () => {
16
37
  setProfileExists(true);
38
+ window.location.reload();
17
39
  };
18
40
 
19
41
  const handleClose = () => {
20
42
  window.location.href = '/';
21
43
  };
22
44
 
23
- if (profileExists === null || profileExists === true) {
45
+ if (profileExists === true && isServerSideAuthenticated) {
46
+ return null;
47
+ }
48
+
49
+ if (profileExists === null) {
24
50
  return null;
25
51
  }
26
52
 
@@ -1,15 +1,19 @@
1
- import { useMemo, useEffect } from 'react';
1
+ import { useMemo, useEffect, useState } from 'react';
2
2
  import Cog6ToothIcon from '@heroicons/react/24/outline/Cog6ToothIcon';
3
3
  import {
4
4
  styleElementInfoStore,
5
5
  resetStyleElementInfo,
6
6
  settingsPanelStore,
7
7
  } from '@/stores/storykeep';
8
+ import { getCtx } from '@/stores/nodes';
8
9
  import { StylesMemory } from '@/components/edit/state/StylesMemory';
9
10
  import {
10
11
  isMarkdownPaneFragmentNode,
11
12
  isGridLayoutNode,
12
13
  } from '@/utils/compositor/typeGuards';
14
+ import { getNodeText } from '@/utils/compositor/nodesHelper';
15
+ import { cloneDeep } from '@/utils/helpers';
16
+ import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
13
17
  import SelectedTailwindClass from '@/components/fields/SelectedTailwindClass';
14
18
  import { tagTitles } from '@/types/compositorTypes';
15
19
  import type {
@@ -19,6 +23,78 @@ import type {
19
23
  GridLayoutNode,
20
24
  } from '@/types/compositorTypes';
21
25
 
26
+ type SpanOverride = {
27
+ mobile?: Record<string, string>;
28
+ tablet?: Record<string, string>;
29
+ desktop?: Record<string, string>;
30
+ };
31
+
32
+ const spanStyleClasses: SpanOverride[] = [
33
+ {
34
+ mobile: {
35
+ bgCLIP: 'text',
36
+ bgGradientDIRECTION: 'r',
37
+ gradientFrom: 'blue-600',
38
+ gradientTo: 'teal-500',
39
+ textCOLOR: 'transparent',
40
+ },
41
+ },
42
+ {
43
+ mobile: {
44
+ textCOLOR: 'blue-600',
45
+ },
46
+ },
47
+ {
48
+ mobile: {
49
+ bgCOLOR: 'yellow-300',
50
+ textCOLOR: 'slate-900',
51
+ px: '1',
52
+ rounded: 'sm',
53
+ },
54
+ },
55
+ {
56
+ mobile: {
57
+ display: 'inline-block',
58
+ bgCOLOR: 'indigo-100',
59
+ textCOLOR: 'indigo-700',
60
+ textSIZE: 'xs',
61
+ fontWEIGHT: 'bold',
62
+ px: '2.5',
63
+ py: '0.5',
64
+ rounded: 'full',
65
+ },
66
+ },
67
+ {
68
+ mobile: {
69
+ bgCLIP: 'text',
70
+ textCOLOR: 'transparent',
71
+ bgGradientDIRECTION: 'r',
72
+ gradientFrom: 'orange-400',
73
+ gradientVia: 'pink-500',
74
+ gradientTo: 'purple-600',
75
+ fontWEIGHT: 'bold',
76
+ },
77
+ },
78
+ {
79
+ mobile: {
80
+ textDECORATION: 'underline',
81
+ textDECORATIONSTYLE: 'wavy',
82
+ textDECORATIONCOLOR: 'teal-400',
83
+ textDECORATIONTHICKNESS: '4',
84
+ textUNDERLINEOFFSET: '4',
85
+ },
86
+ },
87
+ {
88
+ mobile: {
89
+ display: 'inline-block',
90
+ bgCOLOR: 'rose-500',
91
+ textCOLOR: 'white',
92
+ px: '2',
93
+ skew: '-3',
94
+ },
95
+ },
96
+ ];
97
+
22
98
  export interface StyleElementPanelProps {
23
99
  node: FlatNode;
24
100
  parentNode: MarkdownPaneFragmentNode | GridLayoutNode;
@@ -30,6 +106,8 @@ const StyleElementPanel = ({
30
106
  parentNode,
31
107
  onTitleChange,
32
108
  }: StyleElementPanelProps) => {
109
+ const [showPresets, setShowPresets] = useState(true);
110
+
33
111
  if (
34
112
  !node?.tagName ||
35
113
  (!isMarkdownPaneFragmentNode(parentNode) && !isGridLayoutNode(parentNode))
@@ -40,6 +118,18 @@ const StyleElementPanel = ({
40
118
  const defaultClasses = parentNode.defaultClasses?.[node.tagName];
41
119
  const overrideClasses = node.overrideClasses;
42
120
 
121
+ const hasOverrides = useMemo(() => {
122
+ return (
123
+ overrideClasses &&
124
+ ((overrideClasses.mobile &&
125
+ Object.keys(overrideClasses.mobile).length > 0) ||
126
+ (overrideClasses.tablet &&
127
+ Object.keys(overrideClasses.tablet).length > 0) ||
128
+ (overrideClasses.desktop &&
129
+ Object.keys(overrideClasses.desktop).length > 0))
130
+ );
131
+ }, [overrideClasses]);
132
+
43
133
  const mergedClasses = useMemo(() => {
44
134
  const result: {
45
135
  [key: string]: {
@@ -49,7 +139,6 @@ const StyleElementPanel = ({
49
139
  };
50
140
  } = {};
51
141
 
52
- // First add all default classes
53
142
  if (defaultClasses) {
54
143
  Object.keys(defaultClasses.mobile).forEach((className) => {
55
144
  result[className] = {
@@ -64,7 +153,6 @@ const StyleElementPanel = ({
64
153
  });
65
154
  }
66
155
 
67
- // Then overlay any override classes
68
156
  if (overrideClasses) {
69
157
  ['mobile', 'tablet', 'desktop'].forEach((viewport) => {
70
158
  const viewportOverrides =
@@ -114,6 +202,24 @@ const StyleElementPanel = ({
114
202
  });
115
203
  };
116
204
 
205
+ const applySpanPreset = (styleIndex: number) => {
206
+ const ctx = getCtx();
207
+ const allNodes = ctx.allNodes.get();
208
+ const targetNode = cloneDeep(allNodes.get(node.id)) as FlatNode;
209
+ if (!targetNode) return;
210
+
211
+ const preset = spanStyleClasses[styleIndex];
212
+
213
+ targetNode.overrideClasses = {
214
+ ...targetNode.overrideClasses,
215
+ ...preset,
216
+ };
217
+
218
+ ctx.modifyNodes([{ ...targetNode, isChanged: true }]);
219
+
220
+ setShowPresets(false);
221
+ };
222
+
117
223
  useEffect(() => {
118
224
  if (
119
225
  styleElementInfoStore.get().markdownParentId !== parentNode.id ||
@@ -140,6 +246,10 @@ const StyleElementPanel = ({
140
246
  }
141
247
  }, [node?.tagName, onTitleChange]);
142
248
 
249
+ const shouldShowQuickStyles =
250
+ node.tagName === 'span' && !hasOverrides && showPresets;
251
+ const nodeText = shouldShowQuickStyles ? getNodeText(node) : '';
252
+
143
253
  return (
144
254
  <div className="space-y-4">
145
255
  {node.wordCarouselPayload && (
@@ -165,42 +275,90 @@ const StyleElementPanel = ({
165
275
  </div>
166
276
  </div>
167
277
  )}
168
- {Object.keys(mergedClasses).length > 0 ? (
169
- <div className="flex flex-wrap gap-2">
170
- {Object.entries(mergedClasses).map(([className, values]) => (
171
- <SelectedTailwindClass
172
- key={className}
173
- name={className}
174
- values={values}
175
- onRemove={handleRemove}
176
- onUpdate={handleUpdate}
177
- />
178
- ))}
179
- </div>
180
- ) : (
181
- <div className="space-y-4">
182
- <em>No styles.</em>
183
- </div>
184
- )}
185
278
 
186
- <div className="space-y-4">
187
- <ul className="text-mydarkgrey flex flex-wrap gap-x-4 gap-y-1">
188
- <li>
189
- <em>Actions:</em>
190
- </li>
191
- <li>
279
+ {shouldShowQuickStyles ? (
280
+ <div className="space-y-6">
281
+ <div className="space-y-2">
282
+ <h3 className="text-mydarkgrey text-sm font-bold">
283
+ Quick Style Selection
284
+ </h3>
285
+ <p className="text-xs text-gray-500">
286
+ Select a preset style for your text selection.
287
+ </p>
288
+ </div>
289
+
290
+ <div className="flex flex-col gap-4">
291
+ {spanStyleClasses.map((style, index) => {
292
+ const [classesPayload] = processClassesForViewports(
293
+ style as any,
294
+ {},
295
+ 1
296
+ );
297
+ const combinedClasses = classesPayload[0] || '';
298
+
299
+ return (
300
+ <button
301
+ key={index}
302
+ onClick={() => applySpanPreset(index)}
303
+ className="group w-full text-left text-xl transition-colors hover:outline-dotted hover:outline-2 hover:outline-black"
304
+ >
305
+ <span className={combinedClasses}>
306
+ {nodeText || 'Sample Text'}
307
+ </span>
308
+ </button>
309
+ );
310
+ })}
311
+ </div>
312
+
313
+ <div className="border-t border-gray-100 pt-4">
192
314
  <button
193
- onClick={() => handleClickAdd()}
194
- className="text-myblue font-bold underline hover:text-black"
315
+ onClick={() => setShowPresets(false)}
316
+ className="text-myblue w-full text-center text-sm underline hover:text-black"
195
317
  >
196
- Add Style
318
+ Apply your own styles manually
197
319
  </button>
198
- </li>
199
- <li>
200
- <StylesMemory node={node} parentNode={parentNode} />
201
- </li>
202
- </ul>
203
- </div>
320
+ </div>
321
+ </div>
322
+ ) : (
323
+ <>
324
+ {Object.keys(mergedClasses).length > 0 ? (
325
+ <div className="flex flex-wrap gap-2">
326
+ {Object.entries(mergedClasses).map(([className, values]) => (
327
+ <SelectedTailwindClass
328
+ key={className}
329
+ name={className}
330
+ values={values}
331
+ onRemove={handleRemove}
332
+ onUpdate={handleUpdate}
333
+ />
334
+ ))}
335
+ </div>
336
+ ) : (
337
+ <div className="space-y-4">
338
+ <em>No styles.</em>
339
+ </div>
340
+ )}
341
+
342
+ <div className="space-y-4">
343
+ <ul className="text-mydarkgrey flex flex-wrap gap-x-4 gap-y-1">
344
+ <li>
345
+ <em>Actions:</em>
346
+ </li>
347
+ <li>
348
+ <button
349
+ onClick={() => handleClickAdd()}
350
+ className="text-myblue font-bold underline hover:text-black"
351
+ >
352
+ Add Style
353
+ </button>
354
+ </li>
355
+ <li>
356
+ <StylesMemory node={node} parentNode={parentNode} />
357
+ </li>
358
+ </ul>
359
+ </div>
360
+ </>
361
+ )}
204
362
  </div>
205
363
  );
206
364
  };
@@ -32,10 +32,13 @@ type ButtonStylePair = [ButtonStyle, ButtonStyle];
32
32
 
33
33
  const buttonStyleOptions = [
34
34
  'Plain text inline',
35
- 'Fancy text inline',
36
- 'Fancy button',
35
+ 'Primary',
36
+ 'Primary inverse',
37
+ 'Dark',
38
+ 'Dark inverse',
37
39
  ];
38
40
  const buttonStyleClasses: ButtonStylePair[] = [
41
+ // Plain text inline
39
42
  [
40
43
  {
41
44
  fontWEIGHT: ['bold'],
@@ -48,32 +51,93 @@ const buttonStyleClasses: ButtonStylePair[] = [
48
51
  textCOLOR: ['brand-1'],
49
52
  },
50
53
  ],
54
+ // Primary Solid Button
51
55
  [
52
56
  {
53
- bgCOLOR: ['brand-4'],
54
- fontWEIGHT: ['bold'],
55
- px: ['3.5'],
56
- py: ['1.5'],
57
- rounded: ['lg'],
58
- textCOLOR: ['brand-1'],
57
+ alignITEMS: ['center'],
58
+ bgCOLOR: ['blue-600'],
59
+ borderCOLOR: ['transparent'],
60
+ display: ['inline-flex'],
61
+ justifyCONTENT: ['center'],
62
+ px: ['6'],
63
+ py: ['3'],
64
+ rounded: ['md'],
65
+ textCOLOR: ['white'],
66
+ textSIZE: ['base'],
67
+ transition: ['colors'],
68
+ transitionDURATION: ['200'],
69
+ w: ['full'],
59
70
  },
60
71
  {
61
- bgCOLOR: ['brand-3'],
72
+ bgCOLOR: ['blue-700'],
62
73
  },
63
74
  ],
75
+ // Secondary Outline Button
64
76
  [
65
77
  {
66
- bgCOLOR: ['brand-4'],
67
- display: ['inline-block'],
68
- fontWEIGHT: ['bold'],
69
- px: ['3.5'],
70
- py: ['2.5'],
78
+ alignITEMS: ['center'],
79
+ bgCOLOR: ['transparent'],
80
+ borderCOLOR: ['blue-600'],
81
+ borderWIDTH: ['2'],
82
+ display: ['inline-flex'],
83
+ justifyCONTENT: ['center'],
84
+ px: ['6'],
85
+ py: ['3'],
71
86
  rounded: ['md'],
72
- textCOLOR: ['brand-1'],
87
+ textCOLOR: ['blue-600'],
88
+ textSIZE: ['base'],
89
+ transition: ['colors'],
90
+ transitionDURATION: ['200'],
91
+ w: ['full'],
92
+ },
93
+ {
94
+ bgCOLOR: ['blue-50'],
95
+ },
96
+ ],
97
+ [
98
+ {
99
+ alignITEMS: ['center'],
100
+ bgCOLOR: ['black'],
101
+ borderCOLOR: ['transparent'],
102
+ borderWIDTH: ['2'],
103
+ display: ['inline-flex'],
104
+ justifyCONTENT: ['center'],
105
+ px: ['6'],
106
+ py: ['3'],
107
+ rounded: ['md'],
108
+ textCOLOR: ['white'],
109
+ textSIZE: ['base'],
110
+ transition: ['colors'],
111
+ transitionDURATION: ['200'],
112
+ w: ['full'],
113
+ },
114
+ {
115
+ bgCOLOR: ['white'],
116
+ textCOLOR: ['black'],
117
+ borderCOLOR: ['black'],
118
+ borderWIDTH: ['2'],
119
+ },
120
+ ],
121
+ [
122
+ {
123
+ alignITEMS: ['center'],
124
+ bgCOLOR: ['transparent'],
125
+ borderCOLOR: ['black'],
126
+ borderWIDTH: ['2'],
127
+ display: ['inline-flex'],
128
+ justifyCONTENT: ['center'],
129
+ px: ['6'],
130
+ py: ['3'],
131
+ rounded: ['md'],
132
+ textCOLOR: ['black'],
133
+ textSIZE: ['base'],
134
+ transition: ['colors'],
135
+ transitionDURATION: ['200'],
136
+ w: ['full'],
73
137
  },
74
138
  {
75
- bgCOLOR: ['brand-3'],
76
- rotate: ['2'],
139
+ textCOLOR: ['myblack'],
140
+ bgCOLOR: ['slate-100'],
77
141
  },
78
142
  ],
79
143
  ];
@@ -26,7 +26,6 @@ if (healthCheckRedirect !== undefined) {
26
26
  }
27
27
 
28
28
  const brandConfig = await getBrandConfig(tenantId);
29
-
30
29
  const emptyStoryFragment = {
31
30
  id: ulid(),
32
31
  nodeType: 'StoryFragment' as const,
@@ -43,12 +42,15 @@ const loadData = {
43
42
  };
44
43
  const title = 'Sandbox - TractStack Editor';
45
44
  const storyFragmentID = emptyStoryFragment.id;
46
-
47
45
  const fullContentMap = await getFullContentMap(tenantId);
48
46
  const urlParams: Record<string, string | boolean> = {};
49
47
  for (const [key, value] of Astro.url.searchParams) {
50
48
  urlParams[key] = value === '' ? true : value;
51
49
  }
50
+
51
+ const hasProfile = Astro.request.headers
52
+ .get('cookie')
53
+ ?.includes('tractstack_profile=true');
52
54
  ---
53
55
 
54
56
  <Layout
@@ -59,7 +61,7 @@ for (const [key, value] of Astro.url.searchParams) {
59
61
  isStoryKeep={true}
60
62
  isEditor={true}
61
63
  >
62
- <SandboxAuthWrapper client:load />
64
+ <SandboxAuthWrapper client:load isServerSideAuthenticated={!!hasProfile} />
63
65
  <Header
64
66
  title={title}
65
67
  slug="sandbox"
@@ -70,65 +72,71 @@ for (const [key, value] of Astro.url.searchParams) {
70
72
  menu={null}
71
73
  />
72
74
 
73
- <section
74
- id="storykeepHeader"
75
- role="banner"
76
- class="z-101 bg-mywhite left-0 right-0 drop-shadow transition-all duration-200"
77
- >
78
- <StoryKeepHeader
79
- slug="sandbox"
80
- isContext={false}
81
- isSandboxMode={true}
82
- client:only="react"
83
- />
84
- </section>
85
-
86
- <div class="flex min-h-screen">
87
- <StoryKeepToolMode isContext={false} client:only="react" />
88
-
89
- <main id="mainContent" class="relative flex-1 overflow-x-auto">
90
- <div class="bg-myblue/20 bg-mylightgrey h-full p-1.5">
91
- <div
92
- class="h-fit min-h-screen pb-96"
93
- style={{
94
- backgroundImage:
95
- 'repeating-linear-gradient(135deg, transparent, transparent 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 20px)',
96
- }}
75
+ {
76
+ hasProfile && (
77
+ <>
78
+ <section
79
+ id="storykeepHeader"
80
+ role="banner"
81
+ class="z-101 bg-mywhite left-0 right-0 drop-shadow transition-all duration-200"
97
82
  >
98
- <Compositor
99
- id={storyFragmentID}
100
- nodes={loadData}
101
- config={brandConfig}
102
- fullContentMap={fullContentMap}
103
- fullCanonicalURL="/sandbox"
104
- urlParams={urlParams}
105
- availableCodeHooks={Object.keys(codeHookComponents)}
83
+ <StoryKeepHeader
84
+ slug="sandbox"
85
+ isContext={false}
106
86
  isSandboxMode={true}
107
87
  client:only="react"
108
88
  />
89
+ </section>
90
+
91
+ <div class="flex min-h-screen">
92
+ <StoryKeepToolMode isContext={false} client:only="react" />
93
+
94
+ <main id="mainContent" class="relative flex-1 overflow-x-auto">
95
+ <div class="bg-myblue/20 bg-mylightgrey h-full p-1.5">
96
+ <div
97
+ class="h-fit min-h-screen pb-96"
98
+ style={{
99
+ backgroundImage:
100
+ 'repeating-linear-gradient(135deg, transparent, transparent 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 20px)',
101
+ }}
102
+ >
103
+ <Compositor
104
+ id={storyFragmentID}
105
+ nodes={loadData}
106
+ config={brandConfig}
107
+ fullContentMap={fullContentMap}
108
+ fullCanonicalURL="/sandbox"
109
+ urlParams={urlParams}
110
+ availableCodeHooks={Object.keys(codeHookComponents)}
111
+ isSandboxMode={true}
112
+ client:only="react"
113
+ />
114
+ </div>
115
+ </div>
116
+ </main>
109
117
  </div>
110
- </div>
111
- </main>
112
- </div>
113
118
 
114
- <aside
115
- id="settingsControls"
116
- class="z-101 pointer-events-none fixed bottom-16 right-2 flex flex-col items-end gap-2 md:bottom-2"
117
- >
118
- <div class="pointer-events-none flex-grow"></div>
119
+ <aside
120
+ id="settingsControls"
121
+ class="z-101 pointer-events-none fixed bottom-16 right-2 flex flex-col items-end gap-2 md:bottom-2"
122
+ >
123
+ <div class="pointer-events-none flex-grow" />
119
124
 
120
- <div class="pointer-events-auto flex-shrink-0">
121
- <StoryKeepToolBar client:only="react" />
122
- </div>
125
+ <div class="pointer-events-auto flex-shrink-0">
126
+ <StoryKeepToolBar client:only="react" />
127
+ </div>
123
128
 
124
- <div class="pointer-events-auto max-h-full">
125
- <SettingsPanel
126
- config={brandConfig}
127
- availableCodeHooks={Object.keys(codeHookComponents)}
128
- client:only="react"
129
- />
130
- </div>
131
- </aside>
129
+ <div class="pointer-events-auto max-h-full">
130
+ <SettingsPanel
131
+ config={brandConfig}
132
+ availableCodeHooks={Object.keys(codeHookComponents)}
133
+ client:only="react"
134
+ />
135
+ </div>
136
+ </aside>
137
+ </>
138
+ )
139
+ }
132
140
  </Layout>
133
141
 
134
142
  <script>