@agent-native/core 0.176.1 → 0.177.0-nightly-20260828155142
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/templates/analytics/actions/set-workspace-feature-flag.ts +2 -2
- package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +16 -1
- package/corpus/templates/analytics/changelog/2026-08-27-show-analytics-s-own-registered-flags-in-fleet-feature-flag-.md +6 -0
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +204 -32
- package/corpus/templates/assets/changelog/2026-08-26-templates-across-brand-kits.md +1 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +61 -68
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +9 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +3 -3
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +8 -3
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +12 -3
- package/corpus/templates/calendar/changelog/2026-08-28-all-day-events-stay-in-the-compact-top-bar-in-day-and-week-v.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-28-event-creators-can-choose-whether-a-meeting-shows-as-free-or.md +6 -0
- package/corpus/templates/clips/app/global.css +0 -27
- package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
- package/corpus/templates/clips/app/routes/download.tsx +131 -16
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +35 -20
- package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +77 -22
- package/corpus/templates/forms/actions/response-insights.ts +4 -4
- package/corpus/templates/forms/actions/retry-response-deliveries.ts +26 -0
- package/corpus/templates/forms/server/db/schema.ts +64 -17
- package/corpus/templates/forms/server/handlers/submissions.ts +1022 -94
- package/corpus/templates/forms/server/lib/integrations.ts +100 -33
- package/corpus/templates/forms/server/middleware/00-public-cors.ts +1 -1
- package/corpus/templates/forms/server/plugins/db.ts +40 -0
- package/corpus/templates/plan/agent-native.json +1 -4
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +6 -1
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -6
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +78 -12
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +4 -4
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +111 -27
- package/corpus/templates/slides/app/components/editor/SlidesLayersPanel.tsx +20 -34
- package/corpus/templates/slides/app/components/editor/slide-style.ts +2 -0
- package/corpus/templates/slides/app/global.css +35 -0
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +133 -5
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +675 -0
- package/corpus/templates/slides/app/lib/validate-native-slide-html.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -44
- package/corpus/templates/slides/app/root.tsx +1 -22
- package/corpus/templates/slides/changelog/2026-08-27-image-fit-controls-apply-cover-crops-and-position.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-28-shared-slides-with-a-style-block-render-again.md +6 -0
- package/dist/action.d.ts +3 -1
- package/dist/client/FeedbackButton.d.ts +12 -0
- package/dist/client/FeedbackButton.js +62 -28
- package/dist/client/app-providers.js +13 -1
- package/dist/client/feedback-context.d.ts +1 -0
- package/dist/client/feedback-context.js +3 -2
- package/dist/client/host-bridge.d.ts +1 -0
- package/dist/client/observability/ThumbsFeedback.js +73 -10
- package/dist/client/webmcp.d.ts +4 -0
- package/dist/client/webmcp.js +51 -4
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/observability/routes.js +8 -2
- package/dist/observability/store.d.ts +1 -1
- package/dist/observability/store.js +18 -3
- package/dist/observability/types.d.ts +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-routes.d.ts +1 -6
- package/dist/server/action-routes.js +58 -17
- package/dist/server/agent-chat-plugin.js +8 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/webmcp.mdx +31 -12
- package/package.json +1 -1
|
@@ -55,6 +55,14 @@ interface SubmissionPayload {
|
|
|
55
55
|
clientSurface?: string | null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export interface IntegrationDeliverySnapshot {
|
|
59
|
+
id: string;
|
|
60
|
+
type: IntegrationType;
|
|
61
|
+
name: string;
|
|
62
|
+
url: string;
|
|
63
|
+
payload: unknown;
|
|
64
|
+
}
|
|
65
|
+
|
|
58
66
|
/** Human-readable label for a client-surface token. */
|
|
59
67
|
function clientSurfaceLabel(surface: string): string {
|
|
60
68
|
switch (surface) {
|
|
@@ -142,7 +150,7 @@ function formatDebugContext(submission: SubmissionPayload): string[] {
|
|
|
142
150
|
);
|
|
143
151
|
}
|
|
144
152
|
if (submission.activeRunId) {
|
|
145
|
-
lines.push(`
|
|
153
|
+
lines.push(`Request ID: \`${submission.activeRunId}\``);
|
|
146
154
|
}
|
|
147
155
|
if (submission.pageUrl) {
|
|
148
156
|
const appLabel = appLabelFromUrl(submission.pageUrl);
|
|
@@ -308,20 +316,89 @@ const payloadBuilders: Record<
|
|
|
308
316
|
webhook: buildWebhookPayload,
|
|
309
317
|
};
|
|
310
318
|
|
|
319
|
+
export function buildIntegrationDeliverySnapshot(
|
|
320
|
+
integration: FormIntegration,
|
|
321
|
+
submission: SubmissionPayload,
|
|
322
|
+
): IntegrationDeliverySnapshot {
|
|
323
|
+
const buildPayload = payloadBuilders[integration.type] ?? buildWebhookPayload;
|
|
324
|
+
return {
|
|
325
|
+
id: integration.id,
|
|
326
|
+
type: integration.type,
|
|
327
|
+
name: integration.name,
|
|
328
|
+
url: integration.url,
|
|
329
|
+
payload: buildPayload(submission),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function buildIntegrationDeliverySnapshots(
|
|
334
|
+
integrations: FormIntegration[],
|
|
335
|
+
submission: SubmissionPayload,
|
|
336
|
+
): IntegrationDeliverySnapshot[] {
|
|
337
|
+
return integrations
|
|
338
|
+
.filter((integration) => integration.enabled && integration.url)
|
|
339
|
+
.map((integration) =>
|
|
340
|
+
buildIntegrationDeliverySnapshot(integration, submission),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function deliverIntegrationDelivery(
|
|
345
|
+
snapshot: IntegrationDeliverySnapshot,
|
|
346
|
+
idempotencyKey?: string,
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
if (!isWebhookUrlAllowed(snapshot.url)) {
|
|
349
|
+
throw new Error("blocked URL");
|
|
350
|
+
}
|
|
351
|
+
const result = await deliverJsonWebhook({
|
|
352
|
+
url: snapshot.url,
|
|
353
|
+
payload: snapshot.payload,
|
|
354
|
+
...(idempotencyKey
|
|
355
|
+
? { headers: { "Idempotency-Key": idempotencyKey } }
|
|
356
|
+
: {}),
|
|
357
|
+
});
|
|
358
|
+
if (result.ok) return;
|
|
359
|
+
if (result.blocked) throw new Error("blocked URL");
|
|
360
|
+
throw new Error(
|
|
361
|
+
result.status
|
|
362
|
+
? `destination returned ${result.status}`
|
|
363
|
+
: "destination request failed",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
311
367
|
// ---------------------------------------------------------------------------
|
|
312
368
|
// Fire integrations
|
|
313
369
|
// ---------------------------------------------------------------------------
|
|
314
370
|
|
|
315
|
-
|
|
371
|
+
export type DeliveryStatus = "pending" | "succeeded" | "failed";
|
|
372
|
+
export type DeliveryStatuses = Record<string, DeliveryStatus>;
|
|
373
|
+
|
|
374
|
+
export function integrationDeliveryKey(integrationId: string): string {
|
|
375
|
+
return `integration:${integrationId}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
interface FireIntegrationsOptions {
|
|
379
|
+
deliveryStatus?: Readonly<DeliveryStatuses>;
|
|
380
|
+
onStatusChange?: (
|
|
381
|
+
destination: string,
|
|
382
|
+
status: DeliveryStatus,
|
|
383
|
+
) => Promise<void> | void;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Fire enabled integrations, skipping destinations already delivered. */
|
|
316
387
|
export async function fireIntegrations(
|
|
317
388
|
integrations: FormIntegration[],
|
|
318
389
|
submission: SubmissionPayload,
|
|
319
|
-
|
|
390
|
+
options: FireIntegrationsOptions = {},
|
|
391
|
+
): Promise<DeliveryStatuses> {
|
|
320
392
|
const enabled = integrations.filter((i) => i.enabled && i.url);
|
|
321
|
-
|
|
393
|
+
const statuses: DeliveryStatuses = { ...(options.deliveryStatus ?? {}) };
|
|
394
|
+
if (enabled.length === 0) return statuses;
|
|
322
395
|
|
|
323
|
-
await Promise.allSettled(
|
|
396
|
+
const results = await Promise.allSettled(
|
|
324
397
|
enabled.map(async (integration) => {
|
|
398
|
+
const destination = integrationDeliveryKey(integration.id);
|
|
399
|
+
if (options.deliveryStatus?.[destination] === "succeeded") return;
|
|
400
|
+
|
|
401
|
+
let status: DeliveryStatus = "failed";
|
|
325
402
|
// SSRF guard — a form-author can persist any URL in their integration
|
|
326
403
|
// config. Anonymous submissions then trigger a server-side POST. Block
|
|
327
404
|
// private IPs, cloud-metadata endpoints, and non-http(s) schemes
|
|
@@ -330,38 +407,28 @@ export async function fireIntegrations(
|
|
|
330
407
|
console.warn(
|
|
331
408
|
`[integrations] ${integration.type} "${integration.name}" rejected: blocked URL`,
|
|
332
409
|
);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
const result = await deliverJsonWebhook({
|
|
342
|
-
url: integration.url,
|
|
343
|
-
payload,
|
|
344
|
-
});
|
|
345
|
-
if (!result.ok) {
|
|
346
|
-
if (result.blocked) {
|
|
347
|
-
console.warn(
|
|
348
|
-
`[integrations] ${integration.type} "${integration.name}" rejected: blocked URL`,
|
|
349
|
-
);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
410
|
+
} else {
|
|
411
|
+
try {
|
|
412
|
+
await deliverIntegrationDelivery(
|
|
413
|
+
buildIntegrationDeliverySnapshot(integration, submission),
|
|
414
|
+
);
|
|
415
|
+
status = "succeeded";
|
|
416
|
+
} catch (err) {
|
|
352
417
|
console.warn(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
: `[integrations] ${integration.type} "${integration.name}" failed:`,
|
|
356
|
-
result.error,
|
|
418
|
+
`[integrations] ${integration.type} "${integration.name}" failed:`,
|
|
419
|
+
err,
|
|
357
420
|
);
|
|
358
421
|
}
|
|
359
|
-
} catch (err) {
|
|
360
|
-
console.warn(
|
|
361
|
-
`[integrations] ${integration.type} "${integration.name}" failed:`,
|
|
362
|
-
err,
|
|
363
|
-
);
|
|
364
422
|
}
|
|
423
|
+
|
|
424
|
+
statuses[destination] = status;
|
|
425
|
+
await options.onStatusChange?.(destination, status);
|
|
365
426
|
}),
|
|
366
427
|
);
|
|
428
|
+
|
|
429
|
+
const rejected = results.find(
|
|
430
|
+
(result): result is PromiseRejectedResult => result.status === "rejected",
|
|
431
|
+
);
|
|
432
|
+
if (rejected) throw rejected.reason;
|
|
433
|
+
return statuses;
|
|
367
434
|
}
|
|
@@ -150,6 +150,46 @@ CREATE INDEX IF NOT EXISTS form_shares_resource_idx ON form_shares (resource_id,
|
|
|
150
150
|
sqlite: `ALTER TABLE responses ADD COLUMN IF NOT EXISTS client_surface TEXT`,
|
|
151
151
|
},
|
|
152
152
|
},
|
|
153
|
+
{
|
|
154
|
+
version: 13,
|
|
155
|
+
name: "responses-idempotency-key",
|
|
156
|
+
sql: {
|
|
157
|
+
postgres: `ALTER TABLE responses ADD COLUMN IF NOT EXISTS idempotency_key TEXT;
|
|
158
|
+
CREATE UNIQUE INDEX IF NOT EXISTS responses_form_idempotency_key_idx ON responses (form_id, idempotency_key)`,
|
|
159
|
+
sqlite: `ALTER TABLE responses ADD COLUMN IF NOT EXISTS idempotency_key TEXT;
|
|
160
|
+
CREATE UNIQUE INDEX IF NOT EXISTS responses_form_idempotency_key_idx ON responses (form_id, idempotency_key)`,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
version: 14,
|
|
165
|
+
name: "responses-delivery-status",
|
|
166
|
+
sql: {
|
|
167
|
+
postgres: `ALTER TABLE responses ADD COLUMN IF NOT EXISTS delivery_status TEXT`,
|
|
168
|
+
sqlite: `ALTER TABLE responses ADD COLUMN IF NOT EXISTS delivery_status TEXT`,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
version: 15,
|
|
173
|
+
name: "response-delivery-snapshots",
|
|
174
|
+
sql: `ALTER TABLE responses ADD COLUMN IF NOT EXISTS delivery_snapshot TEXT;
|
|
175
|
+
CREATE TABLE IF NOT EXISTS response_deliveries (
|
|
176
|
+
id TEXT PRIMARY KEY,
|
|
177
|
+
response_id TEXT NOT NULL REFERENCES responses(id),
|
|
178
|
+
destination TEXT NOT NULL,
|
|
179
|
+
kind TEXT NOT NULL,
|
|
180
|
+
payload TEXT NOT NULL,
|
|
181
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
182
|
+
claim_token TEXT,
|
|
183
|
+
claimed_at TEXT,
|
|
184
|
+
error_message TEXT,
|
|
185
|
+
created_at TEXT NOT NULL,
|
|
186
|
+
updated_at TEXT NOT NULL
|
|
187
|
+
);
|
|
188
|
+
CREATE UNIQUE INDEX IF NOT EXISTS response_deliveries_response_destination_idx
|
|
189
|
+
ON response_deliveries (response_id, destination);
|
|
190
|
+
CREATE INDEX IF NOT EXISTS response_deliveries_status_idx
|
|
191
|
+
ON response_deliveries (status, claimed_at)`,
|
|
192
|
+
},
|
|
153
193
|
],
|
|
154
194
|
{ table: "forms_migrations" },
|
|
155
195
|
);
|
|
@@ -1063,9 +1063,14 @@ export function SlideInner({
|
|
|
1063
1063
|
// Slides with fmd-slide markup carry their layout in the raw HTML contract;
|
|
1064
1064
|
// render them as-is so supported semantic classes and inline styles survive.
|
|
1065
1065
|
const content = typeof slide.content === "string" ? slide.content : "";
|
|
1066
|
+
const trimmedContent = content.trimStart();
|
|
1067
|
+
const isConvertedMarkdownImage =
|
|
1068
|
+
/^<img\b\s+data-markdown-image(?:\s*=\s*(?:"true"|'true'|true))?(?:\s|>)/i.test(
|
|
1069
|
+
trimmedContent,
|
|
1070
|
+
);
|
|
1066
1071
|
const isRawHtml =
|
|
1067
1072
|
content.includes('class="fmd-slide"') ||
|
|
1068
|
-
|
|
1073
|
+
(trimmedContent.startsWith("<") && !isConvertedMarkdownImage) ||
|
|
1069
1074
|
["blank", "section", "statement", "full-image"].includes(slide.layout);
|
|
1070
1075
|
|
|
1071
1076
|
if (!isRawHtml && slide.layout === "two-column") {
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
IconCircle,
|
|
5
5
|
IconLine,
|
|
6
6
|
IconPlus,
|
|
7
|
-
|
|
7
|
+
IconShape,
|
|
8
8
|
IconSquare,
|
|
9
9
|
IconTextSize,
|
|
10
10
|
IconTriangle,
|
|
@@ -158,7 +158,7 @@ export function EditorActionCluster({
|
|
|
158
158
|
shapeMenuOpen || shapeType ? ACTIVE_CLASS : IDLE_CLASS,
|
|
159
159
|
)}
|
|
160
160
|
>
|
|
161
|
-
<
|
|
161
|
+
<IconShape className="size-4" />
|
|
162
162
|
</button>
|
|
163
163
|
</PopoverTrigger>
|
|
164
164
|
</TooltipTrigger>
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
IconPlus,
|
|
35
35
|
IconSquare,
|
|
36
36
|
IconTextSize,
|
|
37
|
-
|
|
37
|
+
IconBolt,
|
|
38
38
|
IconLayersSubtract,
|
|
39
39
|
} from "@tabler/icons-react";
|
|
40
40
|
import { useTheme } from "next-themes";
|
|
@@ -445,9 +445,9 @@ export default function EditorToolbar({
|
|
|
445
445
|
commands.push({
|
|
446
446
|
id: "element-animations",
|
|
447
447
|
group: "slideTools",
|
|
448
|
-
label: t("
|
|
448
|
+
label: t("animations.title"),
|
|
449
449
|
keywords: ["animation", "motion", "transition"],
|
|
450
|
-
icon:
|
|
450
|
+
icon: IconBolt,
|
|
451
451
|
active: animationsOpen,
|
|
452
452
|
run: onToggleAnimations,
|
|
453
453
|
});
|
|
@@ -505,7 +505,7 @@ export default function EditorToolbar({
|
|
|
505
505
|
group: "slideTools" as const,
|
|
506
506
|
label: t(transition.labelKey),
|
|
507
507
|
keywords: ["slide", "transition", transition.value],
|
|
508
|
-
icon:
|
|
508
|
+
icon: IconBolt,
|
|
509
509
|
active: activeSlideTransition === transition.value,
|
|
510
510
|
run: () => onChangeSlideTransition(transition.value),
|
|
511
511
|
})),
|
|
@@ -803,8 +803,8 @@ export default function EditorToolbar({
|
|
|
803
803
|
: undefined
|
|
804
804
|
}
|
|
805
805
|
>
|
|
806
|
-
<
|
|
807
|
-
{t("
|
|
806
|
+
<IconBolt className="size-4" />
|
|
807
|
+
{t("animations.title")}
|
|
808
808
|
</DropdownMenuItem>
|
|
809
809
|
)}
|
|
810
810
|
{canEdit && currentSlide && onToggleLayers && (
|
|
@@ -7,18 +7,25 @@ import {
|
|
|
7
7
|
IconMaximize,
|
|
8
8
|
IconMinimize,
|
|
9
9
|
} from "@tabler/icons-react";
|
|
10
|
-
import { useEffect, useRef } from "react";
|
|
10
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
11
11
|
import { createPortal } from "react-dom";
|
|
12
12
|
|
|
13
|
+
import {
|
|
14
|
+
IMAGE_OBJECT_POSITION_VALUES,
|
|
15
|
+
type ImageObjectPosition,
|
|
16
|
+
} from "@/lib/slide-image-replacement";
|
|
17
|
+
|
|
13
18
|
interface ImageOverlayProps {
|
|
14
19
|
anchorRect: DOMRect;
|
|
15
20
|
src: string;
|
|
16
21
|
objectFit: "cover" | "contain";
|
|
22
|
+
objectPosition: ImageObjectPosition;
|
|
17
23
|
onGenerate: () => void;
|
|
18
24
|
onLibrary: () => void;
|
|
19
25
|
onUpload: () => void;
|
|
20
26
|
onDownload: () => void;
|
|
21
27
|
onToggleObjectFit: () => void;
|
|
28
|
+
onChangeObjectPosition: (objectPosition: ImageObjectPosition) => void;
|
|
22
29
|
onClose: () => void;
|
|
23
30
|
}
|
|
24
31
|
|
|
@@ -26,15 +33,19 @@ export default function ImageOverlay({
|
|
|
26
33
|
anchorRect,
|
|
27
34
|
src,
|
|
28
35
|
objectFit,
|
|
36
|
+
objectPosition,
|
|
29
37
|
onGenerate,
|
|
30
38
|
onLibrary,
|
|
31
39
|
onUpload,
|
|
32
40
|
onDownload,
|
|
33
41
|
onToggleObjectFit,
|
|
42
|
+
onChangeObjectPosition,
|
|
34
43
|
onClose,
|
|
35
44
|
}: ImageOverlayProps) {
|
|
36
45
|
const t = useT();
|
|
37
46
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
47
|
+
const [menuHeight, setMenuHeight] = useState(0);
|
|
48
|
+
const isPlaceholder = src.startsWith("placeholder:");
|
|
38
49
|
|
|
39
50
|
useEffect(() => {
|
|
40
51
|
const handleClick = (e: MouseEvent) => {
|
|
@@ -53,6 +64,11 @@ export default function ImageOverlay({
|
|
|
53
64
|
};
|
|
54
65
|
}, [onClose]);
|
|
55
66
|
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
const height = menuRef.current?.getBoundingClientRect().height;
|
|
69
|
+
if (height && height !== menuHeight) setMenuHeight(height);
|
|
70
|
+
}, [anchorRect, isPlaceholder, menuHeight, objectFit]);
|
|
71
|
+
|
|
56
72
|
const menuWidth = 180;
|
|
57
73
|
const vw = window.innerWidth;
|
|
58
74
|
const vh = window.innerHeight;
|
|
@@ -61,13 +77,14 @@ export default function ImageOverlay({
|
|
|
61
77
|
|
|
62
78
|
if (vw < 640) {
|
|
63
79
|
left = Math.max(8, (vw - menuWidth) / 2);
|
|
64
|
-
top =
|
|
80
|
+
top = anchorRect.bottom + 8;
|
|
65
81
|
} else {
|
|
66
82
|
left = anchorRect.left - menuWidth - 8;
|
|
67
83
|
top = anchorRect.top + anchorRect.height / 2 - 100;
|
|
68
84
|
if (left < 8) left = 8;
|
|
69
85
|
}
|
|
70
|
-
|
|
86
|
+
const maxTop = Math.max(8, vh - (menuHeight || 220) - 8);
|
|
87
|
+
top = Math.max(8, Math.min(top, maxTop));
|
|
71
88
|
|
|
72
89
|
return createPortal(
|
|
73
90
|
<div
|
|
@@ -116,15 +133,64 @@ export default function ImageOverlay({
|
|
|
116
133
|
<IconDownload className="w-3.5 h-3.5 text-muted-foreground" />
|
|
117
134
|
Download
|
|
118
135
|
</button>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
{!isPlaceholder && (
|
|
137
|
+
<>
|
|
138
|
+
<div className="mx-1.5 border-t border-border" />
|
|
139
|
+
<button onClick={onToggleObjectFit} className="image-overlay-btn">
|
|
140
|
+
{objectFit === "cover" ? (
|
|
141
|
+
<IconMinimize className="w-3.5 h-3.5 text-muted-foreground" />
|
|
142
|
+
) : (
|
|
143
|
+
<IconMaximize className="w-3.5 h-3.5 text-muted-foreground" />
|
|
144
|
+
)}
|
|
145
|
+
Fit: {objectFit === "cover" ? "Cover" : "Contain"}
|
|
146
|
+
</button>
|
|
147
|
+
{objectFit === "cover" && (
|
|
148
|
+
<label className="image-overlay-position">
|
|
149
|
+
<span>{t("styleInspector.position")}</span>
|
|
150
|
+
<select
|
|
151
|
+
aria-label={t("styleInspector.position")}
|
|
152
|
+
value={objectPosition}
|
|
153
|
+
onChange={(event) => {
|
|
154
|
+
const value = event.currentTarget.value;
|
|
155
|
+
if (
|
|
156
|
+
IMAGE_OBJECT_POSITION_VALUES.includes(
|
|
157
|
+
value as ImageObjectPosition,
|
|
158
|
+
)
|
|
159
|
+
) {
|
|
160
|
+
onChangeObjectPosition(value as ImageObjectPosition);
|
|
161
|
+
}
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<option value="left top">
|
|
165
|
+
{t("styleInspector.top")} {t("styleInspector.left")}
|
|
166
|
+
</option>
|
|
167
|
+
<option value="center top">
|
|
168
|
+
{t("styleInspector.top")} {t("styleInspector.center")}
|
|
169
|
+
</option>
|
|
170
|
+
<option value="right top">
|
|
171
|
+
{t("styleInspector.top")} {t("styleInspector.right")}
|
|
172
|
+
</option>
|
|
173
|
+
<option value="left center">{t("styleInspector.left")}</option>
|
|
174
|
+
<option value="center center">
|
|
175
|
+
{t("styleInspector.center")}
|
|
176
|
+
</option>
|
|
177
|
+
<option value="right center">
|
|
178
|
+
{t("styleInspector.right")}
|
|
179
|
+
</option>
|
|
180
|
+
<option value="left bottom">
|
|
181
|
+
{t("styleInspector.bottom")} {t("styleInspector.left")}
|
|
182
|
+
</option>
|
|
183
|
+
<option value="center bottom">
|
|
184
|
+
{t("styleInspector.bottom")} {t("styleInspector.center")}
|
|
185
|
+
</option>
|
|
186
|
+
<option value="right bottom">
|
|
187
|
+
{t("styleInspector.bottom")} {t("styleInspector.right")}
|
|
188
|
+
</option>
|
|
189
|
+
</select>
|
|
190
|
+
</label>
|
|
191
|
+
)}
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
128
194
|
</div>,
|
|
129
195
|
document.body,
|
|
130
196
|
);
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
IconSpacingVertical,
|
|
35
35
|
IconStackBack,
|
|
36
36
|
IconStackFront,
|
|
37
|
-
|
|
37
|
+
IconBolt,
|
|
38
38
|
IconUnderline,
|
|
39
39
|
IconZoomIn,
|
|
40
40
|
IconZoomOut,
|
|
@@ -256,14 +256,14 @@ export function SlideContextToolbar({
|
|
|
256
256
|
MENU_BUTTON_CLASS,
|
|
257
257
|
animationsOpen && TOGGLE_ACTIVE_CLASS,
|
|
258
258
|
)}
|
|
259
|
-
aria-label={t("
|
|
259
|
+
aria-label={t("animations.title")}
|
|
260
260
|
aria-pressed={animationsOpen}
|
|
261
261
|
onClick={onOpenAnimations}
|
|
262
262
|
>
|
|
263
|
-
<
|
|
263
|
+
<IconBolt className="size-3.5" />
|
|
264
264
|
</Button>
|
|
265
265
|
</TooltipTrigger>
|
|
266
|
-
<TooltipContent>{t("
|
|
266
|
+
<TooltipContent>{t("animations.title")}</TooltipContent>
|
|
267
267
|
</Tooltip>
|
|
268
268
|
<div className={TOOLBAR_DIVIDER} />
|
|
269
269
|
</>
|