@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/hubspot/SKILL.md +5 -3
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/CHANGELOG.md +5 -0
- package/corpus/templates/content/app/components/editor/CommentStack.tsx +301 -0
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +651 -149
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +19 -14
- package/corpus/templates/content/app/components/editor/block-field-comment-threads.ts +15 -0
- package/corpus/templates/content/app/components/editor/block-field-comments.tsx +5 -9
- package/corpus/templates/content/app/components/editor/comment-stacks.ts +143 -0
- package/corpus/templates/content/app/i18n-data.ts +8 -0
- package/corpus/templates/content/changelog/2026-10-22-blocks-with-several-comment-threads-fold-into-one-stack-in-t.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-opening-a-page-no-longer-saves-an-edit-again-after-another-o.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +8 -1
- package/corpus/templates/design/.generated/bridge/interact-links.generated.ts +8 -8
- package/corpus/templates/design/CHANGELOG.md +5 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +18 -1
- package/corpus/templates/design/app/components/design/bridge/interact-links.bridge.ts +8 -8
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +15 -13
- package/corpus/templates/design/app/i18n/en-US.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +15 -6
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +29 -5
- package/corpus/templates/design/app/pages/design-editor/prototype-navigation-transition.ts +423 -0
- package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +35 -9
- package/corpus/templates/design/changelog/2026-10-20-the-design-full-app-toggle-on-the-home-page-no-longer-hides-.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-prototype-links-can-play-code-authored-screen-transitions.md +6 -0
- package/corpus/templates/design/shared/motion-authored-css-import.ts +938 -7
- package/corpus/templates/design/shared/motion-html-source.ts +1689 -135
- package/corpus/templates/design/shared/motion-prototype-transition.ts +329 -0
- package/corpus/templates/design/shared/motion-react-source.ts +2154 -126
- package/corpus/templates/design/shared/motion-text-runtime-v1.fixture.txt +221 -0
- 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
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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",
|
|
@@ -22,7 +22,7 @@ proof_requirements:
|
|
|
22
22
|
]
|
|
23
23
|
evidence: []
|
|
24
24
|
superseded_by: null
|
|
25
|
-
last_reviewed: "2026-10-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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?: (
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
{
|
|
747
|
+
{!inline && (
|
|
738
748
|
<div className="flex items-center justify-between gap-2 px-3.5 pt-3 pb-2">
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
718
|
+
(screenId: string, options?: { fromPrototypeNavigation?: boolean }) =>
|
|
719
|
+
focusOverviewScreen(screenId, options),
|
|
711
720
|
[focusOverviewScreen],
|
|
712
721
|
);
|
|
713
722
|
|
package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx
CHANGED
|
@@ -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
|
-
(
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
);
|