@agent-native/core 0.84.22 → 0.84.24

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 (97) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/model-config.ts +1 -1
  5. package/corpus/core/src/cli/skills.ts +17 -3
  6. package/corpus/core/src/client/AssistantChat.tsx +45 -12
  7. package/corpus/core/src/client/active-run-state.ts +22 -0
  8. package/corpus/core/src/client/chat/message-components.tsx +2 -1
  9. package/corpus/core/src/client/chat/tool-call-display.tsx +18 -2
  10. package/corpus/core/src/client/sse-event-processor.ts +157 -40
  11. package/corpus/core/src/notifications/index.ts +1 -0
  12. package/corpus/core/src/notifications/registry.ts +13 -1
  13. package/corpus/core/src/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
  14. package/corpus/core/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
  15. package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +2 -0
  16. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +15 -13
  17. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +18 -1
  18. package/corpus/templates/analytics/server/db/schema.ts +1 -1
  19. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -1
  20. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +76 -7
  21. package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +2 -0
  22. package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +2 -0
  23. package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +2 -0
  24. package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +2 -0
  25. package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +2 -0
  26. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +30 -1
  27. package/corpus/templates/clips/actions/finalize-recording.ts +84 -4
  28. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +256 -0
  29. package/corpus/templates/clips/actions/reprocess-recording.ts +160 -0
  30. package/corpus/templates/clips/app/components/player/share-dialog.tsx +2 -2
  31. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
  32. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
  33. package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
  34. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
  35. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
  36. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
  37. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
  38. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
  39. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
  40. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
  41. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
  42. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +31 -3
  43. package/corpus/templates/clips/changelog/2026-07-01-clip-embeds-now-render-as-a-clean-video-only-player-without-.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-01-shared-clips-now-start-playing-and-scrub-instantly-instead-o.md +6 -0
  45. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  46. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +2 -2
  47. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +41 -31
  48. package/corpus/templates/clips/server/lib/video-remux.ts +222 -0
  49. package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +2 -0
  50. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +75 -19
  51. package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +2 -0
  52. package/corpus/templates/design/app/pages/DesignEditor.tsx +11 -1
  53. package/corpus/templates/design/changelog/2026-07-01-design-chat-no-longer-shows-an-empty-state-selection-chip-in-the-composer.md +6 -0
  54. package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +2 -0
  55. package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +2 -0
  56. package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +2 -0
  57. package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +2 -0
  58. package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +2 -0
  59. package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +2 -0
  60. package/corpus/templates/videos/.agents/skills/frontend-design/SKILL.md +2 -0
  61. package/dist/agent/model-config.js +1 -1
  62. package/dist/agent/model-config.js.map +1 -1
  63. package/dist/cli/skills.d.ts.map +1 -1
  64. package/dist/cli/skills.js +17 -3
  65. package/dist/cli/skills.js.map +1 -1
  66. package/dist/client/AssistantChat.d.ts +2 -1
  67. package/dist/client/AssistantChat.d.ts.map +1 -1
  68. package/dist/client/AssistantChat.js +37 -15
  69. package/dist/client/AssistantChat.js.map +1 -1
  70. package/dist/client/active-run-state.d.ts +2 -0
  71. package/dist/client/active-run-state.d.ts.map +1 -1
  72. package/dist/client/active-run-state.js +17 -0
  73. package/dist/client/active-run-state.js.map +1 -1
  74. package/dist/client/chat/message-components.d.ts.map +1 -1
  75. package/dist/client/chat/message-components.js +3 -1
  76. package/dist/client/chat/message-components.js.map +1 -1
  77. package/dist/client/chat/tool-call-display.d.ts +3 -1
  78. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  79. package/dist/client/chat/tool-call-display.js +8 -8
  80. package/dist/client/chat/tool-call-display.js.map +1 -1
  81. package/dist/client/sse-event-processor.d.ts +1 -0
  82. package/dist/client/sse-event-processor.d.ts.map +1 -1
  83. package/dist/client/sse-event-processor.js +136 -35
  84. package/dist/client/sse-event-processor.js.map +1 -1
  85. package/dist/notifications/index.d.ts +1 -1
  86. package/dist/notifications/index.d.ts.map +1 -1
  87. package/dist/notifications/index.js +1 -1
  88. package/dist/notifications/index.js.map +1 -1
  89. package/dist/notifications/registry.d.ts +5 -0
  90. package/dist/notifications/registry.d.ts.map +1 -1
  91. package/dist/notifications/registry.js +4 -1
  92. package/dist/notifications/registry.js.map +1 -1
  93. package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
  94. package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
  95. package/package.json +1 -1
  96. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
  97. package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Repair the playback of already-stored recordings.
3
+ *
4
+ * Clips uploaded via the streaming path (or before the seekable rewrite
5
+ * existed) can ship raw MediaRecorder output: an MP4 with a trailing `moov`
6
+ * atom or a WebM with no Cues index. Those load slowly and re-buffer on every
7
+ * seek even though the file downloads fine. This action re-fetches the stored
8
+ * media, rewrites it to be start-playable and seekable (MP4 faststart / WebM
9
+ * Cues remux), re-uploads the fixed file, and repoints the recording at it.
10
+ *
11
+ * It's idempotent and non-destructive: clips that are already seekable are
12
+ * skipped, and any clip we can't improve or re-upload is left untouched.
13
+ *
14
+ * Usage:
15
+ * pnpm action reprocess-recording --id=<recordingId>
16
+ * pnpm action reprocess-recording --ids='["id1","id2"]'
17
+ * pnpm action reprocess-recording --all --limit=20
18
+ * pnpm action reprocess-recording --id=<recordingId> --force
19
+ */
20
+
21
+ import { defineAction } from "@agent-native/core";
22
+ import { and, desc, eq, isNotNull } from "drizzle-orm";
23
+ import { z } from "zod";
24
+
25
+ import { getDb, schema } from "../server/db/index.js";
26
+ import {
27
+ getCurrentOwnerEmail,
28
+ ownerEmailMatches,
29
+ } from "../server/lib/recordings.js";
30
+ import {
31
+ ensureRecordingSeekable,
32
+ type EnsureSeekableResult,
33
+ } from "./lib/ensure-seekable-video.js";
34
+
35
+ const MAX_TARGETS_PER_CALL = 100;
36
+ const DEFAULT_ALL_LIMIT = 20;
37
+
38
+ function parseIds(value: unknown): string[] {
39
+ if (Array.isArray(value)) {
40
+ return value.filter((v): v is string => typeof v === "string" && !!v);
41
+ }
42
+ if (typeof value === "string" && value.trim()) {
43
+ try {
44
+ const parsed = JSON.parse(value);
45
+ if (Array.isArray(parsed)) {
46
+ return parsed.filter((v): v is string => typeof v === "string" && !!v);
47
+ }
48
+ } catch {
49
+ // Fall back to a single comma-separated / plain id string.
50
+ return value
51
+ .split(",")
52
+ .map((s) => s.trim())
53
+ .filter(Boolean);
54
+ }
55
+ }
56
+ return [];
57
+ }
58
+
59
+ export default defineAction({
60
+ description:
61
+ "Repair slow-loading or buffering recordings by rewriting their stored video to be start-playable and seekable (MP4 faststart / WebM Cues remux) and re-uploading it. Use for clips that take a long time to load or re-buffer when scrubbing. Pass `id` for one clip, `ids` for several, or `all: true` to sweep the caller's ready clips (bounded by `limit`). Already-seekable clips are skipped unless `force` is true.",
62
+ schema: z.object({
63
+ id: z.string().optional().describe("A single recording id to repair."),
64
+ ids: z
65
+ .union([z.array(z.string()), z.string()])
66
+ .optional()
67
+ .describe(
68
+ "Multiple recording ids (array, or JSON/comma string for CLI).",
69
+ ),
70
+ all: z.coerce
71
+ .boolean()
72
+ .optional()
73
+ .describe(
74
+ "Repair the caller's own ready clips, most recent first, up to `limit`.",
75
+ ),
76
+ limit: z.coerce
77
+ .number()
78
+ .int()
79
+ .min(1)
80
+ .max(MAX_TARGETS_PER_CALL)
81
+ .optional()
82
+ .describe(
83
+ `Max clips to process when using \`all\` (default ${DEFAULT_ALL_LIMIT}).`,
84
+ ),
85
+ force: z.coerce
86
+ .boolean()
87
+ .optional()
88
+ .describe("Re-run even on clips already marked seekable."),
89
+ }),
90
+ run: async (args) => {
91
+ const db = getDb();
92
+ const ownerEmail = getCurrentOwnerEmail();
93
+ const force = Boolean(args.force);
94
+
95
+ let targetIds: string[] = [];
96
+ if (args.id) targetIds.push(args.id);
97
+ targetIds.push(...parseIds(args.ids));
98
+
99
+ if (args.all) {
100
+ const limit = args.limit ?? DEFAULT_ALL_LIMIT;
101
+ const rows = await db
102
+ .select({ id: schema.recordings.id })
103
+ .from(schema.recordings)
104
+ .where(
105
+ and(
106
+ ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
107
+ eq(schema.recordings.status, "ready"),
108
+ isNotNull(schema.recordings.videoUrl),
109
+ ),
110
+ )
111
+ .orderBy(desc(schema.recordings.createdAt))
112
+ .limit(limit);
113
+ targetIds.push(...rows.map((r) => r.id));
114
+ }
115
+
116
+ // De-dupe while preserving order, and bound the batch so a single call
117
+ // can't run unboundedly under the hosted foreground budget.
118
+ targetIds = Array.from(new Set(targetIds)).slice(0, MAX_TARGETS_PER_CALL);
119
+
120
+ if (targetIds.length === 0) {
121
+ return {
122
+ ok: true,
123
+ processed: 0,
124
+ changed: 0,
125
+ results: [] as EnsureSeekableResult[],
126
+ message:
127
+ "No recordings to reprocess. Pass id / ids, or all: true to sweep your clips.",
128
+ };
129
+ }
130
+
131
+ const results: EnsureSeekableResult[] = [];
132
+ for (const recordingId of targetIds) {
133
+ try {
134
+ results.push(
135
+ await ensureRecordingSeekable({ recordingId, ownerEmail, force }),
136
+ );
137
+ } catch (err) {
138
+ console.warn("[reprocess-recording] failed for", recordingId, err);
139
+ results.push({
140
+ recordingId,
141
+ status: "skipped-fetch-failed",
142
+ changed: false,
143
+ detail: err instanceof Error ? err.message : String(err),
144
+ });
145
+ }
146
+ }
147
+
148
+ const changed = results.filter((r) => r.changed).length;
149
+ console.log(
150
+ `Reprocessed ${results.length} recording(s); ${changed} rewritten for smoother playback.`,
151
+ );
152
+
153
+ return {
154
+ ok: true,
155
+ processed: results.length,
156
+ changed,
157
+ results,
158
+ };
159
+ },
160
+ });
@@ -452,8 +452,8 @@ function ClipsEmbedConfigurator({
452
452
 
453
453
  const code =
454
454
  mode === "responsive"
455
- ? `<div style="position:relative;padding-bottom:56.25%;height:0"><iframe src="${src}" frameborder="0" allowfullscreen allow="autoplay; picture-in-picture" style="position:absolute;inset:0;width:100%;height:100%"></iframe></div>`
456
- : `<iframe src="${src}" width="${width}" height="${height}" frameborder="0" allowfullscreen allow="autoplay; picture-in-picture"></iframe>`;
455
+ ? `<div style="position:relative;padding-bottom:56.25%;height:0;background:#000;overflow:hidden"><iframe src="${src}" title="${t("shareDialog.embedIframeTitle")}" frameborder="0" scrolling="no" allowfullscreen allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;inset:0;width:100%;height:100%;border:0;background:#000;overflow:hidden"></iframe></div>`
456
+ : `<iframe src="${src}" title="${t("shareDialog.embedIframeTitle")}" width="${width}" height="${height}" frameborder="0" scrolling="no" allowfullscreen allow="autoplay; fullscreen; picture-in-picture" style="display:block;max-width:100%;border:0;background:#000;overflow:hidden"></iframe>`;
457
457
 
458
458
  return (
459
459
  <div className="space-y-3">
@@ -428,6 +428,7 @@ const messages = {
428
428
  askOwnerPublic: "اطلب من المالك نشره.",
429
429
  responsive: "مستجيب (16:9)",
430
430
  fixedSize: "حجم ثابت",
431
+ embedIframeTitle: "فيديو Clips",
431
432
  width: "عرض",
432
433
  height: "ارتفاع",
433
434
  autoplay: "التشغيل التلقائي",
@@ -445,6 +445,7 @@ const messages = {
445
445
  askOwnerPublic: "Bitten Sie den Eigentümer, es öffentlich zu machen.",
446
446
  responsive: "Responsiv (16:9)",
447
447
  fixedSize: "Feste Größe",
448
+ embedIframeTitle: "Clips-Video",
448
449
  width: "Breite",
449
450
  height: "Höhe",
450
451
  autoplay: "Übersetzt: Autoplay",
@@ -428,6 +428,7 @@ const messages = {
428
428
  askOwnerPublic: "Ask the owner to make it public.",
429
429
  responsive: "Responsive (16:9)",
430
430
  fixedSize: "Fixed size",
431
+ embedIframeTitle: "Clips video",
431
432
  width: "Width",
432
433
  height: "Height",
433
434
  autoplay: "Autoplay",
@@ -440,6 +440,7 @@ const messages = {
440
440
  askOwnerPublic: "Pídele al propietario que lo haga público.",
441
441
  responsive: "Responsivo (16:9)",
442
442
  fixedSize: "Tamaño fijo",
443
+ embedIframeTitle: "Video de Clips",
443
444
  width: "Ancho",
444
445
  height: "Altura",
445
446
  autoplay: "Reproducción automática",
@@ -440,6 +440,7 @@ const messages = {
440
440
  askOwnerPublic: "Demandez au propriétaire de le rendre public.",
441
441
  responsive: "Réactif (16:9)",
442
442
  fixedSize: "Taille fixe",
443
+ embedIframeTitle: "Video Clips",
443
444
  width: "Largeur",
444
445
  height: "Hauteur",
445
446
  autoplay: "Lecture automatique",
@@ -425,6 +425,7 @@ const messages = {
425
425
  askOwnerPublic: "मालिक से इसे सार्वजनिक करने के लिए कहें.",
426
426
  responsive: "प्रतिक्रियाशील (16:9)",
427
427
  fixedSize: "निश्चित आकार",
428
+ embedIframeTitle: "Clips वीडियो",
428
429
  width: "चौड़ाई",
429
430
  height: "ऊंचाई",
430
431
  autoplay: "स्वत: प्ले",
@@ -437,6 +437,7 @@ const messages = {
437
437
  askOwnerPublic: "所有者に公開するよう依頼してください。",
438
438
  responsive: "レスポンシブ (16:9)",
439
439
  fixedSize: "固定サイズ",
440
+ embedIframeTitle: "Clips動画",
440
441
  width: "幅",
441
442
  height: "身長",
442
443
  autoplay: "自動再生",
@@ -430,6 +430,7 @@ const messages = {
430
430
  askOwnerPublic: "소유자에게 공개하도록 요청하세요.",
431
431
  responsive: "반응형(16:9)",
432
432
  fixedSize: "고정 크기",
433
+ embedIframeTitle: "Clips 동영상",
433
434
  width: "너비",
434
435
  height: "키",
435
436
  autoplay: "자동재생",
@@ -438,6 +438,7 @@ const messages = {
438
438
  askOwnerPublic: "Peça ao proprietário para torná-lo público.",
439
439
  responsive: "Responsivo (16:9)",
440
440
  fixedSize: "Tamanho fixo",
441
+ embedIframeTitle: "Video do Clips",
441
442
  width: "Largura",
442
443
  height: "Altura",
443
444
  autoplay: "Reprodução automática",
@@ -412,6 +412,7 @@ const messages = {
412
412
  askOwnerPublic: "请楼主公开一下。",
413
413
  responsive: "反应灵敏 (16:9)",
414
414
  fixedSize: "固定尺寸",
415
+ embedIframeTitle: "Clips 视频",
415
416
  width: "宽度",
416
417
  height: "高度",
417
418
  autoplay: "自动播放",
@@ -412,6 +412,7 @@ const messages = {
412
412
  askOwnerPublic: "請樓主公開一下。",
413
413
  responsive: "反應靈敏 (16:9)",
414
414
  fixedSize: "固定尺寸",
415
+ embedIframeTitle: "Clips 影片",
415
416
  width: "寬度",
416
417
  height: "高度",
417
418
  autoplay: "自動播放",
@@ -78,6 +78,34 @@ export default function EmbedRoute() {
78
78
  });
79
79
  const [pwError, setPwError] = useState<string | null>(null);
80
80
 
81
+ useEffect(() => {
82
+ if (typeof document === "undefined") return;
83
+
84
+ const html = document.documentElement;
85
+ const body = document.body;
86
+ const previous = {
87
+ htmlOverflow: html.style.overflow,
88
+ htmlHeight: html.style.height,
89
+ bodyOverflow: body.style.overflow,
90
+ bodyHeight: body.style.height,
91
+ bodyBackground: body.style.background,
92
+ };
93
+
94
+ html.style.overflow = "hidden";
95
+ html.style.height = "100%";
96
+ body.style.overflow = "hidden";
97
+ body.style.height = "100%";
98
+ body.style.background = "#000";
99
+
100
+ return () => {
101
+ html.style.overflow = previous.htmlOverflow;
102
+ html.style.height = previous.htmlHeight;
103
+ body.style.overflow = previous.bodyOverflow;
104
+ body.style.height = previous.bodyHeight;
105
+ body.style.background = previous.bodyBackground;
106
+ };
107
+ }, []);
108
+
81
109
  const dataQ = useQuery({
82
110
  queryKey: ["public-recording", shareId, password],
83
111
  queryFn: async () => {
@@ -135,7 +163,7 @@ export default function EmbedRoute() {
135
163
 
136
164
  if (dataQ.isLoading) {
137
165
  return (
138
- <div className="flex items-center justify-center h-screen w-full bg-black">
166
+ <div className="fixed inset-0 flex h-dvh w-dvw items-center justify-center overflow-hidden bg-black">
139
167
  <Spinner className="h-8 w-8 text-white/70" />
140
168
  </div>
141
169
  );
@@ -153,14 +181,14 @@ export default function EmbedRoute() {
153
181
 
154
182
  if (!recording) {
155
183
  return (
156
- <div className="flex items-center justify-center h-screen w-full bg-black text-white">
184
+ <div className="fixed inset-0 flex h-dvh w-dvw items-center justify-center overflow-hidden bg-black text-white">
157
185
  <p className="text-sm">{t("embedRoute.unavailable")}</p>
158
186
  </div>
159
187
  );
160
188
  }
161
189
 
162
190
  return (
163
- <div className="h-screen w-screen bg-black overflow-hidden">
191
+ <div className="fixed inset-0 h-dvh w-dvw overflow-hidden bg-black">
164
192
  <VideoPlayer
165
193
  ref={playerRef}
166
194
  recordingId={recording.id}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Clip embeds now render as a clean video-only player without extra page chrome or scrollbars.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Shared clips now start playing and scrub instantly instead of buffering — recordings are made seekable on upload, and you can repair older clips
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "Agent-Native Clips",
4
4
  "short_name": "Clips",
5
- "version": "0.1.11",
5
+ "version": "0.1.12",
6
6
  "description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub.",
7
7
  "minimum_chrome_version": "116",
8
8
  "action": {
@@ -168,10 +168,10 @@
168
168
  Object.assign(frame.style, {
169
169
  display: "block",
170
170
  width: "100%",
171
- height: "430px",
171
+ height: "405px",
172
172
  border: "0",
173
173
  borderRadius: "8px",
174
- background: "transparent",
174
+ background: "#000",
175
175
  overflow: "hidden",
176
176
  });
177
177
 
@@ -121,36 +121,39 @@ function renderPreview(
121
121
  root.textContent = "";
122
122
 
123
123
  const card = document.createElement("section");
124
- card.className = "clips-preview-card";
125
-
126
- const header = document.createElement("header");
127
- header.className = "clips-preview-header";
128
-
129
- const titleWrap = document.createElement("div");
130
- titleWrap.className = "clips-preview-title-wrap";
131
-
132
- const title = document.createElement("div");
133
- title.className = "clips-preview-title";
134
- title.textContent =
135
- state.kind === "playable" || state.kind === "unavailable"
136
- ? state.title
137
- : "Clips preview";
138
-
139
- const subtitle = document.createElement("div");
140
- subtitle.className = "clips-preview-subtitle";
141
- subtitle.textContent = preview.host;
142
-
143
- titleWrap.append(title, subtitle);
144
-
145
- const open = document.createElement("a");
146
- open.className = "clips-preview-open";
147
- open.href = preview.sourceUrl.toString();
148
- open.target = "_blank";
149
- open.rel = "noreferrer noopener";
150
- open.textContent = "Open clip";
151
-
152
- header.append(titleWrap, open);
153
- card.append(header);
124
+ card.className =
125
+ state.kind === "playable"
126
+ ? "clips-preview-card clips-preview-card--player"
127
+ : "clips-preview-card";
128
+
129
+ if (state.kind !== "playable") {
130
+ const header = document.createElement("header");
131
+ header.className = "clips-preview-header";
132
+
133
+ const titleWrap = document.createElement("div");
134
+ titleWrap.className = "clips-preview-title-wrap";
135
+
136
+ const title = document.createElement("div");
137
+ title.className = "clips-preview-title";
138
+ title.textContent =
139
+ state.kind === "unavailable" ? state.title : "Clips preview";
140
+
141
+ const subtitle = document.createElement("div");
142
+ subtitle.className = "clips-preview-subtitle";
143
+ subtitle.textContent = preview.host;
144
+
145
+ titleWrap.append(title, subtitle);
146
+
147
+ const open = document.createElement("a");
148
+ open.className = "clips-preview-open";
149
+ open.href = preview.sourceUrl.toString();
150
+ open.target = "_blank";
151
+ open.rel = "noreferrer noopener";
152
+ open.textContent = "Open clip";
153
+
154
+ header.append(titleWrap, open);
155
+ card.append(header);
156
+ }
154
157
 
155
158
  if (state.kind === "playable") {
156
159
  const player = document.createElement("div");
@@ -161,6 +164,7 @@ function renderPreview(
161
164
  frame.src = preview.embedUrl.toString();
162
165
  frame.allow = "autoplay; fullscreen; picture-in-picture";
163
166
  frame.allowFullscreen = true;
167
+ frame.scrolling = "no";
164
168
  frame.referrerPolicy = "no-referrer";
165
169
 
166
170
  player.append(frame);
@@ -273,6 +277,7 @@ function installStyles(): void {
273
277
  body {
274
278
  margin: 0;
275
279
  min-width: 0;
280
+ width: 100%;
276
281
  background: transparent;
277
282
  }
278
283
 
@@ -292,6 +297,12 @@ function installStyles(): void {
292
297
  box-shadow: var(--clips-shadow);
293
298
  }
294
299
 
300
+ .clips-preview-card--player {
301
+ border: 0;
302
+ background: #000000;
303
+ box-shadow: none;
304
+ }
305
+
295
306
  .clips-preview-header {
296
307
  display: flex;
297
308
  align-items: center;
@@ -348,7 +359,6 @@ function installStyles(): void {
348
359
  position: relative;
349
360
  width: 100%;
350
361
  aspect-ratio: 16 / 9;
351
- min-height: 240px;
352
362
  background: #000000;
353
363
  }
354
364