@agent-native/core-corpus 0.218.0 → 0.219.0

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 (32) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/hubspot/SKILL.md +5 -3
  3. package/corpus/templates/community/qr/package.json +1 -1
  4. package/corpus/templates/content/CHANGELOG.md +5 -0
  5. package/corpus/templates/content/app/components/editor/CommentStack.tsx +301 -0
  6. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +651 -149
  7. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +19 -14
  8. package/corpus/templates/content/app/components/editor/block-field-comment-threads.ts +15 -0
  9. package/corpus/templates/content/app/components/editor/block-field-comments.tsx +5 -9
  10. package/corpus/templates/content/app/components/editor/comment-stacks.ts +143 -0
  11. package/corpus/templates/content/app/i18n-data.ts +8 -0
  12. package/corpus/templates/content/changelog/2026-10-22-blocks-with-several-comment-threads-fold-into-one-stack-in-t.md +6 -0
  13. package/corpus/templates/content/changelog/2026-10-22-opening-a-page-no-longer-saves-an-edit-again-after-another-o.md +6 -0
  14. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +8 -1
  15. package/corpus/templates/design/.generated/bridge/interact-links.generated.ts +8 -8
  16. package/corpus/templates/design/CHANGELOG.md +5 -0
  17. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +18 -1
  18. package/corpus/templates/design/app/components/design/bridge/interact-links.bridge.ts +8 -8
  19. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +15 -13
  20. package/corpus/templates/design/app/i18n/en-US.ts +1 -1
  21. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +15 -6
  22. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +29 -5
  23. package/corpus/templates/design/app/pages/design-editor/prototype-navigation-transition.ts +423 -0
  24. package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +35 -9
  25. package/corpus/templates/design/changelog/2026-10-20-the-design-full-app-toggle-on-the-home-page-no-longer-hides-.md +6 -0
  26. package/corpus/templates/design/changelog/2026-10-22-prototype-links-can-play-code-authored-screen-transitions.md +6 -0
  27. package/corpus/templates/design/shared/motion-authored-css-import.ts +938 -7
  28. package/corpus/templates/design/shared/motion-html-source.ts +1689 -135
  29. package/corpus/templates/design/shared/motion-prototype-transition.ts +329 -0
  30. package/corpus/templates/design/shared/motion-react-source.ts +2154 -126
  31. package/corpus/templates/design/shared/motion-text-runtime-v1.fixture.txt +221 -0
  32. package/package.json +1 -1
@@ -4188,6 +4188,15 @@ function PageEditorSessionBody({
4188
4188
  options.editGeneration ?? editorEditGenerationRef.current;
4189
4189
  const contentObservationEpoch =
4190
4190
  options.contentObservationEpoch ?? contentObservationEpochRef.current;
4191
+ // Text another writer added while this save was in flight is theirs to
4192
+ // save, so it holds back neither the confirmation nor the journal clear.
4193
+ // An entry kept for it outlives this tab's edits, and the next tab to
4194
+ // open the page replays it as a save of its own.
4195
+ const settleConfirmedEdit = () => {
4196
+ if (contentEditVersionRef.current !== contentEditVersion) return;
4197
+ confirmedContentEditVersionRef.current = contentEditVersion;
4198
+ clearConfirmedDraftJournal(editorEditGeneration);
4199
+ };
4191
4200
  lastSavedContentRef.current = refreshUnchangedContentSaveWatermark({
4192
4201
  serverContent: documentContentRef.current,
4193
4202
  serverUpdatedAt: documentUpdatedAtRef.current,
@@ -4268,12 +4277,14 @@ function PageEditorSessionBody({
4268
4277
  { title, content },
4269
4278
  options,
4270
4279
  );
4271
- return {
4272
- contentPersisted:
4273
- !isDocumentUpdateConflict(attested) &&
4274
- !isDocumentUpdateSuperseded(attested) &&
4275
- !isDocumentUpdatePreservationRequired(attested),
4276
- };
4280
+ const confirmed =
4281
+ !isDocumentUpdateConflict(attested) &&
4282
+ !isDocumentUpdateSuperseded(attested) &&
4283
+ !isDocumentUpdatePreservationRequired(attested);
4284
+ // The page already held this edit, as when a peer's save carried it,
4285
+ // so the attestation settles it as a written save would.
4286
+ if (confirmed) settleConfirmedEdit();
4287
+ return { contentPersisted: confirmed };
4277
4288
  }
4278
4289
  if (!hasUpdates) {
4279
4290
  return { contentPersisted: !contentChanged };
@@ -4493,14 +4504,7 @@ function PageEditorSessionBody({
4493
4504
  : null,
4494
4505
  });
4495
4506
  }
4496
- // Text another writer added while this save was in flight is theirs to
4497
- // save, so it holds back neither the confirmation nor the journal clear.
4498
- // An entry kept for it outlives this tab's edits, and the next tab to
4499
- // open the page replays it as a save of its own.
4500
- if (contentEditVersionRef.current === contentEditVersion) {
4501
- confirmedContentEditVersionRef.current = contentEditVersion;
4502
- clearConfirmedDraftJournal(editorEditGeneration);
4503
- }
4507
+ settleConfirmedEdit();
4504
4508
 
4505
4509
  if (autoSync) {
4506
4510
  const status = queryClient.getQueryData<DocumentSyncStatus>(
@@ -8692,6 +8696,7 @@ function PageEditorSessionBody({
8692
8696
  onActivateSuggestion={activateSuggestionOrThread}
8693
8697
  onSelectedThreadChange={setSelectedThreadId}
8694
8698
  onHoveredThreadChange={setHoveredThreadId}
8699
+ hoveredThreadId={hoveredThreadId}
8695
8700
  currentUserEmail={session?.email}
8696
8701
  currentUserName={session?.name}
8697
8702
  currentUserOrgId={session?.orgId}
@@ -16,6 +16,21 @@ export function commentThreadFieldId(
16
16
  return thread.field?.fieldId ?? null;
17
17
  }
18
18
 
19
+ /**
20
+ * The threads the margin shows with one tab open: that tab's and the
21
+ * Property threads. A page without tabs shows them all.
22
+ */
23
+ export function commentRailThreads<
24
+ T extends Pick<CommentThread, "field" | "property">,
25
+ >(threads: readonly T[], openFieldId: string | null): T[] {
26
+ if (openFieldId === null && !threads.some((thread) => thread.field)) {
27
+ return threads as T[];
28
+ }
29
+ return threads.filter(
30
+ (thread) => thread.property || commentThreadFieldId(thread) === openFieldId,
31
+ );
32
+ }
33
+
19
34
  /** A key for a tab that a Map or a filter can hold: "" is the page body. */
20
35
  export function commentTabKey(fieldId: string | null): string {
21
36
  return fieldId ?? "";
@@ -14,6 +14,7 @@ import {
14
14
  import type { CommentThread } from "@/hooks/use-comments";
15
15
 
16
16
  import {
17
+ commentRailThreads,
17
18
  commentThreadFieldId,
18
19
  openReviewCountsByField,
19
20
  type BlockFieldOpenCounts,
@@ -353,15 +354,10 @@ export function useBlockFieldComments({
353
354
  }, [scrollContainerRef, tabs, targetFieldId, targetKey, targetSettled]);
354
355
 
355
356
  const allThreads = threads ?? NO_THREADS;
356
- const railThreads = useMemo(() => {
357
- if (openFieldId === null && !allThreads.some((thread) => thread.field)) {
358
- return allThreads as CommentThread[];
359
- }
360
- return allThreads.filter(
361
- (thread) =>
362
- thread.property || commentThreadFieldId(thread) === openFieldId,
363
- );
364
- }, [allThreads, openFieldId]);
357
+ const railThreads = useMemo(
358
+ () => commentRailThreads(allThreads as CommentThread[], openFieldId),
359
+ [allThreads, openFieldId],
360
+ );
365
361
 
366
362
  const pendingFieldId = pendingComment?.property
367
363
  ? undefined
@@ -0,0 +1,143 @@
1
+ import type { CommentThread } from "@/hooks/use-comments";
2
+
3
+ import {
4
+ commentTabKey,
5
+ commentThreadFieldId,
6
+ } from "./block-field-comment-threads";
7
+ import { inHiddenBlockFieldPanel } from "./blockFieldTabs";
8
+
9
+ /** Where a thread's first highlight sits: its stack, and its document order. */
10
+ export type CommentBlockPlacement = { key: string; order: number };
11
+
12
+ /** Two or more open threads in one block, in document order. */
13
+ export type CommentStack<T> = { key: string; threads: T[] };
14
+
15
+ /**
16
+ * The block a highlight is in, as child indexes down from its editor: the
17
+ * innermost list item holding it, else the editor's top-level node. A
18
+ * highlight outside an editor, such as a Property's, has no block.
19
+ */
20
+ export function commentBlockKey(highlight: Element): string | null {
21
+ const block = commentBlock(highlight);
22
+ return block ? blockPath(block) : null;
23
+ }
24
+
25
+ function commentBlock(highlight: Element) {
26
+ const editor = highlight.closest(".ProseMirror");
27
+ if (!editor) return null;
28
+ const item = highlight.closest("li");
29
+ if (item && editor.contains(item)) return { editor, block: item };
30
+ let block: Element | null = highlight;
31
+ while (block && block.parentElement !== editor) block = block.parentElement;
32
+ return block ? { editor, block } : null;
33
+ }
34
+
35
+ function blockPath({ editor, block }: { editor: Element; block: Element }) {
36
+ const path: number[] = [];
37
+ for (let node: Element = block; node !== editor; ) {
38
+ const parent: Element | null = node.parentElement;
39
+ if (!parent) return null;
40
+ path.unshift(Array.prototype.indexOf.call(parent.children, node));
41
+ node = parent;
42
+ }
43
+ return path.join(".");
44
+ }
45
+
46
+ /** Block keys repeat across Blocks field tabs, so a stack's key has both. */
47
+ export function commentStackKey(fieldId: string | null, blockKey: string) {
48
+ return `${commentTabKey(fieldId)}:${blockKey}`;
49
+ }
50
+
51
+ /**
52
+ * Each thread's first highlight on the page, in document order. Property
53
+ * threads, threads not listed, and highlights in a closed tab are left out.
54
+ */
55
+ export function measureCommentBlocks(
56
+ container: ParentNode,
57
+ threads: readonly CommentThread[],
58
+ ): Map<string, CommentBlockPlacement> {
59
+ const byId = new Map(
60
+ threads
61
+ .filter((thread) => !thread.property)
62
+ .map((thread) => [thread.threadId, thread]),
63
+ );
64
+ const placements = new Map<string, CommentBlockPlacement>();
65
+ // Highlights share blocks, and a block's path walks its siblings.
66
+ const paths = new Map<Element, string | null>();
67
+ for (const highlight of container.querySelectorAll("[data-comment-thread]")) {
68
+ const threadId = highlight.getAttribute("data-comment-thread");
69
+ const thread = threadId ? byId.get(threadId) : undefined;
70
+ if (!thread || placements.has(thread.threadId)) continue;
71
+ if (inHiddenBlockFieldPanel(highlight)) continue;
72
+ const found = commentBlock(highlight);
73
+ if (!found) continue;
74
+ let block = paths.get(found.block);
75
+ if (block === undefined) {
76
+ block = blockPath(found);
77
+ paths.set(found.block, block);
78
+ }
79
+ if (block === null) continue;
80
+ placements.set(thread.threadId, {
81
+ key: commentStackKey(commentThreadFieldId(thread), block),
82
+ order: placements.size,
83
+ });
84
+ }
85
+ return placements;
86
+ }
87
+
88
+ /** The blocks holding two or more open threads, in document order. */
89
+ export function groupCommentStacks<
90
+ T extends Pick<CommentThread, "threadId" | "resolved" | "property">,
91
+ >(
92
+ threads: readonly T[],
93
+ placements: ReadonlyMap<string, CommentBlockPlacement>,
94
+ ): CommentStack<T>[] {
95
+ const groups = new Map<string, T[]>();
96
+ for (const thread of threads) {
97
+ if (thread.resolved || thread.property) continue;
98
+ const placement = placements.get(thread.threadId);
99
+ if (!placement) continue;
100
+ const members = groups.get(placement.key) ?? [];
101
+ members.push(thread);
102
+ groups.set(placement.key, members);
103
+ }
104
+ const order = (thread: T) => placements.get(thread.threadId)?.order ?? 0;
105
+ return [...groups]
106
+ .filter(([, members]) => members.length >= 2)
107
+ .map(([key, members]) => ({
108
+ key,
109
+ threads: members.sort((left, right) => order(left) - order(right)),
110
+ }))
111
+ .sort((left, right) => order(left.threads[0]) - order(right.threads[0]));
112
+ }
113
+
114
+ function latestCommentTime(thread: Pick<CommentThread, "comments">) {
115
+ let latest = -Infinity;
116
+ for (const comment of thread.comments) {
117
+ const time = Date.parse(comment.created_at);
118
+ if (time > latest) latest = time;
119
+ }
120
+ return latest;
121
+ }
122
+
123
+ /**
124
+ * The thread a folded stack shows: the one last touched, else the one with
125
+ * the newest comment. Ties go to the first in document order.
126
+ */
127
+ export function commentStackFront<
128
+ T extends Pick<CommentThread, "threadId" | "comments">,
129
+ >(threads: readonly T[], touches: ReadonlyMap<string, number>): T {
130
+ let front = threads[0];
131
+ let frontTouch = touches.get(front.threadId) ?? -Infinity;
132
+ let frontTime = latestCommentTime(front);
133
+ for (const thread of threads.slice(1)) {
134
+ const touch = touches.get(thread.threadId) ?? -Infinity;
135
+ const time = latestCommentTime(thread);
136
+ if (touch > frontTouch || (touch === frontTouch && time > frontTime)) {
137
+ front = thread;
138
+ frontTouch = touch;
139
+ frontTime = time;
140
+ }
141
+ }
142
+ return front;
143
+ }
@@ -1645,6 +1645,14 @@ const enUS = {
1645
1645
  replyCount_few: "{{count}} replies",
1646
1646
  replyCount_many: "{{count}} replies",
1647
1647
  replyCount_other: "{{count}} replies",
1648
+ stackCollapse: "Collapse",
1649
+ stackShowAll_zero: "Show all {{count}} comments here",
1650
+ stackShowAll_one: "Show {{count}} comment here",
1651
+ stackShowAll_two: "Show all {{count}} comments here",
1652
+ stackShowAll_few: "Show all {{count}} comments here",
1653
+ stackShowAll_many: "Show all {{count}} comments here",
1654
+ stackShowAll_other: "Show all {{count}} comments here",
1655
+ stackMore: "+{{count}} more",
1648
1656
  commentActions: "Comment actions",
1649
1657
  checkSaved: "Check saved comment",
1650
1658
  edit: "Edit",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-10-22
4
+ ---
5
+
6
+ Blocks with several comment threads fold into one stack in the margin, which opens in place when you click it or a highlight
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-22
4
+ ---
5
+
6
+ Opening a page no longer saves an edit again after another open tab's save already stored it.
@@ -22,7 +22,7 @@ proof_requirements:
22
22
  ]
23
23
  evidence: []
24
24
  superseded_by: null
25
- last_reviewed: "2026-10-18"
25
+ last_reviewed: "2026-10-22"
26
26
  ---
27
27
 
28
28
  # Comments
@@ -40,6 +40,7 @@ A reviewer comments on two Blocks in a brief, replies with a Page reference, and
40
40
  - A Comment thread is owned by one Page and may target one or several Blocks or ranges in its Blocks fields.
41
41
  - A thread on a Collection row can instead target one Property value, such as Status or a date. It records the Property's name and the value it had when the thread started. Relations, rollups, and formulas are the exception: what they show depends on who is reading, so the thread records no value and each reader sees their own current one. Once a recorded value changes, the thread shows what it was; once the Property is deleted, the thread says so. It never moves itself onto the newer value. A Property thread has no quoted text, and replies follow the ordinary thread rules.
42
42
  - A Collection table shows how many open threads each row has, and marks each cell whose Property has an open thread. The row's count opens the row's Comments. A cell's marker opens a card on the cell with that Property's open threads, where they can be read, replied to, and resolved without leaving the table, and the card can open the row at any of them. Any cell also offers a Comment button to anyone who can comment on the row, which starts a thread on its Property in the same card; on touch screens, where nothing hovers, the button shows on a selected row and starts the thread in the row's peek, or on the row's full page when the Collection opens rows as pages. Counts include only rows the viewer can read, so a row they cannot open adds nothing. One Action reads every open thread in a Collection, with each thread's row and target, so an agent need not read row by row.
43
+ - In a Page's comment margin, a Block with two or more open threads folds into one stack. It shows the thread last clicked or rested on, else the one with the newest comment, with its reply count and the other threads' authors. Clicking the stack, or any highlight in the Block, opens it in place on that thread; Collapse or a click elsewhere folds it with that thread in front, and only one stack is open at a time. A new comment on a folded stack opens on top of it without opening it, and once posted it is the stack's front. Each Blocks-field tab keeps its own stacks; Property threads, the Comments panel, and the narrow-screen popover never stack.
43
44
  - Rich Comment bodies use the shared Blocks-field grammar; replies and mentions retain the same Page authority.
44
45
  - Anchors follow stable Block identity where possible and preserve historical target context after deletion rather than attaching to plausible new text.
45
46
  - Resolve, reopen, edit, reply, and notification operations use shared Actions and record attributable change.
@@ -75,6 +76,10 @@ Given a Comment that mentions a teammate and Claude, when it is sent, both names
75
76
 
76
77
  Given a Collection row whose Status is Blocked, when a commenter starts a thread on Status from the row's peek, the thread reads "Status · Blocked" with no "Highlight unavailable" warning, and the Status cell in the table shows a marker that opens that thread. When Status later changes to Done, the thread reads "was Blocked"; when the Property is deleted, it reads "Status (removed)". A thread started on a Property that is not on the Page, or on a Property and quoted text at once, is rejected.
77
78
 
79
+ ### Read a busy paragraph
80
+
81
+ Given a paragraph with five open threads and another with one, the margin shows one stack beside the first paragraph, with the newest thread in front and "+4 more", and the single thread's card unchanged beside its own paragraph. Resting the pointer on another highlight in the first paragraph brings its thread to the front. Clicking it opens the stack on that thread with a Collapse row above; Collapse folds it again with that thread still in front. Commenting on new text in that paragraph opens the composer on top of the folded stack, and the posted comment becomes its front.
82
+
78
83
  ### Comment on a cell from the table
79
84
 
80
85
  Given a Collection table and a commenter who cannot edit its rows, when they hover a row's Due cell, a Comment button appears. Posting from the card it opens starts a thread that reads "Due · Nov 2, 2026" without opening the row, and the cell then shows a marker and the row's count goes up. Clicking the marker reopens the card on that thread, where it can be answered, and Open row opens the row's peek on it. When an editor resolves the thread from the same card, the marker and the count clear, and an editor who clicks the cell itself still edits its value.
@@ -137,6 +142,8 @@ A Comment AI proposal now renders in its source thread in place of the receipt r
137
142
 
138
143
  A thread can target one Property value. `content-database-comments.db.test.ts` proves the recorded name and value survive a value change, a relation's linked rows are never recorded for other readers, invalid targets are rejected, per-row and per-Property open counts follow row access and resolution, and the Collection-wide Comment read returns only readable rows. `comment-property-target.test.ts` covers how each Property type reads as text. Rich-text Property values do not yet offer a Property target; they keep body-style quoted Comments.
139
144
 
145
+ A Block with several open threads stacks in the margin. `comment-stacks.test.ts` covers how threads group by Block and which one fronts a stack; `CommentsSidebar.stacks.test.tsx` covers folding, hover, opening from the card or a highlight, Collapse and focus, one open stack, and new comments on folded and open stacks; `CommentsSidebar.stack-tabs.test.tsx` covers Blocks-field tabs and Property threads. A recorded local run walked the "Read a busy paragraph" story with fake data.
146
+
140
147
  ## Proof plan
141
148
 
142
149
  1. Create, reply, mention, resolve, reopen, edit, and delete Comments through UI and Actions.
@@ -56,14 +56,14 @@ export const interactLinksBridgeScript: string = `"use strict";
56
56
  }
57
57
  e.preventDefault();
58
58
  try {
59
- window.parent.postMessage(
60
- {
61
- type: "prototype-navigate",
62
- href: target,
63
- screen: screenName(target)
64
- },
65
- "*"
66
- );
59
+ var message = {
60
+ type: "prototype-navigate",
61
+ href: target,
62
+ screen: screenName(target)
63
+ };
64
+ var transition = link.getAttribute("data-an-prototype-transition");
65
+ if (transition !== null) message.transition = transition;
66
+ window.parent.postMessage(message, "*");
67
67
  } catch (_err) {
68
68
  return;
69
69
  }
@@ -5,6 +5,10 @@ from the command menu (Cmd+K → "What's new") or from Settings.
5
5
 
6
6
  ## 2026-10-22
7
7
 
8
+ ### Added
9
+
10
+ - Prototype links can play code-authored screen transitions.
11
+
8
12
  ### Fixed
9
13
 
10
14
  - Collapsed shader layers no longer interrupt other shaders on the canvas
@@ -53,6 +57,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
53
57
 
54
58
  ### Fixed
55
59
 
60
+ - The Design / Full app toggle on the home page no longer hides behind the Connect AI card.
56
61
  - Read-only Design views render native shader previews in nested frames.
57
62
  - Selected screens with shader effects can be exported together as a PNG
58
63
  - Shader previews stay aligned while you zoom.
@@ -27,6 +27,10 @@ import {
27
27
  import { editedStableSourceElement } from "@shared/code-layer";
28
28
  import type { InteractionState } from "@shared/interaction-states";
29
29
  import type { MotionEasingVariable } from "@shared/motion-easing";
30
+ import {
31
+ decodeMotionPrototypeTransitionAttribute,
32
+ type MotionPrototypeTransitionDecodeResult,
33
+ } from "@shared/motion-prototype-transition";
30
34
  import {
31
35
  appendPenNode,
32
36
  clonePenPath,
@@ -951,7 +955,12 @@ interface DesignCanvasProps {
951
955
  repromptDraftRequest?: RepromptDraftRequest | null;
952
956
  onRepromptDraftConsumed?: (nonce: number) => void;
953
957
  nodeRewriteCanvasTarget?: boolean;
954
- onPrototypeNavigate?: (screen: string, href: string) => void;
958
+ onPrototypeNavigate?: (
959
+ screen: string,
960
+ href: string,
961
+ transition: MotionPrototypeTransitionDecodeResult,
962
+ sourceFrame: HTMLIFrameElement,
963
+ ) => void;
955
964
  motionTracks?: MotionTrackWire[];
956
965
  motionEasingVariables?: MotionEasingVariable[];
957
966
  motionDefaultEase?: string;
@@ -5775,9 +5784,16 @@ export function DesignCanvas({
5775
5784
  return;
5776
5785
  }
5777
5786
  if (e.data.type === "prototype-navigate") {
5787
+ const sourceFrame = iframeRef.current;
5788
+ if (!sourceFrame || sourceFrame.contentWindow !== e.source) return;
5789
+ const transition = decodeMotionPrototypeTransitionAttribute(
5790
+ e.data.transition,
5791
+ );
5778
5792
  onPrototypeNavigate?.(
5779
5793
  String(e.data.screen || ""),
5780
5794
  String(e.data.href || ""),
5795
+ transition,
5796
+ sourceFrame,
5781
5797
  );
5782
5798
  return;
5783
5799
  }
@@ -8517,6 +8533,7 @@ export function DesignCanvas({
8517
8533
  ? ""
8518
8534
  : undefined,
8519
8535
  }}
8536
+ data-an-prototype-preview={interactMode ? "" : undefined}
8520
8537
  data-screen-iframe-id={
8521
8538
  boardSurface
8522
8539
  ? undefined
@@ -92,14 +92,14 @@
92
92
 
93
93
  e.preventDefault();
94
94
  try {
95
- (window.parent as Window).postMessage(
96
- {
97
- type: "prototype-navigate",
98
- href: target,
99
- screen: screenName(target),
100
- },
101
- "*",
102
- );
95
+ var message: Record<string, unknown> = {
96
+ type: "prototype-navigate",
97
+ href: target,
98
+ screen: screenName(target),
99
+ };
100
+ var transition = link.getAttribute("data-an-prototype-transition");
101
+ if (transition !== null) message.transition = transition;
102
+ (window.parent as Window).postMessage(message, "*");
103
103
  } catch (_err) {
104
104
  // coercion-ok: the editor is gone, so there is no screen to switch to.
105
105
  return;
@@ -684,6 +684,14 @@ export default function PromptPopover({
684
684
  const showCreativeContextPicker =
685
685
  Boolean(onCreativeContextChange) &&
686
686
  (creativeContextsLoading || creativeContexts.length > 0);
687
+ const creationModeToggle =
688
+ creationMode && onCreationModeChange ? (
689
+ <CreationModeToggle
690
+ mode={creationMode}
691
+ onChange={onCreationModeChange}
692
+ disabled={loading || uploading || submitting}
693
+ />
694
+ ) : null;
687
695
  const Composer = composerComponent ?? LazyPromptComposer;
688
696
  const promptComposer = (
689
697
  <Composer
@@ -729,25 +737,19 @@ export default function PromptPopover({
729
737
  : `seed:${initialTextKey ?? 0}`
730
738
  }
731
739
  contextMenuItems={contextMenuItems ?? []}
740
+ // Inside the composer so an attached setup card above meets the box directly.
741
+ toolbarSlot={inline ? creationModeToggle : undefined}
732
742
  />
733
743
  );
734
744
 
735
745
  const content = (
736
746
  <>
737
- {(!inline || creationMode) && (
747
+ {!inline && (
738
748
  <div className="flex items-center justify-between gap-2 px-3.5 pt-3 pb-2">
739
- {!inline && (
740
- <span className="text-sm font-medium text-foreground/90">
741
- {title}
742
- </span>
743
- )}
744
- {creationMode && onCreationModeChange ? (
745
- <CreationModeToggle
746
- mode={creationMode}
747
- onChange={onCreationModeChange}
748
- disabled={loading || uploading || submitting}
749
- />
750
- ) : null}
749
+ <span className="text-sm font-medium text-foreground/90">
750
+ {title}
751
+ </span>
752
+ {creationModeToggle}
751
753
  </div>
752
754
  )}
753
755
 
@@ -1459,7 +1459,7 @@ export default {
1459
1459
  sourceWriteUnavailable:
1460
1460
  "Motion code can currently be written only to inline HTML designs.",
1461
1461
  unresolvedStylesheets:
1462
- "Resolve external stylesheets before editing or adding Motion.",
1462
+ "Resolve external or runtime-generated styles before editing or adding Motion.",
1463
1463
  sourceConflict:
1464
1464
  "Motion source changed while this timeline had unsaved edits. Reload from code to discard those edits and use the current source.",
1465
1465
  reloadFromSource: "Reload from code",
@@ -45,6 +45,7 @@ import {
45
45
  getScreenFrameOriginCanvas,
46
46
  shouldPopToOverviewOnZoomChange,
47
47
  } from "../overview-camera";
48
+ import { cancelPrototypeNavigationTransition } from "../prototype-navigation-transition";
48
49
  import { computeInteractZoomToFit } from "../responsive-interact";
49
50
  import { hasSelectableCodeLayerParent } from "../selection-state";
50
51
  import {
@@ -306,8 +307,11 @@ export function useEditorModes({
306
307
  }, []);
307
308
 
308
309
  const focusOverviewScreen = useCallback(
309
- (screenId: string) => {
310
+ (screenId: string, options?: { fromPrototypeNavigation?: boolean }) => {
310
311
  if (!files.some((file) => file.id === screenId)) return;
312
+ if (!options?.fromPrototypeNavigation) {
313
+ cancelPrototypeNavigationTransition();
314
+ }
311
315
  const geometry = exportCanvasFrameGeometryById[screenId];
312
316
  const plan = geometry
313
317
  ? getFocusedScreenNavigationPlan({
@@ -507,9 +511,12 @@ export function useEditorModes({
507
511
  pendingLiveEditsAlreadyHandled?: boolean;
508
512
  targetFileId?: string;
509
513
  keepInteractDevice?: boolean;
514
+ fromPrototypeNavigation?: boolean;
510
515
  },
511
- ) =>
512
- runModeChange(
516
+ ) => {
517
+ const { fromPrototypeNavigation, ...modeOptions } = options ?? {};
518
+ if (!fromPrototypeNavigation) cancelPrototypeNavigationTransition();
519
+ return runModeChange(
513
520
  {
514
521
  activeFile,
515
522
  annotateLab,
@@ -548,8 +555,9 @@ export function useEditorModes({
548
555
  viewModeRef,
549
556
  },
550
557
  next,
551
- options,
552
- ),
558
+ options ? modeOptions : undefined,
559
+ );
560
+ },
553
561
  [
554
562
  activeFile,
555
563
  annotateLab,
@@ -707,7 +715,8 @@ export function useEditorModes({
707
715
  ]);
708
716
 
709
717
  const handleSidebarScreenSelect = useCallback(
710
- (screenId: string) => focusOverviewScreen(screenId),
718
+ (screenId: string, options?: { fromPrototypeNavigation?: boolean }) =>
719
+ focusOverviewScreen(screenId, options),
711
720
  [focusOverviewScreen],
712
721
  );
713
722
 
@@ -1,5 +1,6 @@
1
1
  import { openCommandMenu } from "@agent-native/toolkit/app/shared";
2
2
  import { isBoardFile } from "@shared/board-file";
3
+ import type { MotionPrototypeTransitionDecodeResult } from "@shared/motion-prototype-transition";
3
4
  import { isRunningAppSourceType } from "@shared/source-mode";
4
5
  import { useState, useEffect, useCallback } from "react";
5
6
  import { toast } from "sonner";
@@ -43,6 +44,10 @@ import {
43
44
  resolveOverviewScreenSourceType,
44
45
  shouldUseRuntimeLayerProjection,
45
46
  } from "../pending-edits";
47
+ import {
48
+ getPrototypeDestinationFrameId,
49
+ runPrototypeNavigationTransition,
50
+ } from "../prototype-navigation-transition";
46
51
  import { findInteractLinkTarget } from "../screen-command-utils";
47
52
  import {
48
53
  getContentSignature,
@@ -451,17 +456,36 @@ export function useEditorScreenRendering({
451
456
  // A link to another screen in the Interact preview switches the previewed
452
457
  // screen, keeping Interact and its device, the same hop the screen picker makes.
453
458
  const handleInteractLinkNavigate = useCallback(
454
- (screen: string) => {
459
+ (
460
+ screen: string,
461
+ _href: string,
462
+ transition: MotionPrototypeTransitionDecodeResult,
463
+ sourceFrame: HTMLIFrameElement,
464
+ ) => {
455
465
  const target = findInteractLinkTarget(
456
466
  files,
457
467
  screen,
458
468
  activeFileIdRef.current,
459
469
  );
460
470
  if (!target) return;
461
- handleModeChange("interact", {
462
- targetFileId: target.id,
463
- keepInteractDevice: true,
464
- });
471
+ runPrototypeNavigationTransition(
472
+ transition.status === "valid" ? transition.transition : undefined,
473
+ () => {
474
+ handleModeChange("interact", {
475
+ targetFileId: target.id,
476
+ keepInteractDevice: true,
477
+ fromPrototypeNavigation: true,
478
+ });
479
+ },
480
+ undefined,
481
+ {
482
+ sourceFrame,
483
+ destinationFrameId: getPrototypeDestinationFrameId(
484
+ target.id,
485
+ sourceFrame,
486
+ ),
487
+ },
488
+ );
465
489
  },
466
490
  [files, handleModeChange],
467
491
  );