astro-tractstack 2.0.0-rc.25 → 2.0.0-rc.27

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.0-rc.25",
3
+ "version": "2.0.0-rc.27",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -312,7 +312,7 @@ const getElement = (
312
312
  case 'impression':
313
313
  return <></>;
314
314
  default:
315
- console.warn(`Node.tsx miss on ${type}`);
315
+ console.warn(`Node.tsx miss on ${type}`, node);
316
316
  return <></>;
317
317
  }
318
318
  };
@@ -1,4 +1,4 @@
1
- import { useEffect, useState, memo, type CSSProperties } from 'react';
1
+ import { useEffect, useState, memo, Fragment, type CSSProperties } from 'react';
2
2
  import { getCtx } from '@/stores/nodes';
3
3
  import { viewportKeyStore } from '@/stores/storykeep';
4
4
  import { RenderChildren } from './RenderChildren';
@@ -27,7 +27,7 @@ const CodeHookContainer = ({
27
27
  {Object.entries(payload.params).map(
28
28
  ([key, value]) =>
29
29
  value && (
30
- <>
30
+ <Fragment key={key}>
31
31
  <span className="min-w-24 font-bold text-gray-600">{key}:</span>
32
32
  <div className="ml-2 flex flex-wrap gap-1">
33
33
  {value.split('|').map((item, index) => (
@@ -39,7 +39,7 @@ const CodeHookContainer = ({
39
39
  </span>
40
40
  ))}
41
41
  </div>
42
- </>
42
+ </Fragment>
43
43
  )
44
44
  )}
45
45
  </div>
@@ -13,6 +13,7 @@ import {
13
13
  viewportModeStore,
14
14
  setViewportMode,
15
15
  settingsPanelStore,
16
+ pendingHomePageSlugStore,
16
17
  } from '@/stores/storykeep';
17
18
  import { getCtx, ROOT_NODE_NAME } from '@/stores/nodes';
18
19
  import SaveModal from '@/components/edit/state/SaveModal';
@@ -24,6 +25,7 @@ interface StoryKeepHeaderProps {
24
25
 
25
26
  const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
26
27
  const viewport = useStore(viewportModeStore);
28
+ const pendingHomePageSlug = useStore(pendingHomePageSlugStore);
27
29
  const ctx = getCtx();
28
30
  const hasTitle = useStore(ctx.hasTitle);
29
31
  const hasPanes = useStore(ctx.hasPanes);
@@ -61,7 +63,8 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
61
63
  };
62
64
 
63
65
  const handleVisitPage = () => {
64
- if (canUndo) {
66
+ const hasChanges = canUndo || pendingHomePageSlug;
67
+ if (hasChanges) {
65
68
  if (
66
69
  confirm(
67
70
  'You have unsaved changes. Do you want to visit the page anyway?'
@@ -89,6 +92,9 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
89
92
  { value: 'desktop', Icon: ComputerDesktopIcon, title: 'Desktop Viewport' },
90
93
  ];
91
94
 
95
+ // Show save button if there are undo changes OR pending home page change
96
+ const shouldShowSave = canUndo || pendingHomePageSlug;
97
+
92
98
  if (!hasTitle && !hasPanes) return null;
93
99
 
94
100
  return (
@@ -127,7 +133,7 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
127
133
  className={`${iconClassName} relative`}
128
134
  >
129
135
  <GlobeAltIcon />
130
- {canUndo && (
136
+ {shouldShowSave && (
131
137
  <ExclamationTriangleIcon className="absolute -right-1 -top-1 h-3 w-3 rounded-full bg-white text-amber-500" />
132
138
  )}
133
139
  </button>
@@ -156,7 +162,7 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
156
162
  </div>
157
163
  )}
158
164
 
159
- {canUndo && (
165
+ {shouldShowSave && (
160
166
  <div className="flex flex-wrap items-center justify-center gap-2 text-sm">
161
167
  <button
162
168
  onClick={handleSave}
@@ -17,13 +17,13 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
17
17
  const ctx = getCtx();
18
18
  const { value: toolModeVal } = useStore(ctx.toolModeValStore);
19
19
 
20
- if (toolModeVal !== `styles` || !signal) {
20
+ if (toolModeVal !== 'styles' || !signal) {
21
21
  return null;
22
22
  }
23
23
 
24
24
  return (
25
25
  <div
26
- className="bg-mydarkgrey max-w-md rounded-xl bg-opacity-20 p-0.5 backdrop-blur-sm"
26
+ className="bg-mydarkgrey flex h-full max-w-sm flex-col rounded-xl bg-opacity-20 p-0.5 backdrop-blur-sm"
27
27
  style={
28
28
  {
29
29
  animation: window.matchMedia(
@@ -37,26 +37,30 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
37
37
  }
38
38
  >
39
39
  <style>{`
40
- @keyframes fadeInFromHalf {
41
- 0% { opacity: var(--fade-start, 0.5); }
42
- 100% { opacity: var(--fade-end, 1); }
43
- }
44
- `}</style>
40
+ @keyframes fadeInFromHalf {
41
+ 0% { opacity: var(--fade-start, 0.5); }
42
+ 100% { opacity: var(--fade-end, 1); }
43
+ }
44
+ `}</style>
45
45
  <div
46
- className="w-full rounded-lg border border-gray-200 bg-white p-1.5 shadow-xl md:p-2.5"
46
+ className="flex h-full min-h-0 w-full flex-col rounded-lg border border-gray-200 bg-white bg-opacity-85 shadow-xl"
47
47
  style={{ maxWidth: '90vw' }}
48
48
  >
49
- <div className="mb-4 flex items-center justify-between">
50
- <h3 className="text-myblue text-lg font-bold">{panelTitle}</h3>
51
- <button
52
- onClick={() => settingsPanelStore.set(null)}
53
- className="hover:text-myblue text-gray-500"
54
- >
55
- <XMarkIcon className="h-5 w-5" />
56
- </button>
49
+ {/* Header Section (fixed height) */}
50
+ <div className="flex-shrink-0 p-1.5 md:p-2.5">
51
+ <div className="mb-4 flex items-center justify-between">
52
+ <h3 className="text-myblue text-lg font-bold">{panelTitle}</h3>
53
+ <button
54
+ onClick={() => settingsPanelStore.set(null)}
55
+ className="hover:text-myblue text-gray-500"
56
+ >
57
+ <XMarkIcon className="h-5 w-5" />
58
+ </button>
59
+ </div>
57
60
  </div>
58
61
 
59
- <div className="space-y-4">
62
+ {/* Scrollable Content Section */}
63
+ <div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-1.5 pt-0 md:p-2.5 md:pt-0">
60
64
  <div className="rounded bg-gray-50 p-1.5 md:p-2.5">
61
65
  <PanelSwitch
62
66
  config={config}
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
2
  import { useStore } from '@nanostores/react';
3
3
  import PencilSquareIcon from '@heroicons/react/24/outline/PencilSquareIcon';
4
4
  import PaintBrushIcon from '@heroicons/react/24/outline/PaintBrushIcon';
@@ -8,8 +8,11 @@ import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
8
8
  import BugAntIcon from '@heroicons/react/24/outline/BugAntIcon';
9
9
  import { settingsPanelStore } from '@/stores/storykeep';
10
10
  import { getCtx } from '@/stores/nodes';
11
+ import { debounce } from '@/utils/helpers';
11
12
  import type { ToolModeVal } from '@/types/compositorTypes';
12
13
 
14
+ const SHORT_THRESHOLD = 650;
15
+
13
16
  const storykeepToolModes = [
14
17
  {
15
18
  key: 'styles' as const,
@@ -41,12 +44,6 @@ const storykeepToolModes = [
41
44
  title: 'Move',
42
45
  description: 'Keyboard accessible re-order',
43
46
  },
44
- {
45
- key: 'debug' as const,
46
- Icon: BugAntIcon,
47
- title: 'Debug',
48
- description: 'Debug node ids',
49
- },
50
47
  ] as const;
51
48
 
52
49
  interface StoryKeepToolModeProps {
@@ -54,9 +51,10 @@ interface StoryKeepToolModeProps {
54
51
  }
55
52
 
56
53
  const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
57
- //const signal = useStore(settingsPanelStore);
58
54
  const ctx = getCtx();
59
55
  const { value: toolModeVal } = useStore(ctx.toolModeValStore);
56
+ const showGuids = useStore(ctx.showGuids);
57
+ const navRef = useRef<HTMLElement>(null);
60
58
 
61
59
  const hasTitle = useStore(ctx.hasTitle);
62
60
  const hasPanes = useStore(ctx.hasPanes);
@@ -64,6 +62,8 @@ const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
64
62
  const className =
65
63
  'w-8 h-8 py-1 rounded-xl bg-white text-myblue hover:bg-mygreen/20 hover:text-black hover:rotate-3 cursor-pointer transition-all';
66
64
  const classNameActive = 'w-8 h-8 py-1.5 rounded-md bg-myblue text-white';
65
+ const classNameDebugActive =
66
+ 'w-8 h-8 py-1.5 rounded-md bg-orange-500 text-white';
67
67
 
68
68
  const currentToolMode =
69
69
  storykeepToolModes.find((mode) => mode.key === toolModeVal) ??
@@ -72,49 +72,109 @@ const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
72
72
  const handleClick = (mode: ToolModeVal) => {
73
73
  settingsPanelStore.set(null);
74
74
  ctx.toolModeValStore.set({ value: mode });
75
- ctx.showGuids.set(mode === `debug`);
76
75
  ctx.notifyNode('root');
77
76
  };
78
77
 
79
- // Escape key listener
78
+ const handleDebugToggle = () => {
79
+ ctx.showGuids.set(!showGuids);
80
+ ctx.notifyNode('root');
81
+ };
82
+
80
83
  useEffect(() => {
81
84
  const handleEscapeKey = (event: KeyboardEvent) => {
82
85
  if (event.key === 'Escape') {
83
86
  ctx.toolModeValStore.set({ value: 'text' });
84
- console.log('Tool mode reset to text via Escape');
85
87
  }
86
88
  };
89
+
90
+ const toolModeNav = navRef.current;
91
+
92
+ // If the <nav> element hasn't been rendered yet, do nothing.
93
+ // The hook will re-run when hasTitle/hasPanes changes and it does render.
94
+ if (!toolModeNav) {
95
+ return;
96
+ }
97
+
98
+ const updateToolbarLayout = debounce(() => {
99
+ const isWideAndShort =
100
+ window.innerWidth >= 801 && window.innerHeight <= SHORT_THRESHOLD;
101
+ toolModeNav.classList.toggle('is-compact-widget', isWideAndShort);
102
+ }, 50);
103
+
87
104
  document.addEventListener('keydown', handleEscapeKey);
105
+ window.addEventListener('resize', updateToolbarLayout);
106
+
107
+ updateToolbarLayout(); // Initial check
108
+
88
109
  return () => {
89
110
  document.removeEventListener('keydown', handleEscapeKey);
111
+ window.removeEventListener('resize', updateToolbarLayout);
90
112
  };
91
- }, [ctx]);
113
+ // This dependency array is the key. The effect will re-run when the render conditions change.
114
+ }, [ctx, hasTitle, hasPanes, isContext]);
92
115
 
93
- if (!hasTitle || (!hasPanes && !isContext)) return null;
116
+ if (!hasTitle || (!hasPanes && !isContext)) {
117
+ return null;
118
+ }
94
119
 
95
120
  return (
96
- <nav
97
- id="mainNav"
98
- className="z-102 bg-mywhite fixed bottom-0 left-0 right-0 pt-1.5 md:sticky md:bottom-auto md:left-0 md:top-24 md:h-screen md:w-16 md:pt-0"
99
- >
100
- <div className="flex flex-wrap justify-around gap-4 py-3.5 md:mt-0 md:flex-col md:items-center md:gap-8 md:space-x-0 md:space-y-2 md:py-2">
101
- <div className="text-mydarkgrey h-16 text-center text-sm font-bold">
102
- mode:
103
- <div className="font-action text-myblue pt-1.5 text-center text-xs">
104
- {currentToolMode.title}
121
+ <>
122
+ <style>{`
123
+ #mainNav.is-compact-widget {
124
+ position: fixed;
125
+ bottom: 0.25rem;
126
+ left: 0rem;
127
+ top: auto;
128
+ right: auto;
129
+ height: auto;
130
+ width: auto;
131
+ padding: 0.5rem;
132
+ border-radius: 0 0.75rem 0.75rem 0;
133
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
134
+ background-color: rgba(252, 252, 252, 0.7);
135
+ backdrop-filter: blur(4px);
136
+ border: 1px solid rgba(0, 0, 0, 0.05);
137
+ }
138
+ #mainNav.is-compact-widget > div {
139
+ flex-direction: row;
140
+ flex-wrap: nowrap;
141
+ align-items: center;
142
+ gap: 1rem;
143
+ margin: 0;
144
+ padding: 0;
145
+ height: auto;
146
+ }
147
+ `}</style>
148
+ <nav
149
+ id="mainNav"
150
+ ref={navRef}
151
+ className="z-102 bg-mywhite fixed bottom-0 left-0 right-0 pt-1.5 md:sticky md:bottom-auto md:left-0 md:top-24 md:h-screen md:w-16 md:pt-0"
152
+ >
153
+ <div className="flex flex-wrap justify-around gap-4 py-0.5 md:mt-0 md:flex-col md:items-center md:gap-8 md:space-x-0 md:space-y-2 md:py-2">
154
+ <div className="text-mydarkgrey text-center text-sm font-bold">
155
+ mode:
156
+ <div className="font-action text-myblue pt-1.5 text-center text-xs">
157
+ {currentToolMode.title}
158
+ </div>
105
159
  </div>
106
- </div>
107
- {storykeepToolModes.map(({ key, Icon, description }) => (
108
- <div title={description} key={key}>
109
- {key === toolModeVal ? (
110
- <Icon className={classNameActive} />
111
- ) : (
112
- <Icon className={className} onClick={() => handleClick(key)} />
113
- )}
160
+ {storykeepToolModes.map(({ key, Icon, description }) => (
161
+ <div title={description} key={key}>
162
+ {key === toolModeVal ? (
163
+ <Icon className={classNameActive} />
164
+ ) : (
165
+ <Icon className={className} onClick={() => handleClick(key)} />
166
+ )}
167
+ </div>
168
+ ))}
169
+ <div title="Toggle debug node ids" key="debug">
170
+ <BugAntIcon
171
+ className={showGuids ? classNameDebugActive : className}
172
+ onClick={handleDebugToggle}
173
+ />
114
174
  </div>
115
- ))}
116
- </div>
117
- </nav>
175
+ </div>
176
+ </nav>
177
+ </>
118
178
  );
119
179
  };
120
180
 
@@ -11,6 +11,7 @@ import {
11
11
  fullContentMapStore,
12
12
  getPendingImageOperation,
13
13
  clearPendingImageOperation,
14
+ pendingHomePageSlugStore,
14
15
  } from '@/stores/storykeep';
15
16
  import { startLoadingAnimation } from '@/utils/helpers';
16
17
  import type {
@@ -29,6 +30,7 @@ type SaveStage =
29
30
  | 'SAVING_STORY_FRAGMENTS'
30
31
  | 'LINKING_FILES'
31
32
  | 'PROCESSING_STYLES'
33
+ | 'UPDATING_HOME_PAGE'
32
34
  | 'COMPLETED'
33
35
  | 'ERROR';
34
36
 
@@ -61,13 +63,9 @@ export default function SaveModal({
61
63
  const [debugMessages, setDebugMessages] = useState<string[]>([]);
62
64
  const isSaving = useRef(false);
63
65
  const [isNavigating, setIsNavigating] = useState(false);
64
-
65
- // Determine if we're in create mode
66
66
  const isCreateMode = slug === 'create';
67
-
68
67
  const contentMap = fullContentMapStore.get();
69
-
70
- // Get backend URL
68
+ const pendingHomePageSlug = pendingHomePageSlugStore.get();
71
69
  const goBackend =
72
70
  import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
73
71
  const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
@@ -145,7 +143,8 @@ export default function SaveModal({
145
143
  if (
146
144
  relevantNodeCount === 0 &&
147
145
  nodesWithPendingFiles.length === 0 &&
148
- storyFragmentsWithPendingImages.length === 0
146
+ storyFragmentsWithPendingImages.length === 0 &&
147
+ !pendingHomePageSlug
149
148
  ) {
150
149
  addDebugMessage('No changes to save');
151
150
  setStage('COMPLETED');
@@ -614,6 +613,67 @@ export default function SaveModal({
614
613
  throw new Error(`Failed to process styles: ${errorMsg}`);
615
614
  }
616
615
 
616
+ // Check if we need to update home page
617
+ if (pendingHomePageSlug) {
618
+ setStage('UPDATING_HOME_PAGE');
619
+ setProgress(98);
620
+ addDebugMessage(`Updating home page to: ${pendingHomePageSlug}`);
621
+
622
+ try {
623
+ // First get current brand config
624
+ const response = await fetch(`${goBackend}/api/v1/config/brand`, {
625
+ method: 'GET',
626
+ headers: {
627
+ 'Content-Type': 'application/json',
628
+ 'X-Tenant-ID': tenantId,
629
+ },
630
+ credentials: 'include',
631
+ });
632
+
633
+ if (!response.ok) {
634
+ throw new Error(
635
+ `Failed to get current brand config: ${response.status}`
636
+ );
637
+ }
638
+
639
+ const currentBrandConfig = await response.json();
640
+
641
+ // Update HOME_SLUG
642
+ const updatedBrandConfig = {
643
+ ...currentBrandConfig,
644
+ HOME_SLUG: pendingHomePageSlug,
645
+ };
646
+
647
+ const updateResponse = await fetch(
648
+ `${goBackend}/api/v1/config/brand`,
649
+ {
650
+ method: 'PUT',
651
+ headers: {
652
+ 'Content-Type': 'application/json',
653
+ 'X-Tenant-ID': tenantId,
654
+ },
655
+ credentials: 'include',
656
+ body: JSON.stringify(updatedBrandConfig),
657
+ }
658
+ );
659
+
660
+ if (!updateResponse.ok) {
661
+ throw new Error(
662
+ `Failed to update home page: ${updateResponse.status}`
663
+ );
664
+ }
665
+
666
+ // Clear the pending operation
667
+ pendingHomePageSlugStore.set(null);
668
+ addDebugMessage('Home page updated successfully');
669
+ } catch (error) {
670
+ const errorMsg =
671
+ error instanceof Error ? error.message : 'Unknown error';
672
+ addDebugMessage(`Home page update failed: ${errorMsg}`);
673
+ throw new Error(`Failed to update home page: ${errorMsg}`);
674
+ }
675
+ }
676
+
617
677
  // Success!
618
678
  setStage('COMPLETED');
619
679
  setProgress(100);
@@ -658,6 +718,8 @@ export default function SaveModal({
658
718
  return 'Linking file relationships...';
659
719
  case 'PROCESSING_STYLES':
660
720
  return 'Processing styles...';
721
+ case 'UPDATING_HOME_PAGE':
722
+ return 'Updating home page...';
661
723
  case 'COMPLETED':
662
724
  return `${actionText} ${modeText.toLowerCase()} completed successfully!`;
663
725
  case 'ERROR':
@@ -1,8 +1,11 @@
1
1
  import { useState, useEffect, type ChangeEvent } from 'react';
2
+ import { useStore } from '@nanostores/react';
2
3
  import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
3
4
  import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
4
5
  import LockClosedIcon from '@heroicons/react/24/outline/LockClosedIcon';
6
+ import { Switch } from '@ark-ui/react/switch';
5
7
  import { getCtx } from '@/stores/nodes';
8
+ import { pendingHomePageSlugStore } from '@/stores/storykeep';
6
9
  import { cloneDeep } from '@/utils/helpers';
7
10
  import type { BrandConfig } from '@/types/tractstack';
8
11
  import {
@@ -28,6 +31,8 @@ const StoryFragmentSlugPanel = ({
28
31
  const [validationError, setValidationError] = useState<string | null>(null);
29
32
  const [canSave, setCanSave] = useState(false);
30
33
  const isHomeSlug = slug === config.HOME_SLUG;
34
+ const pendingHomePageSlug = useStore(pendingHomePageSlugStore);
35
+ const isSetAsHomePage = pendingHomePageSlug === slug;
31
36
 
32
37
  const ctx = getCtx();
33
38
  const allNodes = ctx.allNodes.get();
@@ -129,6 +134,14 @@ const StoryFragmentSlugPanel = ({
129
134
  }
130
135
  };
131
136
 
137
+ const handleSetAsHomePageChange = (details: { checked: boolean }) => {
138
+ if (details.checked) {
139
+ pendingHomePageSlugStore.set(slug);
140
+ } else {
141
+ pendingHomePageSlugStore.set(null);
142
+ }
143
+ };
144
+
132
145
  return (
133
146
  <div className="group mb-4 w-full rounded-b-md bg-white px-1.5 py-6">
134
147
  <div className="px-3.5">
@@ -194,72 +207,60 @@ const StoryFragmentSlugPanel = ({
194
207
  </span>
195
208
  </div>
196
209
  </div>
210
+
197
211
  {validationError && (
198
212
  <div className="mt-2 text-sm text-red-600">
199
213
  <ExclamationTriangleIcon className="mr-1 inline h-4 w-4" />
200
214
  {validationError}
201
215
  </div>
202
216
  )}
217
+
203
218
  {isHomeSlug && (
204
- <div className="mt-2 text-sm text-gray-600">
205
- <LockClosedIcon className="mr-1 inline h-4 w-4" />
206
- This is your home page slug and cannot be modified
219
+ <div className="mt-4">
220
+ <div className="inline-flex items-center rounded-full bg-blue-100 px-3 py-1.5 text-sm font-medium text-blue-800">
221
+ <LockClosedIcon className="mr-1.5 h-4 w-4" />
222
+ Home Page
223
+ </div>
224
+ <div className="mt-2 text-sm text-gray-600">
225
+ This is your current home page
226
+ </div>
207
227
  </div>
208
228
  )}
209
- <div className="mt-4 text-lg">
210
- <div className="text-gray-600">
211
- Create a clean, descriptive URL slug that helps users and search
212
- engines understand the page content.
213
- <ul className="ml-4 mt-1">
214
- <li>
215
- <CheckIcon className="inline h-4 w-4" /> Use hyphens to separate
216
- words
217
- </li>
218
- <li>
219
- <CheckIcon className="inline h-4 w-4" /> Keep it short and
220
- descriptive
221
- </li>
222
- <li>
223
- <CheckIcon className="inline h-4 w-4" /> Use only lowercase
224
- letters, numbers, and hyphens
225
- </li>
226
- <li>
227
- <CheckIcon className="inline h-4 w-4" /> Must start and end with
228
- a letter or number
229
- </li>
230
- </ul>
231
- </div>
232
- <div className="py-4">
233
- {!isHomeSlug && (
234
- <>
235
- {charCount < 3 && (
236
- <span className="text-red-500">
237
- Slug must be at least 3 characters
238
- </span>
239
- )}
240
- {charCount >= 3 && charCount < 5 && !validationError && (
241
- <span className="text-gray-500">
242
- Consider adding more characters for better description
243
- </span>
244
- )}
245
- {warning && !validationError && (
246
- <span className="text-yellow-500">
247
- Slug is getting long - consider shortening it
248
- </span>
249
- )}
250
- {isValid && canSave && charCount >= 5 && !validationError && (
251
- <span className="text-green-500">
252
- Good URL length and format!
253
- </span>
254
- )}
255
- {isValid && !canSave && !validationError && (
256
- <span className="text-gray-500">
257
- Valid characters but needs proper formatting to save
258
- </span>
259
- )}
260
- </>
229
+
230
+ {!isHomeSlug && isValid && canSave && (
231
+ <div className="mt-4">
232
+ <div className="flex items-center space-x-3">
233
+ <Switch.Root
234
+ checked={isSetAsHomePage}
235
+ onCheckedChange={handleSetAsHomePageChange}
236
+ className="flex items-center"
237
+ >
238
+ <Switch.Control
239
+ className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
240
+ isSetAsHomePage ? 'bg-cyan-600' : 'bg-gray-200'
241
+ }`}
242
+ >
243
+ <Switch.Thumb
244
+ className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out ${
245
+ isSetAsHomePage ? 'translate-x-5' : 'translate-x-0'
246
+ }`}
247
+ />
248
+ </Switch.Control>
249
+ <Switch.HiddenInput />
250
+ </Switch.Root>
251
+ <span className="text-sm text-gray-700">Set as Home Page</span>
252
+ </div>
253
+ {isSetAsHomePage && (
254
+ <div className="mt-2 text-sm text-cyan-600">
255
+ Will be set as home page when saved
256
+ </div>
261
257
  )}
262
258
  </div>
259
+ )}
260
+
261
+ <div className="mt-4 text-sm text-gray-600">
262
+ Create a clean, descriptive URL slug that helps users and search
263
+ engines understand the page content.
263
264
  </div>
264
265
  </div>
265
266
  </div>