@agent-native/core-corpus 0.216.0 → 0.217.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 (149) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +61 -1
  3. package/corpus/templates/analytics/AGENTS.md +1 -1
  4. package/corpus/templates/analytics/CHANGELOG.md +1 -0
  5. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
  6. package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
  7. package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
  8. package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
  9. package/corpus/templates/analytics/actions/view-screen.ts +29 -3
  10. package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
  11. package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
  12. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
  13. package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
  14. package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
  15. package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
  16. package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
  17. package/corpus/templates/analytics/server/db/schema.ts +67 -0
  18. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
  19. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
  20. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
  21. package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
  22. package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
  23. package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
  24. package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
  25. package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
  26. package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
  27. package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
  28. package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
  29. package/corpus/templates/analytics/server/plugins/db.ts +46 -0
  30. package/corpus/templates/analytics/shared/output-use.ts +205 -0
  31. package/corpus/templates/analytics/shared/session-events.ts +6 -0
  32. package/corpus/templates/calendar/CHANGELOG.md +6 -0
  33. package/corpus/templates/calendar/actions/create-event.ts +2 -4
  34. package/corpus/templates/calendar/actions/update-event.ts +35 -6
  35. package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
  36. package/corpus/templates/community/qr/package.json +1 -1
  37. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
  38. package/corpus/templates/content/CHANGELOG.md +8 -0
  39. package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
  40. package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
  41. package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
  42. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
  43. package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
  44. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
  45. package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
  46. package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
  47. package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
  48. package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
  49. package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
  50. package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
  51. package/corpus/templates/content/actions/_database-utils.ts +93 -7
  52. package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
  53. package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
  54. package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
  55. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
  56. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
  57. package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
  58. package/corpus/templates/content/actions/delete-document.ts +41 -3
  59. package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
  60. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
  61. package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
  62. package/corpus/templates/content/actions/get-document.ts +2 -0
  63. package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
  64. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
  65. package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
  66. package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
  67. package/corpus/templates/content/actions/update-document.ts +139 -20
  68. package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
  69. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
  70. package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
  71. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
  72. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
  73. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
  74. package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
  75. package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
  76. package/corpus/templates/content/app/i18n-data.ts +1 -0
  77. package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
  78. package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
  79. package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
  80. package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
  81. package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
  82. package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
  83. package/corpus/templates/content/parity/matrix.md +42 -42
  84. package/corpus/templates/content/parity/matrix.ts +3 -1
  85. package/corpus/templates/content/server/db/schema.ts +20 -0
  86. package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
  87. package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
  88. package/corpus/templates/content/server/live-copy/binding.ts +31 -0
  89. package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
  90. package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
  91. package/corpus/templates/content/server/live-copy/holds.ts +59 -0
  92. package/corpus/templates/content/server/live-copy/persist.ts +656 -0
  93. package/corpus/templates/content/server/live-copy/switch.ts +136 -0
  94. package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
  95. package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
  96. package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
  97. package/corpus/templates/content/server/live-copy/write.ts +85 -0
  98. package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
  99. package/corpus/templates/content/server/plugins/collab.ts +3 -0
  100. package/corpus/templates/content/server/plugins/db.ts +28 -0
  101. package/corpus/templates/content/shared/api.ts +30 -1
  102. package/corpus/templates/content/shared/database-item-layout.ts +54 -22
  103. package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
  104. package/corpus/templates/content/shared/og-description.ts +258 -36
  105. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
  106. package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
  107. package/corpus/templates/design/CHANGELOG.md +2 -0
  108. package/corpus/templates/design/actions/get-getting-started.ts +53 -0
  109. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
  110. package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
  111. package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
  112. package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
  113. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
  114. package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
  115. package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
  116. package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
  117. package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
  118. package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
  119. package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
  120. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
  121. package/corpus/templates/design/app/global.css +6 -0
  122. package/corpus/templates/design/app/i18n/en-US.ts +17 -0
  123. package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
  124. package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
  125. package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
  126. package/corpus/templates/design/app/pages/Index.tsx +10 -3
  127. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
  128. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
  129. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
  130. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
  131. package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
  132. package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
  133. package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
  134. package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
  135. package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
  136. package/corpus/templates/design/server/lib/getting-started.ts +267 -0
  137. package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
  138. package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
  139. package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
  140. package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
  141. package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
  142. package/corpus/templates/design/shared/design-template-presets.ts +8 -0
  143. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
  144. package/corpus/templates/slides/CHANGELOG.md +6 -0
  145. package/corpus/templates/slides/actions/delete-deck.ts +7 -0
  146. package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
  147. package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
  148. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
  149. package/package.json +1 -1
@@ -0,0 +1,163 @@
1
+ import { Spinner } from "@agent-native/toolkit/design-system";
2
+ import { useId, type ReactNode } from "react";
3
+
4
+ import { Skeleton } from "@/components/ui/skeleton";
5
+
6
+ import type { DesignLibraryTemplate } from "./DesignTemplateLibrary";
7
+ import { TemplatePreview } from "./TemplatePreview";
8
+
9
+ const GRID_CLASS =
10
+ "grid gap-x-4 gap-y-3 [grid-template-columns:repeat(auto-fill,minmax(min(100%,15rem),1fr))]";
11
+
12
+ export const TEMPLATE_GRID_SKELETON_COUNT = 8;
13
+
14
+ export function DesignTemplateGrid({
15
+ templates,
16
+ loading,
17
+ pendingId,
18
+ pendingLabel,
19
+ disabled,
20
+ selectedId,
21
+ loadingLabel,
22
+ emptyLabel,
23
+ onSelect,
24
+ renderActions,
25
+ }: {
26
+ templates: readonly DesignLibraryTemplate[];
27
+ loading: boolean;
28
+ pendingId: string | null;
29
+ pendingLabel: string;
30
+ disabled: boolean;
31
+ selectedId?: string;
32
+ loadingLabel: string;
33
+ emptyLabel: string;
34
+ onSelect: (template: DesignLibraryTemplate, element: HTMLElement) => void;
35
+ renderActions?: (template: DesignLibraryTemplate) => ReactNode;
36
+ }) {
37
+ if (templates.length === 0) {
38
+ return loading ? (
39
+ <div
40
+ className={GRID_CLASS}
41
+ role="status"
42
+ aria-label={loadingLabel}
43
+ aria-busy="true"
44
+ >
45
+ {Array.from({ length: TEMPLATE_GRID_SKELETON_COUNT }, (_, index) => (
46
+ <div
47
+ key={index}
48
+ className="overflow-hidden rounded-[10px] border border-border"
49
+ >
50
+ <Skeleton className="aspect-[2/1] w-full rounded-none" />
51
+ <div className="grid gap-1 px-3.5 py-[13px]">
52
+ <Skeleton className="h-4 w-3/5" />
53
+ <Skeleton className="h-3.5 w-4/5" />
54
+ </div>
55
+ </div>
56
+ ))}
57
+ </div>
58
+ ) : (
59
+ <div
60
+ role="status"
61
+ className="py-8 text-center text-sm text-muted-foreground"
62
+ >
63
+ {emptyLabel}
64
+ </div>
65
+ );
66
+ }
67
+ return (
68
+ <ul className={GRID_CLASS} aria-busy={loading || undefined}>
69
+ {templates.map((template) => (
70
+ <DesignTemplateCard
71
+ key={template.id}
72
+ template={template}
73
+ pending={template.id === pendingId}
74
+ pendingLabel={pendingLabel}
75
+ disabled={disabled}
76
+ selected={template.id === selectedId}
77
+ onSelect={onSelect}
78
+ actions={renderActions?.(template)}
79
+ />
80
+ ))}
81
+ </ul>
82
+ );
83
+ }
84
+
85
+ function DesignTemplateCard({
86
+ template,
87
+ pending,
88
+ pendingLabel,
89
+ disabled,
90
+ selected,
91
+ onSelect,
92
+ actions,
93
+ }: {
94
+ template: DesignLibraryTemplate;
95
+ pending: boolean;
96
+ pendingLabel: string;
97
+ disabled: boolean;
98
+ selected: boolean;
99
+ onSelect: (template: DesignLibraryTemplate, element: HTMLElement) => void;
100
+ actions?: ReactNode;
101
+ }) {
102
+ const titleId = useId();
103
+ const descriptionId = useId();
104
+ return (
105
+ <li
106
+ className="group/card relative min-w-0"
107
+ aria-busy={pending || undefined}
108
+ >
109
+ <button
110
+ type="button"
111
+ aria-labelledby={titleId}
112
+ aria-describedby={template.description ? descriptionId : undefined}
113
+ aria-pressed={selected}
114
+ aria-disabled={disabled || undefined}
115
+ onClick={(event) => {
116
+ if (!disabled) onSelect(template, event.currentTarget);
117
+ }}
118
+ className="block w-full overflow-hidden rounded-[10px] border border-border bg-background text-left transition-colors outline-none hover:border-foreground/30 focus-visible:ring-2 focus-visible:ring-ring aria-pressed:border-foreground/40 aria-disabled:cursor-default"
119
+ >
120
+ <span className="relative block aspect-[2/1] overflow-hidden bg-muted">
121
+ <TemplatePreview
122
+ html={template.previewHtml}
123
+ title={template.title}
124
+ width={template.width}
125
+ height={template.height}
126
+ fit="cover"
127
+ className="absolute inset-0 h-full w-full"
128
+ />
129
+ </span>
130
+ <span className="block px-3.5 py-[13px]">
131
+ <span
132
+ id={titleId}
133
+ className="block truncate text-[13px] leading-4 font-semibold text-foreground"
134
+ >
135
+ {template.title}
136
+ </span>
137
+ {template.description ? (
138
+ <span
139
+ id={descriptionId}
140
+ className="mt-1 block truncate text-[11px] leading-3.5 text-muted-foreground"
141
+ >
142
+ {template.description}
143
+ </span>
144
+ ) : null}
145
+ </span>
146
+ </button>
147
+ {actions ? (
148
+ <div className="absolute top-2 right-2 z-10 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-focus-within/card:opacity-100 [@media(hover:hover)]:group-hover/card:opacity-100">
149
+ {actions}
150
+ </div>
151
+ ) : null}
152
+ {pending ? (
153
+ <div
154
+ role="status"
155
+ className="absolute inset-0 z-20 flex items-center justify-center gap-2 rounded-[10px] bg-background/80 p-4 text-sm font-medium text-foreground backdrop-blur-sm"
156
+ >
157
+ <Spinner className="size-4" />
158
+ {pendingLabel}
159
+ </div>
160
+ ) : null}
161
+ </li>
162
+ );
163
+ }
@@ -4,16 +4,13 @@ import {
4
4
  useActionQuery,
5
5
  } from "@agent-native/core/client/hooks";
6
6
  import { useT } from "@agent-native/core/client/i18n";
7
- import {
8
- TemplateLibraryGrid,
9
- TemplatePreviewDialog,
10
- } from "@agent-native/toolkit/app-shell";
7
+ import { TemplatePreviewDialog } from "@agent-native/toolkit/app-shell";
11
8
  import { ShareButton } from "@agent-native/toolkit/app/sharing";
12
9
  import { designTemplateRetryKey } from "@shared/design-template-retry";
13
10
  import { IconDots } from "@tabler/icons-react";
14
11
  import { useQueryClient } from "@tanstack/react-query";
15
12
  import { nanoid } from "nanoid";
16
- import { useRef, useState, type RefObject } from "react";
13
+ import { useEffect, useMemo, useRef, useState, type RefObject } from "react";
17
14
  import { useNavigate, useSearchParams } from "react-router";
18
15
  import { toast } from "sonner";
19
16
 
@@ -36,6 +33,7 @@ import {
36
33
  DropdownMenuTrigger,
37
34
  } from "@/components/ui/dropdown-menu";
38
35
 
36
+ import { DesignTemplateGrid } from "./DesignTemplateGrid";
39
37
  import { TemplatePreview } from "./TemplatePreview";
40
38
 
41
39
  export interface DesignLibraryTemplate {
@@ -78,9 +76,11 @@ function previewFilenameForHref(
78
76
  export function DesignTemplateLibrary({
79
77
  templates,
80
78
  loading = false,
79
+ previewParam = "templateId",
81
80
  }: {
82
81
  templates: readonly DesignLibraryTemplate[];
83
82
  loading?: boolean;
83
+ previewParam?: string;
84
84
  }) {
85
85
  const t = useT();
86
86
  const navigate = useNavigate();
@@ -92,7 +92,7 @@ export function DesignTemplateLibrary({
92
92
  const pending = useRef(false);
93
93
  const retryIds = useRef(new Map<string, { key: string; id: string }>());
94
94
  const preview = templates.find(
95
- (template) => template.id === params.get("templateId"),
95
+ (template) => template.id === params.get(previewParam),
96
96
  );
97
97
  const [deleting, setDeleting] = useState<DesignLibraryTemplate | null>(null);
98
98
  const restoreFocusRef = useRef<HTMLElement | null>(null);
@@ -100,8 +100,8 @@ export function DesignTemplateLibrary({
100
100
  setParams(
101
101
  (current) => {
102
102
  const next = new URLSearchParams(current);
103
- if (id) next.set("templateId", id);
104
- else next.delete("templateId");
103
+ if (id) next.set(previewParam, id);
104
+ else next.delete(previewParam);
105
105
  return next;
106
106
  },
107
107
  { replace: true },
@@ -158,41 +158,28 @@ export function DesignTemplateLibrary({
158
158
  };
159
159
  return (
160
160
  <>
161
- <TemplateLibraryGrid
162
- items={templates}
161
+ <DesignTemplateGrid
162
+ templates={templates}
163
163
  loading={loading}
164
164
  pendingId={pendingId}
165
165
  pendingLabel={t("templatesPage.opening")}
166
166
  disabled={Boolean(pendingId)}
167
167
  selectedId={preview?.id}
168
- onSelect={(template) => void copy(template)}
169
- labels={{
170
- loading: t("templatesPage.loading"),
171
- empty: t("promptDialog.noTemplatesFound"),
172
- retry: t("homeContext.retry"),
168
+ loadingLabel={t("templatesPage.loading")}
169
+ emptyLabel={t("promptDialog.noTemplatesFound")}
170
+ onSelect={(template, element) => {
171
+ restoreFocusRef.current = element;
172
+ selectPreview(template.id);
173
173
  }}
174
- renderPreview={(template) => (
175
- <div className="design-library-card-preview">
176
- <TemplatePreview
177
- html={template.previewHtml}
178
- title={template.title}
179
- width={template.width}
180
- height={template.height}
181
- className="h-full w-full"
174
+ renderActions={(template) =>
175
+ !template.isBuiltIn && template.isOwner ? (
176
+ <TemplateMenu
177
+ template={template}
178
+ disabled={pendingId !== null}
179
+ onDelete={() => setDeleting(template)}
182
180
  />
183
- </div>
184
- )}
185
- renderActions={(template) => (
186
- <TemplateMenu
187
- template={template}
188
- disabled={pendingId !== null}
189
- onDelete={() => setDeleting(template)}
190
- onPreview={(trigger) => {
191
- restoreFocusRef.current = trigger;
192
- selectPreview(template.id);
193
- }}
194
- />
195
- )}
181
+ ) : null
182
+ }
196
183
  />
197
184
  {preview ? (
198
185
  <DesignTemplatePreviewDialog
@@ -235,24 +222,19 @@ export function DesignTemplateLibrary({
235
222
  function TemplateMenu({
236
223
  template,
237
224
  disabled,
238
- onPreview,
239
225
  onDelete,
240
226
  }: {
241
227
  template: DesignLibraryTemplate;
242
228
  disabled: boolean;
243
- onPreview: (trigger: HTMLElement | null) => void;
244
229
  onDelete: () => void;
245
230
  }) {
246
231
  const t = useT();
247
- const trigger = useRef<HTMLButtonElement>(null);
248
- const openingPreview = useRef(false);
249
232
  return (
250
233
  <DropdownMenu>
251
234
  <DropdownMenuTrigger asChild disabled={disabled}>
252
235
  <Button
253
- ref={trigger}
254
- variant="ghost"
255
- size="icon"
236
+ variant="secondary"
237
+ size="icon-sm"
256
238
  disabled={disabled}
257
239
  aria-label={t("templatesPage.templateActions", {
258
240
  title: template.title,
@@ -261,39 +243,18 @@ function TemplateMenu({
261
243
  <IconDots />
262
244
  </Button>
263
245
  </DropdownMenuTrigger>
264
- <DropdownMenuContent
265
- align="end"
266
- onCloseAutoFocus={(event) => {
267
- if (openingPreview.current) {
268
- event.preventDefault();
269
- openingPreview.current = false;
270
- }
271
- }}
272
- >
246
+ <DropdownMenuContent align="end">
273
247
  <DropdownMenuGroup>
274
- <DropdownMenuItem
275
- disabled={disabled}
276
- onSelect={() => {
277
- openingPreview.current = true;
278
- onPreview(trigger.current);
279
- }}
280
- >
281
- {t("creativeContext.preview")}
248
+ <ShareButton
249
+ resourceType="design-template"
250
+ resourceId={template.id}
251
+ allowedRoles={["viewer", "editor", "admin"]}
252
+ resourceTitle={template.title}
253
+ trigger="label"
254
+ />
255
+ <DropdownMenuItem disabled={disabled} onSelect={onDelete}>
256
+ {t("home.delete")}
282
257
  </DropdownMenuItem>
283
- {!template.isBuiltIn && template.isOwner ? (
284
- <>
285
- <ShareButton
286
- resourceType="design-template"
287
- resourceId={template.id}
288
- allowedRoles={["viewer", "editor", "admin"]}
289
- resourceTitle={template.title}
290
- trigger="label"
291
- />
292
- <DropdownMenuItem disabled={disabled} onSelect={onDelete}>
293
- {t("home.delete")}
294
- </DropdownMenuItem>
295
- </>
296
- ) : null}
297
258
  </DropdownMenuGroup>
298
259
  </DropdownMenuContent>
299
260
  </DropdownMenu>
@@ -321,12 +282,38 @@ function DesignTemplatePreviewDialog({
321
282
  { templateId: template.id },
322
283
  );
323
284
  const [selectedId, setSelectedId] = useState<string | null>(null);
324
- const files =
325
- data && "files" in data
326
- ? data.files.filter((file) => file.fileType === "html")
327
- : [];
285
+ const files = useMemo(
286
+ () =>
287
+ data && "files" in data
288
+ ? data.files.filter((file) => file.fileType === "html")
289
+ : [],
290
+ [data],
291
+ );
328
292
  const active =
329
293
  files.find((file) => file.templateFileId === selectedId) ?? files[0];
294
+ const activeId = active?.templateFileId;
295
+ useEffect(() => {
296
+ if (files.length < 2) return;
297
+ const step = (event: KeyboardEvent) => {
298
+ if (
299
+ event.defaultPrevented ||
300
+ event.altKey ||
301
+ event.metaKey ||
302
+ event.ctrlKey
303
+ )
304
+ return;
305
+ const offset =
306
+ event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
307
+ if (!offset) return;
308
+ const index = files.findIndex((file) => file.templateFileId === activeId);
309
+ const next = files[(index + offset + files.length) % files.length];
310
+ if (!next) return;
311
+ event.preventDefault();
312
+ setSelectedId(next.templateFileId);
313
+ };
314
+ window.addEventListener("keydown", step);
315
+ return () => window.removeEventListener("keydown", step);
316
+ }, [files, activeId]);
330
317
  return (
331
318
  <TemplatePreviewDialog
332
319
  open
@@ -33,6 +33,7 @@ function TemplatePreviewContent({
33
33
  width,
34
34
  height,
35
35
  className,
36
+ fit = "contain",
36
37
  interactive = false,
37
38
  recordSessionReplay = false,
38
39
  onNavigate,
@@ -44,6 +45,7 @@ function TemplatePreviewContent({
44
45
  width?: number | null;
45
46
  height?: number | null;
46
47
  className?: string;
48
+ fit?: "contain" | "cover";
47
49
  interactive?: boolean;
48
50
  recordSessionReplay?: boolean;
49
51
  onNavigate?: (href: string) => void;
@@ -140,8 +142,9 @@ function TemplatePreviewContent({
140
142
  const availableWidth = element.clientWidth;
141
143
  const availableHeight = element.clientHeight;
142
144
  if (availableWidth > 0 && availableHeight > 0) {
145
+ const fitScale = fit === "cover" ? Math.max : Math.min;
143
146
  setScale(
144
- Math.min(
147
+ fitScale(
145
148
  availableWidth / naturalWidth,
146
149
  availableHeight / naturalHeight,
147
150
  ),
@@ -152,7 +155,7 @@ function TemplatePreviewContent({
152
155
  const observer = new ResizeObserver(update);
153
156
  observer.observe(element);
154
157
  return () => observer.disconnect();
155
- }, [naturalHeight, naturalWidth]);
158
+ }, [fit, naturalHeight, naturalWidth]);
156
159
 
157
160
  useEffect(() => {
158
161
  const frame = frameRef.current;
@@ -289,6 +292,7 @@ function TemplatePreviewContent({
289
292
  designId,
290
293
  );
291
294
  }}
295
+ data-fit={fit}
292
296
  tabIndex={interactive ? 0 : -1}
293
297
  aria-hidden={!interactive || undefined}
294
298
  className={cn(
@@ -22,6 +22,12 @@
22
22
  backface-visibility: hidden;
23
23
  }
24
24
 
25
+ .design-template-preview-frame[data-fit="cover"] {
26
+ top: 0;
27
+ transform: translateX(-50%) scale(var(--design-template-scale));
28
+ transform-origin: top center;
29
+ }
30
+
25
31
  .design-library-card-preview {
26
32
  position: absolute;
27
33
  inset: 0;
@@ -164,6 +164,23 @@ export default {
164
164
  expandSidebar: "Expand sidebar",
165
165
  collapseSidebar: "Collapse sidebar",
166
166
  },
167
+ gettingStarted: {
168
+ title: "Getting started",
169
+ progress: "{{completeCount}} of {{totalCount}} complete",
170
+ collapse: "Collapse",
171
+ expand: "Expand",
172
+ completed: "Completed",
173
+ loadError: "Couldn't load your checklist",
174
+ retry: "Retry",
175
+ steps: {
176
+ createDesign: "Create your first design",
177
+ addBrand: "Add your brand",
178
+ useRealData: "Use your real data",
179
+ shareFeedback: "Share for feedback",
180
+ inviteTeammate: "Invite a teammate",
181
+ saveTemplate: "Save a design as a template",
182
+ },
183
+ },
167
184
  settings: {
168
185
  agentTitle: "Manage agent",
169
186
  agentDescription:
@@ -0,0 +1,147 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+
3
+ import {
4
+ clearCanvasOutcomeDesign,
5
+ recordCanvasOutcome,
6
+ type CanvasLoadReadyBucket,
7
+ } from "./canvas-outcome-tracking";
8
+
9
+ /** A canvas that is not ready by then counts as stalled. */
10
+ export const CANVAS_LOAD_STALL_MS = 10_000;
11
+
12
+ export function canvasLoadReadyBucket(
13
+ elapsedMs: number,
14
+ ): CanvasLoadReadyBucket {
15
+ if (elapsedMs < 1_000) return "lt_1s";
16
+ if (elapsedMs <= 4_000) return "1_to_4s";
17
+ return "gt_4s";
18
+ }
19
+
20
+ interface CanvasLoad {
21
+ designId: string;
22
+ startedAt: number;
23
+ settled: boolean;
24
+ hidden: boolean;
25
+ stallTimer: ReturnType<typeof setTimeout>;
26
+ onVisibilityChange: () => void;
27
+ }
28
+
29
+ // One load per design opened: a board mounts a canvas per screen, and screens
30
+ // remount, so counting per canvas would inflate loads and stalls.
31
+ let currentLoad: CanvasLoad | null = null;
32
+
33
+ function startLoad(designId: string): CanvasLoad {
34
+ const load: CanvasLoad = {
35
+ designId,
36
+ startedAt: Date.now(),
37
+ settled: false,
38
+ hidden: document.visibilityState === "hidden",
39
+ stallTimer: setTimeout(
40
+ () => settleLoad(load, "stalled"),
41
+ CANVAS_LOAD_STALL_MS,
42
+ ),
43
+ // Hidden tabs throttle timers and frames, so their load time means nothing.
44
+ onVisibilityChange: () => {
45
+ if (document.visibilityState === "hidden") load.hidden = true;
46
+ },
47
+ };
48
+ document.addEventListener("visibilitychange", load.onVisibilityChange);
49
+ currentLoad = load;
50
+ return load;
51
+ }
52
+
53
+ function endLoad(load: CanvasLoad): void {
54
+ load.settled = true;
55
+ clearTimeout(load.stallTimer);
56
+ document.removeEventListener("visibilitychange", load.onVisibilityChange);
57
+ }
58
+
59
+ function settleLoad(load: CanvasLoad, outcome: "ready" | "stalled"): void {
60
+ if (load.settled) return;
61
+ endLoad(load);
62
+ const { designId } = load;
63
+ if (load.hidden) {
64
+ recordCanvasOutcome("canvas_load", "skipped", "hidden_during_load", {
65
+ designId,
66
+ });
67
+ } else if (outcome === "stalled") {
68
+ recordCanvasOutcome("canvas_load", "failed", "stalled", { designId });
69
+ } else {
70
+ recordCanvasOutcome("canvas_load", "succeeded", "none", {
71
+ designId,
72
+ readyBucket: canvasLoadReadyBucket(Date.now() - load.startedAt),
73
+ });
74
+ }
75
+ }
76
+
77
+ // A load still running when its design closes is not counted.
78
+ function abandonLoad(): void {
79
+ const load = currentLoad;
80
+ currentLoad = null;
81
+ if (load && !load.settled) endLoad(load);
82
+ }
83
+
84
+ /**
85
+ * Counts one `canvas_load` per design opened: the first measured canvas of a
86
+ * design starts it and the first to report ready ends it. Pass
87
+ * `measure: false` for a canvas that may never report ready (a snapshot, a
88
+ * localhost or Fusion preview), or a missing bridge would count as a stall.
89
+ * Call the returned function when the iframe bridge reports ready.
90
+ */
91
+ export function useCanvasLoadOutcome({
92
+ designId,
93
+ measure,
94
+ }: {
95
+ designId: string | undefined;
96
+ measure: boolean;
97
+ }): () => void {
98
+ const latest = useRef({ designId, measure });
99
+ latest.current = { designId, measure };
100
+ const startedLoad = useRef<CanvasLoad | null>(null);
101
+
102
+ useEffect(() => {
103
+ try {
104
+ if (!measure || !designId) {
105
+ // A canvas that stops being measured ends the load it started, or that
106
+ // load would only ever end as a stall.
107
+ if (startedLoad.current && startedLoad.current === currentLoad) {
108
+ abandonLoad();
109
+ }
110
+ startedLoad.current = null;
111
+ return;
112
+ }
113
+ if (currentLoad?.designId === designId) return;
114
+ abandonLoad();
115
+ startedLoad.current = startLoad(designId);
116
+ } catch {
117
+ // coercion-ok: load tracking must never break canvas mounting.
118
+ }
119
+ }, [designId, measure]);
120
+
121
+ // Stable, and reads the latest props: the bridge handlers that call it are
122
+ // registered once and keep whichever callback they first saw.
123
+ return useCallback(() => {
124
+ try {
125
+ const { designId: canvasDesign, measure: measuring } = latest.current;
126
+ if (measuring && canvasDesign && currentLoad?.designId === canvasDesign) {
127
+ settleLoad(currentLoad, "ready");
128
+ }
129
+ } catch {
130
+ // coercion-ok: load tracking must never break the bridge-ready path.
131
+ }
132
+ }, []);
133
+ }
134
+
135
+ /** The editor closed its design: end its load and send pending counts. */
136
+ export function leaveCanvasOutcomeDesign(): void {
137
+ try {
138
+ abandonLoad();
139
+ clearCanvasOutcomeDesign();
140
+ } catch {
141
+ // coercion-ok: tracking teardown must never break navigation.
142
+ }
143
+ }
144
+
145
+ export function __resetCanvasLoadOutcomeForTests(): void {
146
+ abandonLoad();
147
+ }