@agent-native/core-corpus 0.217.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 (81) 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/analytics/CHANGELOG.md +2 -0
  4. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
  5. package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  6. package/corpus/templates/assets/CHANGELOG.md +6 -0
  7. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
  8. package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  9. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
  10. package/corpus/templates/brain/app/routes/sources.tsx +1 -1
  11. package/corpus/templates/brain/vitest.config.ts +7 -0
  12. package/corpus/templates/calendar/CHANGELOG.md +6 -0
  13. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
  14. package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
  15. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
  16. package/corpus/templates/clips/CHANGELOG.md +7 -20
  17. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
  18. package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
  19. package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
  20. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
  21. package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
  22. package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
  23. package/corpus/templates/community/qr/package.json +1 -1
  24. package/corpus/templates/content/CHANGELOG.md +7 -0
  25. package/corpus/templates/content/app/components/editor/CommentStack.tsx +301 -0
  26. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +770 -179
  27. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +75 -27
  28. package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
  29. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
  30. package/corpus/templates/content/app/components/editor/block-field-comment-threads.ts +15 -0
  31. package/corpus/templates/content/app/components/editor/block-field-comments.tsx +5 -9
  32. package/corpus/templates/content/app/components/editor/comment-stacks.ts +143 -0
  33. package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
  34. package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
  35. package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
  36. package/corpus/templates/content/app/global.css +31 -0
  37. package/corpus/templates/content/app/i18n-data.ts +8 -0
  38. package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
  39. package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
  40. package/corpus/templates/content/changelog/2026-10-22-blocks-with-several-comment-threads-fold-into-one-stack-in-t.md +6 -0
  41. package/corpus/templates/content/changelog/2026-10-22-opening-a-page-no-longer-saves-an-edit-again-after-another-o.md +6 -0
  42. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +8 -1
  43. package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
  44. package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
  45. package/corpus/templates/content/shared/suggestion-text.ts +20 -5
  46. package/corpus/templates/design/.generated/bridge/interact-links.generated.ts +8 -8
  47. package/corpus/templates/design/CHANGELOG.md +8 -0
  48. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +18 -1
  49. package/corpus/templates/design/app/components/design/bridge/interact-links.bridge.ts +8 -8
  50. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +15 -13
  51. package/corpus/templates/design/app/i18n/en-US.ts +1 -1
  52. package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
  53. package/corpus/templates/design/app/pages/Index.tsx +18 -64
  54. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +15 -6
  55. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +29 -5
  56. package/corpus/templates/design/app/pages/design-editor/prototype-navigation-transition.ts +423 -0
  57. package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +35 -9
  58. package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
  59. package/corpus/templates/design/changelog/2026-10-20-the-design-full-app-toggle-on-the-home-page-no-longer-hides-.md +6 -0
  60. package/corpus/templates/design/changelog/2026-10-22-prototype-links-can-play-code-authored-screen-transitions.md +6 -0
  61. package/corpus/templates/design/shared/motion-authored-css-import.ts +938 -7
  62. package/corpus/templates/design/shared/motion-html-source.ts +1689 -135
  63. package/corpus/templates/design/shared/motion-prototype-transition.ts +329 -0
  64. package/corpus/templates/design/shared/motion-react-source.ts +2154 -126
  65. package/corpus/templates/design/shared/motion-text-runtime-v1.fixture.txt +221 -0
  66. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  67. package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
  68. package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  69. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
  70. package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
  71. package/corpus/templates/plan/CHANGELOG.md +6 -0
  72. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
  73. package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  74. package/corpus/templates/slides/CHANGELOG.md +1 -0
  75. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
  76. package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
  77. package/corpus/templates/slides/app/pages/Index.tsx +7 -2
  78. package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
  79. package/package.json +1 -1
  80. package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
  81. package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
@@ -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: fixed
3
+ date: 2026-10-16
4
+ ---
5
+
6
+ Accepting a suggestion on a Page that changed since it was suggested no longer shows "Something went wrong"
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-20
4
+ ---
5
+
6
+ Suggested edits that add, remove, split, or merge paragraphs, line breaks, and list items now show where the break goes and save as one clear suggestion
@@ -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.
@@ -483,6 +483,9 @@ function diffTokens(text: string): string[] {
483
483
  // is no longer than either, as diff-match-patch's semantic cleanup does.
484
484
  // Equalities between replacements stay, so separate word swaps remain
485
485
  // separately reviewable, and a line never folds, so blocks stay separate.
486
+ // The exception is a line the merged run begins or ends with: that is one
487
+ // insertion (text plus a new paragraph) Myers aligned across the existing
488
+ // separator, and it would otherwise split whenever another hunk exists.
486
489
  type ChangeRun = {
487
490
  deleted: string;
488
491
  inserted: string;
@@ -518,12 +521,17 @@ function foldShortEqualities(parts: DiffPart[]): DiffPart[] {
518
521
  const right = segments[index + 1];
519
522
  if (typeof equality !== "string" || typeof left !== "object") continue;
520
523
  if (typeof right !== "object") continue;
524
+ const insertions = left.ownDeleted === 0 && right.ownDeleted === 0;
521
525
  const oneSided =
522
- (left.ownDeleted === 0 && right.ownDeleted === 0) ||
523
- (left.ownInserted === 0 && right.ownInserted === 0);
526
+ insertions || (left.ownInserted === 0 && right.ownInserted === 0);
527
+ const merged = insertions
528
+ ? left.inserted + equality + right.inserted
529
+ : left.deleted + equality + right.deleted;
524
530
  if (
525
531
  !oneSided ||
526
- equality.includes("\n") ||
532
+ (equality.includes("\n") &&
533
+ !merged.startsWith(equality) &&
534
+ !merged.endsWith(equality)) ||
527
535
  equality.length > Math.max(left.ownDeleted, left.ownInserted) ||
528
536
  equality.length > Math.max(right.ownDeleted, right.ownInserted)
529
537
  )
@@ -814,16 +822,24 @@ function wholeWordReplacements(
814
822
  const removed = operation.before.changedText;
815
823
  const inserted = operation.after.changedText;
816
824
  const lexicalChange = /[\p{L}\p{M}\p{N}]/u.test(removed + inserted);
825
+ // An edge that is a line break, as when a whole paragraph is deleted,
826
+ // sits beside the neighboring block's word without splitting it.
827
+ const lineEdge = (edge: (text: string) => boolean) =>
828
+ edge(removed) && (!inserted || edge(inserted));
829
+ const widensBefore =
830
+ isWordBefore(before, from) && !lineEdge((text) => text.startsWith("\n"));
831
+ const widensAfter =
832
+ isWordAt(before, to) && !lineEdge((text) => text.endsWith("\n"));
817
833
  let group = { from, to, members: [operation] };
818
834
  if (
819
835
  lexicalChange &&
820
836
  !/<br\/?\s*>/u.test(removed + inserted) &&
821
837
  (removed || !/\s/u.test(inserted)) &&
822
- (isWordBefore(before, from) || isWordAt(before, to))
838
+ (widensBefore || widensAfter)
823
839
  ) {
824
- while (isWordBefore(before, group.from))
840
+ while (widensBefore && isWordBefore(before, group.from))
825
841
  group.from -= characterBefore(before, group.from).length;
826
- while (isWordAt(before, group.to))
842
+ while (widensAfter && isWordAt(before, group.to))
827
843
  group.to += characterAt(before, group.to).length;
828
844
  }
829
845
  // Two edits inside one word are one review decision.
@@ -1022,6 +1038,7 @@ export function markdownSuggestionOperationsForReplacements(input: {
1022
1038
  const [{ from, to }] = replacements;
1023
1039
  if (
1024
1040
  operations.length > 1 &&
1041
+ !before.slice(from, to).includes("\n") &&
1025
1042
  operations.every(
1026
1043
  (operation) =>
1027
1044
  operation.anchor.from >= from && operation.anchor.to <= to,
@@ -650,12 +650,21 @@ function resolveVerbatimRun(
650
650
  export type SuggestionFormattingSlicePart =
651
651
  | { type: "text"; text: string; marks: NonNullable<PMNode["marks"]> }
652
652
  | { type: "break"; text: string }
653
- | { type: "indent"; text: string };
653
+ | { type: "indent"; text: string }
654
+ | { type: "block"; text: string };
654
655
 
656
+ const EMPTY_BLOCK = "<empty-block/>";
657
+ // The syntax that opens a line's block: a heading, list item, task, or quote.
658
+ const BLOCK_MARKER = /^(?:#{1,6} |(?:> )+|[-*+] (?:\[[ xX]\] )?|\d+[.)] )/;
659
+
660
+ // `blocks` admits block syntax and empty paragraphs. Without it a gap holds
661
+ // only line breaks and indentation, which is what places a zero-width
662
+ // boundary on one side of a gap or the other.
655
663
  function structuralGapParts(
656
664
  source: string,
657
665
  from: number,
658
666
  to: number,
667
+ blocks = true,
659
668
  ): SuggestionFormattingSlicePart[] | null {
660
669
  const parts: SuggestionFormattingSlicePart[] = [];
661
670
  let offset = from;
@@ -678,15 +687,36 @@ function structuralGapParts(
678
687
  source.lastIndexOf("\r", offset - 1),
679
688
  ) + 1;
680
689
  const atLineStart = /^\t*$/.test(source.slice(lineStart, offset));
681
- if (!atLineStart || source[offset] !== "\t") return null;
682
- const indentFrom = offset;
683
- while (offset < to && source[offset] === "\t") offset += 1;
684
- parts.push({
685
- type: "indent",
686
- text: "⇥".repeat(offset - indentFrom),
687
- });
688
- const heading = /^(?:#{1,6}) /.exec(source.slice(offset, to));
689
- if (heading) offset += heading[0].length;
690
+ if (!atLineStart) return null;
691
+ if (source[offset] === "\t") {
692
+ const indentFrom = offset;
693
+ while (offset < to && source[offset] === "\t") offset += 1;
694
+ parts.push({
695
+ type: "indent",
696
+ text: "⇥".repeat(offset - indentFrom),
697
+ });
698
+ const heading = /^(?:#{1,6}) /.exec(source.slice(offset, to));
699
+ if (heading) offset += heading[0].length;
700
+ continue;
701
+ }
702
+ if (!blocks) return null;
703
+ // An empty paragraph or a block's opening syntax has no text of its own,
704
+ // so it is shown as written: a pilcrow for the paragraph, the marker
705
+ // otherwise. A marker is only whole when the slice holds all of it.
706
+ const lineEnd = offset + EMPTY_BLOCK.length;
707
+ if (
708
+ source.startsWith(EMPTY_BLOCK, offset) &&
709
+ lineEnd <= to &&
710
+ (lineEnd === source.length || /[\r\n]/.test(source[lineEnd]!))
711
+ ) {
712
+ parts.push({ type: "block", text: "¶" });
713
+ offset = lineEnd;
714
+ continue;
715
+ }
716
+ const marker = BLOCK_MARKER.exec(source.slice(offset, to));
717
+ if (!marker) return null;
718
+ parts.push({ type: "block", text: marker[0] });
719
+ offset += marker[0].length;
690
720
  }
691
721
  return parts;
692
722
  }
@@ -894,7 +924,7 @@ export function suggestionFormattingSourceRange(
894
924
  sourceTo: run.sourceFrom,
895
925
  offset: run.from,
896
926
  structural: Boolean(
897
- structuralGapParts(source, previousSourceTo, run.sourceFrom),
927
+ structuralGapParts(source, previousSourceTo, run.sourceFrom, false),
898
928
  ),
899
929
  afterText: index > 0,
900
930
  beforeText: true,
@@ -910,7 +940,7 @@ export function suggestionFormattingSourceRange(
910
940
  sourceTo: source.length,
911
941
  offset: previousTextTo,
912
942
  structural: Boolean(
913
- structuralGapParts(source, previousSourceTo, source.length),
943
+ structuralGapParts(source, previousSourceTo, source.length, false),
914
944
  ),
915
945
  afterText: mapped.runs.length > 0,
916
946
  beforeText: false,
@@ -983,13 +1013,13 @@ export function suggestionFormattingSourceRange(
983
1013
  if (
984
1014
  gap.afterText &&
985
1015
  offset > gap.sourceFrom &&
986
- structuralGapParts(source, gap.sourceFrom, offset)
1016
+ structuralGapParts(source, gap.sourceFrom, offset, false)
987
1017
  )
988
1018
  candidates.push({ offset: gap.offset, affinity: "left" });
989
1019
  if (
990
1020
  gap.beforeText &&
991
1021
  offset < gap.sourceTo &&
992
- (structuralGapParts(source, offset, gap.sourceTo) ||
1022
+ (structuralGapParts(source, offset, gap.sourceTo, false) ||
993
1023
  !/[\r\n]/.test(opening) ||
994
1024
  (gap.beforeCode && /^[^\r\n]*\r?\n$/.test(opening)))
995
1025
  )
@@ -11,6 +11,8 @@ export type SuggestionPresentationContext = {
11
11
  export type SuggestionPresentationNode =
12
12
  | { type: "text"; value: string }
13
13
  | { type: "indent"; value: string }
14
+ | { type: "block"; value: string }
15
+ | { type: "lineBreak" }
14
16
  | {
15
17
  type: "strong" | "emphasis" | "strike" | "code" | "underline";
16
18
  children: SuggestionPresentationNode[];
@@ -120,7 +122,12 @@ function stripInlineContext(
120
122
  const value = node.value.slice(context.length);
121
123
  return value ? { type: "text", value } : null;
122
124
  }
123
- if (node.type === "indent") return node;
125
+ if (
126
+ node.type === "indent" ||
127
+ node.type === "block" ||
128
+ node.type === "lineBreak"
129
+ )
130
+ return node;
124
131
  const children = node.children.flatMap((child) => {
125
132
  const strippedChild = stripNode(child);
126
133
  return strippedChild ? [strippedChild] : [];
@@ -246,11 +253,19 @@ function computeSuggestionTextPresentationForSource(
246
253
  ? text.replace(/ /g, "·").replace(/\t/g, "⇥")
247
254
  : text
248
255
  ).replace(/\n/g, "↵");
249
- return parts.flatMap((part) =>
250
- part.type === "indent"
251
- ? [{ type: "indent" as const, value: part.text }]
256
+ // A line break beside text breaks the line; on its own it stays a visible ↵.
257
+ const breaksLines = parts.some(
258
+ (part) => part.type === "text" && /\S/.test(part.text),
259
+ );
260
+ return parts.flatMap((part): SuggestionPresentationNode[] =>
261
+ part.type === "indent" || part.type === "block"
262
+ ? [{ type: part.type, value: part.text }]
252
263
  : part.type === "break"
253
- ? [{ type: "text" as const, value: part.text }]
264
+ ? [
265
+ breaksLines
266
+ ? { type: "lineBreak" }
267
+ : { type: "text", value: part.text },
268
+ ]
254
269
  : presentationNode(
255
270
  { type: "text", text: visibleText(part.text), marks: part.marks },
256
271
  (text) => text,
@@ -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
@@ -15,10 +19,12 @@ from the command menu (Cmd+K → "What's new") or from Settings.
15
19
 
16
20
  ### Added
17
21
 
22
+ - A Getting started checklist at the bottom of the sidebar tracks your first design, brand, sharing, teammates, and templates, and checks steps off as you finish them
18
23
  - Pulsing Border adds moving color highlights along an element’s outline, with concentrated bright cores, colored falloff, and controls for motion and shape.
19
24
 
20
25
  ### Improved
21
26
 
27
+ - Templates now open a read-only preview from the card, and the home library shows more starters, including dashboard, messaging, task board and design studio screens.
22
28
  - Effect thumbnails now show how shaders treat the Agent-Native mark against a quiet tonal background, making their differences easier to judge.
23
29
  - Native shader previews use less GPU memory when many mesh effects are visible.
24
30
  - Pulsing Border uses the standard Linear and Angular gradient picker for its moving border colors, with up to eight editable stops and a separate Background color. Thickness sets the bright edge, Softness feathers it, Bloom adjusts glow brightness, and Glow spread sets the halo radius. Smoke, margins, offsets, and layout settings stay grouped in collapsible sections.
@@ -51,6 +57,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
51
57
 
52
58
  ### Fixed
53
59
 
60
+ - The Design / Full app toggle on the home page no longer hides behind the Connect AI card.
54
61
  - Read-only Design views render native shader previews in nested frames.
55
62
  - Selected screens with shader effects can be exported together as a PNG
56
63
  - Shader previews stay aligned while you zoom.
@@ -128,6 +135,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
128
135
 
129
136
  ### Improved
130
137
 
138
+ - Design pages use localized visibility badges and a shared ownership filter.
131
139
  - Animated shader fills render together more efficiently and pause work outside the visible canvas.
132
140
  - Shader property sliders preview changes while dragging
133
141
  - Canvas selection, layer controls, text insertion, grouping, and auto-layout behave more predictably.
@@ -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",
@@ -14,7 +14,7 @@ import {
14
14
  useSetPageTitle,
15
15
  } from "@agent-native/toolkit/app-shell";
16
16
  import { ShareButton } from "@agent-native/toolkit/app/sharing";
17
- import { VisibilityBadge } from "@agent-native/toolkit/sharing";
17
+ import { VisibilityBadge } from "@agent-native/toolkit/app/sharing/VisibilityBadge";
18
18
  import {
19
19
  IconCheckbox,
20
20
  IconChecks,
@@ -36,13 +36,13 @@ import {
36
36
  type TiptapComposerHandle,
37
37
  } from "@agent-native/toolkit/app/chat/composer/index";
38
38
  import { ClientOnly } from "@agent-native/toolkit/app/shared";
39
+ import { OwnershipFilter } from "@agent-native/toolkit/app/sharing/OwnershipFilter";
39
40
  import { designTemplateRetryKey } from "@shared/design-template-retry";
40
41
  import { FULL_APP_BUILDING_LAB } from "@shared/labs";
41
42
  import { derivePromptTitle } from "@shared/prompt-title";
42
43
  import {
43
44
  IconArrowRight,
44
45
  IconChecks,
45
- IconChevronDown,
46
46
  IconChevronLeft,
47
47
  IconChevronRight,
48
48
  IconPlus,
@@ -91,8 +91,6 @@ import {
91
91
  DropdownMenu,
92
92
  DropdownMenuContent,
93
93
  DropdownMenuItem,
94
- DropdownMenuRadioGroup,
95
- DropdownMenuRadioItem,
96
94
  DropdownMenuTrigger,
97
95
  } from "@/components/ui/dropdown-menu";
98
96
  import { Input } from "@/components/ui/input";
@@ -616,11 +614,6 @@ export default function Index() {
616
614
  ? chosenDesignSystemId
617
615
  : templateDesignSystemId;
618
616
 
619
- const ownerFilterLabels: Record<DesignFilter, string> = {
620
- all: t("home.ownedByAnyone"),
621
- mine: t("home.ownedByMe"),
622
- "not-mine": t("home.sharedWithMe"),
623
- };
624
617
  const showAuthors = designFilter !== "mine";
625
618
  const selectedDesignCount = selectedDesignIds.size;
626
619
  const isSelectingDesigns = selectedDesignCount > 0;
@@ -743,17 +736,19 @@ export default function Index() {
743
736
  );
744
737
  }, []);
745
738
 
746
- const handleDesignFilterChange = useCallback((next: string) => {
747
- if (next !== "all" && next !== "mine" && next !== "not-mine") return;
748
- designFilterWasSelectedRef.current = true;
749
- const nextFilter: DesignFilter = next;
750
- setDesignFilter(nextFilter);
751
- writeStoredDesignFilter(nextFilter);
752
- setPage(1);
753
- setSelectedDesignIds((current) =>
754
- current.size === 0 ? current : new Set(),
755
- );
756
- }, []);
739
+ const handleDesignFilterChange = useCallback(
740
+ (next: "all" | "mine" | "shared") => {
741
+ designFilterWasSelectedRef.current = true;
742
+ const nextFilter: DesignFilter = next === "shared" ? "not-mine" : next;
743
+ setDesignFilter(nextFilter);
744
+ writeStoredDesignFilter(nextFilter);
745
+ setPage(1);
746
+ setSelectedDesignIds((current) =>
747
+ current.size === 0 ? current : new Set(),
748
+ );
749
+ },
750
+ [],
751
+ );
757
752
 
758
753
  useEffect(() => {
759
754
  if (
@@ -1572,51 +1567,10 @@ export default function Index() {
1572
1567
  }
1573
1568
  search={searchInput}
1574
1569
  recentActions={
1575
- <DropdownMenu>
1576
- <DropdownMenuTrigger asChild>
1577
- <Button
1578
- type="button"
1579
- variant="outline"
1580
- size="sm"
1581
- aria-label={ownerFilterLabels[designFilter]}
1582
- className="h-9 shrink-0 gap-2"
1583
- >
1584
- <span className="grid">
1585
- {Object.entries(ownerFilterLabels).map(
1586
- ([filter, label]) => (
1587
- <span
1588
- key={filter}
1589
- aria-hidden="true"
1590
- className="invisible col-start-1 row-start-1 whitespace-nowrap"
1591
- >
1592
- {label}
1593
- </span>
1594
- ),
1595
- )}
1596
- <span className="col-start-1 row-start-1 whitespace-nowrap">
1597
- {ownerFilterLabels[designFilter]}
1598
- </span>
1599
- </span>
1600
- <IconChevronDown className="size-4" aria-hidden="true" />
1601
- </Button>
1602
- </DropdownMenuTrigger>
1603
- <DropdownMenuContent align="start">
1604
- <DropdownMenuRadioGroup
1605
- value={designFilter}
1606
- onValueChange={handleDesignFilterChange}
1607
- >
1608
- <DropdownMenuRadioItem value="all" indicator="check">
1609
- {ownerFilterLabels.all}
1610
- </DropdownMenuRadioItem>
1611
- <DropdownMenuRadioItem value="mine" indicator="check">
1612
- {ownerFilterLabels.mine}
1613
- </DropdownMenuRadioItem>
1614
- <DropdownMenuRadioItem value="not-mine" indicator="check">
1615
- {ownerFilterLabels["not-mine"]}
1616
- </DropdownMenuRadioItem>
1617
- </DropdownMenuRadioGroup>
1618
- </DropdownMenuContent>
1619
- </DropdownMenu>
1570
+ <OwnershipFilter
1571
+ value={designFilter === "not-mine" ? "shared" : designFilter}
1572
+ onChange={handleDesignFilterChange}
1573
+ />
1620
1574
  }
1621
1575
  templates={
1622
1576
  templatesError ? (