@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/server/email-template.ts +43 -13
- package/corpus/core/src/sharing/actions/share-resource.ts +26 -4
- package/corpus/core/src/sharing/index.ts +1 -0
- package/corpus/core/src/sharing/registry.ts +23 -0
- package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +3 -1
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/components/meetings/day-header.tsx +1 -1
- package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +1 -22
- package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +26 -1
- package/corpus/templates/clips/app/components/workspace/organization-identity-card.tsx +107 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -4
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +0 -10
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +23 -51
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +1 -8
- package/corpus/templates/clips/changelog/2026-07-29-clearing-all-recording-edits-no-longer-leaves-the-editor-unr.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-clip-share-emails-now-open-with-who-shared-the-clip-add-a-su.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-finished-meeting-pills-now-have-a-clear-close-button.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-meeting-cards-no-longer-flicker-or-disappear-when-hovered-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-organization-name-logo-brand-color-and-default-visibility-no.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-transcript-lines-on-the-meeting-page-no-longer-jump-to-the-r.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-when-a-meeting-wraps-up-the-recording-pill-now-shows-a-green.md +6 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +6 -3
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +112 -32
- package/corpus/templates/clips/desktop/src/styles.css +42 -2
- package/corpus/templates/clips/server/db/index.ts +6 -0
- package/corpus/templates/clips/server/lib/share-email-hero.ts +33 -0
- package/corpus/templates/clips/shared/clips-ai-prefs.ts +0 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/email-template.d.ts +4 -0
- package/dist/server/email-template.d.ts.map +1 -1
- package/dist/server/email-template.js +25 -8
- package/dist/server/email-template.js.map +1 -1
- package/dist/sharing/actions/share-resource.d.ts.map +1 -1
- package/dist/sharing/actions/share-resource.js +23 -4
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/sharing/index.d.ts +1 -1
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js.map +1 -1
- package/dist/sharing/registry.d.ts +23 -0
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/package.json +1 -1
- package/src/server/email-template.ts +43 -13
- package/src/sharing/actions/share-resource.ts +26 -4
- package/src/sharing/index.ts +1 -0
- package/src/sharing/registry.ts +23 -0
- package/corpus/templates/clips/app/routes/_app.settings.organization.tsx +0 -232
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -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
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.130.
|
|
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
|
|
132
|
-
|
|
133
|
-
(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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;"> </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 {
|
|
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:
|
|
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: "
|
|
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({
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}}
|