@agent-native/core 0.130.1 → 0.130.2

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 (57) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/server/email-template.ts +43 -13
  5. package/corpus/core/src/sharing/actions/share-resource.ts +26 -4
  6. package/corpus/core/src/sharing/index.ts +1 -0
  7. package/corpus/core/src/sharing/registry.ts +23 -0
  8. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +3 -1
  9. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  10. package/corpus/templates/clips/app/components/meetings/day-header.tsx +1 -1
  11. package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +1 -22
  12. package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +26 -1
  13. package/corpus/templates/clips/app/components/workspace/organization-identity-card.tsx +107 -0
  14. package/corpus/templates/clips/app/i18n/en-US.ts +3 -4
  15. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +0 -10
  16. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +23 -51
  17. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +1 -8
  18. package/corpus/templates/clips/changelog/2026-07-29-clearing-all-recording-edits-no-longer-leaves-the-editor-unr.md +6 -0
  19. package/corpus/templates/clips/changelog/2026-07-29-clip-share-emails-now-open-with-who-shared-the-clip-add-a-su.md +6 -0
  20. package/corpus/templates/clips/changelog/2026-07-29-finished-meeting-pills-now-have-a-clear-close-button.md +6 -0
  21. package/corpus/templates/clips/changelog/2026-07-29-meeting-cards-no-longer-flicker-or-disappear-when-hovered-in.md +6 -0
  22. package/corpus/templates/clips/changelog/2026-07-29-organization-name-logo-brand-color-and-default-visibility-no.md +6 -0
  23. package/corpus/templates/clips/changelog/2026-07-29-transcript-lines-on-the-meeting-page-no-longer-jump-to-the-r.md +6 -0
  24. package/corpus/templates/clips/changelog/2026-07-29-when-a-meeting-wraps-up-the-recording-pill-now-shows-a-green.md +6 -0
  25. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +6 -3
  26. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +112 -32
  27. package/corpus/templates/clips/desktop/src/styles.css +42 -2
  28. package/corpus/templates/clips/server/db/index.ts +6 -0
  29. package/corpus/templates/clips/server/lib/share-email-hero.ts +33 -0
  30. package/corpus/templates/clips/shared/clips-ai-prefs.ts +0 -1
  31. package/dist/collab/awareness.d.ts +2 -2
  32. package/dist/collab/awareness.d.ts.map +1 -1
  33. package/dist/collab/struct-routes.d.ts +1 -1
  34. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  35. package/dist/notifications/routes.d.ts +3 -3
  36. package/dist/progress/routes.d.ts +1 -1
  37. package/dist/resources/handlers.d.ts +1 -1
  38. package/dist/secrets/routes.d.ts +9 -9
  39. package/dist/server/email-template.d.ts +4 -0
  40. package/dist/server/email-template.d.ts.map +1 -1
  41. package/dist/server/email-template.js +25 -8
  42. package/dist/server/email-template.js.map +1 -1
  43. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  44. package/dist/sharing/actions/share-resource.js +23 -4
  45. package/dist/sharing/actions/share-resource.js.map +1 -1
  46. package/dist/sharing/index.d.ts +1 -1
  47. package/dist/sharing/index.d.ts.map +1 -1
  48. package/dist/sharing/index.js.map +1 -1
  49. package/dist/sharing/registry.d.ts +23 -0
  50. package/dist/sharing/registry.d.ts.map +1 -1
  51. package/dist/sharing/registry.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/server/email-template.ts +43 -13
  54. package/src/sharing/actions/share-resource.ts +26 -4
  55. package/src/sharing/index.ts +1 -0
  56. package/src/sharing/registry.ts +23 -0
  57. package/corpus/templates/clips/app/routes/_app.settings.organization.tsx +0 -232
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: 1635
32
32
  - toolkit files: 160
33
- - template files: 6927
33
+ - template files: 6934
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.130.2
4
+
5
+ ### Patch Changes
6
+
7
+ - ae02242: Share notification emails now lead with "<sharer> shared "<title>" with you" and support overridable body paragraphs, a second dark CTA, and closing paragraphs via the new `getShareEmailExtras` registration hook.
8
+
3
9
  ## 0.130.1
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.130.1",
3
+ "version": "0.130.2",
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": {
@@ -42,6 +42,8 @@ export interface RenderEmailArgs {
42
42
  paragraphs: string[];
43
43
  /** Primary call-to-action rendered as a real button. */
44
44
  cta?: EmailCta;
45
+ /** Optional second button rendered beside the primary CTA, dark-filled. */
46
+ secondaryCta?: EmailCta;
45
47
  /** A treated, copyable URL shown before or after the CTA. */
46
48
  linkBlock?: EmailLinkBlock;
47
49
  /**
@@ -50,6 +52,8 @@ export interface RenderEmailArgs {
50
52
  * by app/template code (never raw user input), and escape any dynamic values.
51
53
  */
52
54
  heroHtml?: string;
55
+ /** Body paragraphs rendered after the CTA and link block. Escaped-by-caller. */
56
+ closingParagraphs?: string[];
53
57
  /** Small muted text under the CTA (e.g. expiry note). */
54
58
  footer?: string;
55
59
  /** Optional app name shown beside the framework logo. */
@@ -128,12 +132,18 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
128
132
  // dynamic values they interpolate.
129
133
  const heroHtml = args.heroHtml ?? "";
130
134
 
131
- const paragraphsHtml = args.paragraphs
132
- .map(
133
- (p) =>
134
- `<p style="margin:0 0 16px 0; font-size:16px; line-height:1.6; color:#d4d4d8;">${p}</p>`,
135
- )
136
- .join("");
135
+ const renderParagraphs = (paragraphs: string[] | undefined): string =>
136
+ (paragraphs ?? [])
137
+ .map(
138
+ (p) =>
139
+ `<p style="margin:0 0 16px 0; font-size:16px; line-height:1.6; color:#d4d4d8;">${p}</p>`,
140
+ )
141
+ .join("");
142
+
143
+ const paragraphsHtml = renderParagraphs(args.paragraphs);
144
+ const closingParagraphsHtml = args.closingParagraphs?.length
145
+ ? `<div style="margin:28px 0 0 0;">${renderParagraphs(args.closingParagraphs)}</div>`
146
+ : "";
137
147
 
138
148
  const linkBlockHtml = args.linkBlock
139
149
  ? `
@@ -148,16 +158,27 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
148
158
  `
149
159
  : "";
150
160
 
161
+ const ctaButtonCell = (
162
+ cta: EmailCta,
163
+ background: string,
164
+ color: string,
165
+ border: string,
166
+ ): string => `
167
+ <td style="border-radius:10px; background:${background}; border:${border};">
168
+ <a href="${escapeAttr(cta.url)}"
169
+ style="display:inline-block; padding:14px 26px; font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size:15px; font-weight:600; color:${color}; text-decoration:none; border-radius:10px;">
170
+ ${escapeHtml(cta.label)}
171
+ </a>
172
+ </td>`;
173
+
174
+ const secondaryCtaHtml = args.secondaryCta
175
+ ? `<td style="width:12px;">&nbsp;</td>${ctaButtonCell(args.secondaryCta, "#141417", "#fafafa", "1px solid #3f3f46")}`
176
+ : "";
177
+
151
178
  const ctaHtml = args.cta
152
179
  ? `
153
180
  <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0 0 0;">
154
- <tr>
155
- <td style="border-radius:10px; background:${ctaBg};">
156
- <a href="${escapeAttr(args.cta.url)}"
157
- style="display:inline-block; padding:14px 26px; font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size:15px; font-weight:600; color:${ctaFg}; text-decoration:none; border-radius:10px;">
158
- ${escapeHtml(args.cta.label)}
159
- </a>
160
- </td>
181
+ <tr>${ctaButtonCell(args.cta, ctaBg, ctaFg, "0")}${secondaryCtaHtml}
161
182
  </tr>
162
183
  </table>
163
184
  `
@@ -211,6 +232,7 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
211
232
  ${args.linkBlock?.placement !== "after-cta" ? linkBlockHtml : ""}
212
233
  ${ctaHtml}
213
234
  ${args.linkBlock?.placement === "after-cta" ? linkBlockHtml : ""}
235
+ ${closingParagraphsHtml}
214
236
  ${footerHtml}
215
237
  </td>
216
238
  </tr>
@@ -237,11 +259,19 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
237
259
  textLines.push(`${args.cta.label}: ${args.cta.url}`);
238
260
  textLines.push("");
239
261
  }
262
+ if (args.secondaryCta) {
263
+ textLines.push(`${args.secondaryCta.label}: ${args.secondaryCta.url}`);
264
+ textLines.push("");
265
+ }
240
266
  if (args.linkBlock?.placement === "after-cta") {
241
267
  textLines.push(args.linkBlock.intro);
242
268
  textLines.push(args.linkBlock.url);
243
269
  textLines.push("");
244
270
  }
271
+ for (const p of args.closingParagraphs ?? []) {
272
+ textLines.push(stripTags(p));
273
+ textLines.push("");
274
+ }
245
275
  if (args.footer) {
246
276
  textLines.push(args.footer);
247
277
  }
@@ -4,13 +4,14 @@ import { z } from "zod";
4
4
  import { defineAction } from "../../action.js";
5
5
  import { getDbExec } from "../../db/client.js";
6
6
  import { getAppProductionUrl } from "../../server/app-url.js";
7
- import { renderEmail, emailStrong } from "../../server/email-template.js";
7
+ import { emailStrong, renderEmail } from "../../server/email-template.js";
8
8
  import { sendEmail, isEmailConfigured } from "../../server/email.js";
9
9
  import { invalidateCollabAccessCache } from "../../server/poll.js";
10
10
  import { getRequestUserEmail } from "../../server/request-context.js";
11
11
  import { getUserProfile } from "../../user-profile/store.js";
12
12
  import { assertAccess, ForbiddenError } from "../access.js";
13
13
  import { requireShareableResource } from "../registry.js";
14
+ import type { ShareEmailExtras } from "../registry.js";
14
15
  import {
15
16
  getExtensionShareChangeTargets,
16
17
  notifyExtensionShareChanged,
@@ -320,12 +321,14 @@ export default defineAction({
320
321
  );
321
322
  }
322
323
  }
324
+ const senderProfile = await getUserProfile(actor);
325
+ const senderDisplayName = senderProfile.name?.trim() || actor;
323
326
  let fromName: string | undefined;
324
327
  let replyTo: string | undefined;
325
328
  if (reg.getSender) {
326
329
  try {
327
330
  const sender = await reg.getSender(resource, {
328
- sender: await getUserProfile(actor),
331
+ sender: senderProfile,
329
332
  });
330
333
  fromName = sender?.fromName?.trim() || undefined;
331
334
  replyTo = sender?.replyTo?.trim() || undefined;
@@ -351,18 +354,37 @@ export default defineAction({
351
354
  );
352
355
  }
353
356
  }
357
+ let extras: ShareEmailExtras | undefined;
358
+ if (reg.getShareEmailExtras) {
359
+ try {
360
+ extras =
361
+ (await reg.getShareEmailExtras(resource, {
362
+ href: notificationUrl,
363
+ sender: senderProfile,
364
+ recipientEmail: principalId,
365
+ })) ?? undefined;
366
+ } catch (err) {
367
+ console.error(
368
+ "[share-resource] share email extras resolver failed; sending the plain notification:",
369
+ err,
370
+ );
371
+ }
372
+ }
354
373
  const subject = `${actor} shared "${resourceTitle}" with you on ${appName}`;
355
374
  const { html, text } = renderEmail({
356
375
  brandName,
357
376
  brandLogoUrl,
358
377
  preheader: subject,
359
- heading: "You've been given access",
360
- paragraphs: [
378
+ heading: `${senderDisplayName} shared "${resourceTitle}" with you`,
379
+ paragraphs: extras?.paragraphs ?? [
361
380
  `${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,
362
381
  `Use the button below to open it. If prompted, sign in with ${emailStrong(principalId)}.`,
363
382
  ],
364
383
  heroHtml,
365
384
  cta: { label: `Open ${reg.displayName}`, url: notificationUrl },
385
+ secondaryCta: extras?.secondaryCta,
386
+ linkBlock: extras?.linkBlock,
387
+ closingParagraphs: extras?.closingParagraphs,
366
388
  footer: `You received this because ${actor} granted you ${args.role} access.`,
367
389
  });
368
390
  await sendEmail({
@@ -22,6 +22,7 @@ export {
22
22
  requireShareableResource,
23
23
  listShareableResources,
24
24
  type ShareableResourceRegistration,
25
+ type ShareEmailExtras,
25
26
  } from "./registry.js";
26
27
 
27
28
  export {
@@ -17,8 +17,20 @@
17
17
  * });
18
18
  */
19
19
 
20
+ import type { EmailCta, EmailLinkBlock } from "../server/email-template.js";
20
21
  import type { UserProfile } from "../user-profile/shared.js";
21
22
 
23
+ export interface ShareEmailExtras {
24
+ /**
25
+ * Replaces the default body paragraphs between the heading and the preview.
26
+ * Pass `[]` to send none. Omit to keep the default copy.
27
+ */
28
+ paragraphs?: string[];
29
+ secondaryCta?: EmailCta;
30
+ linkBlock?: EmailLinkBlock;
31
+ closingParagraphs?: string[];
32
+ }
33
+
22
34
  export interface ShareableResourceRegistration {
23
35
  /** Stable identifier used across actions, UI, and analytics. e.g. "document". */
24
36
  type: string;
@@ -85,6 +97,17 @@ export interface ShareableResourceRegistration {
85
97
  resource: any,
86
98
  ctx: { href: string; alt?: string },
87
99
  ) => string | undefined | Promise<string | undefined>;
100
+ /**
101
+ * Optional resolver for app-specific content in the share-notification
102
+ * email: a second button beside the primary CTA, a treated copyable link,
103
+ * and closing paragraphs beneath it. Closing paragraphs are injected
104
+ * verbatim (use `emailLink` / `emailStrong` for inline markup), so escape
105
+ * any dynamic values.
106
+ */
107
+ getShareEmailExtras?: (
108
+ resource: any,
109
+ ctx: { href: string; sender: UserProfile; recipientEmail: string },
110
+ ) => ShareEmailExtras | undefined | Promise<ShareEmailExtras | undefined>;
88
111
  /**
89
112
  * Drizzle DB accessor from the template's server/db/index.ts. Required —
90
113
  * the framework-level share actions and access helpers call this to reach
@@ -505,7 +505,9 @@ export function EditorToolbar({
505
505
  </DropdownMenuItem>
506
506
  ) : null}
507
507
  <DropdownMenuSeparator />
508
- <DropdownMenuItem onSelect={() => setClearOpen(true)}>
508
+ <DropdownMenuItem
509
+ onSelect={() => setTimeout(() => setClearOpen(true), 0)}
510
+ >
509
511
  <IconTrash className="mr-2 h-4 w-4" />
510
512
  {t("editorToolbar.clearAllEdits")}
511
513
  </DropdownMenuItem>
@@ -665,7 +665,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
665
665
  </div>
666
666
 
667
667
  <div className="shrink-0 space-y-2 px-3 py-2">
668
- <OrgSwitcher settingsPath="/settings/organization" />
668
+ <OrgSwitcher settingsPath="/settings#organization" />
669
669
  <DevDatabaseLink />
670
670
  </div>
671
671
  </>
@@ -10,7 +10,7 @@ interface DayHeaderProps {
10
10
 
11
11
  export function DayHeader({ label }: DayHeaderProps) {
12
12
  return (
13
- <div className="sticky top-0 z-10 -mx-1 px-1 backdrop-blur-md bg-background/85 supports-[backdrop-filter]:bg-background/70">
13
+ <div className="sticky top-0 z-10 -mx-1 bg-background/85 px-1">
14
14
  <div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground py-1.5">
15
15
  {label}
16
16
  </div>
@@ -29,8 +29,6 @@ export interface TranscriptSegment {
29
29
  interface TranscriptBubblesProps {
30
30
  segments: TranscriptSegment[];
31
31
  isLive: boolean;
32
- recordingId?: string | null;
33
- onSeek: (ms: number) => void;
34
32
  /**
35
33
  * Imperative ref hook: parent can scroll a particular segment into view.
36
34
  * Receives a function (segmentIndex) => void.
@@ -93,8 +91,6 @@ function groupConsecutive(segments: TranscriptSegment[]): BubbleGroup[] {
93
91
  export function TranscriptBubbles({
94
92
  segments,
95
93
  isLive,
96
- recordingId,
97
- onSeek,
98
94
  registerScrollTo,
99
95
  }: TranscriptBubblesProps) {
100
96
  const t = useT();
@@ -329,7 +325,6 @@ export function TranscriptBubbles({
329
325
  </div>
330
326
  <div className="space-y-0.5">
331
327
  {group.segments.map(({ seg, index }) => {
332
- const clickable = !!recordingId;
333
328
  return (
334
329
  <Tooltip key={index}>
335
330
  <TooltipTrigger asChild>
@@ -337,23 +332,7 @@ export function TranscriptBubbles({
337
332
  ref={(el) => {
338
333
  segmentRefs.current[index] = el;
339
334
  }}
340
- role={clickable ? "button" : undefined}
341
- tabIndex={clickable ? 0 : -1}
342
- onClick={() => clickable && onSeek(seg.startMs)}
343
- onKeyDown={(e) => {
344
- if (
345
- clickable &&
346
- (e.key === "Enter" || e.key === " ")
347
- ) {
348
- e.preventDefault();
349
- onSeek(seg.startMs);
350
- }
351
- }}
352
- className={cn(
353
- "group/segment -mx-1 rounded-md px-1 py-1 text-left text-sm leading-relaxed text-foreground transition-colors",
354
- clickable &&
355
- "cursor-pointer hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
356
- )}
335
+ className="group/segment -mx-1 rounded-md px-1 py-1 text-left text-sm leading-relaxed text-foreground transition-colors"
357
336
  >
358
337
  {seg.speaker && !isMe && (
359
338
  <span className="me-2 text-[11px] font-medium text-muted-foreground">
@@ -158,7 +158,7 @@ export function BrandingEditor({
158
158
 
159
159
  <div className="space-y-1.5">
160
160
  <Label>{t("brandingEditor.brandColor")}</Label>
161
- <div className="flex items-center gap-2">
161
+ <div className="flex flex-wrap items-center gap-2">
162
162
  <input
163
163
  type="color"
164
164
  value={brandColor}
@@ -191,6 +191,9 @@ export function BrandingEditor({
191
191
 
192
192
  <div className="space-y-1.5">
193
193
  <Label>{t("brandingEditor.logo")}</Label>
194
+ <p className="text-xs text-muted-foreground">
195
+ {t("brandingEditor.logoUsage")}
196
+ </p>
194
197
  <div
195
198
  className={`rounded-md border border-dashed p-4 flex items-center gap-4 ${
196
199
  dragging ? "bg-primary/5 border-primary" : ""
@@ -322,6 +325,28 @@ export function BrandingEditor({
322
325
  {name || t("brandingEditor.organizationFallback")}
323
326
  </div>
324
327
  </div>
328
+ <div className="text-xs uppercase tracking-wide text-muted-foreground mt-4 mb-2">
329
+ {t("brandingEditor.emailHeaderPreview")}
330
+ </div>
331
+ <div className="rounded-md border border-[#27272a] bg-[#0a0a0c] p-4 flex items-center justify-center gap-2">
332
+ {brandLogoUrl ? (
333
+ <img
334
+ src={brandLogoUrl}
335
+ alt=""
336
+ className="h-7 w-7 object-contain"
337
+ />
338
+ ) : (
339
+ <div
340
+ className="h-7 w-7 rounded bg-white/90 flex items-center justify-center font-semibold text-[12px]"
341
+ style={{ color: brandColor }}
342
+ >
343
+ {name.slice(0, 1).toUpperCase() || "C"}
344
+ </div>
345
+ )}
346
+ <span className="font-semibold text-[#fafafa] truncate">
347
+ {name || t("brandingEditor.organizationFallback")}
348
+ </span>
349
+ </div>
325
350
  </div>
326
351
 
327
352
  <div className="flex justify-end">
@@ -0,0 +1,107 @@
1
+ import { useActionQuery, useSession } from "@agent-native/core/client/hooks";
2
+ import { useT } from "@agent-native/core/client/i18n";
3
+ import { useMemo } from "react";
4
+
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6
+ import { Skeleton } from "@/components/ui/skeleton";
7
+ import {
8
+ BrandingEditor,
9
+ type RecordingVisibility,
10
+ } from "@/components/workspace/branding-editor";
11
+ import type { MemberRole } from "@/components/workspace/members-list";
12
+
13
+ interface OrganizationStateResponse {
14
+ organization: {
15
+ id: string;
16
+ name: string;
17
+ brandColor: string;
18
+ brandLogoUrl: string | null;
19
+ defaultVisibility: RecordingVisibility;
20
+ ownerEmail?: string;
21
+ } | null;
22
+ members: { email: string; role: MemberRole }[];
23
+ }
24
+
25
+ /**
26
+ * Organization identity — name, logo, brand color, default recording
27
+ * visibility. It sits directly above membership in the Organization tab
28
+ * because the name and logo are what recipients see in share emails.
29
+ */
30
+ export function OrganizationIdentityCard() {
31
+ const t = useT();
32
+ const { session } = useSession();
33
+ const email = session?.email ?? "";
34
+
35
+ const { data, isPending, isError } =
36
+ useActionQuery<OrganizationStateResponse>(
37
+ "list-organization-state",
38
+ undefined,
39
+ );
40
+
41
+ const organization = data?.organization ?? null;
42
+ const members = useMemo(() => data?.members ?? [], [data?.members]);
43
+ const isAdmin = useMemo(() => {
44
+ if (organization?.ownerEmail && organization.ownerEmail === email) {
45
+ return true;
46
+ }
47
+ const role = members.find((m) => m.email === email)?.role;
48
+ return role === "admin" || role === "owner";
49
+ }, [members, email, organization?.ownerEmail]);
50
+
51
+ if (isPending) return <Skeleton className="h-64 w-full" />;
52
+ // A failed load must not look like "this org has no branding".
53
+ if (isError) {
54
+ return (
55
+ <Card>
56
+ <CardContent className="py-6 text-center text-sm text-muted-foreground">
57
+ {t("organizationSettings.brandingLoadFailed")}
58
+ </CardContent>
59
+ </Card>
60
+ );
61
+ }
62
+ if (!organization) return null;
63
+
64
+ if (!isAdmin) {
65
+ return (
66
+ <Card>
67
+ <CardHeader>
68
+ <CardTitle className="text-base">
69
+ {t("brandingEditor.title")}
70
+ </CardTitle>
71
+ </CardHeader>
72
+ <CardContent>
73
+ <div className="flex items-center gap-3">
74
+ {organization.brandLogoUrl ? (
75
+ <img
76
+ src={organization.brandLogoUrl}
77
+ alt=""
78
+ className="h-10 w-10 rounded object-contain"
79
+ />
80
+ ) : (
81
+ <div
82
+ className="h-10 w-10 rounded"
83
+ style={{ background: organization.brandColor }}
84
+ />
85
+ )}
86
+ <div>
87
+ <div className="font-medium">{organization.name}</div>
88
+ <div className="text-xs text-muted-foreground">
89
+ {t("organizationSettings.adminsOnlyBranding")}
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </CardContent>
94
+ </Card>
95
+ );
96
+ }
97
+
98
+ return (
99
+ <BrandingEditor
100
+ organizationId={organization.id}
101
+ initialName={organization.name}
102
+ initialBrandColor={organization.brandColor}
103
+ initialBrandLogoUrl={organization.brandLogoUrl}
104
+ initialDefaultVisibility={organization.defaultVisibility}
105
+ />
106
+ );
107
+ }
@@ -547,6 +547,8 @@ const messages = {
547
547
  useColor: "Use {{color}}",
548
548
  logo: "Logo",
549
549
  logoPreview: "Logo preview",
550
+ logoUsage: "Shown to recipients in share emails and on public clip pages.",
551
+ emailHeaderPreview: "Share email header",
550
552
  dropReplace: "Drop a new image to replace",
551
553
  dropHere: "Drop a PNG / JPG / SVG here",
552
554
  uploading: "Uploading…",
@@ -643,10 +645,6 @@ All notable user-facing changes to Clips are documented here. Open it any time f
643
645
  ### Added
644
646
 
645
647
  - See what's new right inside Clips. A changelog now lives in the command menu (Cmd+K) and in Settings.`,
646
- profile: "Profile",
647
- email: "Email",
648
- displayName: "Display name",
649
- displayNamePlaceholder: "Your name",
650
648
  playback: "Playback",
651
649
  defaultPlaybackSpeed: "Default playback speed",
652
650
  playbackDescription: "Applied automatically when you open a recording.",
@@ -786,6 +784,7 @@ All notable user-facing changes to Clips are documented here. Open it any time f
786
784
  "No organization yet. Create one from the organization switcher to get started.",
787
785
  description: "Organization admin: branding, members, invites.",
788
786
  adminsOnlyBranding: "Only admins can edit branding.",
787
+ brandingLoadFailed: "Couldn't load organization branding.",
789
788
  members: "Members",
790
789
  pendingInvites: "Pending invites",
791
790
  noPendingInvites: "No pending invites.",
@@ -495,14 +495,6 @@ export default function MeetingDetailRoute() {
495
495
  });
496
496
  };
497
497
 
498
- const handleSeek = (ms: number) => {
499
- if (!meeting?.recordingId) return;
500
- // /r/:recordingId's `t` param is seconds (see parseTimeParam in
501
- // r.$recordingId.tsx), while transcript segments use ms timestamps.
502
- const seconds = Math.max(0, Math.floor(ms / 1000));
503
- navigate(`/r/${meeting.recordingId}?t=${seconds}`);
504
- };
505
-
506
498
  const handleJumpToSegment = (segmentIndex: number) => {
507
499
  transcriptScrollToRef.current?.(segmentIndex);
508
500
  };
@@ -976,8 +968,6 @@ export default function MeetingDetailRoute() {
976
968
  <TranscriptBubbles
977
969
  segments={segments}
978
970
  isLive={isLive}
979
- recordingId={meeting.recordingId}
980
- onSeek={handleSeek}
981
971
  registerScrollTo={(fn) => {
982
972
  transcriptScrollToRef.current = fn;
983
973
  }}