@drawnagency/primitives 0.1.89 → 0.1.91
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/dist/{chunk-75P6HWPN.js → chunk-5BM2Y3NJ.js} +5 -3
- package/dist/{chunk-VK5KNH73.js → chunk-AGWE2LJG.js} +2 -2
- package/dist/{chunk-2QXJ2KGB.js → chunk-KBQNJZUX.js} +1 -1
- package/dist/{chunk-WCBYXNAG.js → chunk-KMQZYORS.js} +1 -0
- package/dist/{chunk-Y24T75HT.js → chunk-MK7YB2A4.js} +1 -1
- package/dist/{chunk-QFV5TSPO.js → chunk-SIYQ37QY.js} +12 -4
- package/dist/{chunk-J75XGXYD.js → chunk-XKRQMTE4.js} +9 -0
- package/dist/{chunk-L2MAJKKC.js → chunk-XUIJHYKI.js} +2 -2
- package/dist/{chunk-MVX4ULYE.js → chunk-ZLG7S54V.js} +241 -82
- package/dist/components/chat/ChatPanel.d.ts +14 -6
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/chat/ChatWidget.d.ts.map +1 -1
- package/dist/components/editor/AudienceIndicator.d.ts +6 -0
- package/dist/components/editor/AudienceIndicator.d.ts.map +1 -1
- package/dist/components/editor/ChildBlockWrapper.d.ts.map +1 -1
- package/dist/components/editor/ColSpanControl.d.ts.map +1 -1
- package/dist/components/editor/DragHandle.d.ts +3 -1
- package/dist/components/editor/DragHandle.d.ts.map +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/editor/StatusIndicator.d.ts +7 -0
- package/dist/components/editor/StatusIndicator.d.ts.map +1 -1
- package/dist/components/editor/index.js +98 -76
- package/dist/components/primitives/index.js +3 -3
- package/dist/components/sections/Container/Container.d.ts +3 -2
- package/dist/components/sections/Container/Container.d.ts.map +1 -1
- package/dist/components/sections/Container/ContainerSettingsForm.d.ts +4 -2
- package/dist/components/sections/Container/ContainerSettingsForm.d.ts.map +1 -1
- package/dist/components/sections/Container/index.d.ts +8 -2
- package/dist/components/sections/Container/index.d.ts.map +1 -1
- package/dist/components/sections/SectionLayout.d.ts +3 -1
- package/dist/components/sections/SectionLayout.d.ts.map +1 -1
- package/dist/components/sections/ViewRenderer.d.ts +3 -1
- package/dist/components/sections/ViewRenderer.d.ts.map +1 -1
- package/dist/components/sections/all-sections.d.ts +8 -2
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +3 -3
- package/dist/components/sections/core-sections.d.ts +8 -2
- package/dist/components/sections/core-sections.d.ts.map +1 -1
- package/dist/components/sections/register-schemas.js +6 -6
- package/dist/components/sections/register.js +6 -6
- package/dist/components/shared/Fab.d.ts +37 -0
- package/dist/components/shared/Fab.d.ts.map +1 -0
- package/dist/components/shared/Tooltip.d.ts +8 -1
- package/dist/components/shared/Tooltip.d.ts.map +1 -1
- package/dist/components/shell/AudiencePreviewMenu.d.ts +7 -0
- package/dist/components/shell/AudiencePreviewMenu.d.ts.map +1 -0
- package/dist/components/shell/AuthoringDefaultsMenu.d.ts +10 -0
- package/dist/components/shell/AuthoringDefaultsMenu.d.ts.map +1 -0
- package/dist/components/shell/BugReportFAB.d.ts.map +1 -1
- package/dist/components/shell/EditorContext.d.ts +3 -0
- package/dist/components/shell/EditorContext.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsChatbot.d.ts.map +1 -1
- package/dist/hooks/useBuildStatus.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/lib/authoring-defaults.d.ts +17 -0
- package/dist/lib/authoring-defaults.d.ts.map +1 -0
- package/dist/lib/container-grid.d.ts +6 -0
- package/dist/lib/container-grid.d.ts.map +1 -1
- package/dist/lib/index.js +3 -3
- package/dist/lib/migrate-sections-transform.js +1 -1
- package/dist/media/index.js +1 -1
- package/dist/schemas/index.js +4 -2
- package/dist/schemas/site-config.d.ts +6 -0
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/ChatPanel.tsx +51 -19
- package/src/components/chat/ChatWidget.tsx +113 -51
- package/src/components/editor/AudienceIndicator.tsx +50 -37
- package/src/components/editor/ChildBlockWrapper.tsx +8 -7
- package/src/components/editor/ColSpanControl.tsx +14 -11
- package/src/components/editor/DragHandle.tsx +5 -2
- package/src/components/editor/PagesModal.tsx +10 -10
- package/src/components/editor/SectionWrapper.tsx +32 -21
- package/src/components/editor/StatusIndicator.tsx +37 -24
- package/src/components/sections/Container/Container.tsx +185 -43
- package/src/components/sections/Container/ContainerSettingsForm.tsx +64 -24
- package/src/components/sections/Container/index.tsx +45 -20
- package/src/components/sections/SectionLayout.tsx +13 -3
- package/src/components/sections/ViewRenderer.tsx +11 -2
- package/src/components/shared/Fab.tsx +57 -0
- package/src/components/shared/Tooltip.tsx +18 -2
- package/src/components/shell/AudiencePreviewMenu.tsx +73 -0
- package/src/components/shell/AuthoringDefaultsMenu.tsx +54 -0
- package/src/components/shell/BugReportFAB.tsx +3 -7
- package/src/components/shell/EditorContext.tsx +5 -1
- package/src/components/shell/EditorShell.tsx +80 -25
- package/src/components/shell/SiteSettingsChatbot.tsx +2 -1
- package/src/hooks/useBuildStatus.ts +35 -4
- package/src/lib/authoring-defaults.ts +42 -0
- package/src/lib/container-grid.ts +14 -0
- package/src/lib/migrate-sections-transform.ts +5 -3
- package/src/schemas/site-config.ts +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
/** Floating action button — the round primary buttons floating over the page.
|
|
5
|
+
*
|
|
6
|
+
* The `column` is both the placement and the behavior hook: base.css hides
|
|
7
|
+
* every member of both columns (shrink + fade) while the chat is open, keyed
|
|
8
|
+
* on the column class, so a new FAB participates in the chat open/close
|
|
9
|
+
* choreography just by rendering through this component. Positions are
|
|
10
|
+
* static on purpose — FABs are only visible while the chat is closed
|
|
11
|
+
* (--chat-space: 0), so they never need to track the docked sidebar.
|
|
12
|
+
*
|
|
13
|
+
* - "viewer": viewport corner stack (z-40, h-12). Rendered on SSR viewer
|
|
14
|
+
* pages too — keep this component hook-free so it stays server-renderable
|
|
15
|
+
* without hydration.
|
|
16
|
+
* - "editor": the /edit FAB column (z-50, h-10), content-edge aligned at lg+
|
|
17
|
+
* via fab-container-right.
|
|
18
|
+
*
|
|
19
|
+
* Renders an <a> when `href` is set, else a <button type="button">. The
|
|
20
|
+
* caller supplies the vertical slot (bottom-*) via className and the icon as
|
|
21
|
+
* children.
|
|
22
|
+
*/
|
|
23
|
+
interface FabCommon {
|
|
24
|
+
/** Accessible name — FABs are icon-only. */
|
|
25
|
+
label: string;
|
|
26
|
+
column: "viewer" | "editor";
|
|
27
|
+
/** Vertical slot (bottom-*) plus any per-FAB extras. */
|
|
28
|
+
className?: string;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type FabProps =
|
|
33
|
+
| ({ href: string } & FabCommon & Omit<ComponentPropsWithoutRef<"a">, "children" | "className" | "aria-label">)
|
|
34
|
+
| ({ href?: undefined; ref?: Ref<HTMLButtonElement> } & FabCommon &
|
|
35
|
+
Omit<ComponentPropsWithoutRef<"button">, "children" | "className" | "aria-label" | "type">);
|
|
36
|
+
|
|
37
|
+
export default function Fab({ label, column, className, children, ...rest }: FabProps) {
|
|
38
|
+
const classes = cn(
|
|
39
|
+
"cursor-pointer fixed flex items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity",
|
|
40
|
+
column === "viewer"
|
|
41
|
+
? "z-40 h-12 w-12 right-4 viewer-fab"
|
|
42
|
+
: "z-50 h-10 w-10 right-4 lg:right-auto fab-container-right",
|
|
43
|
+
className,
|
|
44
|
+
);
|
|
45
|
+
if (rest.href !== undefined) {
|
|
46
|
+
return (
|
|
47
|
+
<a aria-label={label} className={classes} {...(rest as ComponentPropsWithoutRef<"a">)}>
|
|
48
|
+
{children}
|
|
49
|
+
</a>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
<button type="button" aria-label={label} className={classes} {...(rest as ComponentPropsWithoutRef<"button"> & { ref?: Ref<HTMLButtonElement> })}>
|
|
54
|
+
{children}
|
|
55
|
+
</button>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -5,14 +5,30 @@ interface TooltipProps {
|
|
|
5
5
|
content: ReactNode;
|
|
6
6
|
children: ReactNode;
|
|
7
7
|
className?: string;
|
|
8
|
+
/** Where the bubble sits relative to the trigger:
|
|
9
|
+
* - "bottom" (default): below, centered.
|
|
10
|
+
* - "right": to the right (used by the container child rail, a narrow left gutter).
|
|
11
|
+
* - "bottom-clamp": below, centered on wide viewports but right-aligned under `xl`
|
|
12
|
+
* (where the top toolbar hits the viewport width) so a far-right button's bubble
|
|
13
|
+
* can't overflow off the right edge of the screen. */
|
|
14
|
+
side?: "bottom" | "right" | "bottom-clamp";
|
|
8
15
|
}
|
|
9
16
|
|
|
10
|
-
|
|
17
|
+
const sideClasses: Record<NonNullable<TooltipProps["side"]>, string> = {
|
|
18
|
+
bottom: "left-1/2 top-full mt-1.5 -translate-x-1/2",
|
|
19
|
+
right: "left-full top-1/2 ml-1.5 -translate-y-1/2",
|
|
20
|
+
"bottom-clamp": "top-full mt-1.5 left-1/2 -translate-x-1/2 max-xl:left-auto max-xl:right-0 max-xl:translate-x-0",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function Tooltip({ content, children, className, side = "bottom" }: TooltipProps) {
|
|
11
24
|
return (
|
|
12
25
|
<div className={cn("group/tooltip relative inline-flex", className)}>
|
|
13
26
|
{children}
|
|
14
27
|
<span
|
|
15
|
-
className=
|
|
28
|
+
className={cn(
|
|
29
|
+
"pointer-events-none absolute z-50 opacity-0 transition-opacity delay-300 group-hover/tooltip:opacity-100",
|
|
30
|
+
sideClasses[side],
|
|
31
|
+
)}
|
|
16
32
|
role="tooltip"
|
|
17
33
|
>
|
|
18
34
|
<span className="block whitespace-nowrap rounded bg-tooltip px-2 py-1 text-center text-xs leading-relaxed text-tooltip-muted">
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { Check, UsersIcon } from "lucide-react";
|
|
3
|
+
import { IconButton } from "../shared/IconButton";
|
|
4
|
+
import { Popover } from "../shared/Popover";
|
|
5
|
+
import { PopoverItem } from "../shared/PopoverItem";
|
|
6
|
+
import { useEditorContext } from "./EditorContext";
|
|
7
|
+
import type { Audience } from "../../auth/types";
|
|
8
|
+
|
|
9
|
+
interface AudiencePreviewMenuProps {
|
|
10
|
+
audiences: Audience[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AudiencePreviewMenu({ audiences }: AudiencePreviewMenuProps) {
|
|
14
|
+
const { previewAudience, setPreviewAudience } = useEditorContext();
|
|
15
|
+
const [open, setOpen] = useState(false);
|
|
16
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
// A previewed audience can be deleted mid-session; the badge falls back grey.
|
|
18
|
+
const active = previewAudience !== null ? audiences.find((a) => a.name === previewAudience) : undefined;
|
|
19
|
+
|
|
20
|
+
function select(name: string | null) {
|
|
21
|
+
setPreviewAudience(name);
|
|
22
|
+
setOpen(false);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div ref={wrapperRef} className="relative">
|
|
27
|
+
<IconButton
|
|
28
|
+
icon={<UsersIcon size={16} />}
|
|
29
|
+
label="Preview as audience"
|
|
30
|
+
size="md"
|
|
31
|
+
onClick={() => setOpen((v) => !v)}
|
|
32
|
+
aria-pressed={previewAudience !== null}
|
|
33
|
+
aria-haspopup="true"
|
|
34
|
+
aria-expanded={open}
|
|
35
|
+
className="border border-base-200 bg-base-accent"
|
|
36
|
+
/>
|
|
37
|
+
{previewAudience !== null && (
|
|
38
|
+
<span
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
data-testid="audience-preview-badge"
|
|
41
|
+
className="pointer-events-none absolute -top-0.5 -right-0.5 h-2.5 w-2.5 rounded-full border border-base"
|
|
42
|
+
style={{ backgroundColor: active?.color ?? "#9ca3af" }}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
<Popover isOpen={open} onClose={() => setOpen(false)} anchorRef={wrapperRef} className="w-48">
|
|
46
|
+
<ul role="radiogroup" aria-label="Preview as audience" className="w-full overflow-hidden py-1">
|
|
47
|
+
<li>
|
|
48
|
+
<PopoverItem role="radio" aria-checked={previewAudience === null} onClick={() => select(null)}>
|
|
49
|
+
<span className="flex-1 font-medium text-base-contrast">All audiences</span>
|
|
50
|
+
{previewAudience === null && <Check size={14} strokeWidth={3} className="text-primary" />}
|
|
51
|
+
</PopoverItem>
|
|
52
|
+
</li>
|
|
53
|
+
{audiences.map((a) => {
|
|
54
|
+
const checked = previewAudience === a.name;
|
|
55
|
+
return (
|
|
56
|
+
<li key={a.name}>
|
|
57
|
+
<PopoverItem role="radio" aria-checked={checked} onClick={() => select(a.name)}>
|
|
58
|
+
<span
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
className="h-3 w-3 shrink-0 rounded-full border border-base-200"
|
|
61
|
+
style={{ backgroundColor: a.color ?? "#9ca3af" }}
|
|
62
|
+
/>
|
|
63
|
+
<span className="flex-1 font-medium text-base-contrast">{a.displayName}</span>
|
|
64
|
+
{checked && <Check size={14} strokeWidth={3} className="text-primary" />}
|
|
65
|
+
</PopoverItem>
|
|
66
|
+
</li>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</ul>
|
|
70
|
+
</Popover>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { SlidersHorizontalIcon } from "lucide-react";
|
|
3
|
+
import { IconButton } from "../shared/IconButton";
|
|
4
|
+
import { Popover } from "../shared/Popover";
|
|
5
|
+
import { StatusRadioList } from "../editor/StatusIndicator";
|
|
6
|
+
import { AudienceChecklist } from "../editor/AudienceIndicator";
|
|
7
|
+
import type { AuthoringDefaults } from "../../lib/authoring-defaults";
|
|
8
|
+
import type { Audience } from "../../auth/types";
|
|
9
|
+
|
|
10
|
+
interface AuthoringDefaultsMenuProps {
|
|
11
|
+
defaults: AuthoringDefaults;
|
|
12
|
+
audiences: Audience[];
|
|
13
|
+
onChange: (next: AuthoringDefaults) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AuthoringDefaultsMenu({ defaults, audiences, onChange }: AuthoringDefaultsMenuProps) {
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
|
|
20
|
+
function toggleAudience(name: string) {
|
|
21
|
+
const access = defaults.access.includes(name)
|
|
22
|
+
? defaults.access.filter((n) => n !== name)
|
|
23
|
+
: [...defaults.access, name];
|
|
24
|
+
onChange({ ...defaults, access });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div ref={wrapperRef} className="relative">
|
|
29
|
+
<IconButton
|
|
30
|
+
icon={<SlidersHorizontalIcon size={16} />}
|
|
31
|
+
label="Authoring defaults"
|
|
32
|
+
size="md"
|
|
33
|
+
onClick={() => setOpen((v) => !v)}
|
|
34
|
+
aria-haspopup="true"
|
|
35
|
+
aria-expanded={open}
|
|
36
|
+
className="border border-base-200 bg-base-accent"
|
|
37
|
+
/>
|
|
38
|
+
<Popover isOpen={open} onClose={() => setOpen(false)} anchorRef={wrapperRef} className="w-56">
|
|
39
|
+
<div className="px-3 pt-2 pb-1 text-xs font-semibold text-base-contrast-light">
|
|
40
|
+
New sections start as
|
|
41
|
+
</div>
|
|
42
|
+
<StatusRadioList
|
|
43
|
+
status={defaults.status}
|
|
44
|
+
onChange={(status) => onChange({ ...defaults, status })}
|
|
45
|
+
ariaLabel="Default status for new sections"
|
|
46
|
+
/>
|
|
47
|
+
<div className="border-t border-base-200 px-3 pt-2 pb-1 text-xs font-semibold text-base-contrast-light">
|
|
48
|
+
Default audiences
|
|
49
|
+
</div>
|
|
50
|
+
<AudienceChecklist access={defaults.access} audiences={audiences} onToggle={toggleAudience} />
|
|
51
|
+
</Popover>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useState, useCallback, useRef, useEffect } from "react";
|
|
|
2
2
|
import { EditorModal } from "./EditorModal";
|
|
3
3
|
import { useEditorContext } from "./EditorContext";
|
|
4
4
|
import { cn } from "../../lib/cn";
|
|
5
|
+
import Fab from "../shared/Fab";
|
|
5
6
|
|
|
6
7
|
type Category = "Visual" | "Unexpected Behavior" | "Media / Media Library" | "Save / Publish" | "Feature Request" | "Other";
|
|
7
8
|
|
|
@@ -186,17 +187,12 @@ export function BugReportFAB() {
|
|
|
186
187
|
<>
|
|
187
188
|
{/* FAB button */}
|
|
188
189
|
{visible && (
|
|
189
|
-
<
|
|
190
|
-
type="button"
|
|
191
|
-
onClick={handleOpen}
|
|
192
|
-
aria-label="Report a bug"
|
|
193
|
-
className="cursor-pointer fixed bottom-28 right-4 lg:right-auto z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity fab-container-right"
|
|
194
|
-
>
|
|
190
|
+
<Fab label="Report a bug" column="editor" className="bottom-28" onClick={handleOpen}>
|
|
195
191
|
{/* Bug / beetle SVG icon */}
|
|
196
192
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
197
193
|
<path d="M19 8h-1.26A6.003 6.003 0 0012 4a6.003 6.003 0 00-5.74 4H5a1 1 0 000 2h1.05A6.04 6.04 0 006 12H5a1 1 0 000 2h1c0 .34.03.67.08 1H5a1 1 0 000 2h1.26a6 6 0 0011.48 0H19a1 1 0 000-2h-1.08c.05-.33.08-.66.08-1h1a1 1 0 000-2h-1a6.04 6.04 0 00-.05-2H19a1 1 0 000-2zM12 6c1.7 0 3.16.88 4 2.19V8a1 1 0 10-2 0v.04A3.97 3.97 0 0012 8a3.97 3.97 0 00-2 .04V8a1 1 0 10-2 0v.19A4.69 4.69 0 018 6a4 4 0 014 0zm0 14a4 4 0 01-4-4v-4a4 4 0 018 0v4a4 4 0 01-4 4z"/>
|
|
198
194
|
</svg>
|
|
199
|
-
</
|
|
195
|
+
</Fab>
|
|
200
196
|
)}
|
|
201
197
|
|
|
202
198
|
{/* Modal */}
|
|
@@ -17,11 +17,14 @@ interface EditorContextValue {
|
|
|
17
17
|
viewBranch: "saved" | "live";
|
|
18
18
|
historyState: HistoryState;
|
|
19
19
|
historyPageId: string | null;
|
|
20
|
+
/** Audience-preview mode: dim sections this audience can't access. null = off. */
|
|
21
|
+
previewAudience: string | null;
|
|
20
22
|
toggleEditMode: () => void;
|
|
21
23
|
toggleShowAllChrome: () => void;
|
|
22
24
|
setViewBranch: (branch: "saved" | "live") => void;
|
|
23
25
|
setHistoryState: (state: HistoryState) => void;
|
|
24
26
|
setHistoryPageId: (pageId: string | null) => void;
|
|
27
|
+
setPreviewAudience: (audience: string | null) => void;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const EditorContext = createContext<EditorContextValue | null>(null);
|
|
@@ -36,6 +39,7 @@ export function EditorProvider({ children }: EditorProviderProps) {
|
|
|
36
39
|
const [viewBranch, setViewBranchState] = useState<"saved" | "live">("saved");
|
|
37
40
|
const [historyState, setHistoryStateRaw] = useState<HistoryState>(null);
|
|
38
41
|
const [historyPageId, setHistoryPageId] = useState<string | null>(null);
|
|
42
|
+
const [previewAudience, setPreviewAudience] = useState<string | null>(null);
|
|
39
43
|
const setHistoryState = useCallback((state: HistoryState) => {
|
|
40
44
|
// null = "auto": resolved to the current page (or home) at render time
|
|
41
45
|
setHistoryPageId(null);
|
|
@@ -54,7 +58,7 @@ export function EditorProvider({ children }: EditorProviderProps) {
|
|
|
54
58
|
const setViewBranch = useCallback((b: "saved" | "live") => setViewBranchState(b), []);
|
|
55
59
|
|
|
56
60
|
return (
|
|
57
|
-
<EditorContext.Provider value={{ isEditMode, showAllChrome, viewBranch, historyState, historyPageId, toggleEditMode, toggleShowAllChrome, setViewBranch, setHistoryState, setHistoryPageId }}>
|
|
61
|
+
<EditorContext.Provider value={{ isEditMode, showAllChrome, viewBranch, historyState, historyPageId, previewAudience, toggleEditMode, toggleShowAllChrome, setViewBranch, setHistoryState, setHistoryPageId, setPreviewAudience }}>
|
|
58
62
|
{children}
|
|
59
63
|
</EditorContext.Provider>
|
|
60
64
|
);
|
|
@@ -90,7 +90,7 @@ import { generateSiteNav } from "../../lib/nav";
|
|
|
90
90
|
import { siteNavChangeEvent, pageSelectEvent, darkModeEvent, siteNameChangeEvent, historySelectEvent } from "../../lib/events";
|
|
91
91
|
import {
|
|
92
92
|
homePage, pageById, pageBySlug, pageDisplayTitle, addSectionToPage, removeSectionFromPages,
|
|
93
|
-
reorderSectionInPage,
|
|
93
|
+
reorderSectionInPage, audiencePasses,
|
|
94
94
|
setHomePage, setPageArchived, setPageFields, setPageAudience, setPageDocument, deletePage, addPage, reorderPages,
|
|
95
95
|
moveSection as moveSectionReducer,
|
|
96
96
|
} from "../../lib/pages";
|
|
@@ -101,6 +101,8 @@ import { UploadRejectionAlert, formatRejections } from "../shared/UploadRejectio
|
|
|
101
101
|
import { Button } from "../shared/Button";
|
|
102
102
|
import { SplitButton } from "../shared/SplitButton";
|
|
103
103
|
import { IconButton } from "../shared/IconButton";
|
|
104
|
+
import Fab from "../shared/Fab";
|
|
105
|
+
import { Tooltip } from "../shared/Tooltip";
|
|
104
106
|
import { SegmentedControl } from "../shared/SegmentedControl";
|
|
105
107
|
import { computeDisplaySections } from "./view-branch";
|
|
106
108
|
import { SettingsIcon } from "../shared/icons";
|
|
@@ -110,6 +112,9 @@ import { HistoryToolbar } from "./HistoryToolbar";
|
|
|
110
112
|
import { RestoreModal } from "./RestoreModal";
|
|
111
113
|
import { EditorLoading } from "./EditorLoading";
|
|
112
114
|
import ViewRenderer from "../sections/ViewRenderer";
|
|
115
|
+
import { AuthoringDefaultsMenu } from "./AuthoringDefaultsMenu";
|
|
116
|
+
import { AudiencePreviewMenu } from "./AudiencePreviewMenu";
|
|
117
|
+
import { loadAuthoringDefaults, saveAuthoringDefaults, filterAccessToKnown, type AuthoringDefaults } from "../../lib/authoring-defaults";
|
|
113
118
|
|
|
114
119
|
export { useMediaLibrary } from "./MediaLibraryContext";
|
|
115
120
|
|
|
@@ -173,6 +178,11 @@ export default function EditorShell({
|
|
|
173
178
|
const [sections, setSections] = useState<LoadedSection[]>([]);
|
|
174
179
|
const [siteIndex, setSiteIndex] = useState<SiteIndex>({ siteId, pages: [], sections: {} });
|
|
175
180
|
const [audiences, setAudiences] = useState<Audience[]>(initialAudiences);
|
|
181
|
+
const [authoringDefaults, setAuthoringDefaults] = useState<AuthoringDefaults>(loadAuthoringDefaults);
|
|
182
|
+
const onAuthoringDefaultsChange = useCallback((next: AuthoringDefaults) => {
|
|
183
|
+
setAuthoringDefaults(next);
|
|
184
|
+
saveAuthoringDefaults(next);
|
|
185
|
+
}, []);
|
|
176
186
|
const [localChangesExist, setLocalChangesExist] = useState(false);
|
|
177
187
|
const [showDiscardConfirm, setShowDiscardConfirm] = useState(false);
|
|
178
188
|
const [showSiteSettings, setShowSiteSettings] = useState(false);
|
|
@@ -1157,7 +1167,14 @@ export default function EditorShell({
|
|
|
1157
1167
|
const content = resolveInsertedContent(selection, getAllSections(), newId);
|
|
1158
1168
|
if (!content) return;
|
|
1159
1169
|
const id = newId();
|
|
1160
|
-
|
|
1170
|
+
// Access is constrained to the audiences this page can offer (all site
|
|
1171
|
+
// audiences on an unrestricted page), so stale or out-of-scope default
|
|
1172
|
+
// names never land in a section's access list.
|
|
1173
|
+
const meta = {
|
|
1174
|
+
type: content.type,
|
|
1175
|
+
status: authoringDefaults.status,
|
|
1176
|
+
access: filterAccessToKnown(authoringDefaults.access, pageAudiences),
|
|
1177
|
+
};
|
|
1161
1178
|
const newSection: LoadedSection = { section: { id, ...content } as LoadedSection["section"], meta };
|
|
1162
1179
|
|
|
1163
1180
|
setSections((prev) => [...prev, newSection]);
|
|
@@ -1173,7 +1190,7 @@ export default function EditorShell({
|
|
|
1173
1190
|
// save-time sync by design).
|
|
1174
1191
|
void collabRef.current?.requestEdit(id);
|
|
1175
1192
|
},
|
|
1176
|
-
[persistence, resolvedActivePage],
|
|
1193
|
+
[persistence, resolvedActivePage, authoringDefaults, pageAudiences],
|
|
1177
1194
|
);
|
|
1178
1195
|
|
|
1179
1196
|
// Runs after the confirmation modal is accepted. Removes the section from the
|
|
@@ -1512,6 +1529,9 @@ export default function EditorShell({
|
|
|
1512
1529
|
collabPeers={collab.peers}
|
|
1513
1530
|
collabSelfId={collabBootstrap?.userId ?? null}
|
|
1514
1531
|
collabStatus={collab.status}
|
|
1532
|
+
audiences={audiences}
|
|
1533
|
+
authoringDefaults={authoringDefaults}
|
|
1534
|
+
onAuthoringDefaultsChange={onAuthoringDefaultsChange}
|
|
1515
1535
|
/>
|
|
1516
1536
|
|
|
1517
1537
|
{/* Bug reports persist via Supabase. On the password-only (zero-Supabase)
|
|
@@ -1529,18 +1549,13 @@ export default function EditorShell({
|
|
|
1529
1549
|
scope renders the provider and can't read the context directly),
|
|
1530
1550
|
hide the FAB so it can't open the draft editor over a history view. */}
|
|
1531
1551
|
{resolvedActivePage?.document?.enabled && !historyActive && (
|
|
1532
|
-
<
|
|
1533
|
-
type="button"
|
|
1534
|
-
onClick={() => setShowDocumentEditor(true)}
|
|
1535
|
-
aria-label="Edit as document"
|
|
1536
|
-
className="cursor-pointer fixed bottom-40 right-4 lg:right-auto z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity fab-container-right"
|
|
1537
|
-
>
|
|
1552
|
+
<Fab label="Edit as document" column="editor" className="bottom-40" onClick={() => setShowDocumentEditor(true)}>
|
|
1538
1553
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
1539
1554
|
<rect x="3" y="5" width="18" height="14" rx="1" />
|
|
1540
1555
|
<path d="M7 10h10" />
|
|
1541
1556
|
<path d="M7 14h7" />
|
|
1542
1557
|
</svg>
|
|
1543
|
-
</
|
|
1558
|
+
</Fab>
|
|
1544
1559
|
)}
|
|
1545
1560
|
{showDocumentEditor && resolvedActivePage?.document?.enabled && (
|
|
1546
1561
|
<Suspense fallback={<DocumentOverlayFallback />}>
|
|
@@ -1607,6 +1622,12 @@ export default function EditorShell({
|
|
|
1607
1622
|
</div>
|
|
1608
1623
|
)}
|
|
1609
1624
|
|
|
1625
|
+
{/* bp-band-shift: rides the chat dock's FLIP glide (base.css). On
|
|
1626
|
+
/edit it lives HERE, on the content column only — Layout's
|
|
1627
|
+
<main> can't carry it because the toolbar/FABs/chat panel above
|
|
1628
|
+
are position:fixed inside main, and a transformed ancestor
|
|
1629
|
+
would re-anchor them. */}
|
|
1630
|
+
<div className="bp-band-shift">
|
|
1610
1631
|
<HistoryOrEditorContent
|
|
1611
1632
|
index={siteIndex}
|
|
1612
1633
|
activeSections={activeSections}
|
|
@@ -1635,6 +1656,7 @@ export default function EditorShell({
|
|
|
1635
1656
|
onTakeover={confirmTakeover}
|
|
1636
1657
|
/>
|
|
1637
1658
|
</HistoryOrEditorContent>
|
|
1659
|
+
</div>
|
|
1638
1660
|
<GlobalModal />
|
|
1639
1661
|
<SiteSettingsModal
|
|
1640
1662
|
isOpen={showSiteSettings}
|
|
@@ -1889,7 +1911,7 @@ function HistoryOrEditorContent({
|
|
|
1889
1911
|
activeSections: LoadedSection[];
|
|
1890
1912
|
activePageId: string | null;
|
|
1891
1913
|
}) {
|
|
1892
|
-
const { historyState, historyPageId, setHistoryPageId } = useEditorContext();
|
|
1914
|
+
const { historyState, historyPageId, setHistoryPageId, previewAudience } = useEditorContext();
|
|
1893
1915
|
const wasInHistory = useRef(false);
|
|
1894
1916
|
|
|
1895
1917
|
const historyPage = historyState ? resolveHistoryPage(historyState, historyPageId, activePageId) : null;
|
|
@@ -1922,7 +1944,7 @@ function HistoryOrEditorContent({
|
|
|
1922
1944
|
const pageSections = historyState.sections
|
|
1923
1945
|
.filter((s) => position.has(s.section.id))
|
|
1924
1946
|
.sort((a, b) => (position.get(a.section.id) ?? 0) - (position.get(b.section.id) ?? 0));
|
|
1925
|
-
return <ViewRenderer sections={pageSections} />;
|
|
1947
|
+
return <ViewRenderer sections={pageSections} previewAudience={previewAudience} />;
|
|
1926
1948
|
}
|
|
1927
1949
|
return <>{children}</>;
|
|
1928
1950
|
}
|
|
@@ -2001,7 +2023,7 @@ function EditorContent({
|
|
|
2001
2023
|
getEditIntent: (id: string) => () => Promise<boolean>;
|
|
2002
2024
|
onTakeover: (id: string) => void;
|
|
2003
2025
|
}) {
|
|
2004
|
-
const { isEditMode, viewBranch } = useEditorContext();
|
|
2026
|
+
const { isEditMode, viewBranch, previewAudience } = useEditorContext();
|
|
2005
2027
|
const { openModal, closeModal } = useEditorModal();
|
|
2006
2028
|
const [pendingInsertIndex, setPendingInsertIndex] = useState<number | null>(null);
|
|
2007
2029
|
const dismissPendingInsert = useCallback(() => setPendingInsertIndex(null), []);
|
|
@@ -2092,7 +2114,12 @@ function EditorContent({
|
|
|
2092
2114
|
)}
|
|
2093
2115
|
|
|
2094
2116
|
<ErrorBoundary label={`${definition.label} (${section.type})`}>
|
|
2095
|
-
<SectionLayout
|
|
2117
|
+
<SectionLayout
|
|
2118
|
+
type={section.type}
|
|
2119
|
+
status={meta.status}
|
|
2120
|
+
dimNonPublished={!isEditMode}
|
|
2121
|
+
dimmed={previewAudience !== null && !audiencePasses(meta.access, previewAudience)}
|
|
2122
|
+
>
|
|
2096
2123
|
<SectionWrapper
|
|
2097
2124
|
sectionId={section.id}
|
|
2098
2125
|
sectionType={section.type}
|
|
@@ -2340,6 +2367,9 @@ function EditorToolbar({
|
|
|
2340
2367
|
collabPeers,
|
|
2341
2368
|
collabSelfId,
|
|
2342
2369
|
collabStatus,
|
|
2370
|
+
audiences,
|
|
2371
|
+
authoringDefaults,
|
|
2372
|
+
onAuthoringDefaultsChange,
|
|
2343
2373
|
}: {
|
|
2344
2374
|
buttonState: "synced" | "publish" | "saveAndPublish";
|
|
2345
2375
|
localChangesExist: boolean;
|
|
@@ -2362,6 +2392,9 @@ function EditorToolbar({
|
|
|
2362
2392
|
collabPeers: CollabPeer[];
|
|
2363
2393
|
collabSelfId: string | null;
|
|
2364
2394
|
collabStatus: CollabStatus;
|
|
2395
|
+
audiences: Audience[];
|
|
2396
|
+
authoringDefaults: AuthoringDefaults;
|
|
2397
|
+
onAuthoringDefaultsChange: (next: AuthoringDefaults) => void;
|
|
2365
2398
|
}) {
|
|
2366
2399
|
const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState, historyPageId, setHistoryPageId } = useEditorContext();
|
|
2367
2400
|
|
|
@@ -2382,8 +2415,12 @@ function EditorToolbar({
|
|
|
2382
2415
|
return (
|
|
2383
2416
|
<>
|
|
2384
2417
|
{isEditMode && (
|
|
2418
|
+
// Full-width and static on purpose: the docked chat sidebar slides in
|
|
2419
|
+
// BELOW this bar (ChatPanel docks at top-16 on /edit), so the toolbar
|
|
2420
|
+
// never moves during the chat open/close transition. h-16 matches the
|
|
2421
|
+
// panel's top offset (and the body's pt-16) exactly.
|
|
2385
2422
|
<div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
|
|
2386
|
-
<div className="mx-auto max-w-screen-xl grid grid-cols-3 items-center px-4
|
|
2423
|
+
<div className="mx-auto max-w-screen-xl grid h-16 grid-cols-3 items-center px-4">
|
|
2387
2424
|
<div className="flex items-center gap-2">
|
|
2388
2425
|
{buttonState === "saveAndPublish" && (
|
|
2389
2426
|
<SplitButton
|
|
@@ -2432,18 +2469,35 @@ function EditorToolbar({
|
|
|
2432
2469
|
</div>
|
|
2433
2470
|
<div className="flex items-center justify-end gap-2">
|
|
2434
2471
|
<ProcessingIndicator items={processingItems} />
|
|
2435
|
-
<
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
<
|
|
2439
|
-
|
|
2472
|
+
<Tooltip side="bottom" content="Authoring defaults">
|
|
2473
|
+
<AuthoringDefaultsMenu defaults={authoringDefaults} audiences={audiences} onChange={onAuthoringDefaultsChange} />
|
|
2474
|
+
</Tooltip>
|
|
2475
|
+
<Tooltip side="bottom" content="Preview as audience">
|
|
2476
|
+
<AudiencePreviewMenu audiences={audiences} />
|
|
2477
|
+
</Tooltip>
|
|
2478
|
+
<div aria-hidden="true" className="mx-1 h-6 w-px self-center bg-base-200" />
|
|
2479
|
+
<Tooltip side="bottom" content="Media library">
|
|
2480
|
+
<IconButton icon={<ImageIcon size={16} />} label="Media library" size="md" onClick={onMediaClick} className="border border-base-200 bg-base-accent" />
|
|
2481
|
+
</Tooltip>
|
|
2482
|
+
<Tooltip side="bottom" content="Reorder sections">
|
|
2483
|
+
<IconButton icon={<ListOrderedIcon size={16} />} label="Reorder sections" size="md" onClick={onOrderingClick} className="border border-base-200 bg-base-accent" />
|
|
2484
|
+
</Tooltip>
|
|
2485
|
+
<Tooltip side="bottom" content="Pages">
|
|
2486
|
+
<IconButton icon={<FilesIcon size={16} />} label="Pages" size="md" onClick={onPagesClick} className="border border-base-200 bg-base-accent" />
|
|
2487
|
+
</Tooltip>
|
|
2488
|
+
<Tooltip side="bottom" content="Toggle controls">
|
|
2489
|
+
<ShowAllChromeToggle />
|
|
2490
|
+
</Tooltip>
|
|
2491
|
+
<Tooltip side="bottom-clamp" content="Site settings">
|
|
2492
|
+
<IconButton icon={<SettingsIcon size={16} />} label="Site settings" size="md" onClick={onSettingsClick} className="border border-base-200 bg-base-accent" />
|
|
2493
|
+
</Tooltip>
|
|
2440
2494
|
</div>
|
|
2441
2495
|
</div>
|
|
2442
2496
|
</div>
|
|
2443
2497
|
)}
|
|
2444
2498
|
{!isEditMode && (
|
|
2445
2499
|
<div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
|
|
2446
|
-
<div className="mx-auto max-w-screen-xl flex items-center justify-center px-4
|
|
2500
|
+
<div className="mx-auto max-w-screen-xl flex h-16 items-center justify-center px-4">
|
|
2447
2501
|
<SegmentedControl
|
|
2448
2502
|
options={[
|
|
2449
2503
|
{ value: "live", label: "Published" },
|
|
@@ -2456,10 +2510,11 @@ function EditorToolbar({
|
|
|
2456
2510
|
</div>
|
|
2457
2511
|
)}
|
|
2458
2512
|
{!historyState && (
|
|
2459
|
-
<
|
|
2513
|
+
<Fab
|
|
2514
|
+
label={isEditMode ? "Switch to view mode" : "Switch to edit mode"}
|
|
2515
|
+
column="editor"
|
|
2516
|
+
className="bottom-16"
|
|
2460
2517
|
onClick={toggleEditMode}
|
|
2461
|
-
className="cursor-pointer fixed bottom-16 right-4 lg:right-auto z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity fab-container-right"
|
|
2462
|
-
aria-label={isEditMode ? "Switch to view mode" : "Switch to edit mode"}
|
|
2463
2518
|
>
|
|
2464
2519
|
{isEditMode ? (
|
|
2465
2520
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
@@ -2471,7 +2526,7 @@ function EditorToolbar({
|
|
|
2471
2526
|
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
|
2472
2527
|
</svg>
|
|
2473
2528
|
)}
|
|
2474
|
-
</
|
|
2529
|
+
</Fab>
|
|
2475
2530
|
)}
|
|
2476
2531
|
</>
|
|
2477
2532
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { Button } from "../shared/Button";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
|
+
import { contrastColor } from "../../lib/contrast";
|
|
4
5
|
import ChatPanel from "../chat/ChatPanel";
|
|
5
6
|
import type { SiteConfig, ChatbotConfig, ChatbotTask } from "../../schemas/site-config";
|
|
6
7
|
import type { Audience } from "../../auth/types";
|
|
@@ -35,7 +36,7 @@ function AudienceChips({ audiences, selected, onToggle }: { audiences: Audience[
|
|
|
35
36
|
<button key={a.name} type="button" onClick={() => onToggle(a.name)}
|
|
36
37
|
className={cn("rounded-full border px-2 py-0.5 text-xs",
|
|
37
38
|
selected.includes(a.name) ? "border-primary bg-primary text-primary-contrast" : "border-base-200 bg-base-accent text-base-contrast")}
|
|
38
|
-
style={a.color && selected.includes(a.name) ? { backgroundColor: a.color, borderColor: a.color } : undefined}>
|
|
39
|
+
style={a.color && selected.includes(a.name) ? { backgroundColor: a.color, borderColor: a.color, color: contrastColor(a.color) } : undefined}>
|
|
39
40
|
{a.displayName}
|
|
40
41
|
</button>
|
|
41
42
|
))}
|
|
@@ -23,6 +23,14 @@ const FADE_DURATION = 1000;
|
|
|
23
23
|
// whose deploy was never confirmed (e.g. superseded by a later build) — show
|
|
24
24
|
// idle rather than a phantom in-progress publish.
|
|
25
25
|
const STALE_BUILD_MS = 15 * 60 * 1000;
|
|
26
|
+
// Hard cap on an ACTIVE tracking session. Completion normally arrives via a terminal
|
|
27
|
+
// poll (ready/error), which the Netlify webhook writes to the deploy-status row. If
|
|
28
|
+
// that signal is lost/delayed (webhook dropped or unconfigured) the row stays
|
|
29
|
+
// "building" forever and the elapsed counter would climb indefinitely. This backstop
|
|
30
|
+
// stops the runaway counter and hides the indicator after the cap — the same "a build
|
|
31
|
+
// shouldn't run longer than this" invariant as STALE_BUILD_MS, applied to the live
|
|
32
|
+
// session rather than only on mount.
|
|
33
|
+
const MAX_TRACKING_MS = STALE_BUILD_MS;
|
|
26
34
|
|
|
27
35
|
export function useBuildStatus(): BuildStatusResult {
|
|
28
36
|
const [state, setState] = useState<BuildState>("idle");
|
|
@@ -31,6 +39,7 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
31
39
|
const clearRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
32
40
|
const fadeRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
33
41
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
42
|
+
const giveUpRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
43
|
const isPolling = useRef(false);
|
|
35
44
|
const mountedRef = useRef(true);
|
|
36
45
|
|
|
@@ -52,6 +61,23 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
52
61
|
isPolling.current = false;
|
|
53
62
|
}, []);
|
|
54
63
|
|
|
64
|
+
const clearGiveUp = useCallback(() => {
|
|
65
|
+
if (giveUpRef.current) { clearTimeout(giveUpRef.current); giveUpRef.current = null; }
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
// Arm the runaway backstop: if the cap elapses before a terminal signal, stop
|
|
69
|
+
// polling + the timer and drop back to idle (hide the indicator). `elapsedMs` lets a
|
|
70
|
+
// build already in progress at mount count against the same absolute cap.
|
|
71
|
+
const armGiveUp = useCallback((elapsedMs = 0) => {
|
|
72
|
+
clearGiveUp();
|
|
73
|
+
giveUpRef.current = setTimeout(() => {
|
|
74
|
+
if (!mountedRef.current) return;
|
|
75
|
+
stopPolling();
|
|
76
|
+
stopTimer();
|
|
77
|
+
setState("idle");
|
|
78
|
+
}, Math.max(0, MAX_TRACKING_MS - elapsedMs));
|
|
79
|
+
}, [clearGiveUp, stopPolling, stopTimer]);
|
|
80
|
+
|
|
55
81
|
const fetchStatus = useCallback(async (): Promise<BuildStatusResponse | null> => {
|
|
56
82
|
try {
|
|
57
83
|
const res = await fetch("/api/build-status");
|
|
@@ -86,6 +112,7 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
86
112
|
if (data.state === "ready" || data.state === "error") {
|
|
87
113
|
stopPolling();
|
|
88
114
|
stopTimer();
|
|
115
|
+
clearGiveUp();
|
|
89
116
|
if (data.state === "ready") {
|
|
90
117
|
clearRef.current = setTimeout(() => {
|
|
91
118
|
if (!mountedRef.current) return;
|
|
@@ -98,7 +125,7 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
127
|
},
|
|
101
|
-
[stopPolling, stopTimer],
|
|
128
|
+
[stopPolling, stopTimer, clearGiveUp],
|
|
102
129
|
);
|
|
103
130
|
|
|
104
131
|
const startPolling = useCallback(() => {
|
|
@@ -130,6 +157,7 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
130
157
|
// counts real elapsed time instead of sitting frozen at 0:00.
|
|
131
158
|
startTimer(Number.isFinite(ageMs) ? Math.max(0, Math.floor(ageMs / 1000)) : 0);
|
|
132
159
|
startPolling();
|
|
160
|
+
armGiveUp(Number.isFinite(ageMs) ? ageMs : 0);
|
|
133
161
|
return;
|
|
134
162
|
}
|
|
135
163
|
|
|
@@ -143,24 +171,27 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
143
171
|
mountedRef.current = false;
|
|
144
172
|
stopPolling();
|
|
145
173
|
stopTimer();
|
|
174
|
+
clearGiveUp();
|
|
146
175
|
if (clearRef.current) clearTimeout(clearRef.current);
|
|
147
176
|
if (fadeRef.current) clearTimeout(fadeRef.current);
|
|
148
177
|
};
|
|
149
|
-
}, [fetchStatus, handleStatusUpdate, startPolling, startTimer, stopPolling, stopTimer]);
|
|
178
|
+
}, [fetchStatus, handleStatusUpdate, startPolling, startTimer, stopPolling, stopTimer, armGiveUp, clearGiveUp]);
|
|
150
179
|
|
|
151
180
|
const startTracking = useCallback(() => {
|
|
152
181
|
if (clearRef.current) { clearTimeout(clearRef.current); clearRef.current = null; }
|
|
153
182
|
setState("building");
|
|
154
183
|
startTimer();
|
|
155
184
|
startPolling();
|
|
156
|
-
|
|
185
|
+
armGiveUp(0);
|
|
186
|
+
}, [startPolling, startTimer, armGiveUp]);
|
|
157
187
|
|
|
158
188
|
const dismiss = useCallback(() => {
|
|
159
189
|
if (clearRef.current) { clearTimeout(clearRef.current); clearRef.current = null; }
|
|
160
190
|
setState("idle");
|
|
161
191
|
stopPolling();
|
|
162
192
|
stopTimer();
|
|
163
|
-
|
|
193
|
+
clearGiveUp();
|
|
194
|
+
}, [stopPolling, stopTimer, clearGiveUp]);
|
|
164
195
|
|
|
165
196
|
return { state, elapsedSeconds, startTracking, dismiss };
|
|
166
197
|
}
|