@jiggai/kitchen-plugin-marketing 0.4.1 → 0.5.1

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.
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "5",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "5",
8
+ "when": 1743811200000,
9
+ "tag": "0001_initial",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -219,7 +219,20 @@ function initializeDatabase(teamId) {
219
219
  const migrationsDir = (0, import_path.join)(PLUGIN_ROOT, "db", "migrations");
220
220
  (0, import_migrator.migrate)(db, { migrationsFolder: migrationsDir });
221
221
  } catch (error) {
222
- console.warn("Migration warning:", error?.message);
222
+ try {
223
+ const sqlPath = (0, import_path.join)(PLUGIN_ROOT, "db", "migrations", "0001_initial.sql");
224
+ if ((0, import_fs.existsSync)(sqlPath)) {
225
+ const sql2 = require("fs").readFileSync(sqlPath, "utf8");
226
+ const statements = sql2.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
227
+ for (const stmt of statements) {
228
+ try {
229
+ sqlite.exec(stmt + ";");
230
+ } catch {
231
+ }
232
+ }
233
+ }
234
+ } catch {
235
+ }
223
236
  }
224
237
  return { db, sqlite };
225
238
  }
@@ -1002,8 +1015,187 @@ async function handleRequest(req, ctx) {
1002
1015
  return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
1003
1016
  }
1004
1017
  }
1018
+ const MEDIA_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".openclaw", "kitchen", "plugins", "marketing", "media");
1019
+ function ensureMediaDir(team) {
1020
+ const dir = (0, import_path2.join)(MEDIA_DIR, team);
1021
+ (0, import_fs2.mkdirSync)(dir, { recursive: true });
1022
+ return dir;
1023
+ }
1024
+ if (req.path === "/media" && req.method === "POST") {
1025
+ try {
1026
+ const body = req.body;
1027
+ if (!body?.data) return apiError(400, "VALIDATION_ERROR", "data (base64) is required");
1028
+ let base64 = body.data;
1029
+ let detectedMime = body.mimeType || "application/octet-stream";
1030
+ const dataUrlMatch = base64.match(/^data:([^;]+);base64,(.+)$/);
1031
+ if (dataUrlMatch) {
1032
+ detectedMime = dataUrlMatch[1];
1033
+ base64 = dataUrlMatch[2];
1034
+ }
1035
+ const buf = Buffer.from(base64, "base64");
1036
+ const id = (0, import_crypto2.randomUUID)();
1037
+ const ext = (0, import_path2.extname)(body.filename || "") || mimeToExt(detectedMime);
1038
+ const storedFilename = `${id}${ext}`;
1039
+ const dir = ensureMediaDir(teamId);
1040
+ const filePath = (0, import_path2.join)(dir, storedFilename);
1041
+ (0, import_fs2.writeFileSync)(filePath, buf);
1042
+ const { db } = initializeDatabase(teamId);
1043
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1044
+ const userId = getUserId(req);
1045
+ const record = {
1046
+ id,
1047
+ teamId,
1048
+ filename: storedFilename,
1049
+ originalName: body.filename || storedFilename,
1050
+ mimeType: detectedMime,
1051
+ size: buf.length,
1052
+ width: null,
1053
+ height: null,
1054
+ alt: body.alt || null,
1055
+ tags: JSON.stringify(body.tags || []),
1056
+ url: `/api/plugins/marketing/media/${id}/file?team=${encodeURIComponent(teamId)}`,
1057
+ thumbnailUrl: null,
1058
+ createdAt: now,
1059
+ createdBy: userId
1060
+ };
1061
+ await db.insert(media).values(record);
1062
+ return {
1063
+ status: 201,
1064
+ data: {
1065
+ id,
1066
+ filename: record.originalName,
1067
+ mimeType: detectedMime,
1068
+ size: buf.length,
1069
+ url: record.url,
1070
+ alt: record.alt,
1071
+ tags: body.tags || [],
1072
+ createdAt: now
1073
+ }
1074
+ };
1075
+ } catch (error) {
1076
+ return apiError(500, "UPLOAD_ERROR", error?.message || "Upload failed");
1077
+ }
1078
+ }
1079
+ if (req.path === "/media" && req.method === "GET") {
1080
+ try {
1081
+ const { db } = initializeDatabase(teamId);
1082
+ const { limit, offset } = parsePagination(req.query);
1083
+ const conditions = [(0, import_drizzle_orm2.eq)(media.teamId, teamId)];
1084
+ if (req.query.mimeType) {
1085
+ conditions.push((0, import_drizzle_orm2.like)(media.mimeType, `${req.query.mimeType}%`));
1086
+ }
1087
+ const totalResult = await db.select({ count: import_drizzle_orm2.sql`count(*)` }).from(media).where((0, import_drizzle_orm2.and)(...conditions));
1088
+ const total = totalResult[0]?.count ?? 0;
1089
+ const items = await db.select().from(media).where((0, import_drizzle_orm2.and)(...conditions)).orderBy((0, import_drizzle_orm2.desc)(media.createdAt)).limit(limit).offset(offset);
1090
+ const data = items.map((m) => {
1091
+ let thumbnailDataUrl;
1092
+ if (m.mimeType.startsWith("image/")) {
1093
+ const fp = (0, import_path2.join)(MEDIA_DIR, teamId, m.filename);
1094
+ if ((0, import_fs2.existsSync)(fp)) {
1095
+ const raw = (0, import_fs2.readFileSync)(fp);
1096
+ if (raw.length < 2 * 1024 * 1024) {
1097
+ thumbnailDataUrl = `data:${m.mimeType};base64,${raw.toString("base64")}`;
1098
+ }
1099
+ }
1100
+ }
1101
+ return {
1102
+ id: m.id,
1103
+ filename: m.originalName,
1104
+ mimeType: m.mimeType,
1105
+ size: m.size,
1106
+ url: m.url,
1107
+ thumbnailDataUrl,
1108
+ alt: m.alt,
1109
+ tags: JSON.parse(m.tags || "[]"),
1110
+ createdAt: m.createdAt
1111
+ };
1112
+ });
1113
+ return {
1114
+ status: 200,
1115
+ data: { data, total, offset, limit, hasMore: offset + limit < total }
1116
+ };
1117
+ } catch (error) {
1118
+ return apiError(500, "DATABASE_ERROR", error?.message || "Failed to list media");
1119
+ }
1120
+ }
1121
+ const mediaIdMatch = req.path.match(/^\/media\/([a-f0-9-]+)$/);
1122
+ if (mediaIdMatch && req.method === "GET") {
1123
+ try {
1124
+ const { db } = initializeDatabase(teamId);
1125
+ const [item] = await db.select().from(media).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(media.id, mediaIdMatch[1]), (0, import_drizzle_orm2.eq)(media.teamId, teamId)));
1126
+ if (!item) return apiError(404, "NOT_FOUND", "Media not found");
1127
+ let dataUrl;
1128
+ const fp = (0, import_path2.join)(MEDIA_DIR, teamId, item.filename);
1129
+ if ((0, import_fs2.existsSync)(fp)) {
1130
+ const raw = (0, import_fs2.readFileSync)(fp);
1131
+ dataUrl = `data:${item.mimeType};base64,${raw.toString("base64")}`;
1132
+ }
1133
+ return {
1134
+ status: 200,
1135
+ data: {
1136
+ id: item.id,
1137
+ filename: item.originalName,
1138
+ mimeType: item.mimeType,
1139
+ size: item.size,
1140
+ url: item.url,
1141
+ dataUrl,
1142
+ alt: item.alt,
1143
+ tags: JSON.parse(item.tags || "[]"),
1144
+ createdAt: item.createdAt
1145
+ }
1146
+ };
1147
+ } catch (error) {
1148
+ return apiError(500, "DATABASE_ERROR", error?.message || "Failed to get media");
1149
+ }
1150
+ }
1151
+ const mediaFileMatch = req.path.match(/^\/media\/([a-f0-9-]+)\/file$/);
1152
+ if (mediaFileMatch && req.method === "GET") {
1153
+ try {
1154
+ const { db } = initializeDatabase(teamId);
1155
+ const [item] = await db.select().from(media).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(media.id, mediaFileMatch[1]), (0, import_drizzle_orm2.eq)(media.teamId, teamId)));
1156
+ if (!item) return apiError(404, "NOT_FOUND", "Media not found");
1157
+ const fp = (0, import_path2.join)(MEDIA_DIR, teamId, item.filename);
1158
+ if (!(0, import_fs2.existsSync)(fp)) return apiError(404, "NOT_FOUND", "File missing from disk");
1159
+ const raw = (0, import_fs2.readFileSync)(fp);
1160
+ const dataUrl = `data:${item.mimeType};base64,${raw.toString("base64")}`;
1161
+ return { status: 200, data: { dataUrl, mimeType: item.mimeType, filename: item.originalName } };
1162
+ } catch (error) {
1163
+ return apiError(500, "FILE_ERROR", error?.message || "Failed to serve file");
1164
+ }
1165
+ }
1166
+ if (mediaIdMatch && req.method === "DELETE") {
1167
+ try {
1168
+ const { db } = initializeDatabase(teamId);
1169
+ const [item] = await db.select().from(media).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(media.id, mediaIdMatch[1]), (0, import_drizzle_orm2.eq)(media.teamId, teamId)));
1170
+ if (!item) return apiError(404, "NOT_FOUND", "Media not found");
1171
+ const fp = (0, import_path2.join)(MEDIA_DIR, teamId, item.filename);
1172
+ try {
1173
+ (0, import_fs2.unlinkSync)(fp);
1174
+ } catch {
1175
+ }
1176
+ await db.delete(media).where((0, import_drizzle_orm2.eq)(media.id, mediaIdMatch[1]));
1177
+ return { status: 200, data: { deleted: true, id: mediaIdMatch[1] } };
1178
+ } catch (error) {
1179
+ return apiError(500, "DATABASE_ERROR", error?.message || "Failed to delete media");
1180
+ }
1181
+ }
1005
1182
  return apiError(501, "NOT_IMPLEMENTED", `No handler for ${req.method} ${req.path}`);
1006
1183
  }
1184
+ function mimeToExt(mime) {
1185
+ const map = {
1186
+ "image/jpeg": ".jpg",
1187
+ "image/png": ".png",
1188
+ "image/gif": ".gif",
1189
+ "image/webp": ".webp",
1190
+ "image/svg+xml": ".svg",
1191
+ "video/mp4": ".mp4",
1192
+ "video/webm": ".webm",
1193
+ "video/quicktime": ".mov",
1194
+ "audio/mpeg": ".mp3",
1195
+ "audio/wav": ".wav"
1196
+ };
1197
+ return map[mime] || "";
1198
+ }
1007
1199
  // Annotate the CommonJS export names for ESM import in node:
1008
1200
  0 && (module.exports = {
1009
1201
  handleRequest
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "5",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "5",
8
+ "when": 1743811200000,
9
+ "tag": "0001_initial",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
package/dist/index.js CHANGED
@@ -204,7 +204,20 @@ function initializeDatabase(teamId) {
204
204
  const migrationsDir = (0, import_path.join)(PLUGIN_ROOT, "db", "migrations");
205
205
  (0, import_migrator.migrate)(db, { migrationsFolder: migrationsDir });
206
206
  } catch (error) {
207
- console.warn("Migration warning:", error?.message);
207
+ try {
208
+ const sqlPath = (0, import_path.join)(PLUGIN_ROOT, "db", "migrations", "0001_initial.sql");
209
+ if ((0, import_fs.existsSync)(sqlPath)) {
210
+ const sql2 = require("fs").readFileSync(sqlPath, "utf8");
211
+ const statements = sql2.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
212
+ for (const stmt of statements) {
213
+ try {
214
+ sqlite.exec(stmt + ";");
215
+ } catch {
216
+ }
217
+ }
218
+ }
219
+ } catch {
220
+ }
208
221
  }
209
222
  return { db, sqlite };
210
223
  }
@@ -185,9 +185,9 @@
185
185
  background: "var(--ck-bg-base, #0b0c10)",
186
186
  border: "1px solid var(--ck-border-subtle)",
187
187
  borderRadius: "14px",
188
- width: "95vw",
189
- maxWidth: "900px",
190
- maxHeight: "90vh",
188
+ width: "96vw",
189
+ maxWidth: "1200px",
190
+ maxHeight: "92vh",
191
191
  overflow: "auto",
192
192
  display: "flex",
193
193
  flexDirection: "column"
@@ -201,7 +201,7 @@
201
201
  },
202
202
  modalBody: { display: "flex", flex: 1, minHeight: 0 },
203
203
  modalLeft: { flex: 1, padding: "1rem 1.25rem", borderRight: "1px solid var(--ck-border-subtle)", display: "flex", flexDirection: "column", gap: "0.75rem" },
204
- modalRight: { width: "280px", padding: "1rem 1.25rem", flexShrink: 0 },
204
+ modalRight: { width: "380px", padding: "1rem 1.25rem", flexShrink: 0 },
205
205
  modalFooter: {
206
206
  display: "flex",
207
207
  alignItems: "center",
@@ -823,12 +823,118 @@
823
823
  placeholder: "Paste image or video URL\u2026"
824
824
  })
825
825
  ),
826
- // Right — preview
826
+ // Right — social-post-style preview
827
827
  h(
828
828
  "div",
829
829
  { style: s.modalRight },
830
- h("div", { style: { fontWeight: 600, fontSize: "0.9rem", color: "var(--ck-text-primary)", marginBottom: "0.5rem" } }, "Post Preview"),
831
- modalContent.trim() ? h("div", { style: { ...s.previewPanel, whiteSpace: "pre-wrap", fontSize: "0.85rem", color: "var(--ck-text-secondary)" } }, modalContent) : h("div", { style: { color: "var(--ck-text-tertiary)", fontSize: "0.85rem" } }, "Start writing your post for a preview")
830
+ h("div", { style: { fontWeight: 600, fontSize: "0.85rem", color: "var(--ck-text-secondary)", marginBottom: "0.75rem" } }, "Post Preview"),
831
+ h(
832
+ "div",
833
+ {
834
+ style: {
835
+ background: "rgba(22,22,28,0.95)",
836
+ borderRadius: "12px",
837
+ border: "1px solid rgba(255,255,255,0.08)",
838
+ overflow: "hidden"
839
+ }
840
+ },
841
+ // Header
842
+ h(
843
+ "div",
844
+ { style: { display: "flex", alignItems: "center", gap: "0.65rem", padding: "0.85rem 1rem 0" } },
845
+ h("div", {
846
+ style: {
847
+ width: "40px",
848
+ height: "40px",
849
+ borderRadius: "50%",
850
+ background: "rgba(127,90,240,0.25)",
851
+ display: "flex",
852
+ alignItems: "center",
853
+ justifyContent: "center",
854
+ fontSize: "1.1rem",
855
+ color: "rgba(127,90,240,0.9)",
856
+ flexShrink: 0
857
+ }
858
+ }, "\u{1F464}"),
859
+ h(
860
+ "div",
861
+ null,
862
+ h(
863
+ "div",
864
+ { style: { display: "flex", alignItems: "center", gap: "0.3rem" } },
865
+ h("span", { style: { fontWeight: 700, fontSize: "0.9rem", color: "var(--ck-text-primary)" } }, "Your Brand"),
866
+ h("span", { style: { color: "rgba(99,179,237,0.9)", fontSize: "0.85rem" } }, "\u2713")
867
+ ),
868
+ h(
869
+ "div",
870
+ { style: { fontSize: "0.75rem", color: "var(--ck-text-tertiary)" } },
871
+ modalDate ? new Date(modalDate).toLocaleDateString(void 0, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "Just now"
872
+ )
873
+ )
874
+ ),
875
+ // Body
876
+ h(
877
+ "div",
878
+ { style: { padding: "0.65rem 1rem 0.75rem" } },
879
+ modalContent.trim() ? h("div", {
880
+ style: {
881
+ whiteSpace: "pre-wrap",
882
+ fontSize: "0.9rem",
883
+ color: "var(--ck-text-primary)",
884
+ lineHeight: "1.5",
885
+ maxHeight: "300px",
886
+ overflowY: "auto",
887
+ wordBreak: "break-word"
888
+ }
889
+ }, modalContent) : h("div", {
890
+ style: { color: "var(--ck-text-tertiary)", fontSize: "0.85rem", fontStyle: "italic", padding: "1.5rem 0", textAlign: "center" }
891
+ }, "Start writing to see a preview")
892
+ ),
893
+ // Media preview
894
+ modalMediaUrl && h("img", {
895
+ src: modalMediaUrl,
896
+ style: { width: "100%", display: "block" },
897
+ onError: (e) => {
898
+ e.target.style.display = "none";
899
+ }
900
+ }),
901
+ // Engagement bar
902
+ h(
903
+ "div",
904
+ {
905
+ style: {
906
+ display: "flex",
907
+ justifyContent: "space-around",
908
+ padding: "0.6rem 1rem",
909
+ borderTop: "1px solid rgba(255,255,255,0.06)",
910
+ fontSize: "0.8rem",
911
+ color: "var(--ck-text-tertiary)"
912
+ }
913
+ },
914
+ h("span", null, "\u2764\uFE0F 0"),
915
+ h("span", null, "\u{1F4AC} 0"),
916
+ h("span", null, "\u{1F501} 0"),
917
+ h("span", null, "\u{1F4CA} 0")
918
+ )
919
+ ),
920
+ // Platform pills
921
+ modalPlatforms.length > 0 && h(
922
+ "div",
923
+ {
924
+ style: { display: "flex", flexWrap: "wrap", gap: "0.35rem", marginTop: "0.65rem" }
925
+ },
926
+ ...modalPlatforms.map((pl) => h("span", {
927
+ key: pl,
928
+ style: {
929
+ background: "rgba(127,90,240,0.12)",
930
+ border: "1px solid rgba(127,90,240,0.25)",
931
+ borderRadius: "999px",
932
+ padding: "0.1rem 0.4rem",
933
+ fontSize: "0.7rem",
934
+ color: "var(--ck-text-secondary)"
935
+ }
936
+ }, pl))
937
+ )
832
938
  )
833
939
  ),
834
940
  // Footer
@@ -120,6 +120,11 @@
120
120
  const [scheduledAt, setScheduledAt] = useState("");
121
121
  const [mediaUrl, setMediaUrl] = useState("");
122
122
  const [showMedia, setShowMedia] = useState(false);
123
+ const [mediaLibrary, setMediaLibrary] = useState([]);
124
+ const [showMediaPicker, setShowMediaPicker] = useState(false);
125
+ const [uploading, setUploading] = useState(false);
126
+ const [selectedMediaIds, setSelectedMediaIds] = useState([]);
127
+ const fileInputRef = useRef(null);
123
128
  const successTimeout = useRef(null);
124
129
  const postizHeaders = useMemo(() => {
125
130
  try {
@@ -154,10 +159,64 @@
154
159
  } catch {
155
160
  }
156
161
  }, [apiBase, teamId]);
162
+ const loadMedia = useCallback(async () => {
163
+ try {
164
+ const res = await fetch(`${apiBase}/media?team=${encodeURIComponent(teamId)}&limit=100`);
165
+ const json = await res.json();
166
+ setMediaLibrary(Array.isArray(json.data) ? json.data : []);
167
+ } catch {
168
+ }
169
+ }, [apiBase, teamId]);
170
+ const handleFileUpload = useCallback(async (files) => {
171
+ if (!files || files.length === 0) return;
172
+ setUploading(true);
173
+ setError(null);
174
+ try {
175
+ for (const file of Array.from(files)) {
176
+ const base64 = await new Promise((resolve, reject) => {
177
+ const reader = new FileReader();
178
+ reader.onload = () => resolve(reader.result);
179
+ reader.onerror = reject;
180
+ reader.readAsDataURL(file);
181
+ });
182
+ const res = await fetch(`${apiBase}/media?team=${encodeURIComponent(teamId)}`, {
183
+ method: "POST",
184
+ headers: { "content-type": "application/json" },
185
+ body: JSON.stringify({ data: base64, filename: file.name, mimeType: file.type })
186
+ });
187
+ if (!res.ok) {
188
+ const err = await res.json().catch(() => ({}));
189
+ throw new Error(err.message || `Upload failed (${res.status})`);
190
+ }
191
+ const item = await res.json();
192
+ setSelectedMediaIds((prev) => [...prev, item.id]);
193
+ }
194
+ await loadMedia();
195
+ showSuccess(`Uploaded ${files.length} file${files.length > 1 ? "s" : ""}`);
196
+ } catch (e) {
197
+ setError(e?.message || "Upload failed");
198
+ } finally {
199
+ setUploading(false);
200
+ if (fileInputRef.current) fileInputRef.current.value = "";
201
+ }
202
+ }, [apiBase, teamId, loadMedia]);
203
+ const deleteMedia = useCallback(async (id) => {
204
+ try {
205
+ await fetch(`${apiBase}/media/${id}?team=${encodeURIComponent(teamId)}`, { method: "DELETE" });
206
+ setSelectedMediaIds((prev) => prev.filter((x) => x !== id));
207
+ await loadMedia();
208
+ } catch {
209
+ }
210
+ }, [apiBase, teamId, loadMedia]);
211
+ const toggleMediaSelect = (id) => {
212
+ setSelectedMediaIds(
213
+ (prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
214
+ );
215
+ };
157
216
  useEffect(() => {
158
217
  setLoading(true);
159
- Promise.all([loadDrivers(), loadPosts()]).finally(() => setLoading(false));
160
- }, [loadDrivers, loadPosts]);
218
+ Promise.all([loadDrivers(), loadPosts(), loadMedia()]).finally(() => setLoading(false));
219
+ }, [loadDrivers, loadPosts, loadMedia]);
161
220
  const connectedDrivers = useMemo(() => drivers.filter((d) => d.connected), [drivers]);
162
221
  const disconnectedDrivers = useMemo(() => drivers.filter((d) => !d.connected), [drivers]);
163
222
  const togglePlatform = (platform) => {
@@ -348,7 +407,7 @@
348
407
  )
349
408
  )
350
409
  ),
351
- // Media URL (collapsible)
410
+ // Media (upload, URL, or library picker)
352
411
  h(
353
412
  "div",
354
413
  null,
@@ -359,14 +418,228 @@
359
418
  }, showMedia ? "\u2212 Media" : "+ Media"),
360
419
  showMedia && h(
361
420
  "div",
362
- { className: "mt-2" },
363
- h("input", {
364
- type: "url",
365
- value: mediaUrl,
366
- onChange: (e) => setMediaUrl(e.target.value),
367
- placeholder: "Paste image or video URL\u2026",
368
- style: t.input
369
- })
421
+ { className: "mt-2 space-y-2" },
422
+ // Upload + URL row
423
+ h(
424
+ "div",
425
+ { className: "flex gap-2 items-center" },
426
+ h("input", {
427
+ ref: fileInputRef,
428
+ type: "file",
429
+ accept: "image/*,video/*",
430
+ multiple: true,
431
+ style: { display: "none" },
432
+ onChange: (e) => handleFileUpload(e.target.files)
433
+ }),
434
+ h("button", {
435
+ type: "button",
436
+ onClick: () => fileInputRef.current?.click(),
437
+ style: { ...t.btnGhost, padding: "0.35rem 0.7rem", fontSize: "0.8rem", whiteSpace: "nowrap" },
438
+ disabled: uploading
439
+ }, uploading ? "\u23F3 Uploading\u2026" : "\u{1F4C1} Upload"),
440
+ h("button", {
441
+ type: "button",
442
+ onClick: () => {
443
+ loadMedia();
444
+ setShowMediaPicker(!showMediaPicker);
445
+ },
446
+ style: { ...t.btnGhost, padding: "0.35rem 0.7rem", fontSize: "0.8rem", whiteSpace: "nowrap" }
447
+ }, showMediaPicker ? "Hide Library" : "\u{1F5BC}\uFE0F Library"),
448
+ h("input", {
449
+ type: "url",
450
+ value: mediaUrl,
451
+ onChange: (e) => setMediaUrl(e.target.value),
452
+ placeholder: "\u2026or paste a URL",
453
+ style: { ...t.input, flex: 1 }
454
+ })
455
+ ),
456
+ // Selected media thumbnails
457
+ selectedMediaIds.length > 0 && h(
458
+ "div",
459
+ { className: "flex flex-wrap gap-2" },
460
+ ...selectedMediaIds.map((id) => {
461
+ const item = mediaLibrary.find((m) => m.id === id);
462
+ if (!item) return null;
463
+ return h(
464
+ "div",
465
+ {
466
+ key: id,
467
+ style: {
468
+ position: "relative",
469
+ width: "72px",
470
+ height: "72px",
471
+ borderRadius: "8px",
472
+ overflow: "hidden",
473
+ border: "2px solid rgba(127,90,240,0.5)"
474
+ }
475
+ },
476
+ item.mimeType?.startsWith("video/") ? h("div", {
477
+ style: {
478
+ width: "100%",
479
+ height: "100%",
480
+ background: "rgba(0,0,0,0.4)",
481
+ display: "flex",
482
+ alignItems: "center",
483
+ justifyContent: "center",
484
+ color: "white",
485
+ fontSize: "1.2rem"
486
+ }
487
+ }, "\u{1F3A5}") : h("img", {
488
+ src: item.thumbnailDataUrl || item.url,
489
+ style: { width: "100%", height: "100%", objectFit: "cover" }
490
+ }),
491
+ h("button", {
492
+ type: "button",
493
+ onClick: () => toggleMediaSelect(id),
494
+ style: {
495
+ position: "absolute",
496
+ top: "2px",
497
+ right: "2px",
498
+ background: "rgba(0,0,0,0.6)",
499
+ border: "none",
500
+ borderRadius: "50%",
501
+ width: "18px",
502
+ height: "18px",
503
+ color: "white",
504
+ fontSize: "0.65rem",
505
+ cursor: "pointer",
506
+ display: "flex",
507
+ alignItems: "center",
508
+ justifyContent: "center",
509
+ lineHeight: "1"
510
+ }
511
+ }, "\u2715")
512
+ );
513
+ })
514
+ ),
515
+ // Media library picker grid
516
+ showMediaPicker && h(
517
+ "div",
518
+ {
519
+ style: {
520
+ background: "rgba(255,255,255,0.02)",
521
+ border: "1px solid var(--ck-border-subtle)",
522
+ borderRadius: "10px",
523
+ padding: "0.75rem",
524
+ maxHeight: "260px",
525
+ overflowY: "auto"
526
+ }
527
+ },
528
+ h(
529
+ "div",
530
+ { className: "text-xs font-medium mb-2", style: t.faint },
531
+ `Media Library (${mediaLibrary.length} items)`
532
+ ),
533
+ mediaLibrary.length === 0 ? h("div", { className: "text-xs py-4 text-center", style: t.faint }, "No media yet. Upload some files!") : h(
534
+ "div",
535
+ {
536
+ style: {
537
+ display: "grid",
538
+ gridTemplateColumns: "repeat(auto-fill, minmax(80px, 1fr))",
539
+ gap: "0.5rem"
540
+ }
541
+ },
542
+ ...mediaLibrary.map((item) => {
543
+ const isSelected = selectedMediaIds.includes(item.id);
544
+ return h(
545
+ "div",
546
+ {
547
+ key: item.id,
548
+ onClick: () => toggleMediaSelect(item.id),
549
+ style: {
550
+ position: "relative",
551
+ cursor: "pointer",
552
+ width: "100%",
553
+ paddingTop: "100%",
554
+ borderRadius: "8px",
555
+ overflow: "hidden",
556
+ border: isSelected ? "2px solid rgba(127,90,240,0.7)" : "1px solid var(--ck-border-subtle)",
557
+ boxShadow: isSelected ? "0 0 8px rgba(127,90,240,0.3)" : "none"
558
+ }
559
+ },
560
+ item.mimeType?.startsWith("video/") ? h("div", {
561
+ style: {
562
+ position: "absolute",
563
+ inset: "0",
564
+ background: "rgba(0,0,0,0.4)",
565
+ display: "flex",
566
+ alignItems: "center",
567
+ justifyContent: "center",
568
+ color: "white",
569
+ fontSize: "1.5rem"
570
+ }
571
+ }, "\u{1F3A5}") : h("img", {
572
+ src: item.thumbnailDataUrl || item.url,
573
+ style: {
574
+ position: "absolute",
575
+ inset: "0",
576
+ width: "100%",
577
+ height: "100%",
578
+ objectFit: "cover"
579
+ }
580
+ }),
581
+ isSelected && h("div", {
582
+ style: {
583
+ position: "absolute",
584
+ top: "4px",
585
+ right: "4px",
586
+ background: "rgba(127,90,240,0.85)",
587
+ borderRadius: "50%",
588
+ width: "20px",
589
+ height: "20px",
590
+ display: "flex",
591
+ alignItems: "center",
592
+ justifyContent: "center",
593
+ color: "white",
594
+ fontSize: "0.7rem",
595
+ fontWeight: "700"
596
+ }
597
+ }, "\u2713"),
598
+ h("button", {
599
+ type: "button",
600
+ onClick: (e) => {
601
+ e.stopPropagation();
602
+ deleteMedia(item.id);
603
+ },
604
+ style: {
605
+ position: "absolute",
606
+ bottom: "4px",
607
+ right: "4px",
608
+ background: "rgba(220,38,38,0.7)",
609
+ border: "none",
610
+ borderRadius: "50%",
611
+ width: "18px",
612
+ height: "18px",
613
+ color: "white",
614
+ fontSize: "0.6rem",
615
+ cursor: "pointer",
616
+ display: "flex",
617
+ alignItems: "center",
618
+ justifyContent: "center",
619
+ opacity: "0.6",
620
+ lineHeight: "1"
621
+ },
622
+ title: "Delete from library"
623
+ }, "\u{1F5D1}"),
624
+ h("div", {
625
+ style: {
626
+ position: "absolute",
627
+ bottom: "0",
628
+ left: "0",
629
+ right: "0",
630
+ background: "rgba(0,0,0,0.6)",
631
+ padding: "2px 4px",
632
+ fontSize: "0.55rem",
633
+ color: "rgba(255,255,255,0.8)",
634
+ whiteSpace: "nowrap",
635
+ overflow: "hidden",
636
+ textOverflow: "ellipsis"
637
+ }
638
+ }, item.filename)
639
+ );
640
+ })
641
+ )
642
+ )
370
643
  )
371
644
  ),
372
645
  // Schedule (only if any selected platform supports it)
@@ -410,94 +683,181 @@
410
683
  error && h("div", { className: "text-xs", style: { color: "rgba(248,113,113,0.95)" } }, error),
411
684
  success && h("div", { className: "text-xs", style: { color: "rgba(74,222,128,0.9)" } }, success)
412
685
  ),
413
- // RIGHT — live preview pane
686
+ // RIGHT — social-post-style preview
414
687
  h(
415
688
  "div",
416
689
  {
417
690
  style: {
418
- width: "320px",
691
+ width: "380px",
419
692
  flexShrink: 0,
420
- background: "rgba(255,255,255,0.02)",
693
+ background: "rgba(0,0,0,0.25)",
421
694
  border: "1px solid var(--ck-border-subtle)",
422
- borderRadius: "10px",
423
- padding: "1rem",
695
+ borderRadius: "16px",
696
+ padding: "1.25rem",
424
697
  display: "flex",
425
698
  flexDirection: "column",
426
699
  alignSelf: "flex-start"
427
700
  }
428
701
  },
429
- h("div", { className: "text-sm font-medium mb-3", style: t.text }, "Post Preview"),
430
- // Selected platforms
702
+ h("div", {
703
+ style: { fontSize: "0.85rem", fontWeight: 600, color: "var(--ck-text-secondary)", marginBottom: "1rem" }
704
+ }, "Post Preview"),
705
+ // Social post card
706
+ h(
707
+ "div",
708
+ {
709
+ style: {
710
+ background: "rgba(22,22,28,0.95)",
711
+ borderRadius: "12px",
712
+ border: "1px solid rgba(255,255,255,0.08)",
713
+ overflow: "hidden"
714
+ }
715
+ },
716
+ // Post header (avatar + name + handle)
717
+ h(
718
+ "div",
719
+ {
720
+ style: {
721
+ display: "flex",
722
+ alignItems: "center",
723
+ gap: "0.65rem",
724
+ padding: "0.85rem 1rem 0"
725
+ }
726
+ },
727
+ // Avatar circle
728
+ h("div", {
729
+ style: {
730
+ width: "40px",
731
+ height: "40px",
732
+ borderRadius: "50%",
733
+ background: "rgba(127,90,240,0.25)",
734
+ display: "flex",
735
+ alignItems: "center",
736
+ justifyContent: "center",
737
+ fontSize: "1.1rem",
738
+ color: "rgba(127,90,240,0.9)",
739
+ flexShrink: 0
740
+ }
741
+ }, "\u{1F464}"),
742
+ h(
743
+ "div",
744
+ null,
745
+ h(
746
+ "div",
747
+ { style: { display: "flex", alignItems: "center", gap: "0.3rem" } },
748
+ h("span", {
749
+ style: { fontWeight: 700, fontSize: "0.9rem", color: "var(--ck-text-primary)" }
750
+ }, "Your Brand"),
751
+ h("span", { style: { color: "rgba(99,179,237,0.9)", fontSize: "0.85rem" } }, "\u2713")
752
+ ),
753
+ h("div", {
754
+ style: { fontSize: "0.75rem", color: "var(--ck-text-tertiary)" }
755
+ }, scheduledAt ? `Scheduled \xB7 ${new Date(scheduledAt).toLocaleDateString(void 0, { month: "short", day: "numeric" })}` : "Just now")
756
+ )
757
+ ),
758
+ // Post body
759
+ h(
760
+ "div",
761
+ { style: { padding: "0.65rem 1rem 0.75rem" } },
762
+ content.trim() ? h("div", {
763
+ style: {
764
+ whiteSpace: "pre-wrap",
765
+ fontSize: "0.9rem",
766
+ color: "var(--ck-text-primary)",
767
+ lineHeight: "1.5",
768
+ maxHeight: "260px",
769
+ overflowY: "auto",
770
+ wordBreak: "break-word"
771
+ }
772
+ }, content) : h("div", {
773
+ style: {
774
+ color: "var(--ck-text-tertiary)",
775
+ fontSize: "0.85rem",
776
+ fontStyle: "italic",
777
+ padding: "1.5rem 0",
778
+ textAlign: "center"
779
+ }
780
+ }, "Start writing to see a preview")
781
+ ),
782
+ // Media preview
783
+ (selectedMediaIds.length > 0 || mediaUrl && showMedia) && h(
784
+ "div",
785
+ {
786
+ style: { padding: "0 0 0" }
787
+ },
788
+ ...selectedMediaIds.map((id) => {
789
+ const item = mediaLibrary.find((m) => m.id === id);
790
+ if (!item) return null;
791
+ return item.mimeType?.startsWith("video/") ? h("div", {
792
+ key: id,
793
+ style: {
794
+ background: "rgba(0,0,0,0.4)",
795
+ padding: "1.5rem",
796
+ textAlign: "center",
797
+ color: "var(--ck-text-secondary)",
798
+ fontSize: "0.85rem"
799
+ }
800
+ }, `\u{1F3A5} ${item.filename}`) : h("img", {
801
+ key: id,
802
+ src: item.thumbnailDataUrl || item.url,
803
+ style: { width: "100%", display: "block" }
804
+ });
805
+ }),
806
+ mediaUrl && showMedia && h("img", {
807
+ src: mediaUrl,
808
+ style: { width: "100%", display: "block" },
809
+ onError: (e) => {
810
+ e.target.style.display = "none";
811
+ }
812
+ })
813
+ ),
814
+ // Engagement bar (fake social actions)
815
+ h(
816
+ "div",
817
+ {
818
+ style: {
819
+ display: "flex",
820
+ justifyContent: "space-around",
821
+ padding: "0.6rem 1rem",
822
+ borderTop: "1px solid rgba(255,255,255,0.06)",
823
+ fontSize: "0.8rem",
824
+ color: "var(--ck-text-tertiary)"
825
+ }
826
+ },
827
+ h("span", null, "\u2764\uFE0F 0"),
828
+ h("span", null, "\u{1F4AC} 0"),
829
+ h("span", null, "\u{1F501} 0"),
830
+ h("span", null, "\u{1F4CA} 0")
831
+ )
832
+ ),
833
+ // Platform pills below card
431
834
  selectedPlatforms.length > 0 && h(
432
835
  "div",
433
- { className: "flex flex-wrap gap-1 mb-3" },
836
+ {
837
+ style: { display: "flex", flexWrap: "wrap", gap: "0.35rem", marginTop: "0.75rem" }
838
+ },
434
839
  ...selectedPlatforms.map((pl) => {
435
840
  const drv = drivers.find((d) => d.platform === pl);
436
841
  return h("span", {
437
842
  key: pl,
438
843
  style: {
439
- background: "rgba(127,90,240,0.15)",
440
- border: "1px solid rgba(127,90,240,0.3)",
844
+ background: "rgba(127,90,240,0.12)",
845
+ border: "1px solid rgba(127,90,240,0.25)",
441
846
  borderRadius: "999px",
442
- padding: "0.12rem 0.45rem",
443
- fontSize: "0.72rem",
847
+ padding: "0.1rem 0.4rem",
848
+ fontSize: "0.7rem",
444
849
  color: "var(--ck-text-secondary)"
445
850
  }
446
851
  }, drv ? `${drv.icon} ${drv.label}` : pl);
447
852
  })
448
853
  ),
449
- // Scheduling info
450
- scheduledAt && h("div", {
451
- className: "text-xs mb-3",
452
- style: { color: "rgba(251,191,36,0.85)" }
453
- }, `\u23F1 Scheduled: ${new Date(scheduledAt).toLocaleString()}`),
454
- // Content preview
455
- content.trim() ? h("div", {
456
- style: {
457
- background: "rgba(255,255,255,0.03)",
458
- border: "1px solid var(--ck-border-subtle)",
459
- borderRadius: "10px",
460
- padding: "0.85rem",
461
- whiteSpace: "pre-wrap",
462
- fontSize: "0.85rem",
463
- color: "var(--ck-text-primary)",
464
- lineHeight: "1.55",
465
- maxHeight: "300px",
466
- overflowY: "auto",
467
- wordBreak: "break-word"
468
- }
469
- }, content) : h("div", {
470
- style: {
471
- color: "var(--ck-text-tertiary)",
472
- fontSize: "0.85rem",
473
- fontStyle: "italic",
474
- padding: "2rem 0.5rem",
475
- textAlign: "center"
476
- }
477
- }, "Start writing to see a preview"),
478
- // Media preview
479
- mediaUrl && showMedia && h(
480
- "div",
481
- { className: "mt-3" },
482
- h("img", {
483
- src: mediaUrl,
484
- style: {
485
- maxWidth: "100%",
486
- borderRadius: "8px",
487
- border: "1px solid var(--ck-border-subtle)"
488
- },
489
- onError: (e) => {
490
- e.target.style.display = "none";
491
- }
492
- })
493
- ),
494
854
  // Character limit bar
495
855
  charLimit && content.length > 0 && h(
496
856
  "div",
497
- { className: "mt-3" },
857
+ { style: { marginTop: "0.75rem" } },
498
858
  h(
499
859
  "div",
500
- { style: { height: "4px", borderRadius: "2px", background: "rgba(255,255,255,0.06)", overflow: "hidden" } },
860
+ { style: { height: "3px", borderRadius: "2px", background: "rgba(255,255,255,0.06)", overflow: "hidden" } },
501
861
  h("div", {
502
862
  style: {
503
863
  height: "100%",
@@ -509,10 +869,11 @@
509
869
  })
510
870
  ),
511
871
  h("div", {
512
- className: "text-xs mt-1",
513
872
  style: {
514
- color: content.length > charLimit ? "rgba(248,113,113,0.9)" : "var(--ck-text-tertiary)",
515
- textAlign: "right"
873
+ fontSize: "0.7rem",
874
+ marginTop: "0.2rem",
875
+ textAlign: "right",
876
+ color: content.length > charLimit ? "rgba(248,113,113,0.9)" : "var(--ck-text-tertiary)"
516
877
  }
517
878
  }, `${content.length} / ${charLimit}`)
518
879
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiggai/kitchen-plugin-marketing",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Marketing Suite plugin for ClawKitchen",
5
5
  "main": "dist/index.js",
6
6
  "files": [