@agent-native/core 0.146.2 → 0.146.3
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/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +56 -23
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -0
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +4 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +91 -25
- package/corpus/templates/slides/app/hooks/use-deck-role.ts +14 -4
- package/corpus/templates/slides/app/i18n/en-US.ts +2 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +19 -7
- package/corpus/templates/slides/app/pages/Index.tsx +11 -2
- package/corpus/templates/slides/app/root.tsx +5 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/docs/content/docs-components.mdx +122 -11
- package/package.json +1 -1
|
@@ -96,7 +96,10 @@ interface EditorToolbarProps {
|
|
|
96
96
|
onShowHistory: () => void;
|
|
97
97
|
historyButtonRef: React.RefObject<HTMLButtonElement | null>;
|
|
98
98
|
currentSlide?: Slide;
|
|
99
|
-
onUpdateSlide?: (
|
|
99
|
+
onUpdateSlide?: (
|
|
100
|
+
updates: Partial<Omit<Slide, "id">>,
|
|
101
|
+
slideIdOverride?: string,
|
|
102
|
+
) => void;
|
|
100
103
|
/** Active users on the current slide (from collab awareness) */
|
|
101
104
|
activeUsers?: CollabUser[];
|
|
102
105
|
/** Whether the agent has a durable presence entry on this slide */
|
|
@@ -352,7 +355,7 @@ export default function EditorToolbar({
|
|
|
352
355
|
// That row rides on SlideEditor, which only mounts for a real slide, so an
|
|
353
356
|
// empty deck must keep this fallback or it has no way to add one.
|
|
354
357
|
const contextToolbarVisible = canEdit && Boolean(currentSlide);
|
|
355
|
-
const { undo, redo, canUndo, canRedo } = useDecks();
|
|
358
|
+
const { undo, redo, canUndo, canRedo, getDeck } = useDecks();
|
|
356
359
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
357
360
|
const [importing, setImporting] = useState(false);
|
|
358
361
|
const { setTheme, resolvedTheme } = useTheme();
|
|
@@ -553,7 +556,7 @@ export default function EditorToolbar({
|
|
|
553
556
|
>
|
|
554
557
|
<div className="py-1.5">
|
|
555
558
|
{/* Layout section */}
|
|
556
|
-
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground
|
|
559
|
+
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
557
560
|
{t("editorToolbar.layout")}
|
|
558
561
|
</div>
|
|
559
562
|
{slideLayoutOptions.map((opt) => (
|
|
@@ -573,7 +576,7 @@ export default function EditorToolbar({
|
|
|
573
576
|
|
|
574
577
|
{/* Background section */}
|
|
575
578
|
<div className="mx-2 my-1.5 border-t border-border" />
|
|
576
|
-
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground
|
|
579
|
+
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
577
580
|
{t("editorToolbar.background")}
|
|
578
581
|
</div>
|
|
579
582
|
<div className="px-3 pb-2">
|
|
@@ -619,7 +622,7 @@ export default function EditorToolbar({
|
|
|
619
622
|
|
|
620
623
|
{/* Image & Assets section */}
|
|
621
624
|
<div className="mx-2 my-1.5 border-t border-border" />
|
|
622
|
-
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground
|
|
625
|
+
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
623
626
|
{t("editorToolbar.media")}
|
|
624
627
|
</div>
|
|
625
628
|
<button
|
|
@@ -647,27 +650,46 @@ export default function EditorToolbar({
|
|
|
647
650
|
|
|
648
651
|
{/* Diagrams section */}
|
|
649
652
|
<div className="mx-2 my-1.5 border-t border-border" />
|
|
650
|
-
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground
|
|
653
|
+
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
651
654
|
{t("editorToolbar.diagrams")}
|
|
652
655
|
</div>
|
|
653
656
|
<button
|
|
654
|
-
onClick={() => {
|
|
657
|
+
onClick={async () => {
|
|
655
658
|
if (!onUpdateSlide || !currentSlide) return;
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
+
// Pin the slide this insert targets before the async
|
|
660
|
+
// import/parse below, so navigating to another slide in the
|
|
661
|
+
// meantime can't redirect where the diagram lands.
|
|
662
|
+
const targetSlideId = currentSlide.id;
|
|
663
|
+
const defaultMermaidDefinition = `graph TD
|
|
659
664
|
A[Start] --> B{Decision}
|
|
660
665
|
B -->|Yes| C[Action A]
|
|
661
666
|
B -->|No| D[Action B]
|
|
662
667
|
C --> E[End]
|
|
663
|
-
D --> E
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
668
|
+
D --> E`;
|
|
669
|
+
try {
|
|
670
|
+
const { convertMermaidToExcalidraw } =
|
|
671
|
+
await import("./MermaidToExcalidrawPanel");
|
|
672
|
+
const data = await convertMermaidToExcalidraw(
|
|
673
|
+
defaultMermaidDefinition,
|
|
674
|
+
);
|
|
675
|
+
// The target slide may have been deleted while the import/
|
|
676
|
+
// parse above was in flight — patch-deck rejects the whole
|
|
677
|
+
// batch for a missing slide id, so re-check with the live
|
|
678
|
+
// deck (not the stale `currentSlide` this closure captured)
|
|
679
|
+
// and drop the result instead of enqueueing a dead op.
|
|
680
|
+
const stillExists = getDeck(deckId)?.slides.some(
|
|
681
|
+
(s) => s.id === targetSlideId,
|
|
682
|
+
);
|
|
683
|
+
if (!stillExists) return;
|
|
684
|
+
onUpdateSlide({ excalidrawData: data }, targetSlideId);
|
|
685
|
+
setLayoutOpen(false);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
console.error("Insert Mermaid diagram failed:", err);
|
|
688
|
+
toast.error(t("editorToolbar.insertMermaidFailed"), {
|
|
689
|
+
description:
|
|
690
|
+
err instanceof Error ? err.message : undefined,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
671
693
|
}}
|
|
672
694
|
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
|
|
673
695
|
>
|
|
@@ -696,6 +718,8 @@ graph TD
|
|
|
696
718
|
<button
|
|
697
719
|
onClick={async () => {
|
|
698
720
|
if (!onUpdateSlide || !currentSlide) return;
|
|
721
|
+
// Same pinning as the insert handler above.
|
|
722
|
+
const targetSlideId = currentSlide.id;
|
|
699
723
|
try {
|
|
700
724
|
const match = currentSlide.content.match(
|
|
701
725
|
/<div\s+class="mermaid"[^>]*>([\s\S]*?)<\/div>/i,
|
|
@@ -706,13 +730,22 @@ graph TD
|
|
|
706
730
|
const data = await convertMermaidToExcalidraw(
|
|
707
731
|
match[1].trim(),
|
|
708
732
|
);
|
|
709
|
-
|
|
733
|
+
// Same stale-target guard as the insert handler above.
|
|
734
|
+
const stillExists = getDeck(deckId)?.slides.some(
|
|
735
|
+
(s) => s.id === targetSlideId,
|
|
736
|
+
);
|
|
737
|
+
if (!stillExists) return;
|
|
738
|
+
onUpdateSlide({ excalidrawData: data }, targetSlideId);
|
|
710
739
|
setLayoutOpen(false);
|
|
711
|
-
} catch (err
|
|
740
|
+
} catch (err) {
|
|
712
741
|
console.error("Mermaid to Excalidraw failed:", err);
|
|
742
|
+
toast.error(t("editorToolbar.convertMermaidFailed"), {
|
|
743
|
+
description:
|
|
744
|
+
err instanceof Error ? err.message : undefined,
|
|
745
|
+
});
|
|
713
746
|
}
|
|
714
747
|
}}
|
|
715
|
-
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-[hsl(var(--accent-cyan))]
|
|
748
|
+
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-[hsl(var(--accent-cyan))] hover:bg-accent/50 transition-colors"
|
|
716
749
|
>
|
|
717
750
|
<IconTransform className="w-3 h-3" />
|
|
718
751
|
{t("editorToolbar.convertMermaidToExcalidraw")}
|
|
@@ -734,7 +767,7 @@ graph TD
|
|
|
734
767
|
|
|
735
768
|
{/* Transitions section */}
|
|
736
769
|
<div className="mx-2 my-1.5 border-t border-border" />
|
|
737
|
-
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground
|
|
770
|
+
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
738
771
|
{t("editorToolbar.transition")}
|
|
739
772
|
</div>
|
|
740
773
|
<div className="px-3 pb-2.5 grid grid-cols-4 gap-1">
|
|
@@ -767,7 +800,7 @@ graph TD
|
|
|
767
800
|
{onSetAspectRatio && (
|
|
768
801
|
<>
|
|
769
802
|
<div className="mx-2 my-1.5 border-t border-border" />
|
|
770
|
-
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground
|
|
803
|
+
<div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
771
804
|
{t("editorToolbar.aspectRatio")}
|
|
772
805
|
</div>
|
|
773
806
|
<div className="px-3 pb-2.5 grid grid-cols-4 gap-1">
|
|
@@ -4409,6 +4409,7 @@ export default function SlideEditor({
|
|
|
4409
4409
|
|
|
4410
4410
|
<DrawOverlay
|
|
4411
4411
|
visible={!!drawMode}
|
|
4412
|
+
scopeKey={slideId || slide.id}
|
|
4412
4413
|
onClose={() => onExitDrawMode?.()}
|
|
4413
4414
|
onSend={(annotations, instruction, canvasSize) => {
|
|
4414
4415
|
const summary = annotations
|
|
@@ -31,7 +31,10 @@ export function SlideOverflowWarning({
|
|
|
31
31
|
<div
|
|
32
32
|
role="status"
|
|
33
33
|
aria-live="polite"
|
|
34
|
-
|
|
34
|
+
// w-max/nowrap: the containing block is the zoomed slide canvas, so at
|
|
35
|
+
// low zoom the banner would otherwise wrap to a height taller than its
|
|
36
|
+
// own -top-12 offset and spill down over the slide.
|
|
37
|
+
className="absolute -top-12 left-0 z-20 flex w-max items-center gap-2 whitespace-nowrap rounded-md border border-foreground/40 px-2 py-1 text-xs text-foreground"
|
|
35
38
|
onPointerDown={(event) => event.stopPropagation()}
|
|
36
39
|
onClick={(event) => event.stopPropagation()}
|
|
37
40
|
>
|
|
@@ -143,6 +143,27 @@ export type DeckPersistenceResult =
|
|
|
143
143
|
| { persisted: false; reason: "request-failed"; error: unknown }
|
|
144
144
|
| { persisted: false; reason: "not-found" };
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Ask the server whether a deck row exists, keeping "absent" and "could not
|
|
148
|
+
* check" distinct — a rejected write is not by itself proof the row is missing.
|
|
149
|
+
*/
|
|
150
|
+
async function probeDeckPersisted(id: string): Promise<DeckPersistenceResult> {
|
|
151
|
+
try {
|
|
152
|
+
const result = await callAction<unknown>(
|
|
153
|
+
"get-deck",
|
|
154
|
+
{ id },
|
|
155
|
+
{
|
|
156
|
+
method: "GET",
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
return normalizeActionDeck(result)
|
|
160
|
+
? { persisted: true }
|
|
161
|
+
: { persisted: false, reason: "not-found" };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return { persisted: false, reason: "request-failed", error };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
146
167
|
export function describeDeckPersistenceFailure(
|
|
147
168
|
result: DeckPersistenceResult,
|
|
148
169
|
fallback: string,
|
|
@@ -178,6 +199,7 @@ interface DeckContextType {
|
|
|
178
199
|
sourceDeckId: string,
|
|
179
200
|
newId: string,
|
|
180
201
|
title?: string,
|
|
202
|
+
onFailure?: () => void,
|
|
181
203
|
) => Deck | null;
|
|
182
204
|
deleteDeck: (id: string) => void;
|
|
183
205
|
updateDeck: (
|
|
@@ -851,18 +873,52 @@ async function fetchDeckListLightFromAPI(): Promise<{ id: string }[] | null> {
|
|
|
851
873
|
}
|
|
852
874
|
}
|
|
853
875
|
|
|
876
|
+
const DECK_FETCH_RETRY_ATTEMPTS = 3;
|
|
877
|
+
const DECK_FETCH_RETRY_DELAY_MS = 400;
|
|
878
|
+
|
|
879
|
+
// `get-deck` returns a real 404/403 only when the deck is genuinely gone or
|
|
880
|
+
// the caller genuinely lacks access (see actions/get-deck.ts). A network blip
|
|
881
|
+
// or 5xx is transient and must not be coerced into the same "not found" null
|
|
882
|
+
// the caller uses to show the owner-facing "deck unavailable" pane — that
|
|
883
|
+
// flashed a wrong message on brief server hiccups even though the deck still
|
|
884
|
+
// existed.
|
|
885
|
+
function isConfirmedDeckAbsence(err: unknown): boolean {
|
|
886
|
+
const status = (err as { status?: unknown } | null)?.status;
|
|
887
|
+
return status === 404 || status === 403;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// A timeout already made the caller wait the full action timeout window once
|
|
891
|
+
// (see DEFAULT_ACTION_TIMEOUT_MS in use-action.ts); retrying would multiply
|
|
892
|
+
// that wait instead of surfacing the failure — same reasoning as
|
|
893
|
+
// `isActionTimeout` in the shared action-query retry policy.
|
|
894
|
+
function isDeckFetchTimeout(err: unknown): boolean {
|
|
895
|
+
return (err as { timedOut?: unknown } | null)?.timedOut === true;
|
|
896
|
+
}
|
|
897
|
+
|
|
854
898
|
async function fetchDeckFromAPI(id: string): Promise<Deck | null> {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
899
|
+
for (let attempt = 1; attempt <= DECK_FETCH_RETRY_ATTEMPTS; attempt++) {
|
|
900
|
+
try {
|
|
901
|
+
const result = await callAction<unknown>(
|
|
902
|
+
"get-deck",
|
|
903
|
+
{ id },
|
|
904
|
+
{ method: "GET" },
|
|
905
|
+
);
|
|
906
|
+
return normalizeActionDeck(result);
|
|
907
|
+
} catch (err) {
|
|
908
|
+
if (
|
|
909
|
+
isConfirmedDeckAbsence(err) ||
|
|
910
|
+
isDeckFetchTimeout(err) ||
|
|
911
|
+
attempt === DECK_FETCH_RETRY_ATTEMPTS
|
|
912
|
+
) {
|
|
913
|
+
console.error(`Failed to fetch deck ${id}:`, err);
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
await new Promise((resolve) =>
|
|
917
|
+
setTimeout(resolve, attempt * DECK_FETCH_RETRY_DELAY_MS),
|
|
918
|
+
);
|
|
919
|
+
}
|
|
865
920
|
}
|
|
921
|
+
return null;
|
|
866
922
|
}
|
|
867
923
|
|
|
868
924
|
export function deckIdFromPathname(pathname: string): string | null {
|
|
@@ -1776,24 +1832,18 @@ export function DeckProvider({ children }: { children: ReactNode }) {
|
|
|
1776
1832
|
}
|
|
1777
1833
|
}
|
|
1778
1834
|
|
|
1779
|
-
|
|
1780
|
-
const result = await callAction<unknown>(
|
|
1781
|
-
"get-deck",
|
|
1782
|
-
{ id },
|
|
1783
|
-
{ method: "GET" },
|
|
1784
|
-
);
|
|
1785
|
-
return normalizeActionDeck(result)
|
|
1786
|
-
? { persisted: true }
|
|
1787
|
-
: { persisted: false, reason: "not-found" };
|
|
1788
|
-
} catch (error) {
|
|
1789
|
-
return { persisted: false, reason: "request-failed", error };
|
|
1790
|
-
}
|
|
1835
|
+
return probeDeckPersisted(id);
|
|
1791
1836
|
},
|
|
1792
1837
|
[],
|
|
1793
1838
|
);
|
|
1794
1839
|
|
|
1795
1840
|
const duplicateDeck = useCallback(
|
|
1796
|
-
(
|
|
1841
|
+
(
|
|
1842
|
+
sourceDeckId: string,
|
|
1843
|
+
newId: string,
|
|
1844
|
+
title?: string,
|
|
1845
|
+
onFailure?: () => void,
|
|
1846
|
+
): Deck | null => {
|
|
1797
1847
|
if (pendingDuplicateSourceIdsRef.current.has(sourceDeckId)) return null;
|
|
1798
1848
|
const source = decks.find((d) => d.id === sourceDeckId);
|
|
1799
1849
|
if (!source) return null;
|
|
@@ -1843,10 +1893,26 @@ export function DeckProvider({ children }: { children: ReactNode }) {
|
|
|
1843
1893
|
).then(() => undefined);
|
|
1844
1894
|
pendingCreatePromisesRef.current.set(newId, duplicatePromise);
|
|
1845
1895
|
duplicatePromise
|
|
1846
|
-
.catch((err) => {
|
|
1896
|
+
.catch(async (err) => {
|
|
1897
|
+
// A rejected request is not proof the row is missing: a timeout or
|
|
1898
|
+
// dropped response can land after the server committed the insert.
|
|
1899
|
+
// Discarding the copy then would delete work that actually exists
|
|
1900
|
+
// and tell the user it failed, so confirm against the server first.
|
|
1901
|
+
const probe = await probeDeckPersisted(newId);
|
|
1902
|
+
if (probe.persisted) {
|
|
1903
|
+
console.warn(
|
|
1904
|
+
`Duplicate request for ${newId} failed but the deck persisted:`,
|
|
1905
|
+
err,
|
|
1906
|
+
);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1847
1909
|
console.error("Duplicate failed:", err);
|
|
1848
|
-
// Roll back: drop the optimistic deck from local state.
|
|
1910
|
+
// Roll back: drop the optimistic deck from local state. The caller
|
|
1911
|
+
// (via onFailure) is responsible for navigating away if the user is
|
|
1912
|
+
// still sitting on this now-gone deck's route — otherwise they're
|
|
1913
|
+
// stranded on a "Deck unavailable" screen with no way back.
|
|
1849
1914
|
setDecks((prev) => prev.filter((d) => d.id !== newId));
|
|
1915
|
+
onFailure?.();
|
|
1850
1916
|
})
|
|
1851
1917
|
.finally(() => {
|
|
1852
1918
|
pendingCreateIdsRef.current.delete(newId);
|
|
@@ -14,10 +14,18 @@ interface SharesResponse {
|
|
|
14
14
|
* `Viewer` = no edit affordances, but the editor shell is still navigable;
|
|
15
15
|
* any other role (Owner / Editor / Admin) gets full editing.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* `assumeEditorWhileLoading` should only be true when a faster, already-known
|
|
18
|
+
* signal (e.g. `deck.createdByMe`) confirms the caller is the owner — that
|
|
19
|
+
* avoids a flash of view-only chrome for owners without ever defaulting a
|
|
20
|
+
* viewer into edit affordances while the real role is still loading. Without
|
|
21
|
+
* that signal, `canEdit` stays false until the role query resolves, so a
|
|
22
|
+
* viewer can never trigger an edit that the server will reject and silently
|
|
23
|
+
* roll back.
|
|
19
24
|
*/
|
|
20
|
-
export function useDeckRole(
|
|
25
|
+
export function useDeckRole(
|
|
26
|
+
deckId: string | undefined,
|
|
27
|
+
assumeEditorWhileLoading = false,
|
|
28
|
+
): {
|
|
21
29
|
role: SharesResponse["role"] | undefined;
|
|
22
30
|
canEdit: boolean;
|
|
23
31
|
isLoading: boolean;
|
|
@@ -29,6 +37,8 @@ export function useDeckRole(deckId: string | undefined): {
|
|
|
29
37
|
);
|
|
30
38
|
const role = query.data?.role;
|
|
31
39
|
const canEdit =
|
|
32
|
-
role === undefined
|
|
40
|
+
role === undefined
|
|
41
|
+
? assumeEditorWhileLoading
|
|
42
|
+
: role === "owner" || role !== "viewer";
|
|
33
43
|
return { role, canEdit, isLoading: query.isLoading };
|
|
34
44
|
}
|
|
@@ -212,8 +212,10 @@ const messages = {
|
|
|
212
212
|
assetLibrary: "Asset Library",
|
|
213
213
|
diagrams: "Diagrams",
|
|
214
214
|
insertMermaidDiagram: "Insert Mermaid Diagram",
|
|
215
|
+
insertMermaidFailed: "Failed to insert diagram",
|
|
215
216
|
excalidrawCanvas: "Excalidraw Canvas",
|
|
216
217
|
convertMermaidToExcalidraw: "Convert Mermaid to Excalidraw",
|
|
218
|
+
convertMermaidFailed: "Failed to convert diagram",
|
|
217
219
|
removeExcalidrawCanvas: "Remove Excalidraw Canvas",
|
|
218
220
|
transition: "Transition",
|
|
219
221
|
transition_instant: "Instant",
|
|
@@ -44,7 +44,7 @@ import { QuestionFlow } from "@/components/editor/QuestionFlow";
|
|
|
44
44
|
import SlideEditor from "@/components/editor/SlideEditor";
|
|
45
45
|
import { TweaksPanel } from "@/components/editor/TweaksPanel";
|
|
46
46
|
import { Button } from "@/components/ui/button";
|
|
47
|
-
import { useDecks } from "@/context/DeckContext";
|
|
47
|
+
import { deckIdFromPathname, useDecks } from "@/context/DeckContext";
|
|
48
48
|
import { useAgentGenerating } from "@/hooks/use-agent-generating";
|
|
49
49
|
import { useDeckDesignSystem } from "@/hooks/use-deck-design-system";
|
|
50
50
|
import { useDeckPresence } from "@/hooks/use-deck-presence";
|
|
@@ -275,8 +275,10 @@ export default function DeckEditor() {
|
|
|
275
275
|
});
|
|
276
276
|
// Mirror Google Slides: viewers see the editor shell with edit affordances
|
|
277
277
|
// disabled (rather than a separate "viewer" route). Owners/Editors/Admins
|
|
278
|
-
// get the full editor.
|
|
279
|
-
|
|
278
|
+
// get the full editor. Only assume edit access while the role is still
|
|
279
|
+
// loading when `createdByMe` already confirms ownership — otherwise a
|
|
280
|
+
// viewer would briefly see (and could click) edit affordances.
|
|
281
|
+
const { canEdit } = useDeckRole(id, deck?.createdByMe === true);
|
|
280
282
|
const isNewDeckGenerating = shouldShowNewDeckGeneratingProgress({
|
|
281
283
|
generating,
|
|
282
284
|
isNewDeckCreation: wasNewDeckCreation.current,
|
|
@@ -1001,9 +1003,10 @@ export default function DeckEditor() {
|
|
|
1001
1003
|
onShowHistory={() => setHistoryOpen(!historyOpen)}
|
|
1002
1004
|
historyButtonRef={historyButtonRef}
|
|
1003
1005
|
currentSlide={currentSlide}
|
|
1004
|
-
onUpdateSlide={(updates) =>
|
|
1005
|
-
|
|
1006
|
-
|
|
1006
|
+
onUpdateSlide={(updates, slideIdOverride) => {
|
|
1007
|
+
const targetId = slideIdOverride ?? currentSlide?.id;
|
|
1008
|
+
if (targetId) updateSlide(id, targetId, updates);
|
|
1009
|
+
}}
|
|
1007
1010
|
activeUsers={slideActiveUsers.filter((u) => u.email !== session?.email)}
|
|
1008
1011
|
agentPresent={agentPresent}
|
|
1009
1012
|
agentActive={agentActive}
|
|
@@ -1025,7 +1028,16 @@ export default function DeckEditor() {
|
|
|
1025
1028
|
onToggleTextBoxMode={toggleTextBoxMode}
|
|
1026
1029
|
onDuplicateDeck={() => {
|
|
1027
1030
|
const newId = `deck-${nanoid()}`;
|
|
1028
|
-
const optimistic = duplicateDeck(id, newId)
|
|
1031
|
+
const optimistic = duplicateDeck(id, newId, undefined, () => {
|
|
1032
|
+
// The background duplicate-deck action failed after we already
|
|
1033
|
+
// navigated to the optimistic copy. If the user is still there,
|
|
1034
|
+
// send them back instead of stranding them on a "Deck
|
|
1035
|
+
// unavailable" screen for a deck that no longer exists.
|
|
1036
|
+
if (deckIdFromPathname(window.location.pathname) === newId) {
|
|
1037
|
+
navigate("/");
|
|
1038
|
+
}
|
|
1039
|
+
toast.error(t("home.duplicateFailed"));
|
|
1040
|
+
});
|
|
1029
1041
|
if (optimistic) navigate(`/deck/${optimistic.id}`);
|
|
1030
1042
|
}}
|
|
1031
1043
|
onExportPdf={async () => {
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
describeDeckPersistenceFailure,
|
|
46
46
|
type Deck,
|
|
47
47
|
} from "@/context/DeckContext";
|
|
48
|
-
import { useDecks } from "@/context/DeckContext";
|
|
48
|
+
import { deckIdFromPathname, useDecks } from "@/context/DeckContext";
|
|
49
49
|
import { useAgentGenerating } from "@/hooks/use-agent-generating";
|
|
50
50
|
import { useDesignSystems } from "@/hooks/use-design-systems";
|
|
51
51
|
import { useWorkspaceDefaults } from "@/hooks/use-workspace-defaults";
|
|
@@ -1088,7 +1088,16 @@ export default function Index() {
|
|
|
1088
1088
|
(id: string) => {
|
|
1089
1089
|
let copy: ReturnType<typeof duplicateDeck> | undefined;
|
|
1090
1090
|
flushSync(() => {
|
|
1091
|
-
copy = duplicateDeck(id, `deck-${nanoid()}
|
|
1091
|
+
copy = duplicateDeck(id, `deck-${nanoid()}`, undefined, () => {
|
|
1092
|
+
// The background duplicate-deck action failed after we already
|
|
1093
|
+
// navigated to the optimistic copy's route. If the user is still
|
|
1094
|
+
// there, send them back to the deck list instead of stranding them
|
|
1095
|
+
// on a "Deck unavailable" screen for a deck that no longer exists.
|
|
1096
|
+
if (deckIdFromPathname(window.location.pathname) === copy?.id) {
|
|
1097
|
+
navigate("/");
|
|
1098
|
+
}
|
|
1099
|
+
toast.error(t("home.duplicateFailed"));
|
|
1100
|
+
});
|
|
1092
1101
|
});
|
|
1093
1102
|
// The context refuses a second copy of the same deck while the first
|
|
1094
1103
|
// one's action is still in flight.
|
|
@@ -97,7 +97,11 @@ function useExitSelectionOnOutsideClick() {
|
|
|
97
97
|
const target = e.target as HTMLElement;
|
|
98
98
|
if (
|
|
99
99
|
target.closest(".slide-content") ||
|
|
100
|
-
target.closest(".slide-image-clickable")
|
|
100
|
+
target.closest(".slide-image-clickable") ||
|
|
101
|
+
target.closest("[data-slide-context-toolbar]") ||
|
|
102
|
+
target.closest(
|
|
103
|
+
'[role="dialog"], [role="menu"], [role="listbox"], [data-radix-popper-content-wrapper], [data-radix-menu-content]',
|
|
104
|
+
)
|
|
101
105
|
) {
|
|
102
106
|
return;
|
|
103
107
|
}
|
|
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
17
17
|
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
|
-
error?: undefined;
|
|
21
20
|
configured?: undefined;
|
|
22
21
|
connectPath?: undefined;
|
|
23
22
|
url: string;
|
|
24
23
|
id: string;
|
|
25
24
|
provider: string;
|
|
25
|
+
error?: undefined;
|
|
26
26
|
}>;
|
|
27
27
|
export default _default;
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
|
|
14
14
|
count: number;
|
|
15
15
|
updated?: undefined;
|
|
16
|
-
ok?: undefined;
|
|
17
16
|
error?: undefined;
|
|
17
|
+
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
19
|
count?: undefined;
|
|
20
20
|
updated: number;
|
|
21
|
-
ok?: undefined;
|
|
22
21
|
error?: undefined;
|
|
22
|
+
ok?: undefined;
|
|
23
23
|
} | {
|
|
24
24
|
count?: undefined;
|
|
25
25
|
updated?: undefined;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
ok?: undefined;
|
|
52
51
|
error: string;
|
|
52
|
+
ok?: undefined;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -37,17 +37,17 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
|
|
|
37
37
|
ok?: undefined;
|
|
38
38
|
status?: undefined;
|
|
39
39
|
} | {
|
|
40
|
+
error?: undefined;
|
|
40
41
|
ok: boolean;
|
|
41
42
|
status: string;
|
|
42
|
-
error?: undefined;
|
|
43
43
|
} | {
|
|
44
44
|
ok?: undefined;
|
|
45
45
|
error: string;
|
|
46
46
|
removed?: undefined;
|
|
47
47
|
} | {
|
|
48
|
+
error?: undefined;
|
|
48
49
|
ok: boolean;
|
|
49
50
|
removed: boolean;
|
|
50
|
-
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — validate an optional candidate value
|
|
@@ -58,13 +58,13 @@ export declare function createTestSecretHandler(): import("h3").EventHandlerWith
|
|
|
58
58
|
error: string;
|
|
59
59
|
note?: undefined;
|
|
60
60
|
} | {
|
|
61
|
+
error?: undefined;
|
|
61
62
|
ok: boolean;
|
|
62
63
|
note?: undefined;
|
|
63
|
-
error?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
67
|
note: string;
|
|
67
|
-
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
+
error?: undefined;
|
|
98
99
|
ok: boolean;
|
|
99
100
|
key: string;
|
|
100
|
-
error?: undefined;
|
|
101
101
|
} | {
|
|
102
|
+
error?: undefined;
|
|
102
103
|
ok: boolean;
|
|
103
104
|
removed: boolean;
|
|
104
|
-
error?: undefined;
|
|
105
105
|
}>>;
|
|
@@ -191,6 +191,113 @@ Each card is a `###` heading (optionally linked) followed by the description. Ca
|
|
|
191
191
|
|
|
192
192
|
---
|
|
193
193
|
|
|
194
|
+
## Notice
|
|
195
|
+
|
|
196
|
+
<Notice tone="risk" title="Bold, not subtle">
|
|
197
|
+
|
|
198
|
+
Use `Notice` when a `Callout` would be too easy to skim past — something the reader must not miss, like a destructive action or a hard requirement. It shares the same five tones as `Callout` (`info`, `decision`, `risk`, `warning`, `success`) but renders as a filled card with an icon and optional title instead of a left-border note.
|
|
199
|
+
|
|
200
|
+
</Notice>
|
|
201
|
+
|
|
202
|
+
```mdx
|
|
203
|
+
<Notice tone="risk" title="Bold, not subtle">
|
|
204
|
+
|
|
205
|
+
Use `Notice` when a `Callout` would be too easy to skim past — something the reader must not miss, like a destructive action or a hard requirement.
|
|
206
|
+
|
|
207
|
+
</Notice>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`title` is optional — omit it for a shorter card with just the icon and body. Leave a blank line between the opening tag and the body, same as `Callout`. Reach for `Callout` first; use `Notice` only when a subtle left-border note isn't enough attention.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Banner
|
|
215
|
+
|
|
216
|
+
<Banner
|
|
217
|
+
tone="warning"
|
|
218
|
+
body="This page describes the v9 API. See [Migrating to v10](/docs/migrating-v10) for what changed."
|
|
219
|
+
/>
|
|
220
|
+
|
|
221
|
+
```mdx
|
|
222
|
+
<Banner
|
|
223
|
+
tone="warning"
|
|
224
|
+
body="This page describes the v9 API. See [Migrating to v10](/docs/migrating-v10) for what changed."
|
|
225
|
+
/>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`Banner` is self-closing — `body` is a flat attribute (kept short and single-purpose on purpose), not markdown children. Place it right under a page's title or a section heading — deprecated, beta, moved, version-scoped. For content the reader needs to actually read (not just notice), use `Notice` or `Callout` instead.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Accordion
|
|
233
|
+
|
|
234
|
+
<Accordion>
|
|
235
|
+
|
|
236
|
+
### Do I need Toolkit to use Core?
|
|
237
|
+
|
|
238
|
+
No. Toolkit is optional shared UI — Core works standalone.
|
|
239
|
+
|
|
240
|
+
### Can I mix Templates and Toolkit?
|
|
241
|
+
|
|
242
|
+
Yes, most first-party templates already do.
|
|
243
|
+
|
|
244
|
+
### Where do I ask questions?
|
|
245
|
+
|
|
246
|
+
The `#agent-native` Discord channel, linked in the site header.
|
|
247
|
+
|
|
248
|
+
</Accordion>
|
|
249
|
+
|
|
250
|
+
```mdx
|
|
251
|
+
<Accordion>
|
|
252
|
+
|
|
253
|
+
### Do I need Toolkit to use Core?
|
|
254
|
+
|
|
255
|
+
No. Toolkit is optional shared UI — Core works standalone.
|
|
256
|
+
|
|
257
|
+
### Can I mix Templates and Toolkit?
|
|
258
|
+
|
|
259
|
+
Yes, most first-party templates already do.
|
|
260
|
+
|
|
261
|
+
</Accordion>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Same authoring shape as `Steps`/`Cards` — a `### Question` heading per item followed by its answer — but each item renders collapsed behind native `<details>`. Best for FAQ-style content where most readers only need one or two entries, not a full top-to-bottom read.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Badge
|
|
269
|
+
|
|
270
|
+
<Badge label="Beta" color="orange" icon="rocket" />
|
|
271
|
+
|
|
272
|
+
<Badge label="Deprecated" color="red" />
|
|
273
|
+
|
|
274
|
+
<Badge label="v9.1+" color="blue" shape="pill" />
|
|
275
|
+
|
|
276
|
+
<Badge label="Server-only" color="gray" stroke />
|
|
277
|
+
|
|
278
|
+
```mdx
|
|
279
|
+
<Badge label="Beta" color="orange" icon="rocket" />
|
|
280
|
+
|
|
281
|
+
<Badge label="Deprecated" color="red" />
|
|
282
|
+
|
|
283
|
+
<Badge label="v9.1+" color="blue" shape="pill" />
|
|
284
|
+
|
|
285
|
+
<Badge label="Server-only" color="gray" stroke />
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
A small status chip for the top of a page or section. Self-closing, block-level only — **not** embeddable mid-sentence (the docs MDX pipeline only recognizes a custom component when its tag opens its own line, so `text <Badge label="x"/> more text` renders as literal text, not a chip). Place it on its own line, typically right after a heading. Leave a blank line between adjacent Badges (or any two block components) — without one, the scanner can't tell where one ends and the next begins and falls back to rendering literal text.
|
|
289
|
+
|
|
290
|
+
| Prop | Values |
|
|
291
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
292
|
+
| `color` | `gray` · `blue` · `green` · `yellow` · `orange` · `red` · `purple` · `white` · `surface` · `white-destructive` · `surface-destructive` |
|
|
293
|
+
| `size` | `xs` · `sm` · `md` · `lg` |
|
|
294
|
+
| `shape` | `rounded` (default) · `pill` |
|
|
295
|
+
| `icon` | one of a curated set — `circle-check`, `circle-info`, `alert-triangle`, `alert-octagon`, `ban`, `clock`, `star`, `flame`, `sparkles`, `lock`, `rocket` |
|
|
296
|
+
| `stroke` | `true` for an outlined, transparent-background variant |
|
|
297
|
+
| `disabled` | `true` to dim it for an inactive/unavailable state |
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
194
301
|
## Comparison
|
|
195
302
|
|
|
196
303
|
<Comparison>
|
|
@@ -478,14 +585,18 @@ Like `TabsBlock`, `Columns` is self-closing with a single JSON `columns` attribu
|
|
|
478
585
|
|
|
479
586
|
## When to use which component
|
|
480
587
|
|
|
481
|
-
| Situation
|
|
482
|
-
|
|
|
483
|
-
| Reader must not miss something
|
|
484
|
-
|
|
|
485
|
-
|
|
|
486
|
-
|
|
|
487
|
-
|
|
|
488
|
-
|
|
|
489
|
-
|
|
|
490
|
-
|
|
|
491
|
-
|
|
|
588
|
+
| Situation | Component |
|
|
589
|
+
| ----------------------------------------------------- | ------------------------------------ |
|
|
590
|
+
| Reader must not miss something, inline in prose | `Callout` (warning / info / success) |
|
|
591
|
+
| Reader must not miss something, standalone | `Notice` |
|
|
592
|
+
| Deprecated/beta/version notice at page or section top | `Banner` |
|
|
593
|
+
| Status or version tag at page or section top | `Badge` |
|
|
594
|
+
| Sequential procedure with titled steps | `Steps` |
|
|
595
|
+
| FAQ-style content, one answer at a time | `Accordion` |
|
|
596
|
+
| Navigation or feature overview grid | `Cards` |
|
|
597
|
+
| Before vs. after, old vs. new (prose) | `Comparison` |
|
|
598
|
+
| Same task in multiple tools or frameworks | `Tabs` |
|
|
599
|
+
| Side-by-side code (labeled columns) | `Columns` |
|
|
600
|
+
| Project directory structure | `FileTree` |
|
|
601
|
+
| Inline keyboard shortcut | `[[Key]]` |
|
|
602
|
+
| Architecture or flow diagram | Mermaid fence |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.146.
|
|
3
|
+
"version": "0.146.3",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|