astro-tractstack 2.0.0-rc.8 → 2.0.0

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 (141) hide show
  1. package/LICENSE +8 -97
  2. package/README.md +7 -5
  3. package/bin/create-tractstack.js +35 -11
  4. package/dist/index.js +106 -29
  5. package/package.json +10 -5
  6. package/templates/css/frontend.css +1 -1
  7. package/templates/custom/minimal/CodeHook.astro +13 -12
  8. package/templates/custom/minimal/CustomRoutes.astro +25 -31
  9. package/templates/custom/with-examples/CodeHook.astro +22 -11
  10. package/templates/custom/with-examples/CustomRoutes.astro +4 -8
  11. package/templates/custom/with-examples/ProductCard.astro +29 -0
  12. package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
  13. package/templates/custom/with-examples/ProductGrid.astro +64 -0
  14. package/templates/custom/with-examples/pages/Collections.astro +58 -98
  15. package/templates/gitignore +42 -0
  16. package/templates/prettierignore +5 -0
  17. package/templates/prettierrc +19 -0
  18. package/templates/src/client/app.js +127 -0
  19. package/templates/src/client/htmx.min.js +3519 -0
  20. package/templates/src/client/view.js +429 -0
  21. package/templates/src/components/Footer.astro +4 -9
  22. package/templates/src/components/Header.astro +67 -60
  23. package/templates/src/components/Menu.tsx +188 -52
  24. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
  25. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
  26. package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
  27. package/templates/src/components/codehooks/EpinetWrapper.tsx +1 -0
  28. package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
  29. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
  30. package/templates/src/components/codehooks/ListContent.astro +32 -162
  31. package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
  32. package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
  33. package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
  34. package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
  35. package/templates/src/components/compositor/Node.tsx +3 -6
  36. package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
  37. package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
  38. package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
  39. package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
  40. package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
  41. package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
  42. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
  43. package/templates/src/components/edit/Header.tsx +10 -4
  44. package/templates/src/components/edit/PanelSwitch.tsx +11 -7
  45. package/templates/src/components/edit/SettingsPanel.tsx +29 -18
  46. package/templates/src/components/edit/ToolBar.tsx +1 -28
  47. package/templates/src/components/edit/ToolMode.tsx +45 -32
  48. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
  49. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
  50. package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
  51. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
  52. package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
  53. package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
  54. package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
  55. package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
  56. package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
  57. package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
  58. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
  59. package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
  60. package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
  61. package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
  62. package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
  63. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
  64. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
  65. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
  66. package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
  67. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
  68. package/templates/src/components/edit/state/SaveModal.tsx +316 -169
  69. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
  70. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
  71. package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
  72. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
  73. package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
  74. package/templates/src/components/fields/ArtpackImage.tsx +4 -1
  75. package/templates/src/components/fields/BackgroundImage.tsx +1 -1
  76. package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
  77. package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
  78. package/templates/src/components/fields/ImageUpload.tsx +1 -1
  79. package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
  80. package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
  81. package/templates/src/components/form/ActionBuilderField.tsx +306 -87
  82. package/templates/src/components/search/SearchModal.tsx +420 -0
  83. package/templates/src/components/search/SearchResults.tsx +367 -0
  84. package/templates/src/components/search/SearchWrapper.tsx +46 -0
  85. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
  86. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
  87. package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
  88. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
  89. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +37 -33
  90. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +55 -7
  91. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +17 -2
  92. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
  93. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
  94. package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
  95. package/templates/src/components/tenant/RegistrationForm.tsx +1 -1
  96. package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
  97. package/templates/src/constants/shapes.ts +9 -0
  98. package/templates/src/constants.ts +2121 -16
  99. package/templates/src/hooks/useSearch.ts +228 -0
  100. package/templates/src/layouts/Layout.astro +213 -104
  101. package/templates/src/lib/storyData.ts +4 -1
  102. package/templates/src/pages/[...slug]/edit.astro +14 -14
  103. package/templates/src/pages/[...slug].astro +82 -21
  104. package/templates/src/pages/api/orphan-analysis.ts +0 -1
  105. package/templates/src/pages/api/tailwind.ts +23 -21
  106. package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
  107. package/templates/src/pages/context/[...contextSlug].astro +7 -2
  108. package/templates/src/pages/storykeep/advanced.astro +5 -4
  109. package/templates/src/pages/storykeep/branding.astro +5 -4
  110. package/templates/src/pages/storykeep/content.astro +5 -4
  111. package/templates/src/pages/storykeep/init.astro +40 -1
  112. package/templates/src/pages/storykeep/login.astro +1 -1
  113. package/templates/src/pages/storykeep.astro +5 -4
  114. package/templates/src/stores/nodes.ts +59 -88
  115. package/templates/src/stores/orphanAnalysis.ts +19 -21
  116. package/templates/src/stores/storykeep.ts +7 -0
  117. package/templates/src/types/compositorTypes.ts +6 -0
  118. package/templates/src/types/tractstack.ts +17 -0
  119. package/templates/src/utils/actions/lispLexer.ts +2 -2
  120. package/templates/src/utils/actions/preParse_Action.ts +3 -0
  121. package/templates/src/utils/api/beliefHelpers.ts +12 -36
  122. package/templates/src/utils/api/menuHelpers.ts +2 -2
  123. package/templates/src/utils/api.ts +26 -0
  124. package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
  125. package/templates/src/utils/compositor/allowInsert.ts +5 -3
  126. package/templates/src/utils/compositor/nodesHelper.ts +4 -0
  127. package/templates/src/utils/compositor/processMarkdown.ts +16 -2
  128. package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
  129. package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
  130. package/templates/src/utils/compositor/typeGuards.ts +1 -0
  131. package/templates/src/utils/customHelpers.ts +38 -0
  132. package/templates/src/utils/helpers.ts +2 -2
  133. package/templates/src/utils/layout.ts +65 -144
  134. package/utils/inject-files.ts +95 -18
  135. package/templates/src/client/analytics-events.js +0 -207
  136. package/templates/src/client/belief-events.js +0 -191
  137. package/templates/src/client/sse.js +0 -613
  138. package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
  139. package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
  140. package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
  141. package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
@@ -5,10 +5,12 @@ import {
5
5
  type SetStateAction,
6
6
  type ChangeEvent,
7
7
  } from 'react';
8
+ import { useStore } from '@nanostores/react';
8
9
  import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
9
10
  import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
11
+ import { fullContentMapStore } from '@/stores/storykeep';
10
12
  import { getCtx } from '@/stores/nodes';
11
- import { cloneDeep } from '@/utils/helpers';
13
+ import { cloneDeep, findUniqueSlug, titleToSlug } from '@/utils/helpers';
12
14
  import { PaneConfigMode, type PaneNode } from '@/types/compositorTypes';
13
15
 
14
16
  interface PaneTitlePanelProps {
@@ -18,36 +20,141 @@ interface PaneTitlePanelProps {
18
20
 
19
21
  const PaneTitlePanel = ({ nodeId, setMode }: PaneTitlePanelProps) => {
20
22
  const [title, setTitle] = useState('');
21
- const [isValid, setIsValid] = useState(false);
22
- const [warning, setWarning] = useState(false);
23
- const [charCount, setCharCount] = useState(0);
23
+ const [slug, setSlug] = useState('');
24
+ const [isValidTitle, setIsValidTitle] = useState(false);
25
+ const [isValidSlug, setIsValidSlug] = useState(false);
26
+ const [warningTitle, setWarningTitle] = useState(false);
27
+ const [warningSlug, setWarningSlug] = useState(false);
28
+ const [titleCharCount, setTitleCharCount] = useState(0);
29
+ const [slugCharCount, setSlugCharCount] = useState(0);
30
+ const [slugValidationError, setSlugValidationError] = useState<string | null>(
31
+ null
32
+ );
33
+ const [canSaveSlug, setCanSaveSlug] = useState(false);
24
34
 
35
+ const $contentMap = useStore(fullContentMapStore);
25
36
  const ctx = getCtx();
26
37
  const allNodes = ctx.allNodes.get();
27
38
  const paneNode = allNodes.get(nodeId) as PaneNode;
28
39
  if (!paneNode) return null;
29
40
 
41
+ const existingSlugs = $contentMap
42
+ .filter(
43
+ (item) =>
44
+ ['Pane', 'StoryFragment'].includes(item.type) && item.id !== nodeId
45
+ )
46
+ .map((item) => item.slug);
47
+
30
48
  useEffect(() => {
31
49
  setTitle(paneNode.title);
32
- setCharCount(paneNode.title.length);
33
- }, [paneNode.title]);
50
+ setSlug(paneNode.slug);
51
+ setTitleCharCount(paneNode.title.length);
52
+ setSlugCharCount(paneNode.slug.length);
53
+ checkSlugLiveValidity(paneNode.slug);
54
+ }, [paneNode.title, paneNode.slug]);
34
55
 
35
56
  const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
36
57
  const newTitle = e.target.value;
37
58
  if (newTitle.length <= 50) {
38
- // Prevent more than 70 chars
39
59
  setTitle(newTitle);
40
- setCharCount(newTitle.length);
41
- setIsValid(newTitle.length >= 5 && newTitle.length <= 35);
42
- setWarning(newTitle.length > 35 && newTitle.length <= 50);
60
+ setTitleCharCount(newTitle.length);
61
+ setIsValidTitle(newTitle.length >= 5 && newTitle.length <= 35);
62
+ setWarningTitle(newTitle.length > 35 && newTitle.length <= 50);
63
+ }
64
+ };
65
+
66
+ const handleSlugChange = (e: ChangeEvent<HTMLInputElement>) => {
67
+ const newSlug = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
68
+
69
+ if (newSlug.length <= 50) {
70
+ setSlug(newSlug);
71
+ checkSlugLiveValidity(newSlug);
72
+ }
73
+ };
74
+
75
+ const checkSlugLiveValidity = (value: string) => {
76
+ const length = value.length;
77
+ setSlugCharCount(length);
78
+
79
+ // Basic format check for allowed characters
80
+ if (!/^[a-z0-9-]*$/.test(value)) {
81
+ setSlugValidationError(
82
+ 'Only lowercase letters, numbers, and hyphens allowed'
83
+ );
84
+ setIsValidSlug(false);
85
+ setCanSaveSlug(false);
86
+ return false;
43
87
  }
88
+
89
+ // Length checks
90
+ setIsValidSlug(length >= 3 && length <= 40);
91
+ setWarningSlug(length > 40 && length <= 50);
92
+ setSlugValidationError(null);
93
+
94
+ // Check if we can save
95
+ if (length >= 3) {
96
+ const saveValidation = checkSlugSaveValidity(value);
97
+ setCanSaveSlug(saveValidation.isValid);
98
+ if (!saveValidation.isValid) {
99
+ setSlugValidationError(saveValidation.error || null);
100
+ }
101
+ } else {
102
+ setCanSaveSlug(false);
103
+ }
104
+
105
+ return true;
106
+ };
107
+
108
+ const checkSlugSaveValidity = (
109
+ value: string
110
+ ): { isValid: boolean; error?: string } => {
111
+ // Strict pattern that prevents leading/trailing hyphens and multiple consecutive hyphens
112
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
113
+ return {
114
+ isValid: false,
115
+ error:
116
+ 'Slug must start and end with letters or numbers, and no consecutive hyphens',
117
+ };
118
+ }
119
+
120
+ // Check duplicates
121
+ if (existingSlugs.includes(value)) {
122
+ return {
123
+ isValid: false,
124
+ error: 'This slug is already in use',
125
+ };
126
+ }
127
+
128
+ return { isValid: true };
44
129
  };
45
130
 
46
131
  const handleTitleBlur = () => {
47
132
  if (title.length >= 5) {
48
- // Only update if meets minimum length
133
+ // Auto-generate slug if slug is empty or still system-generated
134
+ let updatedSlug = slug;
135
+ if (!slug || slug === paneNode.slug) {
136
+ const generatedSlug = titleToSlug(title);
137
+ const uniqueSlug = findUniqueSlug(generatedSlug, existingSlugs);
138
+ updatedSlug = uniqueSlug;
139
+ setSlug(uniqueSlug);
140
+ checkSlugLiveValidity(uniqueSlug);
141
+ }
142
+
49
143
  const ctx = getCtx();
50
- const updatedNode = { ...cloneDeep(paneNode), title, isChanged: true };
144
+ const updatedNode = {
145
+ ...cloneDeep(paneNode),
146
+ title,
147
+ slug: updatedSlug,
148
+ isChanged: true,
149
+ };
150
+ ctx.modifyNodes([updatedNode]);
151
+ }
152
+ };
153
+
154
+ const handleSlugBlur = () => {
155
+ if (canSaveSlug) {
156
+ const ctx = getCtx();
157
+ const updatedNode = { ...cloneDeep(paneNode), slug, isChanged: true };
51
158
  ctx.modifyNodes([updatedNode]);
52
159
  }
53
160
  };
@@ -56,7 +163,7 @@ const PaneTitlePanel = ({ nodeId, setMode }: PaneTitlePanelProps) => {
56
163
  <div className="group mb-4 w-full rounded-b-md bg-white px-1.5 py-6 shadow-inner">
57
164
  <div className="px-3.5">
58
165
  <div className="mb-4 flex justify-between">
59
- <h3 className="text-lg font-bold">Pane Title</h3>
166
+ <h3 className="text-lg font-bold">Pane Title & Slug</h3>
60
167
  <button
61
168
  onClick={() => setMode(PaneConfigMode.DEFAULT)}
62
169
  className="text-myblue hover:text-black"
@@ -65,73 +172,133 @@ const PaneTitlePanel = ({ nodeId, setMode }: PaneTitlePanelProps) => {
65
172
  </button>
66
173
  </div>
67
174
 
68
- <div className="relative">
69
- <input
70
- type="text"
71
- value={title}
72
- onChange={handleTitleChange}
73
- onBlur={handleTitleBlur}
74
- onKeyDown={(e) => {
75
- if (e.key === 'Enter') {
76
- e.currentTarget.blur();
77
- }
78
- }}
79
- className={`w-full rounded-md border px-2 py-1 pr-16 ${
80
- charCount < 5
81
- ? 'border-red-500 bg-red-50'
82
- : isValid
83
- ? 'border-green-500 bg-green-50'
84
- : warning
85
- ? 'border-yellow-500 bg-yellow-50'
86
- : 'border-gray-300'
87
- }`}
88
- placeholder="Enter story fragment title (50-60 characters recommended)"
89
- />
90
- <div className="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-2">
91
- {charCount < 5 ? (
92
- <ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
93
- ) : isValid ? (
94
- <CheckIcon className="h-5 w-5 text-green-500" />
95
- ) : warning ? (
96
- <ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
97
- ) : null}
98
- <span
99
- className={`text-sm ${
100
- charCount < 5
101
- ? 'text-red-500'
102
- : isValid
103
- ? 'text-green-500'
104
- : warning
105
- ? 'text-yellow-500'
106
- : 'text-gray-500'
175
+ {/* Title Input */}
176
+ <div className="mb-6">
177
+ <label className="mb-2 block text-sm font-bold text-gray-700">
178
+ Title
179
+ </label>
180
+ <div className="relative">
181
+ <input
182
+ type="text"
183
+ value={title}
184
+ onChange={handleTitleChange}
185
+ onBlur={handleTitleBlur}
186
+ onKeyDown={(e) => {
187
+ if (e.key === 'Enter') {
188
+ e.currentTarget.blur();
189
+ }
190
+ }}
191
+ className={`w-full rounded-md border px-2 py-1 pr-16 ${
192
+ titleCharCount < 5
193
+ ? 'border-red-500 bg-red-50'
194
+ : isValidTitle
195
+ ? 'border-green-500 bg-green-50'
196
+ : warningTitle
197
+ ? 'border-yellow-500 bg-yellow-50'
198
+ : 'border-gray-300'
107
199
  }`}
108
- >
109
- {charCount}/50
110
- </span>
200
+ placeholder="Enter pane title (5-35 characters recommended)"
201
+ />
202
+ <div className="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-2">
203
+ {titleCharCount < 5 ? (
204
+ <ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
205
+ ) : isValidTitle ? (
206
+ <CheckIcon className="h-5 w-5 text-green-500" />
207
+ ) : warningTitle ? (
208
+ <ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
209
+ ) : null}
210
+ <span
211
+ className={`text-sm ${
212
+ titleCharCount < 5
213
+ ? 'text-red-500'
214
+ : isValidTitle
215
+ ? 'text-green-500'
216
+ : warningTitle
217
+ ? 'text-yellow-500'
218
+ : 'text-gray-500'
219
+ }`}
220
+ >
221
+ {titleCharCount}/50
222
+ </span>
223
+ </div>
111
224
  </div>
112
225
  </div>
113
- <div className="mt-4 space-y-4 text-lg">
114
- <div className="text-gray-600">
115
- Write a clear, descriptive title for this piece of content. This is
116
- used for your own internal analytics.
117
- </div>
118
- <div className="py-4">
119
- {charCount < 5 && (
120
- <span className="text-red-500">
121
- Title must be at least 5 characters
122
- </span>
123
- )}
124
- {charCount >= 5 && charCount < 10 && (
125
- <span className="text-gray-500">
126
- Add {10 - charCount} more characters for optimal length
226
+
227
+ {/* Slug Input */}
228
+ <div className="mb-4">
229
+ <label className="mb-2 block text-sm font-bold text-gray-700">
230
+ Slug (URL)
231
+ </label>
232
+ <div className="relative">
233
+ <input
234
+ type="text"
235
+ value={slug}
236
+ onChange={handleSlugChange}
237
+ onBlur={handleSlugBlur}
238
+ onKeyDown={(e) => {
239
+ if (e.key === 'Enter') {
240
+ e.currentTarget.blur();
241
+ }
242
+ }}
243
+ className={`w-full rounded-md border px-2 py-1 pr-16 ${
244
+ slugValidationError || slugCharCount < 3
245
+ ? 'border-red-500 bg-red-50'
246
+ : isValidSlug && canSaveSlug
247
+ ? 'border-green-500 bg-green-50'
248
+ : warningSlug
249
+ ? 'border-yellow-500 bg-yellow-50'
250
+ : 'border-gray-300'
251
+ }`}
252
+ placeholder="Enter pane slug (3-40 characters recommended)"
253
+ />
254
+ <div className="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-2">
255
+ {slugValidationError || slugCharCount < 3 ? (
256
+ <ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
257
+ ) : isValidSlug && canSaveSlug ? (
258
+ <CheckIcon className="h-5 w-5 text-green-500" />
259
+ ) : warningSlug ? (
260
+ <ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
261
+ ) : null}
262
+ <span
263
+ className={`text-sm ${
264
+ slugValidationError || slugCharCount < 3
265
+ ? 'text-red-500'
266
+ : isValidSlug && canSaveSlug
267
+ ? 'text-green-500'
268
+ : warningSlug
269
+ ? 'text-yellow-500'
270
+ : 'text-gray-500'
271
+ }`}
272
+ >
273
+ {slugCharCount}/50
127
274
  </span>
128
- )}
129
- {warning && (
130
- <span className="text-yellow-500">Title is getting long</span>
131
- )}
132
- {isValid && (
133
- <span className="text-green-500">Perfect title length!</span>
134
- )}
275
+ </div>
276
+ </div>
277
+ {slugValidationError && (
278
+ <div className="mt-2 text-sm text-red-600">
279
+ <ExclamationTriangleIcon className="mr-1 inline h-4 w-4" />
280
+ {slugValidationError}
281
+ </div>
282
+ )}
283
+ </div>
284
+
285
+ {/* Help Text */}
286
+ <div className="space-y-4 text-sm text-gray-600">
287
+ <div>
288
+ <h4 className="font-bold">Title Guidelines:</h4>
289
+ <ul className="ml-4 mt-1 list-disc">
290
+ <li>5-35 characters recommended for optimal display</li>
291
+ <li>Clear, descriptive title for the pane content</li>
292
+ </ul>
293
+ </div>
294
+ <div>
295
+ <h4 className="font-bold">Slug Guidelines:</h4>
296
+ <ul className="ml-4 mt-1 list-disc">
297
+ <li>Used for analytics tracking</li>
298
+ <li>Only lowercase letters, numbers, and hyphens</li>
299
+ <li>Must start and end with letter or number</li>
300
+ <li>No consecutive hyphens</li>
301
+ </ul>
135
302
  </div>
136
303
  </div>
137
304
  </div>
@@ -155,25 +155,23 @@ const StyleBreakPanel = ({ node, parentNode, config }: BasePanelProps) => {
155
155
  </div>
156
156
 
157
157
  <div className="space-y-4">
158
- <div className="grid grid-cols-2 gap-4">
159
- <ColorPickerCombo
160
- title="Shape Color"
161
- defaultColor={settings.svgFill}
162
- onColorChange={(color: string) =>
163
- setSettings((prev) => ({ ...prev, svgFill: color }))
164
- }
165
- config={config!}
166
- />
167
- <ColorPickerCombo
168
- title="Background Color"
169
- defaultColor={settings.bgColor}
170
- onColorChange={(color: string) =>
171
- setSettings((prev) => ({ ...prev, bgColor: color }))
172
- }
173
- config={config!}
174
- allowNull={true}
175
- />
176
- </div>
158
+ <ColorPickerCombo
159
+ title="Shape Color"
160
+ defaultColor={settings.svgFill}
161
+ onColorChange={(color: string) =>
162
+ setSettings((prev) => ({ ...prev, svgFill: color }))
163
+ }
164
+ config={config!}
165
+ />
166
+ <ColorPickerCombo
167
+ title="Background Color"
168
+ defaultColor={settings.bgColor}
169
+ onColorChange={(color: string) =>
170
+ setSettings((prev) => ({ ...prev, bgColor: color }))
171
+ }
172
+ config={config!}
173
+ allowNull={true}
174
+ />
177
175
  </div>
178
176
  </div>
179
177
  );
@@ -1,5 +1,5 @@
1
1
  import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
2
- import { Combobox, Switch } from '@ark-ui/react';
2
+ import { Combobox, Switch, Portal } from '@ark-ui/react';
3
3
  import { createListCollection } from '@ark-ui/react/collection';
4
4
  import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
5
5
  import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
@@ -8,7 +8,7 @@ import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTria
8
8
  import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
9
9
  import { settingsPanelStore } from '@/stores/storykeep';
10
10
  import { getCtx } from '@/stores/nodes';
11
- import { cloneDeep } from '@/utils/helpers';
11
+ import { cloneDeep, useDropdownDirection } from '@/utils/helpers';
12
12
  import { isPaneNode } from '@/utils/compositor/typeGuards';
13
13
  import type { PaneNode, BasePanelProps } from '@/types/compositorTypes';
14
14
 
@@ -30,7 +30,8 @@ const StyleCodeHookPanel = ({
30
30
 
31
31
  const [localTarget, setLocalTarget] = useState(node.codeHookTarget || '');
32
32
  const [query, setQuery] = useState('');
33
- const inputRef = useRef<HTMLInputElement>(null);
33
+ const comboboxRef = useRef<HTMLDivElement>(null);
34
+ const { openAbove } = useDropdownDirection(comboboxRef);
34
35
 
35
36
  // Parse the nested options from JSON string
36
37
  const [localOptions, setLocalOptions] = useState<OptionState[]>(() => {
@@ -320,42 +321,52 @@ const StyleCodeHookPanel = ({
320
321
  loopFocus={true}
321
322
  openOnKeyPress={true}
322
323
  composite={true}
324
+ positioning={{
325
+ placement: openAbove ? 'top' : 'bottom',
326
+ gutter: 4,
327
+ sameWidth: true,
328
+ }}
323
329
  >
324
- <div className="relative">
325
- <Combobox.Input
326
- ref={inputRef}
327
- className={commonInputClass}
328
- placeholder="Select a code hook..."
329
- onBlur={handleBlur}
330
- />
331
- <Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
332
- <ChevronUpDownIcon
333
- className="text-mydarkgrey h-5 w-5"
334
- aria-hidden="true"
330
+ <Combobox.Control ref={comboboxRef}>
331
+ <div className="relative">
332
+ <Combobox.Input
333
+ className={commonInputClass}
334
+ placeholder="Select a code hook..."
335
+ onBlur={handleBlur}
335
336
  />
336
- </Combobox.Trigger>
337
- </div>
338
-
339
- <Combobox.Content className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
340
- {collection.items.length === 0 ? (
341
- <div className="text-mydarkgrey relative cursor-default select-none px-4 py-2">
342
- Nothing found.
343
- </div>
344
- ) : (
345
- collection.items.map((hook) => (
346
- <Combobox.Item
347
- key={hook}
348
- item={hook}
349
- className="codehook-item relative cursor-default select-none py-2 pl-10 pr-4"
350
- >
351
- <span className="block truncate">{hook}</span>
352
- <span className="codehook-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
353
- <CheckIcon className="h-5 w-5" aria-hidden="true" />
354
- </span>
355
- </Combobox.Item>
356
- ))
357
- )}
358
- </Combobox.Content>
337
+ <Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
338
+ <ChevronUpDownIcon
339
+ className="text-mydarkgrey h-5 w-5"
340
+ aria-hidden="true"
341
+ />
342
+ </Combobox.Trigger>
343
+ </div>
344
+ </Combobox.Control>
345
+
346
+ <Portal>
347
+ <Combobox.Positioner style={{ zIndex: 1002 }}>
348
+ <Combobox.Content className="z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
349
+ {collection.items.length === 0 ? (
350
+ <div className="text-mydarkgrey relative cursor-default select-none px-4 py-2">
351
+ Nothing found.
352
+ </div>
353
+ ) : (
354
+ collection.items.map((hook) => (
355
+ <Combobox.Item
356
+ key={hook}
357
+ item={hook}
358
+ className="codehook-item relative cursor-default select-none py-2 pl-10 pr-4"
359
+ >
360
+ <span className="block truncate">{hook}</span>
361
+ <span className="codehook-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
362
+ <CheckIcon className="h-5 w-5" aria-hidden="true" />
363
+ </span>
364
+ </Combobox.Item>
365
+ ))
366
+ )}
367
+ </Combobox.Content>
368
+ </Combobox.Positioner>
369
+ </Portal>
359
370
  </Combobox.Root>
360
371
  </div>
361
372
  {!isValidCodeHook && localTarget && (