@agent-native/core 0.123.1 → 0.124.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 (86) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/deploy/build.ts +8 -0
  5. package/corpus/core/src/server/builder-browser.ts +76 -0
  6. package/corpus/core/src/server/core-routes-plugin.ts +15 -5
  7. package/corpus/templates/assets/app/global.css +0 -4
  8. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +195 -167
  9. package/corpus/templates/assets/changelog/2026-07-23-drafts-preview-panel-now-fills-the-full-width-with-a-large-p.md +6 -0
  10. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  11. package/corpus/templates/content/actions/_builder-cms-write-client.ts +55 -9
  12. package/corpus/templates/content/actions/_database-source-utils.ts +756 -182
  13. package/corpus/templates/content/actions/change-content-database-source-role.ts +21 -20
  14. package/corpus/templates/content/actions/execute-builder-source-execution.ts +49 -8
  15. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +42 -9
  16. package/corpus/templates/content/actions/prepare-builder-source-review.ts +88 -90
  17. package/corpus/templates/content/actions/preview-builder-source-review.ts +4 -3
  18. package/corpus/templates/content/actions/refresh-content-database-source.ts +36 -9
  19. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +28 -24
  20. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +62 -12
  21. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +91 -16
  22. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +15 -1
  23. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +234 -44
  24. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +194 -16
  25. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +95 -42
  26. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +4 -2
  27. package/corpus/templates/content/app/components/editor/extensions/CodeBlockNode.tsx +10 -166
  28. package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +32 -8
  29. package/corpus/templates/content/app/components/editor/extensions/VideoNode.ts +14 -0
  30. package/corpus/templates/content/app/components/editor/registrySlashItems.ts +5 -1
  31. package/corpus/templates/content/app/hooks/use-content-database.ts +59 -2
  32. package/corpus/templates/content/app/hooks/use-documents.ts +18 -26
  33. package/corpus/templates/content/app/root.tsx +1 -6
  34. package/corpus/templates/content/changelog/2026-07-24-builder-can-now-publish-approved-updates-to-entries-that-are.md +6 -0
  35. package/corpus/templates/content/changelog/2026-07-24-builder-collection-loading-no-longer-restarts-from-the-begin.md +6 -0
  36. package/corpus/templates/content/changelog/2026-07-24-builder-connections-now-complete-securely-from-netlify-deplo.md +6 -0
  37. package/corpus/templates/content/changelog/2026-07-24-builder-draft-creation-now-scopes-execution-to-the-selected-.md +6 -0
  38. package/corpus/templates/content/changelog/2026-07-24-builder-review-now-distinguishes-preparing-a-safe-dry-run-from.md +6 -0
  39. package/corpus/templates/content/changelog/2026-07-24-builder-source-imports-now-recover-from-a-transient-paginati.md +6 -0
  40. package/corpus/templates/content/changelog/2026-07-24-builder-source-refresh-now-preserves-the-provider-bound-fiel.md +6 -0
  41. package/corpus/templates/content/changelog/2026-07-24-builder-update-reviews-now-open-reliably-on-large-collection.md +6 -0
  42. package/corpus/templates/content/changelog/2026-07-24-builder-write-policy-controls-now-reflect-successful-changes.md +6 -0
  43. package/corpus/templates/content/changelog/2026-07-24-date-properties-now-save-reliably-when-selected-with-the-nat.md +6 -0
  44. package/corpus/templates/content/changelog/2026-07-24-editor-slash-search-now-keeps-multi-word-block-names-open.md +6 -0
  45. package/corpus/templates/content/changelog/2026-07-24-exact-editor-slash-commands-now-execute-on-enter-even-if-the.md +6 -0
  46. package/corpus/templates/content/changelog/2026-07-24-new-collaborative-pages-now-save-their-first-edits.md +6 -0
  47. package/corpus/templates/content/changelog/2026-07-24-new-local-builder-rows-no-longer-remain-locked-behind-a-none.md +6 -0
  48. package/corpus/templates/content/changelog/2026-07-24-opening-or-closing-a-collaborative-document-no-longer-lets-u.md +6 -0
  49. package/corpus/templates/content/changelog/2026-07-24-selected-text-now-opens-the-link-editor-with-command-k-or-co.md +6 -0
  50. package/corpus/templates/content/changelog/2026-07-24-slash-menu-block-choices-no-longer-stall-pointer-interaction.md +6 -0
  51. package/corpus/templates/content/changelog/2026-07-24-uploaded-and-embedded-media-now-stay-intact-while-collaborative-document-drafts-save.md +6 -0
  52. package/corpus/templates/content/changelog/2026-07-24-video-link-insertion-now-keeps-its-source-panel-open-while-t.md +6 -0
  53. package/corpus/templates/content/changelog/2026-07-26-builder-publishing-now-shows-safe-validation-details-instead.md +6 -0
  54. package/corpus/templates/content/changelog/2026-07-26-builder-reviews-stay-responsive-on-large-collections-and-pre.md +6 -0
  55. package/corpus/templates/content/changelog/2026-07-26-collaborative-documents-no-longer-flash-duplicated-recent-bl.md +6 -0
  56. package/corpus/templates/content/changelog/2026-07-26-corrected-builder-updates-are-now-reviewed-separately-from-f.md +6 -0
  57. package/corpus/templates/content/changelog/2026-07-26-documents-now-remain-read-only-until-their-live-collaborativ.md +6 -0
  58. package/corpus/templates/content/changelog/2026-07-26-slash-command-menus-now-stay-reachable-when-editing-near-the.md +6 -0
  59. package/corpus/templates/content/changelog/2026-07-26-the-slash-menu-now-shows-one-clear-callout-choice-instead-of.md +6 -0
  60. package/corpus/templates/content/shared/api.ts +1 -0
  61. package/corpus/templates/content/shared/builder-mdx.ts +73 -16
  62. package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +38 -21
  63. package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +23 -20
  64. package/corpus/toolkit/CHANGELOG.md +6 -0
  65. package/corpus/toolkit/package.json +1 -1
  66. package/corpus/toolkit/src/editor/useCollabReconcile.ts +14 -1
  67. package/dist/collab/awareness.d.ts +2 -2
  68. package/dist/collab/awareness.d.ts.map +1 -1
  69. package/dist/collab/routes.d.ts +1 -1
  70. package/dist/deploy/build.js +6 -0
  71. package/dist/deploy/build.js.map +1 -1
  72. package/dist/notifications/routes.d.ts +3 -3
  73. package/dist/observability/routes.d.ts +3 -3
  74. package/dist/server/builder-browser.d.ts +13 -0
  75. package/dist/server/builder-browser.d.ts.map +1 -1
  76. package/dist/server/builder-browser.js +59 -0
  77. package/dist/server/builder-browser.js.map +1 -1
  78. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  79. package/dist/server/core-routes-plugin.js +10 -5
  80. package/dist/server/core-routes-plugin.js.map +1 -1
  81. package/dist/server/realtime-token.d.ts +1 -1
  82. package/dist/server/transcribe-voice.d.ts +1 -1
  83. package/package.json +2 -2
  84. package/src/deploy/build.ts +8 -0
  85. package/src/server/builder-browser.ts +76 -0
  86. package/src/server/core-routes-plugin.ts +15 -5
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1735
32
32
  - toolkit files: 160
33
- - template files: 6751
33
+ - template files: 6778
@@ -1,5 +1,25 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.124.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e9dd025: `agent-native dev --inspect` (and `--inspect-brk`, optionally `=<port>`) now
8
+ attaches the Node inspector to **only** the Nitro API-server process, on a
9
+ single known port (default 9229). It selects Nitro's `node-process` dev runner
10
+ so the server is a real, attachable process, and injects `NODE_OPTIONS` through
11
+ a Vite preload that runs before Vite's own startup — so Vite, pnpm, and the CLI
12
+ are never inspected and there is exactly one debugger target. Set
13
+ `NITRO_DEV_RUNNER` yourself to override the runner.
14
+
15
+ ## 0.123.2
16
+
17
+ ### Patch Changes
18
+
19
+ - 14818b6: Route Builder authorization callbacks for Netlify previews through the immutable deployment URL while returning popup status to the visible preview.
20
+ - Updated dependencies [14818b6]
21
+ - @agent-native/toolkit@0.10.8
22
+
3
23
  ## 0.123.1
4
24
 
5
25
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.123.1",
3
+ "version": "0.124.0",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -3778,6 +3778,14 @@ export default bundle;
3778
3778
  "virtual:agents-bundle": agentsBundleModuleSource,
3779
3779
  },
3780
3780
  replace: {
3781
+ // Netlify exposes DEPLOY_ID only while building. Embed it into the Nitro
3782
+ // function so preview OAuth relays can target this immutable deployment
3783
+ // even though the value is unavailable in the function runtime.
3784
+ "process.env.AGENT_NATIVE_BUILD_ID": JSON.stringify(
3785
+ process.env.DEPLOY_ID?.trim() ||
3786
+ process.env.AGENT_NATIVE_BUILD_ID?.trim() ||
3787
+ "",
3788
+ ),
3781
3789
  "process.env.AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID": JSON.stringify(
3782
3790
  process.env.GA_MEASUREMENT_ID?.trim() || "",
3783
3791
  ),
@@ -36,6 +36,10 @@ const BUILDER_RELAY_PURPOSE = "builder-preview-callback-relay";
36
36
  const BUILDER_RELAY_STATE_VERSION = 1;
37
37
  const BUILDER_RELAY_TTL_MS = 10 * 60 * 1000;
38
38
  const BUILDER_RELAY_REQUEST_SKEW_MS = 2 * 60 * 1000;
39
+ const IMMUTABLE_NETLIFY_RELAY_HOST =
40
+ /^(?<deploy>[a-f0-9]{24})--(?<site>[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.netlify\.app$/;
41
+ const NETLIFY_DEPLOY_PREVIEW_HOST =
42
+ /^deploy-preview-\d+--(?<site>[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.netlify\.app$/;
39
43
 
40
44
  export interface BuilderPreviewRelayState {
41
45
  v: 1;
@@ -165,6 +169,45 @@ export function isTrustedBuilderRelayTargetOrigin(value: string): boolean {
165
169
  )
166
170
  );
167
171
  }
172
+
173
+ /**
174
+ * Netlify's deploy-preview alias is convenient for people but mutable, so it
175
+ * must never be the signed relay destination. The deploy builder embeds
176
+ * Netlify's DEPLOY_ID into the Nitro server bundle, while SITE_NAME remains
177
+ * available to Functions at runtime. Use that pair only when it
178
+ * identifies the same site as the visible preview alias; otherwise preserve
179
+ * the visible origin so callback validation fails closed.
180
+ */
181
+ export function resolveBuilderPreviewRelayTargetOrigin(
182
+ previewOrigin: string,
183
+ ): string {
184
+ let previewUrl: URL;
185
+ try {
186
+ previewUrl = new URL(previewOrigin);
187
+ } catch {
188
+ return previewOrigin;
189
+ }
190
+ const previewMatch = NETLIFY_DEPLOY_PREVIEW_HOST.exec(
191
+ previewUrl.hostname.toLowerCase(),
192
+ );
193
+ if (!previewMatch?.groups?.site) return previewOrigin;
194
+
195
+ const buildId = process.env.AGENT_NATIVE_BUILD_ID?.trim().toLowerCase();
196
+ const siteName = process.env.SITE_NAME?.trim().toLowerCase();
197
+ if (
198
+ !buildId ||
199
+ !siteName ||
200
+ !/^[a-f0-9]{24}$/.test(buildId) ||
201
+ siteName !== previewMatch.groups.site
202
+ ) {
203
+ return previewOrigin;
204
+ }
205
+
206
+ const immutableOrigin = `https://${buildId}--${siteName}.netlify.app`;
207
+ return IMMUTABLE_NETLIFY_RELAY_HOST.test(new URL(immutableOrigin).hostname)
208
+ ? immutableOrigin
209
+ : previewOrigin;
210
+ }
168
211
  export function signBuilderPreviewRelayState(input: {
169
212
  ownerEmail: string;
170
213
  targetOrigin: string;
@@ -1228,6 +1271,39 @@ export function resolveSafePreviewUrl(
1228
1271
  return getBuilderBrowserOriginForEvent(event);
1229
1272
  }
1230
1273
 
1274
+ export function resolveBuilderPreviewRelayParentOrigin(options: {
1275
+ openerOrigin?: string | null;
1276
+ targetOrigin: string;
1277
+ }): string {
1278
+ if (!options.openerOrigin) return options.targetOrigin;
1279
+ let openerUrl: URL;
1280
+ let targetUrl: URL;
1281
+ try {
1282
+ openerUrl = new URL(options.openerOrigin);
1283
+ targetUrl = new URL(options.targetOrigin);
1284
+ } catch {
1285
+ return options.targetOrigin;
1286
+ }
1287
+ if (
1288
+ openerUrl.origin !== options.openerOrigin ||
1289
+ !isSafeBuilderRelayTargetOrigin(openerUrl.origin)
1290
+ ) {
1291
+ return options.targetOrigin;
1292
+ }
1293
+ if (openerUrl.origin === targetUrl.origin) return openerUrl.origin;
1294
+
1295
+ const openerMatch = NETLIFY_DEPLOY_PREVIEW_HOST.exec(
1296
+ openerUrl.hostname.toLowerCase(),
1297
+ );
1298
+ const targetMatch = IMMUTABLE_NETLIFY_RELAY_HOST.exec(
1299
+ targetUrl.hostname.toLowerCase(),
1300
+ );
1301
+ return openerMatch?.groups?.site &&
1302
+ openerMatch.groups.site === targetMatch?.groups?.site
1303
+ ? openerUrl.origin
1304
+ : options.targetOrigin;
1305
+ }
1306
+
1231
1307
  export function resolveBuilderCallbackReturnUrl(options: {
1232
1308
  event: H3Event;
1233
1309
  openerOrigin?: string | null;
@@ -129,6 +129,8 @@ import {
129
129
  resolveBuilderCallbackReturnUrl,
130
130
  getBuilderBrowserStatusForEvent,
131
131
  resolveBuilderBranchProjectId,
132
+ resolveBuilderPreviewRelayParentOrigin,
133
+ resolveBuilderPreviewRelayTargetOrigin,
132
134
  resolveSafePreviewUrl,
133
135
  runBuilderAgent,
134
136
  signBuilderCallbackState,
@@ -2225,7 +2227,8 @@ export function createCoreRoutesPlugin(
2225
2227
  try {
2226
2228
  relay = signBuilderPreviewRelayState({
2227
2229
  ownerEmail,
2228
- targetOrigin: previewOrigin,
2230
+ targetOrigin:
2231
+ resolveBuilderPreviewRelayTargetOrigin(previewOrigin),
2229
2232
  basePath: getAppBasePath(),
2230
2233
  });
2231
2234
  } catch (err) {
@@ -2598,6 +2601,13 @@ export function createCoreRoutesPlugin(
2598
2601
  );
2599
2602
  }
2600
2603
 
2604
+ const relayOpenerOrigin =
2605
+ requestUrl.searchParams.get(BUILDER_OPENER_PARAM);
2606
+ const relayParentOrigin = resolveBuilderPreviewRelayParentOrigin({
2607
+ openerOrigin: relayOpenerOrigin,
2608
+ targetOrigin: relayPayload.targetOrigin,
2609
+ });
2610
+
2601
2611
  const privateKey = requestUrl.searchParams.get("p-key");
2602
2612
  const publicKey = requestUrl.searchParams.get("api-key");
2603
2613
  if (!privateKey || !publicKey) {
@@ -2609,7 +2619,7 @@ export function createCoreRoutesPlugin(
2609
2619
  );
2610
2620
  return createBuilderBrowserCallbackErrorPage(
2611
2621
  "Builder didn't return credentials. Restart the connect flow from settings.",
2612
- { parentOrigin: relayPayload.targetOrigin },
2622
+ { parentOrigin: relayParentOrigin },
2613
2623
  );
2614
2624
  }
2615
2625
 
@@ -2665,7 +2675,7 @@ export function createCoreRoutesPlugin(
2665
2675
  "text/html; charset=utf-8",
2666
2676
  );
2667
2677
  return createBuilderBrowserCallbackErrorPage(message, {
2668
- parentOrigin: relayPayload.targetOrigin,
2678
+ parentOrigin: relayParentOrigin,
2669
2679
  });
2670
2680
  }
2671
2681
 
@@ -2675,8 +2685,8 @@ export function createCoreRoutesPlugin(
2675
2685
  "text/html; charset=utf-8",
2676
2686
  );
2677
2687
  return createBuilderBrowserCallbackPage(
2678
- `${relayPayload.targetOrigin}${relayPayload.basePath || "/"}`,
2679
- { parentOrigin: relayPayload.targetOrigin },
2688
+ `${relayParentOrigin}${relayPayload.basePath || "/"}`,
2689
+ { parentOrigin: relayParentOrigin },
2680
2690
  );
2681
2691
  }
2682
2692
 
@@ -207,10 +207,6 @@
207
207
  grid-template-columns: minmax(0, 1fr) 284px;
208
208
  }
209
209
 
210
- .assets-live-candidates-grid {
211
- grid-template-columns: minmax(0, 1fr) minmax(18rem, 22rem);
212
- }
213
-
214
210
  .assets-brand-kit-settings-grid {
215
211
  grid-template-columns: minmax(0, 1fr) 320px;
216
212
  }
@@ -1521,6 +1521,7 @@ type LaneGalleryItem = {
1521
1521
  thumbnail: ReactNode; // i18n-ignore structural preview slot name
1522
1522
  menu?: ReactNode;
1523
1523
  primaryActions?: ReactNode;
1524
+ headerActions?: ReactNode;
1524
1525
  onToggle?: (checked: boolean) => void;
1525
1526
  };
1526
1527
 
@@ -3909,6 +3910,89 @@ export function LiveCandidatesStage({
3909
3910
  );
3910
3911
  }
3911
3912
 
3913
+ function candidateHeaderActions({
3914
+ canUseCandidate,
3915
+ saving,
3916
+ promoting,
3917
+ candidateLibraryId,
3918
+ onSaveCandidate,
3919
+ onAddToReferences,
3920
+ onUseCandidate,
3921
+ onDismiss,
3922
+ }: {
3923
+ canUseCandidate: boolean;
3924
+ saving?: boolean;
3925
+ promoting?: boolean;
3926
+ candidateLibraryId?: string | null;
3927
+ onSaveCandidate?: (folderId: string | null) => void;
3928
+ onAddToReferences?: () => void;
3929
+ onUseCandidate?: () => void;
3930
+ onDismiss: () => void;
3931
+ }) {
3932
+ const busy = saving || promoting || dismissing;
3933
+ const actionLibraryId = candidateLibraryId || libraryId;
3934
+ const candidateFolders =
3935
+ foldersByLibraryId[actionLibraryId] ??
3936
+ (actionLibraryId === libraryId ? folders : []);
3937
+ if (!canUseCandidate) {
3938
+ return (
3939
+ <Button
3940
+ variant="outline"
3941
+ size="sm"
3942
+ className="h-7 px-2 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive"
3943
+ onClick={onDismiss}
3944
+ disabled={busy}
3945
+ >
3946
+ {t("library.dismiss")}
3947
+ </Button>
3948
+ );
3949
+ }
3950
+ return (
3951
+ <div className="flex flex-wrap items-center gap-1.5">
3952
+ {onUseCandidate ? (
3953
+ <Button
3954
+ size="sm"
3955
+ className="h-7 px-2 text-xs"
3956
+ onClick={onUseCandidate}
3957
+ disabled={busy}
3958
+ >
3959
+ {t("library.useCandidate")}
3960
+ </Button>
3961
+ ) : null}
3962
+ <CandidateSaveMenu
3963
+ libraryId={actionLibraryId}
3964
+ folders={candidateFolders}
3965
+ allowCreateFolder={allowCreateFolder}
3966
+ saving={saving}
3967
+ disabled={busy}
3968
+ onSave={(folderId) => onSaveCandidate?.(folderId)}
3969
+ />
3970
+ <Button
3971
+ variant="outline"
3972
+ size="sm"
3973
+ className="h-7 px-2 text-xs"
3974
+ onClick={onAddToReferences}
3975
+ disabled={busy}
3976
+ >
3977
+ {promoting ? (
3978
+ <Spinner className="h-3.5 w-3.5" />
3979
+ ) : (
3980
+ t("library.addToReferences")
3981
+ )}
3982
+ </Button>
3983
+ <Button
3984
+ variant="ghost"
3985
+ size="sm"
3986
+ className="h-7 px-2 text-xs text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
3987
+ onClick={onDismiss}
3988
+ disabled={busy}
3989
+ >
3990
+ {t("library.dismiss")}
3991
+ </Button>
3992
+ </div>
3993
+ );
3994
+ }
3995
+
3912
3996
  function slotItem(slot: VariantSlot): LaneGalleryItem {
3913
3997
  const isFailed = slot.status === "failed";
3914
3998
  const canUseCandidate = slot.status === "ready" && Boolean(slot.assetId);
@@ -3953,6 +4037,21 @@ export function LiveCandidatesStage({
3953
4037
  slot,
3954
4038
  }),
3955
4039
  }),
4040
+ headerActions: candidateHeaderActions({
4041
+ canUseCandidate,
4042
+ saving,
4043
+ promoting,
4044
+ candidateLibraryId: libraryId,
4045
+ onSaveCandidate: (folderId) => onSave(slot, folderId),
4046
+ onAddToReferences: () => onMoveToReferences(slot),
4047
+ onUseCandidate: onUse ? () => onUse(slot) : undefined,
4048
+ onDismiss: () =>
4049
+ setDismissTarget({
4050
+ kind: "slot",
4051
+ title,
4052
+ slot,
4053
+ }),
4054
+ }),
3956
4055
  };
3957
4056
  }
3958
4057
 
@@ -3997,6 +4096,21 @@ export function LiveCandidatesStage({
3997
4096
  asset,
3998
4097
  }),
3999
4098
  }),
4099
+ headerActions: candidateHeaderActions({
4100
+ canUseCandidate: true,
4101
+ saving,
4102
+ promoting,
4103
+ candidateLibraryId: asset.libraryId,
4104
+ onSaveCandidate: (folderId) => onSaveDraft(asset, folderId),
4105
+ onAddToReferences: () => onMoveDraftToReferences(asset),
4106
+ onUseCandidate: onUseDraft ? () => onUseDraft(asset) : undefined,
4107
+ onDismiss: () =>
4108
+ setDismissTarget({
4109
+ kind: "asset",
4110
+ title: assetDisplayTitle(asset),
4111
+ asset,
4112
+ }),
4113
+ }),
4000
4114
  };
4001
4115
  }
4002
4116
 
@@ -4063,154 +4177,103 @@ export function LiveCandidatesStage({
4063
4177
  </AlertDialog>
4064
4178
 
4065
4179
  <section className="min-w-0 overflow-hidden rounded-lg border border-border bg-background">
4066
- <div className="flex min-w-0 items-center justify-between gap-3 border-b border-border px-3 py-2.5 sm:px-4">
4067
- <div className="flex min-w-0 flex-1 items-center">
4068
- <h3 className="shrink-0 text-sm font-semibold">
4069
- {t("library.candidates")}
4070
- </h3>
4071
- </div>
4072
- <div className="flex shrink-0 items-center gap-1.5">
4073
- <LiveCandidatesActions
4074
- slots={slots}
4075
- draftAssets={draftAssets}
4076
- libraryId={libraryId}
4077
- />
4078
- </div>
4079
- </div>
4080
- <div className="assets-live-candidates-grid grid min-w-0">
4081
- <div className="min-w-0 bg-muted/10 p-2.5 sm:p-3">
4082
- <div
4083
- className={[
4084
- "group relative overflow-hidden rounded-lg border border-border bg-background shadow-sm",
4085
- activeItem?.busy ? "opacity-80" : "",
4086
- ].join(" ")}
4087
- aria-busy={activeItem?.busy}
4088
- >
4089
- <div className="h-36 bg-muted/30 sm:h-44 lg:h-56 2xl:h-64">
4090
- {activeItem?.onOpen ? (
4091
- <button
4092
- type="button"
4093
- onClick={activeItem.onOpen}
4094
- className="block h-full w-full text-left"
4095
- >
4096
- {activeItem.preview}
4097
- </button>
4098
- ) : activeItem?.href ? (
4099
- <Link to={activeItem.href} className="block h-full w-full">
4100
- {activeItem.preview}
4101
- </Link>
4102
- ) : (
4103
- activeItem?.preview
4104
- )}
4105
- </div>
4106
- {activeItem?.busy ? (
4107
- <div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-background/20">
4108
- <Spinner className="h-5 w-5" />
4109
- </div>
4110
- ) : null}
4111
- {activeItem?.onOpen ? (
4112
- <Button
4113
- variant="secondary"
4114
- size="sm"
4115
- className="absolute right-2 top-2 h-8 gap-1.5 bg-background/85 px-2.5 text-xs opacity-0 shadow-sm backdrop-blur transition group-hover:opacity-100 focus-within:opacity-100"
4116
- onClick={activeItem.onOpen}
4117
- >
4118
- <IconArrowUpRight className="h-3.5 w-3.5" />
4119
- {t("library.details")}
4120
- </Button>
4121
- ) : activeItem?.href ? (
4122
- <Button
4123
- asChild
4124
- variant="secondary"
4125
- size="sm"
4126
- className="absolute right-2 top-2 h-8 gap-1.5 bg-background/85 px-2.5 text-xs opacity-0 shadow-sm backdrop-blur transition group-hover:opacity-100 focus-within:opacity-100"
4127
- >
4128
- <Link to={activeItem.href}>
4129
- <IconArrowUpRight className="h-3.5 w-3.5" />
4130
- {t("library.details")}
4131
- </Link>
4132
- </Button>
4133
- ) : null}
4134
- </div>
4135
- <div className="mt-2.5 flex gap-2 overflow-x-auto pb-1">
4136
- {items.map((item) => {
4137
- const active = item.id === activeItem?.id;
4138
- return (
4139
- <button
4140
- key={item.id}
4141
- type="button"
4142
- onClick={() => setActiveItemId(item.id)}
4143
- className={[
4144
- "group relative h-16 w-24 shrink-0 overflow-hidden rounded-md border bg-background transition",
4145
- active
4146
- ? "border-primary ring-2 ring-primary/25"
4147
- : "border-border/80 hover:border-foreground/30",
4148
- ].join(" ")}
4149
- aria-label={t("library.showCandidate", {
4150
- title: item.title,
4151
- })}
4152
- aria-pressed={active}
4153
- >
4154
- {item.thumbnail}
4155
- <span className="pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-linear-to-t from-background/90 to-transparent" />
4156
- {item.busy && item.showBusyOverlay !== false ? (
4157
- <span className="absolute right-1.5 top-1.5 rounded-full bg-background/90 p-1 shadow-sm">
4158
- <Spinner className="h-3 w-3" />
4159
- </span>
4160
- ) : null}
4161
- </button>
4162
- );
4163
- })}
4164
- </div>
4165
- </div>
4166
- <aside className="flex min-w-0 flex-col justify-between gap-3 border-t border-border bg-background p-3 lg:border-l lg:border-t-0 lg:p-4">
4167
- <div className="min-w-0 space-y-3">
4168
- <div className="min-w-0">
4169
- <div className="flex min-w-0 flex-wrap items-center gap-1.5">
4170
- {activeItem?.status ? (
4171
- <CandidateStatusPill status={activeItem.status} />
4172
- ) : null}
4173
- {activeItem?.metadata ? (
4174
- <Badge
4175
- variant="outline"
4176
- className="h-6 max-w-full rounded-full px-2 text-[11px]"
4177
- >
4178
- {activeItem.metadata}
4179
- </Badge>
4180
- ) : null}
4181
- </div>
4182
- <div className="mt-2 truncate text-sm font-semibold">
4183
- {activeItem?.title}
4184
- </div>
4185
- {activeItem?.subtitle ? (
4186
- <div className="mt-1 truncate text-xs text-muted-foreground">
4187
- {activeItem.subtitle}
4188
- </div>
4189
- ) : null}
4190
- </div>
4191
- {activeItem?.primaryActions ? (
4192
- <div>{activeItem.primaryActions}</div>
4193
- ) : null}
4194
- </div>
4180
+ <div className="flex min-w-0 flex-wrap items-center justify-between gap-2 border-b border-border px-3 py-2 sm:px-4">
4181
+ <h3 className="shrink-0 text-sm font-semibold">
4182
+ {t("library.candidates")}
4183
+ </h3>
4184
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
4185
+ {activeItem?.headerActions}
4195
4186
  {activeItem?.onOpen ? (
4196
4187
  <Button
4197
4188
  variant="ghost"
4198
4189
  size="sm"
4199
- className="gap-1.5"
4190
+ className="h-7 gap-1 px-2 text-xs"
4200
4191
  onClick={activeItem.onOpen}
4201
4192
  >
4202
4193
  <IconArrowUpRight className="h-3.5 w-3.5" />
4203
4194
  {t("library.openDetails")}
4204
4195
  </Button>
4205
4196
  ) : activeItem?.href ? (
4206
- <Button asChild variant="ghost" size="sm" className="gap-1.5">
4197
+ <Button
4198
+ asChild
4199
+ variant="ghost"
4200
+ size="sm"
4201
+ className="h-7 gap-1 px-2 text-xs"
4202
+ >
4207
4203
  <Link to={activeItem.href}>
4208
4204
  <IconArrowUpRight className="h-3.5 w-3.5" />
4209
4205
  {t("library.openDetails")}
4210
4206
  </Link>
4211
4207
  </Button>
4212
4208
  ) : null}
4213
- </aside>
4209
+ <LiveCandidatesActions
4210
+ slots={slots}
4211
+ draftAssets={draftAssets}
4212
+ libraryId={libraryId}
4213
+ />
4214
+ </div>
4215
+ </div>
4216
+ <div className="min-w-0 bg-muted/10 p-2.5 sm:p-3">
4217
+ <div
4218
+ className={[
4219
+ "group relative overflow-hidden rounded-lg border border-border bg-background shadow-sm",
4220
+ activeItem?.busy ? "opacity-80" : "",
4221
+ ].join(" ")}
4222
+ aria-busy={activeItem?.busy}
4223
+ >
4224
+ <div className="h-[min(60vh,30rem)] overflow-hidden bg-muted/30">
4225
+ {activeItem?.onOpen ? (
4226
+ <button
4227
+ type="button"
4228
+ onClick={activeItem.onOpen}
4229
+ className="block h-full w-full text-left"
4230
+ >
4231
+ {activeItem.preview}
4232
+ </button>
4233
+ ) : activeItem?.href ? (
4234
+ <Link to={activeItem.href} className="block h-full w-full">
4235
+ {activeItem.preview}
4236
+ </Link>
4237
+ ) : (
4238
+ activeItem?.preview
4239
+ )}
4240
+ </div>
4241
+ {activeItem?.busy ? (
4242
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-background/20">
4243
+ <Spinner className="h-5 w-5" />
4244
+ </div>
4245
+ ) : null}
4246
+ </div>
4247
+ <div className="mt-2.5 flex gap-2 overflow-x-auto pb-1">
4248
+ {items.map((item) => {
4249
+ const active = item.id === activeItem?.id;
4250
+ return (
4251
+ <button
4252
+ key={item.id}
4253
+ type="button"
4254
+ onClick={() => setActiveItemId(item.id)}
4255
+ className={[
4256
+ "group relative h-16 w-24 shrink-0 overflow-hidden rounded-md border bg-background transition",
4257
+ active
4258
+ ? "border-primary ring-2 ring-primary/25"
4259
+ : "border-border/80 hover:border-foreground/30",
4260
+ ].join(" ")}
4261
+ aria-label={t("library.showCandidate", {
4262
+ title: item.title,
4263
+ })}
4264
+ aria-pressed={active}
4265
+ >
4266
+ {item.thumbnail}
4267
+ <span className="pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-linear-to-t from-background/90 to-transparent" />
4268
+ {item.busy && item.showBusyOverlay !== false ? (
4269
+ <span className="absolute right-1.5 top-1.5 rounded-full bg-background/90 p-1 shadow-sm">
4270
+ <Spinner className="h-3 w-3" />
4271
+ </span>
4272
+ ) : null}
4273
+ </button>
4274
+ );
4275
+ })}
4276
+ </div>
4214
4277
  </div>
4215
4278
  </section>
4216
4279
 
@@ -4223,41 +4286,6 @@ export function LiveCandidatesStage({
4223
4286
  );
4224
4287
  }
4225
4288
 
4226
- function CandidateStatusPill({ status }: { status: string }) {
4227
- const t = useT();
4228
- const normalized = status.toLowerCase();
4229
- const label =
4230
- normalized === "pending"
4231
- ? t("library.generating")
4232
- : normalized === "ready"
4233
- ? t("library.ready")
4234
- : normalized === "failed"
4235
- ? t("library.failed")
4236
- : normalized === "draft"
4237
- ? t("library.draft")
4238
- : status;
4239
- const className =
4240
- normalized === "ready"
4241
- ? "border-primary/30 bg-primary/10 text-primary"
4242
- : normalized === "failed"
4243
- ? "border-destructive/30 bg-destructive/10 text-destructive"
4244
- : normalized === "pending"
4245
- ? "border-border bg-muted/70 text-muted-foreground"
4246
- : "border-border bg-background text-muted-foreground";
4247
-
4248
- return (
4249
- <span
4250
- className={[
4251
- "inline-flex h-6 max-w-full items-center gap-1.5 rounded-full border px-2 text-[11px] font-medium",
4252
- className,
4253
- ].join(" ")}
4254
- >
4255
- <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-current" />
4256
- <span className="truncate">{label}</span>
4257
- </span>
4258
- );
4259
- }
4260
-
4261
4289
  function VariantPreview({
4262
4290
  slot,
4263
4291
  fit = "cover",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ Drafts preview panel now fills the full width with a large preview area; save, add to references, and dismiss actions moved to the candidates header for all brand kits
@@ -35,6 +35,7 @@ export interface ExistingBuilderSourceRowIdentity {
35
35
  sourceRowId: string;
36
36
  sourceQualifiedId: string;
37
37
  sourceDisplayKey: string;
38
+ provenance?: string | null;
38
39
  lastSourceUpdatedAt: string | null;
39
40
  sourceValuesJson?: string | null;
40
41
  }