astro-tractstack 2.0.0-rc.12 → 2.0.0-rc.14

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 (24) hide show
  1. package/package.json +1 -1
  2. package/templates/src/components/Header.astro +44 -43
  3. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +6 -3
  4. package/templates/src/components/edit/SettingsPanel.tsx +4 -1
  5. package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
  6. package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -2
  7. package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +1 -1
  8. package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +1 -1
  9. package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +1 -1
  10. package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +1 -1
  11. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +1 -1
  12. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +1 -1
  13. package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +1 -1
  14. package/templates/src/components/fields/BackgroundImageWrapper.tsx +1 -1
  15. package/templates/src/components/fields/ViewportComboBox.tsx +10 -3
  16. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +1 -1
  17. package/templates/src/pages/[...slug]/edit.astro +1 -1
  18. package/templates/src/pages/storykeep/advanced.astro +1 -1
  19. package/templates/src/pages/storykeep/branding.astro +1 -1
  20. package/templates/src/pages/storykeep/content.astro +1 -1
  21. package/templates/src/pages/storykeep/init.astro +3 -1
  22. package/templates/src/pages/storykeep.astro +1 -1
  23. package/templates/src/stores/nodes.ts +32 -1
  24. package/templates/src/stores/storykeep.ts +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.0-rc.12",
3
+ "version": "2.0.0-rc.14",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -400,60 +400,61 @@ const authStatus = {
400
400
  });
401
401
  }
402
402
 
403
- // Admin Modal JavaScript - Only runs when admin elements exist
404
- const heartBtn = document.getElementById('admin-heart-btn');
405
- const modal = document.getElementById('admin-modal');
406
- const closeBtn = document.getElementById('admin-modal-close');
407
- const iframe = document.getElementById('admin-sysop-iframe');
408
-
409
- if (heartBtn && modal && closeBtn && iframe) {
410
- // Open modal when heart icon is clicked
411
- heartBtn.addEventListener('click', function (e) {
403
+ // Admin Modal JavaScript - Use event delegation
404
+ document.addEventListener('click', function (e) {
405
+ if (e.target && (e.target as Element).closest('#admin-heart-btn')) {
412
406
  e.preventDefault();
413
- modal.classList.remove('hidden');
414
- modal.setAttribute('aria-hidden', 'false');
415
-
416
- // Focus management for accessibility
417
- closeBtn.focus();
418
-
419
- // Prevent body scroll when modal is open
420
- document.body.style.overflow = 'hidden';
421
- });
407
+ const modal = document.getElementById('admin-modal');
408
+ const closeBtn = document.getElementById('admin-modal-close');
409
+
410
+ if (modal && closeBtn) {
411
+ modal.classList.remove('hidden');
412
+ modal.setAttribute('aria-hidden', 'false');
413
+ closeBtn.focus();
414
+ document.body.style.overflow = 'hidden';
415
+ }
416
+ return;
417
+ }
422
418
 
423
- // Close modal when X button is clicked
424
- closeBtn.addEventListener('click', function (e) {
419
+ if (e.target && (e.target as Element).closest('#admin-modal-close')) {
425
420
  e.preventDefault();
426
421
  closeModal();
427
- });
422
+ return;
423
+ }
428
424
 
429
- // Close modal when clicking outside the iframe
430
- modal.addEventListener('click', function (e) {
431
- if (e.target === modal) {
432
- closeModal();
433
- }
434
- });
425
+ if (e.target && (e.target as Element).id === 'admin-modal') {
426
+ closeModal();
427
+ return;
428
+ }
429
+ });
435
430
 
436
- // Close modal with Escape key
437
- document.addEventListener('keydown', function (e) {
438
- if (e.key === 'Escape' && !modal.classList.contains('hidden')) {
431
+ document.addEventListener('keydown', function (e) {
432
+ if (e.key === 'Escape') {
433
+ const modal = document.getElementById('admin-modal');
434
+ if (modal && !modal.classList.contains('hidden')) {
439
435
  closeModal();
440
436
  }
441
- });
437
+ }
438
+ });
442
439
 
443
- function closeModal() {
444
- modal?.classList.add('hidden');
445
- modal?.setAttribute('aria-hidden', 'true');
440
+ function closeModal() {
441
+ const modal = document.getElementById('admin-modal');
442
+ const heartBtn = document.getElementById('admin-heart-btn');
446
443
 
447
- // Restore body scroll
448
- document.body.style.overflow = '';
444
+ modal?.classList.add('hidden');
445
+ modal?.setAttribute('aria-hidden', 'true');
446
+ document.body.style.overflow = '';
447
+ heartBtn?.focus();
448
+ }
449
449
 
450
- // Return focus to heart button for accessibility
451
- heartBtn?.focus();
450
+ // iframe error handling
451
+ document.addEventListener('DOMContentLoaded', function () {
452
+ const iframe = document.getElementById('admin-sysop-iframe');
453
+ if (iframe) {
454
+ iframe.addEventListener('error', function () {
455
+ console.error('Failed to load admin panel');
456
+ });
452
457
  }
453
-
454
- iframe.addEventListener('error', function () {
455
- console.error('Failed to load admin panel');
456
- });
457
- }
458
+ });
458
459
  }
459
460
  </script>
@@ -185,7 +185,7 @@ const EpinetDurationSelector = ({
185
185
  const startUTCTime = createUTCDateTime(startDate, localFilters.startHour);
186
186
  const endUTCTime = createUTCDateTime(endDate, localFilters.endHour);
187
187
 
188
- if (endUTCTime <= startUTCTime) {
188
+ if (endUTCTime < startUTCTime) {
189
189
  setErrorMessage('End time must be after start time.');
190
190
  return;
191
191
  }
@@ -549,7 +549,7 @@ const EpinetDurationSelector = ({
549
549
 
550
550
  return (
551
551
  <>
552
- <div className="space-y-4">
552
+ <div className="space-y-4 overflow-visible">
553
553
  {$epinetCustomFilters.enabled && (
554
554
  <div
555
555
  className={`space-y-4 rounded-lg border-2 border-dashed border-gray-200 bg-gray-50 p-4`}
@@ -865,7 +865,10 @@ const EpinetDurationSelector = ({
865
865
  </Select.Control>
866
866
  <Portal>
867
867
  <Select.Positioner>
868
- <Select.Content className="z-10 mt-2 max-h-96 w-[var(--trigger-width)] overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
868
+ <Select.Content
869
+ className="z-10 mt-2 max-h-96 overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
870
+ style={{ width: 'var(--trigger-width)' }}
871
+ >
869
872
  {paginatedUserCounts.length > 0 ? (
870
873
  [
871
874
  <Select.Item
@@ -42,7 +42,10 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
42
42
  100% { opacity: var(--fade-end, 1); }
43
43
  }
44
44
  `}</style>
45
- <div className="w-full max-w-lg rounded-lg border border-gray-200 bg-white p-1.5 shadow-xl md:p-2.5">
45
+ <div
46
+ className="w-full rounded-lg border border-gray-200 bg-white p-1.5 shadow-xl md:p-2.5"
47
+ style={{ maxWidth: '90vw' }}
48
+ >
46
49
  <div className="mb-4 flex items-center justify-between">
47
50
  <h3 className="text-myblue text-lg font-bold">{panelTitle}</h3>
48
51
  <button
@@ -165,7 +165,7 @@ ${additionalInstructions}`;
165
165
  Content has been generated successfully! Click "Apply
166
166
  Content" to use this content with your selected design.
167
167
  </p>
168
- <div className="max-h-[60vh] overflow-y-auto rounded-md border border-gray-200 bg-gray-50 p-4">
168
+ <div className="overflow-y-auto rounded-md border border-gray-200 bg-gray-50 p-4">
169
169
  <pre className="whitespace-pre-wrap font-mono text-sm text-gray-800">
170
170
  {generatedContent}
171
171
  </pre>
@@ -222,7 +222,7 @@ const PaneMagicPathPanel = ({ nodeId, setMode }: PaneMagicPathPanelProps) => {
222
222
  </div>
223
223
 
224
224
  <div className="flex w-full flex-wrap gap-8">
225
- <div className="min-w-[400px] flex-1">
225
+ <div className="flex-1">
226
226
  <MagicPathBuilder
227
227
  paths={heldPaths}
228
228
  setPaths={handleHeldPathsChange}
@@ -232,7 +232,7 @@ const PaneMagicPathPanel = ({ nodeId, setMode }: PaneMagicPathPanelProps) => {
232
232
  />
233
233
  </div>
234
234
 
235
- <div className="min-w-[400px] flex-1">
235
+ <div className="flex-1">
236
236
  <MagicPathBuilder
237
237
  paths={withheldPaths}
238
238
  setPaths={handleWithheldPathsChange}
@@ -249,7 +249,7 @@ const StyleElementPanelAdd = ({
249
249
  `;
250
250
 
251
251
  return (
252
- <div className="min-h-[400px] max-w-md space-y-4">
252
+ <div className="max-w-md space-y-4">
253
253
  <style>{comboboxItemStyles}</style>
254
254
 
255
255
  <div className="flex flex-row flex-nowrap justify-between">
@@ -195,7 +195,7 @@ const StyleImagePanelAdd = ({ node, parentNode, childId }: BasePanelProps) => {
195
195
  `;
196
196
 
197
197
  return (
198
- <div className="min-h-[400px] max-w-md space-y-4">
198
+ <div className="max-w-md space-y-4">
199
199
  <style>{comboboxItemStyles}</style>
200
200
 
201
201
  <div className="flex flex-row flex-nowrap justify-between">
@@ -200,7 +200,7 @@ const StyleLiElementAddPanel = ({
200
200
  `;
201
201
 
202
202
  return (
203
- <div className="min-h-[400px] max-w-md space-y-4">
203
+ <div className="max-w-md space-y-4">
204
204
  <style>{comboboxItemStyles}</style>
205
205
 
206
206
  <div className="flex flex-row flex-nowrap justify-between">
@@ -164,7 +164,7 @@ const StyleLinkPanelAdd = ({ node }: BasePanelProps) => {
164
164
  `;
165
165
 
166
166
  return (
167
- <div className="min-h-[400px] max-w-md space-y-4">
167
+ <div className="max-w-md space-y-4">
168
168
  <style>{comboboxItemStyles}</style>
169
169
 
170
170
  <div className="flex flex-row flex-nowrap justify-between">
@@ -221,7 +221,7 @@ const StyleLinkConfigPanel = ({ node, config }: StyleLinkConfigPanelProps) => {
221
221
  </div>
222
222
 
223
223
  <div className="space-y-2">
224
- <div className="relative max-h-[60vh] min-h-[400px] overflow-y-auto">
224
+ <div className="relative overflow-y-auto">
225
225
  <div className="absolute inset-x-0">
226
226
  <label className="text-mydarkgrey mb-2 block text-sm">
227
227
  {actionType === 'goto'
@@ -174,7 +174,7 @@ const StyleParentPanelAdd = ({ node, layer }: BasePanelProps) => {
174
174
  `;
175
175
 
176
176
  return (
177
- <div className="min-h-[400px] max-w-md space-y-4">
177
+ <div className="max-w-md space-y-4">
178
178
  <style>{comboboxItemStyles}</style>
179
179
 
180
180
  <div className="flex flex-row flex-nowrap justify-between">
@@ -191,7 +191,7 @@ const StyleWidgetPanelAdd = ({ node, parentNode, childId }: BasePanelProps) => {
191
191
  `;
192
192
 
193
193
  return (
194
- <div className="min-h-[400px] max-w-md space-y-4">
194
+ <div className="max-w-md space-y-4">
195
195
  <style>{comboboxItemStyles}</style>
196
196
 
197
197
  <div className="flex flex-row flex-nowrap justify-between">
@@ -120,7 +120,7 @@ const BackgroundImageWrapper = ({
120
120
  <label className="block text-sm font-bold text-gray-700">
121
121
  Position
122
122
  </label>
123
- <div className="flex space-x-4">
123
+ <div className="flex flex-wrap space-x-4">
124
124
  {(
125
125
  [
126
126
  'background',
@@ -13,7 +13,7 @@ import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
13
13
  import DevicePhoneMobileIcon from '@heroicons/react/24/outline/DevicePhoneMobileIcon';
14
14
  import DeviceTabletIcon from '@heroicons/react/24/outline/DeviceTabletIcon';
15
15
  import ComputerDesktopIcon from '@heroicons/react/24/outline/ComputerDesktopIcon';
16
- import { classNames } from '@/utils/helpers';
16
+ import { classNames, useDropdownDirection } from '@/utils/helpers';
17
17
  import { tailwindToHex, colorValues } from '@/utils/compositor/tailwindColors';
18
18
  import type { BrandConfig } from '@/types/tractstack';
19
19
 
@@ -46,6 +46,8 @@ const ViewportComboBox = ({
46
46
  const [query, setQuery] = useState('');
47
47
  const [isNowNegative, setIsNowNegative] = useState(isNegative);
48
48
  const inputRef = useRef<HTMLInputElement>(null);
49
+ const comboboxRef = useRef<HTMLDivElement>(null);
50
+ const { openAbove, maxHeight } = useDropdownDirection(comboboxRef);
49
51
 
50
52
  const Icon =
51
53
  viewport === 'mobile'
@@ -155,7 +157,7 @@ const ViewportComboBox = ({
155
157
  openOnKeyPress={true}
156
158
  composite={true}
157
159
  >
158
- <div className="relative">
160
+ <div ref={comboboxRef} className="relative">
159
161
  <div className="relative flex items-center">
160
162
  {isColorValue && (
161
163
  <div
@@ -187,7 +189,12 @@ const ViewportComboBox = ({
187
189
  </Combobox.Trigger>
188
190
  </div>
189
191
  </div>
190
- <Combobox.Content className="absolute z-50 mt-1 max-h-32 w-full overflow-auto rounded-md bg-white py-1 text-xl shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
192
+ <Combobox.Content
193
+ className={`absolute z-50 mt-1 w-full overflow-auto rounded-md bg-white py-1 text-xl shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${
194
+ openAbove ? 'bottom-full mb-1' : 'top-full'
195
+ }`}
196
+ style={{ maxHeight: `${maxHeight}px` }}
197
+ >
191
198
  {collection.items.length === 0 ? (
192
199
  <div className="text-mydarkgrey relative cursor-default select-none px-4 py-2">
193
200
  Nothing found.
@@ -369,7 +369,7 @@ export default function StoryKeepDashboard_Analytics({
369
369
  </div>
370
370
 
371
371
  {/* User Journey Section */}
372
- <div className="mb-6 overflow-hidden">
372
+ <div className="mb-6 overflow-visible">
373
373
  <h3 className="mb-4 text-lg font-bold text-gray-900">
374
374
  User Journey Analytics
375
375
  </h3>
@@ -201,7 +201,7 @@ for (const [key, value] of Astro.url.searchParams) {
201
201
  <!-- Floating Controls (Settings Panel & HUD OR ToolBar) -->
202
202
  <aside
203
203
  id="settingsControls"
204
- class="z-101 pointer-events-none fixed bottom-24 right-0 flex max-h-screen flex-col items-end gap-2 overflow-y-auto p-4 md:bottom-0"
204
+ class="z-101 pointer-events-none fixed bottom-24 right-0 flex max-h-screen flex-col items-end gap-2 overflow-visible p-4 md:bottom-0"
205
205
  >
206
206
  <div class="pointer-events-auto">
207
207
  <StoryKeepToolBar client:only="react" />
@@ -54,7 +54,7 @@ try {
54
54
 
55
55
  <Layout title={title} slug="storykeep" isStoryKeep={true}>
56
56
  <main id="main-content" class="min-h-screen w-full">
57
- <div class="max-w-5xl p-8">
57
+ <div class="max-w-5xl p-3.5 md:p-8">
58
58
  <StoryKeepDashboard
59
59
  client:only="react"
60
60
  fullContentMap={fullContentMap}
@@ -43,7 +43,7 @@ try {
43
43
 
44
44
  <Layout title={title} slug="storykeep" isStoryKeep={true}>
45
45
  <main id="main-content" class="min-h-screen w-full">
46
- <div class="max-w-5xl p-8">
46
+ <div class="max-w-5xl p-3.5 md:p-8">
47
47
  <BrandingPageWrapper
48
48
  client:only="react"
49
49
  fullContentMap={fullContentMap}
@@ -50,7 +50,7 @@ try {
50
50
 
51
51
  <Layout title={title} slug="storykeep" isStoryKeep={true}>
52
52
  <main id="main-content" class="min-h-screen w-full">
53
- <div class="max-w-5xl p-8">
53
+ <div class="max-w-5xl p-3.5 md:p-8">
54
54
  <StoryKeepDashboard
55
55
  client:only="react"
56
56
  fullContentMap={fullContentMap}
@@ -31,6 +31,8 @@ const mainStylesUrl = isDev
31
31
  <link rel="stylesheet" href={mainStylesUrl} />
32
32
  </head>
33
33
  <body class="h-full">
34
- <RegistrationForm client:load isInitMode={true} />
34
+ <div class="max-w-5xl p-3.5 md:p-8">
35
+ <RegistrationForm client:load isInitMode={true} />
36
+ </div>
35
37
  </body>
36
38
  </html>
@@ -52,7 +52,7 @@ try {
52
52
 
53
53
  <Layout title={title} isStoryKeep={true} slug="storykeep">
54
54
  <main id="main-content" class="min-h-screen w-full">
55
- <div class="p-3.5 md:p-8">
55
+ <div class="max-w-5xl p-3.5 md:p-8">
56
56
  <StoryKeepDashboard
57
57
  client:only="react"
58
58
  fullContentMap={fullContentMap}
@@ -2316,10 +2316,41 @@ export class NodesContext {
2316
2316
 
2317
2317
  getDirtyNodesClassData(): { dirtyPaneIds: string[]; classes: string[] } {
2318
2318
  const dirtyNodes = this.getDirtyNodes();
2319
+
2319
2320
  const dirtyPaneIds = dirtyNodes
2320
2321
  .filter((node) => node.nodeType === 'Pane')
2321
2322
  .map((node) => node.id);
2322
- const classes = extractClassesFromNodes(dirtyNodes);
2323
+
2324
+ // Collect all nodes that need class extraction
2325
+ const allNodesToExtract: BaseNode[] = [];
2326
+
2327
+ // Find root dirty nodes (dirty nodes whose parents are NOT dirty)
2328
+ const dirtyNodeIds = new Set(dirtyNodes.map((n) => n.id));
2329
+ const rootDirtyNodes = dirtyNodes.filter(
2330
+ (node) => !node.parentId || !dirtyNodeIds.has(node.parentId)
2331
+ );
2332
+
2333
+ // For each root dirty node, traverse all descendants
2334
+ rootDirtyNodes.forEach((rootNode) => {
2335
+ // Add the root node itself
2336
+ allNodesToExtract.push(rootNode);
2337
+
2338
+ // Traverse all descendants using breadth-first
2339
+ const queue = [...this.getChildNodeIDs(rootNode.id)];
2340
+ while (queue.length > 0) {
2341
+ const currentId = queue.shift();
2342
+ if (!currentId) continue;
2343
+
2344
+ const currentNode = this.allNodes.get().get(currentId);
2345
+ if (currentNode) {
2346
+ allNodesToExtract.push(currentNode);
2347
+ const childrenIds = this.getChildNodeIDs(currentId);
2348
+ queue.push(...childrenIds);
2349
+ }
2350
+ }
2351
+ });
2352
+
2353
+ const classes = extractClassesFromNodes(allNodesToExtract);
2323
2354
 
2324
2355
  return { dirtyPaneIds, classes };
2325
2356
  }
@@ -1,6 +1,7 @@
1
1
  import { atom, map } from 'nanostores';
2
2
  import { persistentAtom } from '@nanostores/persistent';
3
3
  import { handleSettingsPanelMobile } from '@/utils/layout';
4
+ import { getCtx, ROOT_NODE_NAME } from '@/stores/nodes';
4
5
  import type {
5
6
  FullContentMapItem,
6
7
  Theme,
@@ -116,6 +117,10 @@ export const setViewportMode = (mode: ViewportKey) => {
116
117
  } else {
117
118
  viewportKeyStore.setKey('value', mode);
118
119
  }
120
+
121
+ // Notify root node to trigger coordinated re-render
122
+ const ctx = getCtx();
123
+ ctx.notifyNode(ROOT_NODE_NAME);
119
124
  };
120
125
 
121
126
  export const toggleShowAnalytics = () => {