@drawnagency/primitives 0.1.12 → 0.1.13

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 (54) hide show
  1. package/dist/{chunk-Q7OKHD6I.js → chunk-46QI4FDZ.js} +1 -1
  2. package/dist/{chunk-PHCEJP7I.js → chunk-EAEX6DS7.js} +4 -1
  3. package/dist/{chunk-2YYC2VJY.js → chunk-P24YUT3O.js} +1 -1
  4. package/dist/components/editor/AudienceIndicator.d.ts +9 -0
  5. package/dist/components/editor/AudienceIndicator.d.ts.map +1 -0
  6. package/dist/components/editor/IndicatorPill.d.ts +18 -0
  7. package/dist/components/editor/IndicatorPill.d.ts.map +1 -0
  8. package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
  9. package/dist/components/editor/StatusIndicator.d.ts +29 -0
  10. package/dist/components/editor/StatusIndicator.d.ts.map +1 -0
  11. package/dist/components/editor/index.d.ts +3 -2
  12. package/dist/components/editor/index.d.ts.map +1 -1
  13. package/dist/components/shared/Popover.d.ts +1 -1
  14. package/dist/components/shared/Popover.d.ts.map +1 -1
  15. package/dist/components/shell/BuildStatusIndicator.d.ts +9 -0
  16. package/dist/components/shell/BuildStatusIndicator.d.ts.map +1 -0
  17. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  18. package/dist/hooks/index.d.ts +1 -0
  19. package/dist/hooks/index.d.ts.map +1 -1
  20. package/dist/hooks/useBuildStatus.d.ts +11 -0
  21. package/dist/hooks/useBuildStatus.d.ts.map +1 -0
  22. package/dist/hooks/useEditorPublish.d.ts +2 -1
  23. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  24. package/dist/index.js +3 -3
  25. package/dist/lib/index.js +2 -2
  26. package/dist/schemas/index.js +2 -2
  27. package/dist/schemas/site-config.d.ts +6 -4
  28. package/dist/schemas/site-config.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/src/components/editor/{AudiencePicker.tsx → AudienceIndicator.tsx} +25 -49
  31. package/src/components/editor/IndicatorPill.tsx +119 -0
  32. package/src/components/editor/SectionWrapper.tsx +18 -14
  33. package/src/components/editor/StatusIndicator.tsx +148 -0
  34. package/src/components/editor/index.ts +3 -2
  35. package/src/components/shared/Popover.tsx +26 -4
  36. package/src/components/shared/PopoverItem.tsx +1 -1
  37. package/src/components/shared/SplitButton.tsx +2 -2
  38. package/src/components/shell/BuildStatusIndicator.tsx +67 -0
  39. package/src/components/shell/EditorShell.tsx +23 -0
  40. package/src/hooks/index.ts +1 -0
  41. package/src/hooks/useBuildStatus.ts +139 -0
  42. package/src/hooks/useEditorPublish.ts +6 -2
  43. package/src/schemas/site-config.ts +5 -1
  44. package/dist/components/editor/AudiencePicker.d.ts +0 -9
  45. package/dist/components/editor/AudiencePicker.d.ts.map +0 -1
  46. package/dist/components/editor/StatusBadge.d.ts +0 -7
  47. package/dist/components/editor/StatusBadge.d.ts.map +0 -1
  48. package/dist/components/editor/StatusDots.d.ts +0 -25
  49. package/dist/components/editor/StatusDots.d.ts.map +0 -1
  50. package/dist/components/editor/StatusPicker.d.ts +0 -9
  51. package/dist/components/editor/StatusPicker.d.ts.map +0 -1
  52. package/src/components/editor/StatusBadge.tsx +0 -30
  53. package/src/components/editor/StatusDots.tsx +0 -131
  54. package/src/components/editor/StatusPicker.tsx +0 -86
@@ -0,0 +1,148 @@
1
+ import { Check } from "lucide-react";
2
+ import { cn } from "../../lib/cn";
3
+ import { IndicatorPill } from "./IndicatorPill";
4
+ import { PopoverItem } from "../shared/PopoverItem";
5
+
6
+ type StatusColor = "draft" | "live" | "archived" | "modified";
7
+ type Status = "draft" | "live" | "archived";
8
+
9
+ interface DotDef {
10
+ color: StatusColor;
11
+ }
12
+
13
+ interface DeriveInput {
14
+ mainStatus: string | null;
15
+ savedStatus: string;
16
+ contentDiffers: boolean;
17
+ isLocalOnly: boolean;
18
+ }
19
+
20
+ interface DeriveResult {
21
+ dots: DotDef[];
22
+ unsaved: boolean;
23
+ description: string;
24
+ }
25
+
26
+ const descriptions: Record<string, string> = {
27
+ "live": "Live — synced",
28
+ "live,modified": "Live — unpublished content edits",
29
+ "live,draft": "Live on site, changed to draft",
30
+ "live,archived": "Live on site, will be hidden on publish",
31
+ "draft": "Draft — editor only",
32
+ "draft,modified": "Draft — unpublished content edits",
33
+ "draft,live": "Draft, will become live on publish",
34
+ "archived": "Archived",
35
+ "archived,modified": "Archived — unpublished content edits",
36
+ "archived,live": "Archived, will become live on publish",
37
+ "modified": "Unpublished content edits",
38
+ };
39
+
40
+ export function deriveStatusDisplay({ mainStatus, savedStatus, contentDiffers, isLocalOnly }: DeriveInput): DeriveResult {
41
+ const unsaved = isLocalOnly;
42
+
43
+ if (!mainStatus) {
44
+ return {
45
+ dots: [{ color: savedStatus as StatusColor }],
46
+ unsaved,
47
+ description: `${savedStatus} — new section`,
48
+ };
49
+ }
50
+
51
+ if (mainStatus !== savedStatus) {
52
+ const key = `${mainStatus},${savedStatus}`;
53
+ return {
54
+ dots: [{ color: mainStatus as StatusColor }, { color: savedStatus as StatusColor }],
55
+ unsaved,
56
+ description: descriptions[key] ?? `${mainStatus} → ${savedStatus}`,
57
+ };
58
+ }
59
+
60
+ if (contentDiffers) {
61
+ const key = `${mainStatus},modified`;
62
+ return {
63
+ dots: [{ color: mainStatus as StatusColor }, { color: "modified" }],
64
+ unsaved,
65
+ description: descriptions[key] ?? `${mainStatus} — with edits`,
66
+ };
67
+ }
68
+
69
+ return {
70
+ dots: [{ color: mainStatus as StatusColor }],
71
+ unsaved: false,
72
+ description: descriptions[mainStatus] ?? mainStatus,
73
+ };
74
+ }
75
+
76
+ const dotColorClasses: Record<StatusColor, string> = {
77
+ live: "bg-green-500",
78
+ draft: "bg-gray-400",
79
+ archived: "bg-white border-gray-300",
80
+ modified: "bg-orange-400",
81
+ };
82
+
83
+
84
+ const STATUSES: Status[] = ["draft", "live", "archived"];
85
+
86
+ interface StatusIndicatorProps {
87
+ mainStatus: string | null;
88
+ savedStatus: string;
89
+ contentDiffers: boolean;
90
+ isLocalOnly: boolean;
91
+ status: Status;
92
+ dirty?: boolean;
93
+ onChange: (status: Status) => void;
94
+ }
95
+
96
+ export function StatusIndicator({
97
+ mainStatus,
98
+ savedStatus,
99
+ contentDiffers,
100
+ isLocalOnly,
101
+ status,
102
+ dirty,
103
+ onChange,
104
+ }: StatusIndicatorProps) {
105
+ const display = deriveStatusDisplay({ mainStatus, savedStatus, contentDiffers, isLocalOnly });
106
+
107
+ const pillDots = display.dots.map((d) => ({
108
+ className: dotColorClasses[d.color],
109
+ }));
110
+
111
+ const unsavedFromDirtyOrLocal = dirty || display.unsaved;
112
+ const labelText = `${status.charAt(0).toUpperCase() + status.slice(1)}${unsavedFromDirtyOrLocal ? " (Unsaved)" : ""}`;
113
+
114
+ return (
115
+ <IndicatorPill
116
+ dots={pillDots}
117
+ label={labelText}
118
+ ariaLabel={display.description}
119
+ buttonClassName="text-base-contrast-light"
120
+ clickContent={(onClose) => (
121
+ <ul role="radiogroup" aria-label="Section status" className="w-full overflow-hidden py-1">
122
+ {STATUSES.map((s) => {
123
+ const checked = s === status;
124
+ return (
125
+ <li key={s}>
126
+ <PopoverItem
127
+ role="radio"
128
+ aria-checked={checked}
129
+ onClick={() => {
130
+ if (s !== status) onChange(s);
131
+ onClose();
132
+ }}
133
+ >
134
+ <span
135
+ aria-hidden="true"
136
+ className={cn("h-3 w-3 shrink-0 rounded-full border border-base-200", dotColorClasses[s])}
137
+ />
138
+ <span className="flex-1 font-medium capitalize text-base-contrast">{s}</span>
139
+ {checked && <Check size={14} strokeWidth={3} className="text-primary" />}
140
+ </PopoverItem>
141
+ </li>
142
+ );
143
+ })}
144
+ </ul>
145
+ )}
146
+ />
147
+ );
148
+ }
@@ -3,6 +3,7 @@ export { InsertButton } from "./InsertButton";
3
3
  export { DeleteButton } from "./DeleteButton";
4
4
  export { SettingsButton } from "./SettingsButton";
5
5
  export { SettingsForm } from "./SettingsForm";
6
- export { StatusBadge } from "./StatusBadge";
6
+ export { IndicatorPill } from "./IndicatorPill";
7
+ export { StatusIndicator, deriveStatusDisplay } from "./StatusIndicator";
8
+ export { AudienceIndicator } from "./AudienceIndicator";
7
9
  export { SectionWrapper } from "./SectionWrapper";
8
- export { StatusDots, deriveStatusDots } from "./StatusDots";
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, type ReactNode, type RefObject } from "react";
1
+ import { useEffect, useLayoutEffect, useRef, useState, type ReactNode, type RefObject } from "react";
2
2
  import { useFocusTrap } from "../../hooks/useFocusTrap";
3
3
  import { cn } from "../../lib/cn";
4
4
 
@@ -7,7 +7,7 @@ interface PopoverProps {
7
7
  onClose: () => void;
8
8
  anchorRef: RefObject<HTMLElement | null>;
9
9
  children: ReactNode;
10
- align?: "start" | "end";
10
+ align?: "start" | "end" | "auto";
11
11
  className?: string;
12
12
  }
13
13
 
@@ -16,13 +16,33 @@ export function Popover({
16
16
  onClose,
17
17
  anchorRef,
18
18
  children,
19
- align = "start",
19
+ align = "auto",
20
20
  className,
21
21
  }: PopoverProps) {
22
22
  const panelRef = useRef<HTMLDivElement>(null);
23
+ const [resolvedAlign, setResolvedAlign] = useState<"start" | "end">("start");
23
24
 
24
25
  useFocusTrap(panelRef, isOpen);
25
26
 
27
+ useLayoutEffect(() => {
28
+ if (!isOpen || align !== "auto") {
29
+ if (!isOpen) setResolvedAlign("start");
30
+ return;
31
+ }
32
+ const panel = panelRef.current;
33
+ if (!panel) return;
34
+ panel.style.left = "0";
35
+ panel.style.right = "auto";
36
+ const rect = panel.getBoundingClientRect();
37
+ if (rect.right > window.innerWidth) {
38
+ setResolvedAlign("end");
39
+ } else {
40
+ setResolvedAlign("start");
41
+ }
42
+ panel.style.left = "";
43
+ panel.style.right = "";
44
+ }, [isOpen, align]);
45
+
26
46
  useEffect(() => {
27
47
  if (!isOpen) return;
28
48
  function onKeyDown(e: KeyboardEvent) {
@@ -44,6 +64,8 @@ export function Popover({
44
64
 
45
65
  if (!isOpen) return null;
46
66
 
67
+ const effectiveAlign = align === "auto" ? resolvedAlign : align;
68
+
47
69
  return (
48
70
  <div
49
71
  ref={panelRef}
@@ -51,7 +73,7 @@ export function Popover({
51
73
  aria-modal="false"
52
74
  className={cn(
53
75
  "absolute top-full z-50 mt-1 rounded-md border border-base-200 bg-base shadow-lg",
54
- align === "end" ? "right-0" : "left-0",
76
+ effectiveAlign === "end" ? "right-0" : "left-0",
55
77
  className,
56
78
  )}
57
79
  >
@@ -12,7 +12,7 @@ export const PopoverItem = forwardRef<HTMLButtonElement, PopoverItemProps>(
12
12
  ref={ref}
13
13
  type="button"
14
14
  className={cn(
15
- "flex w-full cursor-pointer items-center gap-3 px-3 py-1.5 text-left hover:bg-base-accent",
15
+ "flex w-full cursor-pointer items-center gap-3 px-3 py-1.5 text-left text-xs hover:bg-base-accent",
16
16
  className,
17
17
  )}
18
18
  {...rest}
@@ -69,7 +69,7 @@ export function SplitButton({
69
69
  >
70
70
  <ChevronDown size={14} />
71
71
  </button>
72
- <Popover isOpen={open} onClose={() => setOpen(false)} anchorRef={chevronRef} align="end" className="w-44">
72
+ <Popover isOpen={open} onClose={() => setOpen(false)} anchorRef={chevronRef} className="w-full">
73
73
  <ul role="menu" className="py-1">
74
74
  {options.map((option) => (
75
75
  <li key={option.label}>
@@ -84,7 +84,7 @@ export function SplitButton({
84
84
  }}
85
85
  className={cn(option.disabled && "opacity-50 cursor-not-allowed")}
86
86
  >
87
- <span className="text-sm font-medium text-base-contrast">{option.label}</span>
87
+ <span className="font-medium text-base-contrast">{option.label}</span>
88
88
  </PopoverItem>
89
89
  </li>
90
90
  ))}
@@ -0,0 +1,67 @@
1
+ import { cn } from "../../lib/cn";
2
+ import { X } from "lucide-react";
3
+
4
+ interface BuildStatusIndicatorProps {
5
+ state: "idle" | "building" | "ready" | "error";
6
+ deployUrl: string | null;
7
+ visible: boolean;
8
+ onDismiss: () => void;
9
+ }
10
+
11
+ const stateConfig = {
12
+ building: {
13
+ label: "Deploying...",
14
+ className: "border-orange-500 text-orange-600 hover:bg-orange-50",
15
+ dotClassName: "bg-orange-500 animate-pulse",
16
+ },
17
+ ready: {
18
+ label: "Live",
19
+ className: "border-green-600 text-green-600 hover:bg-green-50",
20
+ dotClassName: "bg-green-600",
21
+ },
22
+ error: {
23
+ label: "Deploy failed",
24
+ className: "border-red-600 text-red-600 hover:bg-red-50",
25
+ dotClassName: "bg-red-600",
26
+ },
27
+ } as const;
28
+
29
+ export function BuildStatusIndicator({
30
+ state,
31
+ deployUrl,
32
+ visible,
33
+ onDismiss,
34
+ }: BuildStatusIndicatorProps) {
35
+ if (!visible || state === "idle") return null;
36
+
37
+ const config = stateConfig[state];
38
+
39
+ return (
40
+ <span className="inline-flex items-center gap-0">
41
+ <a
42
+ href={deployUrl ?? "#"}
43
+ target="_blank"
44
+ rel="noopener noreferrer"
45
+ aria-label={config.label}
46
+ className={cn(
47
+ "inline-flex items-center gap-1.5 rounded-l px-3 py-1.5 text-xs font-medium border transition-colors",
48
+ config.className,
49
+ )}
50
+ >
51
+ <span className={cn("h-1.5 w-1.5 rounded-full", config.dotClassName)} />
52
+ {config.label}
53
+ </a>
54
+ <button
55
+ type="button"
56
+ onClick={onDismiss}
57
+ aria-label="Dismiss build status"
58
+ className={cn(
59
+ "inline-flex items-center rounded-r border border-l-0 px-1.5 py-1.5 transition-colors cursor-pointer",
60
+ config.className,
61
+ )}
62
+ >
63
+ <X size={12} />
64
+ </button>
65
+ </span>
66
+ );
67
+ }
@@ -36,6 +36,8 @@ import {
36
36
  import { useEditorPersistence } from "../../hooks/useEditorPersistence";
37
37
  import { useEditorPublish } from "../../hooks/useEditorPublish";
38
38
  import { useContentLifecycle } from "../../hooks/useContentLifecycle";
39
+ import { useBuildStatus } from "../../hooks/useBuildStatus";
40
+ import { BuildStatusIndicator } from "./BuildStatusIndicator";
39
41
  import { useMediaPipeline } from "../../hooks/useMediaPipeline";
40
42
  import { formatTimestamp } from "../../lib/timestamp";
41
43
  import { generateNavLinks } from "../../lib/nav";
@@ -123,6 +125,8 @@ export default function EditorShell({
123
125
  markSectionDirty: persistence.markSectionDirty,
124
126
  });
125
127
 
128
+ const buildStatus = useBuildStatus();
129
+
126
130
  const { isPublishing, publishFeedback, handleSave, handlePublish, handleSaveAndPublish } = useEditorPublish({
127
131
  flushNow: persistence.flushNow,
128
132
  cancelPendingFlush: persistence.cancelPendingFlush,
@@ -159,6 +163,7 @@ export default function EditorShell({
159
163
  setSavedSha(newSavedSha);
160
164
  setMainSha((prev) => newMainSha ?? prev);
161
165
  },
166
+ onPublishComplete: buildStatus.startTracking,
162
167
  });
163
168
 
164
169
  const { buttonState } = useContentLifecycle({
@@ -581,6 +586,10 @@ export default function EditorShell({
581
586
  setShowMediaLibrary(true);
582
587
  }}
583
588
  processingItems={mediaPipeline.processingItems}
589
+ buildState={buildStatus.state}
590
+ buildDeployUrl={buildStatus.deployUrl}
591
+ buildVisible={buildStatus.visible}
592
+ onBuildDismiss={buildStatus.dismiss}
584
593
  />
585
594
 
586
595
  <EditorContent
@@ -879,6 +888,10 @@ function EditorToolbar({
879
888
  onSettingsClick,
880
889
  onMediaClick,
881
890
  processingItems,
891
+ buildState,
892
+ buildDeployUrl,
893
+ buildVisible,
894
+ onBuildDismiss,
882
895
  }: {
883
896
  buttonState: "synced" | "publish" | "saveAndPublish";
884
897
  localChangesExist: boolean;
@@ -891,6 +904,10 @@ function EditorToolbar({
891
904
  onSettingsClick: () => void;
892
905
  onMediaClick: () => void;
893
906
  processingItems: QueueItem[];
907
+ buildState: "idle" | "building" | "ready" | "error";
908
+ buildDeployUrl: string | null;
909
+ buildVisible: boolean;
910
+ onBuildDismiss: () => void;
894
911
  }) {
895
912
  const { isEditMode, viewBranch, setViewBranch, toggleEditMode } = useEditorContext();
896
913
 
@@ -945,6 +962,12 @@ function EditorToolbar({
945
962
  Discard Changes
946
963
  </Button>
947
964
  )}
965
+ <BuildStatusIndicator
966
+ state={buildState}
967
+ deployUrl={buildDeployUrl}
968
+ visible={buildVisible}
969
+ onDismiss={onBuildDismiss}
970
+ />
948
971
  </div>
949
972
  <div className="flex items-center gap-2">
950
973
  <ProcessingIndicator items={processingItems} />
@@ -1,4 +1,5 @@
1
1
  export { useActiveHeadings } from "./useActiveHeadings";
2
+ export { useBuildStatus } from "./useBuildStatus";
2
3
  export { useContentLifecycle } from "./useContentLifecycle";
3
4
  export { useEditorPersistence } from "./useEditorPersistence";
4
5
  export { useEditorPublish } from "./useEditorPublish";
@@ -0,0 +1,139 @@
1
+ import { useState, useCallback, useEffect, useRef } from "react";
2
+
3
+ type BuildState = "idle" | "building" | "ready" | "error";
4
+
5
+ interface BuildStatusResponse {
6
+ state: "building" | "ready" | "error";
7
+ deployUrl: string;
8
+ commitSha: string | null;
9
+ updatedAt: string;
10
+ }
11
+
12
+ interface BuildStatusResult {
13
+ state: BuildState;
14
+ deployUrl: string | null;
15
+ visible: boolean;
16
+ dismiss: () => void;
17
+ startTracking: () => void;
18
+ }
19
+
20
+ const POLL_INTERVAL = 5000;
21
+ const AUTO_DISMISS_DELAY = 10000;
22
+
23
+ export function useBuildStatus(): BuildStatusResult {
24
+ const [state, setState] = useState<BuildState>("idle");
25
+ const [deployUrl, setDeployUrl] = useState<string | null>(null);
26
+ const [dismissed, setDismissed] = useState(false);
27
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
28
+ const dismissRef = useRef<ReturnType<typeof setTimeout> | null>(null);
29
+ const isPolling = useRef(false);
30
+
31
+ const stopPolling = useCallback(() => {
32
+ if (pollRef.current) {
33
+ clearInterval(pollRef.current);
34
+ pollRef.current = null;
35
+ }
36
+ isPolling.current = false;
37
+ }, []);
38
+
39
+ const fetchStatus = useCallback(async (): Promise<BuildStatusResponse | null> => {
40
+ try {
41
+ const res = await fetch("/api/build-status");
42
+ if (!res.ok) return null;
43
+ const data = await res.json();
44
+ const validStates = ["building", "ready", "error"];
45
+ if (!data || !validStates.includes(data.state)) return null;
46
+ return data as BuildStatusResponse;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }, []);
51
+
52
+ const handleStatusUpdate = useCallback(
53
+ (data: BuildStatusResponse | null, isInitialLoad: boolean) => {
54
+ if (!data) {
55
+ if (isInitialLoad) {
56
+ setState("idle");
57
+ }
58
+ return;
59
+ }
60
+
61
+ if (isInitialLoad && data.state === "ready") {
62
+ setState("idle");
63
+ return;
64
+ }
65
+
66
+ setState(data.state);
67
+ setDeployUrl(data.deployUrl);
68
+ setDismissed(false);
69
+
70
+ if (data.state === "ready" || data.state === "error") {
71
+ stopPolling();
72
+
73
+ if (data.state === "ready") {
74
+ dismissRef.current = setTimeout(() => {
75
+ setDismissed(true);
76
+ }, AUTO_DISMISS_DELAY);
77
+ }
78
+ }
79
+ },
80
+ [stopPolling],
81
+ );
82
+
83
+ const startPolling = useCallback(() => {
84
+ if (isPolling.current) return;
85
+ isPolling.current = true;
86
+
87
+ pollRef.current = setInterval(async () => {
88
+ const data = await fetchStatus();
89
+ handleStatusUpdate(data, false);
90
+ }, POLL_INTERVAL);
91
+ }, [fetchStatus, handleStatusUpdate]);
92
+
93
+ // Initial fetch on mount
94
+ useEffect(() => {
95
+ let cancelled = false;
96
+
97
+ async function check() {
98
+ const data = await fetchStatus();
99
+ if (cancelled) return;
100
+ handleStatusUpdate(data, true);
101
+
102
+ if (data && data.state === "building") {
103
+ startPolling();
104
+ }
105
+ }
106
+
107
+ check();
108
+
109
+ return () => {
110
+ cancelled = true;
111
+ stopPolling();
112
+ if (dismissRef.current) clearTimeout(dismissRef.current);
113
+ };
114
+ }, [fetchStatus, handleStatusUpdate, startPolling, stopPolling]);
115
+
116
+ const dismiss = useCallback(() => {
117
+ setDismissed(true);
118
+ stopPolling();
119
+ if (dismissRef.current) {
120
+ clearTimeout(dismissRef.current);
121
+ dismissRef.current = null;
122
+ }
123
+ }, [stopPolling]);
124
+
125
+ const startTracking = useCallback(() => {
126
+ setState("building");
127
+ setDeployUrl(null);
128
+ setDismissed(false);
129
+ if (dismissRef.current) {
130
+ clearTimeout(dismissRef.current);
131
+ dismissRef.current = null;
132
+ }
133
+ startPolling();
134
+ }, [startPolling]);
135
+
136
+ const visible = state !== "idle" && !dismissed;
137
+
138
+ return { state, deployUrl, visible, dismiss, startTracking };
139
+ }
@@ -31,6 +31,7 @@ interface PublishDeps {
31
31
  pendingMediaDeletions: string[];
32
32
  onMediaPublished: (publishedItems: MediaItem[], publishedDeletions: string[]) => void;
33
33
  onShasUpdated: (savedSha: string | null, mainSha: string | null) => void;
34
+ onPublishComplete?: () => void;
34
35
  }
35
36
 
36
37
  interface GatheredMedia {
@@ -55,6 +56,7 @@ export function useEditorPublish({
55
56
  pendingMediaDeletions,
56
57
  onMediaPublished,
57
58
  onShasUpdated,
59
+ onPublishComplete,
58
60
  }: PublishDeps) {
59
61
  const [isPublishing, setIsPublishing] = useState(false);
60
62
  const [publishFeedback, setPublishFeedback] = useState<string | null>(null);
@@ -237,13 +239,14 @@ export function useEditorPublish({
237
239
 
238
240
  onShasUpdated(null, sha);
239
241
  showFeedback("Published", 3000);
242
+ onPublishComplete?.();
240
243
  } catch (error) {
241
244
  console.error("Publish failed:", error);
242
245
  showFeedback("Publish failed", 5000);
243
246
  } finally {
244
247
  setIsPublishing(false);
245
248
  }
246
- }, [onShasUpdated, showFeedback]);
249
+ }, [onShasUpdated, showFeedback, onPublishComplete]);
247
250
 
248
251
  const handleSaveAndPublish = useCallback(async () => {
249
252
  if (!siteConfig) return;
@@ -339,13 +342,14 @@ export function useEditorPublish({
339
342
 
340
343
  onShasUpdated(null, sha);
341
344
  showFeedback("Published", 3000);
345
+ onPublishComplete?.();
342
346
  } catch (error) {
343
347
  console.error("Publish failed:", error);
344
348
  showFeedback("Publish failed", 5000);
345
349
  } finally {
346
350
  setIsPublishing(false);
347
351
  }
348
- }, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, deletedSectionIds, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, showFeedback]);
352
+ }, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, deletedSectionIds, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, showFeedback, onPublishComplete]);
349
353
 
350
354
  return { isPublishing, publishFeedback, handleSave, handlePublish, handleSaveAndPublish };
351
355
  }
@@ -2,9 +2,13 @@ import { z } from "zod";
2
2
  import { MediaConfigSchema } from "./media";
3
3
  import { HexColorSchema } from "./shared";
4
4
 
5
+ const StatusSchema = z.enum(["draft", "live", "archived", "published"]).transform(
6
+ (val) => val === "published" ? "live" as const : val,
7
+ );
8
+
5
9
  export const SectionMetaSchema = z.object({
6
10
  type: z.string(),
7
- status: z.enum(["draft", "live", "archived"]),
11
+ status: StatusSchema,
8
12
  access: z.array(z.string()),
9
13
  });
10
14
 
@@ -1,9 +0,0 @@
1
- import type { Audience } from "../../auth/types";
2
- interface Props {
3
- access: string[];
4
- audiences: Audience[];
5
- onChange: (access: string[]) => void;
6
- }
7
- export declare function AudiencePicker({ access, audiences, onChange }: Props): import("react/jsx-runtime").JSX.Element;
8
- export {};
9
- //# sourceMappingURL=AudiencePicker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AudiencePicker.d.ts","sourceRoot":"","sources":["../../../src/components/editor/AudiencePicker.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,UAAU,KAAK;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACtC;AAED,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,KAAK,2CAyFpE"}
@@ -1,7 +0,0 @@
1
- interface StatusBadgeProps {
2
- status: string;
3
- dirty?: boolean;
4
- }
5
- export declare function StatusBadge({ status, dirty }: StatusBadgeProps): import("react/jsx-runtime").JSX.Element;
6
- export {};
7
- //# sourceMappingURL=StatusBadge.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"StatusBadge.d.ts","sourceRoot":"","sources":["../../../src/components/editor/StatusBadge.tsx"],"names":[],"mappings":"AAIA,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAQD,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,gBAAgB,2CAc9D"}
@@ -1,25 +0,0 @@
1
- type StatusColor = "draft" | "live" | "archived" | "modified";
2
- interface DotDef {
3
- color: StatusColor;
4
- }
5
- interface DeriveInput {
6
- mainStatus: string | null;
7
- savedStatus: string;
8
- contentDiffers: boolean;
9
- isLocalOnly: boolean;
10
- }
11
- interface DeriveResult {
12
- dots: DotDef[];
13
- label: "unsaved" | null;
14
- description: string;
15
- }
16
- export declare function deriveStatusDots({ mainStatus, savedStatus, contentDiffers, isLocalOnly }: DeriveInput): DeriveResult;
17
- interface StatusDotsProps {
18
- mainStatus: string | null;
19
- savedStatus: string;
20
- contentDiffers: boolean;
21
- isLocalOnly: boolean;
22
- }
23
- export declare function StatusDots(props: StatusDotsProps): import("react/jsx-runtime").JSX.Element;
24
- export {};
25
- //# sourceMappingURL=StatusDots.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"StatusDots.d.ts","sourceRoot":"","sources":["../../../src/components/editor/StatusDots.tsx"],"names":[],"mappings":"AAIA,KAAK,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;AAE9D,UAAU,MAAM;IACd,KAAK,EAAE,WAAW,CAAC;CACpB;AAED,UAAU,WAAW;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AAqBD,wBAAgB,gBAAgB,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,EAAE,WAAW,GAAG,YAAY,CA8CpH;AAED,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,2CAiChD"}
@@ -1,9 +0,0 @@
1
- type Status = "draft" | "live" | "archived";
2
- interface Props {
3
- status: Status;
4
- dirty?: boolean;
5
- onChange: (status: Status) => void;
6
- }
7
- export declare function StatusPicker({ status, dirty, onChange }: Props): import("react/jsx-runtime").JSX.Element;
8
- export {};
9
- //# sourceMappingURL=StatusPicker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"StatusPicker.d.ts","sourceRoot":"","sources":["../../../src/components/editor/StatusPicker.tsx"],"names":[],"mappings":"AAMA,KAAK,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;AAE5C,UAAU,KAAK;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAgBD,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,KAAK,2CAyD9D"}