@cedros/data-react 0.1.0

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 (152) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +177 -0
  3. package/dist/admin/api.d.ts +19 -0
  4. package/dist/admin/api.js +108 -0
  5. package/dist/admin/components.d.ts +36 -0
  6. package/dist/admin/components.js +22 -0
  7. package/dist/admin/history.d.ts +17 -0
  8. package/dist/admin/history.js +103 -0
  9. package/dist/admin/icons.d.ts +15 -0
  10. package/dist/admin/icons.js +18 -0
  11. package/dist/admin/index.d.ts +13 -0
  12. package/dist/admin/index.js +12 -0
  13. package/dist/admin/permissions.d.ts +4 -0
  14. package/dist/admin/permissions.js +45 -0
  15. package/dist/admin/plugin.d.ts +4 -0
  16. package/dist/admin/plugin.js +180 -0
  17. package/dist/admin/primitives/ConfirmDialog.d.ts +14 -0
  18. package/dist/admin/primitives/ConfirmDialog.js +7 -0
  19. package/dist/admin/primitives/DataTable.d.ts +14 -0
  20. package/dist/admin/primitives/DataTable.js +7 -0
  21. package/dist/admin/primitives/DiffViewer.d.ts +11 -0
  22. package/dist/admin/primitives/DiffViewer.js +8 -0
  23. package/dist/admin/primitives/FormFieldRow.d.ts +23 -0
  24. package/dist/admin/primitives/FormFieldRow.js +16 -0
  25. package/dist/admin/primitives/JsonCodeEditor.d.ts +10 -0
  26. package/dist/admin/primitives/JsonCodeEditor.js +42 -0
  27. package/dist/admin/primitives/Pagination.d.ts +8 -0
  28. package/dist/admin/primitives/Pagination.js +8 -0
  29. package/dist/admin/primitives/Toolbar.d.ts +23 -0
  30. package/dist/admin/primitives/Toolbar.js +10 -0
  31. package/dist/admin/primitives/alerts.d.ts +21 -0
  32. package/dist/admin/primitives/alerts.js +44 -0
  33. package/dist/admin/sectionIds.d.ts +20 -0
  34. package/dist/admin/sectionIds.js +33 -0
  35. package/dist/admin/sections/CollectionsSection.d.ts +2 -0
  36. package/dist/admin/sections/CollectionsSection.js +125 -0
  37. package/dist/admin/sections/ContractVerifySection.d.ts +11 -0
  38. package/dist/admin/sections/ContractVerifySection.js +98 -0
  39. package/dist/admin/sections/CustomDataSection.d.ts +2 -0
  40. package/dist/admin/sections/CustomDataSection.js +256 -0
  41. package/dist/admin/sections/DataOpsSection.d.ts +26 -0
  42. package/dist/admin/sections/DataOpsSection.js +245 -0
  43. package/dist/admin/sections/HistorySection.d.ts +2 -0
  44. package/dist/admin/sections/HistorySection.js +26 -0
  45. package/dist/admin/sections/MonetizationSection.d.ts +2 -0
  46. package/dist/admin/sections/MonetizationSection.js +140 -0
  47. package/dist/admin/sections/NavigationSection.d.ts +13 -0
  48. package/dist/admin/sections/NavigationSection.js +195 -0
  49. package/dist/admin/sections/PagesSection.d.ts +2 -0
  50. package/dist/admin/sections/PagesSection.js +157 -0
  51. package/dist/admin/sections/SchemaDesignerSection.d.ts +2 -0
  52. package/dist/admin/sections/SchemaDesignerSection.js +167 -0
  53. package/dist/admin/sections/SiteSettingsSection.d.ts +12 -0
  54. package/dist/admin/sections/SiteSettingsSection.js +122 -0
  55. package/dist/admin/sections/TippingSection.d.ts +2 -0
  56. package/dist/admin/sections/TippingSection.js +178 -0
  57. package/dist/admin/sections/media/MediaDetail.d.ts +12 -0
  58. package/dist/admin/sections/media/MediaDetail.js +74 -0
  59. package/dist/admin/sections/media/MediaGrid.d.ts +14 -0
  60. package/dist/admin/sections/media/MediaGrid.js +22 -0
  61. package/dist/admin/sections/media/MediaSection.d.ts +2 -0
  62. package/dist/admin/sections/media/MediaSection.js +97 -0
  63. package/dist/admin/sections/media/MediaUploader.d.ts +7 -0
  64. package/dist/admin/sections/media/MediaUploader.js +72 -0
  65. package/dist/admin/sections/media/types.d.ts +33 -0
  66. package/dist/admin/sections/media/types.js +1 -0
  67. package/dist/admin/styles.css +533 -0
  68. package/dist/admin/types.d.ts +85 -0
  69. package/dist/admin/types.js +1 -0
  70. package/dist/index.d.ts +4 -0
  71. package/dist/index.js +3 -0
  72. package/dist/react/CmsContent.d.ts +20 -0
  73. package/dist/react/CmsContent.js +31 -0
  74. package/dist/react/entries.d.ts +9 -0
  75. package/dist/react/entries.js +25 -0
  76. package/dist/react/fetch.d.ts +11 -0
  77. package/dist/react/fetch.js +32 -0
  78. package/dist/react/index.d.ts +10 -0
  79. package/dist/react/index.js +9 -0
  80. package/dist/react/metadata.d.ts +44 -0
  81. package/dist/react/metadata.js +142 -0
  82. package/dist/react/sanitize.d.ts +17 -0
  83. package/dist/react/sanitize.js +326 -0
  84. package/dist/react/server.d.ts +14 -0
  85. package/dist/react/server.js +13 -0
  86. package/dist/react/sitemap.d.ts +28 -0
  87. package/dist/react/sitemap.js +91 -0
  88. package/dist/react/slugs.d.ts +27 -0
  89. package/dist/react/slugs.js +52 -0
  90. package/dist/react/types.d.ts +85 -0
  91. package/dist/react/types.js +1 -0
  92. package/dist/react/visitor.d.ts +7 -0
  93. package/dist/react/visitor.js +18 -0
  94. package/dist/site-templates/BlogTemplates.d.ts +95 -0
  95. package/dist/site-templates/BlogTemplates.js +64 -0
  96. package/dist/site-templates/ContactPageTemplate.d.ts +14 -0
  97. package/dist/site-templates/ContactPageTemplate.js +5 -0
  98. package/dist/site-templates/DashboardOverviewTemplate.d.ts +29 -0
  99. package/dist/site-templates/DashboardOverviewTemplate.js +17 -0
  100. package/dist/site-templates/DashboardShell.d.ts +28 -0
  101. package/dist/site-templates/DashboardShell.js +10 -0
  102. package/dist/site-templates/DocsSidebar.d.ts +14 -0
  103. package/dist/site-templates/DocsSidebar.js +13 -0
  104. package/dist/site-templates/DocsTemplates.d.ts +60 -0
  105. package/dist/site-templates/DocsTemplates.js +47 -0
  106. package/dist/site-templates/HomePageTemplate.d.ts +15 -0
  107. package/dist/site-templates/HomePageTemplate.js +10 -0
  108. package/dist/site-templates/LegalPageTemplate.d.ts +12 -0
  109. package/dist/site-templates/LegalPageTemplate.js +6 -0
  110. package/dist/site-templates/MarkdownContent.d.ts +7 -0
  111. package/dist/site-templates/MarkdownContent.js +24 -0
  112. package/dist/site-templates/NotFoundTemplate.d.ts +9 -0
  113. package/dist/site-templates/NotFoundTemplate.js +5 -0
  114. package/dist/site-templates/SiteFooter.d.ts +13 -0
  115. package/dist/site-templates/SiteFooter.js +4 -0
  116. package/dist/site-templates/SiteLayout.d.ts +14 -0
  117. package/dist/site-templates/SiteLayout.js +6 -0
  118. package/dist/site-templates/TopNav.d.ts +10 -0
  119. package/dist/site-templates/TopNav.js +8 -0
  120. package/dist/site-templates/blogControls.d.ts +19 -0
  121. package/dist/site-templates/blogControls.js +37 -0
  122. package/dist/site-templates/codeBlock.d.ts +9 -0
  123. package/dist/site-templates/codeBlock.js +31 -0
  124. package/dist/site-templates/content-styles.css +410 -0
  125. package/dist/site-templates/contentIndex.d.ts +65 -0
  126. package/dist/site-templates/contentIndex.js +181 -0
  127. package/dist/site-templates/contentUi.d.ts +14 -0
  128. package/dist/site-templates/contentUi.js +24 -0
  129. package/dist/site-templates/docs-styles.css +259 -0
  130. package/dist/site-templates/docsNavigation.d.ts +18 -0
  131. package/dist/site-templates/docsNavigation.js +50 -0
  132. package/dist/site-templates/index.d.ts +28 -0
  133. package/dist/site-templates/index.js +25 -0
  134. package/dist/site-templates/monetization-styles.css +154 -0
  135. package/dist/site-templates/paywallControls.d.ts +22 -0
  136. package/dist/site-templates/paywallControls.js +9 -0
  137. package/dist/site-templates/routing.d.ts +12 -0
  138. package/dist/site-templates/routing.js +36 -0
  139. package/dist/site-templates/solanaAtaSetup.d.ts +11 -0
  140. package/dist/site-templates/solanaAtaSetup.js +38 -0
  141. package/dist/site-templates/solanaMicropayments.d.ts +65 -0
  142. package/dist/site-templates/solanaMicropayments.js +115 -0
  143. package/dist/site-templates/styles.css +332 -0
  144. package/dist/site-templates/tipControls.d.ts +24 -0
  145. package/dist/site-templates/tipControls.js +43 -0
  146. package/dist/site-templates/tocExtractor.d.ts +16 -0
  147. package/dist/site-templates/tocExtractor.js +58 -0
  148. package/dist/site-templates/tocScrollSpy.d.ts +16 -0
  149. package/dist/site-templates/tocScrollSpy.js +37 -0
  150. package/dist/templates.d.ts +8 -0
  151. package/dist/templates.js +20 -0
  152. package/package.json +58 -0
@@ -0,0 +1,12 @@
1
+ import type { PluginContext } from "../../types.js";
2
+ import type { Asset } from "./types.js";
3
+ interface MediaDetailProps {
4
+ asset: Asset;
5
+ pluginContext: PluginContext;
6
+ canWrite: boolean;
7
+ onClose: () => void;
8
+ onDeleted: () => void;
9
+ onUpdated: (asset: Asset) => void;
10
+ }
11
+ export default function MediaDetail({ asset, pluginContext, canWrite, onClose, onDeleted, onUpdated }: MediaDetailProps): React.JSX.Element;
12
+ export {};
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useState } from "react";
3
+ import { requestJson } from "../../api.js";
4
+ import { AdminButton, StatusNotice, TextInput } from "../../components.js";
5
+ export default function MediaDetail({ asset, pluginContext, canWrite, onClose, onDeleted, onUpdated }) {
6
+ const [altText, setAltText] = useState(asset.alt_text || "");
7
+ const [saving, setSaving] = useState(false);
8
+ const [deleting, setDeleting] = useState(false);
9
+ const [confirmDelete, setConfirmDelete] = useState(false);
10
+ const [status, setStatus] = useState(null);
11
+ const handleSave = useCallback(async () => {
12
+ setSaving(true);
13
+ setStatus(null);
14
+ try {
15
+ const updated = await requestJson(pluginContext, `/admin/media/assets/${asset.id}`, { method: "PUT", body: { alt_text: altText || null } });
16
+ onUpdated(updated);
17
+ setStatus({ tone: "success", message: "Saved." });
18
+ }
19
+ catch (err) {
20
+ setStatus({ tone: "error", message: err.message });
21
+ }
22
+ finally {
23
+ setSaving(false);
24
+ }
25
+ }, [altText, asset.id, pluginContext, onUpdated]);
26
+ const handleDelete = useCallback(async () => {
27
+ setDeleting(true);
28
+ setStatus(null);
29
+ try {
30
+ await requestJson(pluginContext, `/admin/media/assets/${asset.id}`, {
31
+ method: "DELETE"
32
+ });
33
+ onDeleted();
34
+ }
35
+ catch (err) {
36
+ setStatus({ tone: "error", message: err.message });
37
+ setDeleting(false);
38
+ }
39
+ }, [asset.id, pluginContext, onDeleted]);
40
+ const originalUrl = variantUrl(asset, null);
41
+ return (_jsxs("div", { className: "cedros-media-detail", children: [_jsxs("div", { className: "cedros-media-detail__header", children: [_jsx("h3", { className: "cedros-data-card__title", children: asset.filename }), _jsx(AdminButton, { variant: "ghost", size: "sm", onClick: onClose, children: "Close" })] }), _jsx("img", { className: "cedros-media-detail__preview", src: originalUrl, alt: asset.alt_text || asset.filename }), _jsxs("div", { className: "cedros-media-detail__meta", children: [_jsxs("div", { className: "cedros-data-key", children: [asset.content_type, " \u00B7 ", formatBytes(asset.size_bytes), asset.width && asset.height ? ` \u00B7 ${asset.width}\u00D7${asset.height}` : ""] }), _jsxs("div", { className: "cedros-data-key", style: { marginTop: "0.25rem" }, children: ["Uploaded ", new Date(asset.created_at).toLocaleDateString()] })] }), _jsxs("div", { className: "cedros-media-detail__urls", children: [_jsx("span", { className: "cedros-data-label", children: "URLs" }), _jsx(UrlRow, { label: "Original", url: originalUrl }), asset.variants.map((v) => (_jsx(UrlRow, { label: v.suffix, url: v.url }, v.suffix)))] }), _jsx(TextInput, { label: "Alt text", value: altText, onChange: (e) => setAltText(e.target.value), placeholder: "Describe this image", disabled: !canWrite }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "primary", size: "sm", onClick: () => void handleSave(), disabled: saving || !canWrite, children: saving ? "Saving..." : "Save" }), !confirmDelete ? (_jsx(AdminButton, { variant: "danger", size: "sm", onClick: () => setConfirmDelete(true), disabled: deleting || !canWrite, children: "Delete" })) : (_jsxs(_Fragment, { children: [_jsx(AdminButton, { variant: "danger", size: "sm", onClick: () => void handleDelete(), disabled: deleting, children: deleting ? "Deleting..." : "Confirm Delete" }), _jsx(AdminButton, { variant: "ghost", size: "sm", onClick: () => setConfirmDelete(false), disabled: deleting, children: "Cancel" })] }))] }), status && _jsx(StatusNotice, { tone: status.tone, message: status.message })] }));
42
+ }
43
+ function UrlRow({ label, url }) {
44
+ const [copied, setCopied] = useState(false);
45
+ const copy = () => {
46
+ void navigator.clipboard.writeText(url).then(() => {
47
+ setCopied(true);
48
+ setTimeout(() => setCopied(false), 1500);
49
+ });
50
+ };
51
+ return (_jsxs("div", { className: "cedros-media-url-row", children: [_jsx("span", { className: "cedros-data-pill", children: label }), _jsx("code", { className: "cedros-data-key cedros-media-url-row__url", children: url }), _jsx(AdminButton, { variant: "ghost", size: "sm", onClick: copy, children: copied ? "Copied" : "Copy" })] }));
52
+ }
53
+ function variantUrl(asset, suffix) {
54
+ if (suffix) {
55
+ const v = asset.variants.find((v) => v.suffix === suffix);
56
+ if (v)
57
+ return v.url;
58
+ }
59
+ // For original, try to find a variant with suffix matching the original format,
60
+ // otherwise fall back to the webp or first variant
61
+ const webp = asset.variants.find((v) => v.suffix === "webp");
62
+ if (webp)
63
+ return webp.url;
64
+ if (asset.variants.length > 0)
65
+ return asset.variants[0].url;
66
+ return asset.original_key;
67
+ }
68
+ function formatBytes(bytes) {
69
+ if (bytes < 1024)
70
+ return `${bytes} B`;
71
+ if (bytes < 1024 * 1024)
72
+ return `${(bytes / 1024).toFixed(1)} KB`;
73
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
74
+ }
@@ -0,0 +1,14 @@
1
+ import type { Asset } from "./types.js";
2
+ interface MediaGridProps {
3
+ assets: Asset[];
4
+ selectedId: string | null;
5
+ onSelect: (asset: Asset) => void;
6
+ viewMode: "grid" | "list";
7
+ onViewModeChange: (mode: "grid" | "list") => void;
8
+ page: number;
9
+ pageSize: number;
10
+ onPageChange: (page: number) => void;
11
+ hasMore: boolean;
12
+ }
13
+ export default function MediaGrid({ assets, selectedId, onSelect, viewMode, onViewModeChange, page, pageSize, onPageChange, hasMore }: MediaGridProps): React.JSX.Element;
14
+ export {};
@@ -0,0 +1,22 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { AdminButton } from "../../components.js";
3
+ export default function MediaGrid({ assets, selectedId, onSelect, viewMode, onViewModeChange, page, pageSize, onPageChange, hasMore }) {
4
+ return (_jsxs("div", { children: [_jsxs("div", { className: "cedros-data-toolbar", style: { marginBottom: "0.6rem" }, children: [_jsx("div", { className: "cedros-data-toolbar__left", children: _jsxs("span", { className: "cedros-data-label", children: [assets.length, " asset", assets.length !== 1 ? "s" : ""] }) }), _jsxs("div", { className: "cedros-data-toolbar__right", children: [_jsx("button", { className: `cedros-data-chip ${viewMode === "grid" ? "cedros-data-chip--active" : ""}`, onClick: () => onViewModeChange("grid"), children: "Grid" }), _jsx("button", { className: `cedros-data-chip ${viewMode === "list" ? "cedros-data-chip--active" : ""}`, onClick: () => onViewModeChange("list"), children: "List" })] })] }), assets.length === 0 ? (_jsx("p", { className: "cedros-data-empty", children: "No assets found." })) : viewMode === "grid" ? (_jsx("div", { className: "cedros-media-grid", children: assets.map((asset) => (_jsxs("button", { className: `cedros-media-grid__item ${selectedId === asset.id ? "cedros-media-grid__item--selected" : ""}`, onClick: () => onSelect(asset), children: [_jsx("img", { className: "cedros-media-grid__thumb", src: thumbnailUrl(asset), alt: asset.alt_text || asset.filename, loading: "lazy" }), _jsx("span", { className: "cedros-media-grid__name", children: asset.filename })] }, asset.id))) })) : (_jsx("div", { className: "cedros-data-table-wrap", children: _jsxs("table", { className: "cedros-data-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Preview" }), _jsx("th", { children: "Filename" }), _jsx("th", { children: "Type" }), _jsx("th", { children: "Size" }), _jsx("th", { children: "Dimensions" })] }) }), _jsx("tbody", { children: assets.map((asset) => (_jsxs("tr", { onClick: () => onSelect(asset), style: { cursor: "pointer" }, children: [_jsx("td", { children: _jsx("img", { src: thumbnailUrl(asset), alt: asset.alt_text || asset.filename, style: { width: 40, height: 40, objectFit: "cover", borderRadius: 4 }, loading: "lazy" }) }), _jsx("td", { children: asset.filename }), _jsx("td", { children: _jsx("span", { className: "cedros-data-pill", children: asset.content_type }) }), _jsx("td", { children: formatBytes(asset.size_bytes) }), _jsx("td", { children: asset.width && asset.height ? `${asset.width}x${asset.height}` : "-" })] }, asset.id))) })] }) })), _jsxs("div", { className: "cedros-data-pagination", style: { marginTop: "0.6rem" }, children: [_jsx(AdminButton, { variant: "ghost", size: "sm", disabled: page === 0, onClick: () => onPageChange(page - 1), children: "Previous" }), _jsxs("span", { className: "cedros-data-label", children: ["Page ", page + 1] }), _jsx(AdminButton, { variant: "ghost", size: "sm", disabled: !hasMore, onClick: () => onPageChange(page + 1), children: "Next" })] })] }));
5
+ }
6
+ function thumbnailUrl(asset) {
7
+ const thumb = asset.variants.find((v) => v.suffix === "thumb");
8
+ if (thumb)
9
+ return thumb.url;
10
+ const webp = asset.variants.find((v) => v.suffix === "webp");
11
+ if (webp)
12
+ return webp.url;
13
+ // Fallback to constructing URL from original key
14
+ return asset.original_key;
15
+ }
16
+ function formatBytes(bytes) {
17
+ if (bytes < 1024)
18
+ return `${bytes} B`;
19
+ if (bytes < 1024 * 1024)
20
+ return `${(bytes / 1024).toFixed(1)} KB`;
21
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
22
+ }
@@ -0,0 +1,2 @@
1
+ import type { AdminSectionProps } from "../../types.js";
2
+ export default function MediaSection({ pluginContext, pageSize }: AdminSectionProps): React.JSX.Element;
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { requestJson } from "../../api.js";
4
+ import { AdminButton, Card, StatusNotice } from "../../components.js";
5
+ import MediaDetail from "./MediaDetail.js";
6
+ import MediaGrid from "./MediaGrid.js";
7
+ import MediaUploader from "./MediaUploader.js";
8
+ export default function MediaSection({ pluginContext, pageSize }) {
9
+ const canRead = pluginContext.hasPermission("data:media:read");
10
+ const canWrite = pluginContext.hasPermission("data:media:write");
11
+ const [storageConfig, setStorageConfig] = useState(null);
12
+ const [configLoading, setConfigLoading] = useState(true);
13
+ const [configError, setConfigError] = useState("");
14
+ const [testResult, setTestResult] = useState(null);
15
+ const [testLoading, setTestLoading] = useState(false);
16
+ const [assets, setAssets] = useState([]);
17
+ const [assetsLoading, setAssetsLoading] = useState(false);
18
+ const [page, setPage] = useState(0);
19
+ const [viewMode, setViewMode] = useState("grid");
20
+ const [selectedAsset, setSelectedAsset] = useState(null);
21
+ const effectivePageSize = pageSize || 24;
22
+ const loadConfig = useCallback(async () => {
23
+ setConfigLoading(true);
24
+ setConfigError("");
25
+ try {
26
+ const data = await requestJson(pluginContext, "/admin/storage/config");
27
+ setStorageConfig(data);
28
+ }
29
+ catch (err) {
30
+ setConfigError(err.message);
31
+ }
32
+ finally {
33
+ setConfigLoading(false);
34
+ }
35
+ }, [pluginContext]);
36
+ const loadAssets = useCallback(async (pageNum) => {
37
+ setAssetsLoading(true);
38
+ try {
39
+ const data = await requestJson(pluginContext, `/admin/media/assets?limit=${effectivePageSize}&offset=${pageNum * effectivePageSize}`);
40
+ setAssets(data);
41
+ }
42
+ catch {
43
+ // Silently fail on asset load - config screen will show errors
44
+ }
45
+ finally {
46
+ setAssetsLoading(false);
47
+ }
48
+ }, [pluginContext, effectivePageSize]);
49
+ useEffect(() => {
50
+ void loadConfig();
51
+ }, [loadConfig]);
52
+ useEffect(() => {
53
+ if (storageConfig?.enabled) {
54
+ void loadAssets(page);
55
+ }
56
+ }, [storageConfig?.enabled, page, loadAssets]);
57
+ const handleTest = useCallback(async () => {
58
+ setTestLoading(true);
59
+ setTestResult(null);
60
+ try {
61
+ await requestJson(pluginContext, "/admin/storage/test", { method: "POST" });
62
+ setTestResult({ tone: "success", message: "Connection successful." });
63
+ }
64
+ catch (err) {
65
+ setTestResult({ tone: "error", message: err.message });
66
+ }
67
+ finally {
68
+ setTestLoading(false);
69
+ }
70
+ }, [pluginContext]);
71
+ const handlePageChange = useCallback((newPage) => {
72
+ setPage(newPage);
73
+ setSelectedAsset(null);
74
+ }, []);
75
+ const handleUploadComplete = useCallback(() => {
76
+ setPage(0);
77
+ void loadAssets(0);
78
+ }, [loadAssets]);
79
+ const handleAssetDeleted = useCallback(() => {
80
+ setSelectedAsset(null);
81
+ void loadAssets(page);
82
+ }, [page, loadAssets]);
83
+ const handleAssetUpdated = useCallback((updated) => {
84
+ setSelectedAsset(updated);
85
+ setAssets((prev) => prev.map((a) => (a.id === updated.id ? updated : a)));
86
+ }, []);
87
+ if (configLoading) {
88
+ return (_jsx("div", { className: "cedros-data", children: _jsx("p", { className: "cedros-data-empty", children: "Loading storage configuration..." }) }));
89
+ }
90
+ if (configError) {
91
+ return (_jsx("div", { className: "cedros-data", children: _jsx(StatusNotice, { tone: "error", message: configError }) }));
92
+ }
93
+ if (!storageConfig?.enabled) {
94
+ return (_jsx("div", { className: "cedros-data", children: _jsx("header", { className: "cedros-data__header", children: _jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Media Library" }), _jsxs("p", { className: "cedros-data__subtitle", children: ["Storage is not enabled. Start the server with ", _jsx("code", { children: "--enable-storage" }), " and configure ", _jsx("code", { children: "STORAGE_*" }), " environment variables."] })] }) }) }));
95
+ }
96
+ return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Media Library" }), _jsxs("p", { className: "cedros-data__subtitle", children: ["Upload and manage images. Storage: ", storageConfig.config?.bucket ?? "unknown", " (", storageConfig.config?.region ?? "?", ")"] }), !canRead && (_jsxs("p", { className: "cedros-data__subtitle", children: ["Read-only mode. Missing ", _jsx("code", { children: "data:media:read" }), " permission."] }))] }), _jsx("div", { className: "cedros-data-actions", children: _jsx(AdminButton, { variant: "secondary", size: "sm", onClick: () => void handleTest(), disabled: testLoading || !canWrite, children: testLoading ? "Testing..." : "Test Connection" }) })] }), testResult && _jsx(StatusNotice, { tone: testResult.tone, message: testResult.message }), canWrite && (_jsx(MediaUploader, { pluginContext: pluginContext, onUploadComplete: handleUploadComplete })), _jsx(Card, { title: "Assets", subtitle: assetsLoading ? "Loading..." : undefined, children: selectedAsset ? (_jsx(MediaDetail, { asset: selectedAsset, pluginContext: pluginContext, canWrite: canWrite, onClose: () => setSelectedAsset(null), onDeleted: handleAssetDeleted, onUpdated: handleAssetUpdated })) : (_jsx(MediaGrid, { assets: assets, selectedId: selectedAsset ? selectedAsset.id : null, onSelect: setSelectedAsset, viewMode: viewMode, onViewModeChange: setViewMode, page: page, pageSize: effectivePageSize, onPageChange: handlePageChange, hasMore: assets.length === effectivePageSize })) })] }));
97
+ }
@@ -0,0 +1,7 @@
1
+ import type { PluginContext } from "../../types.js";
2
+ interface MediaUploaderProps {
3
+ pluginContext: PluginContext;
4
+ onUploadComplete: () => void;
5
+ }
6
+ export default function MediaUploader({ pluginContext, onUploadComplete }: MediaUploaderProps): React.JSX.Element;
7
+ export {};
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useRef, useState } from "react";
3
+ import { AdminButton, StatusNotice } from "../../components.js";
4
+ const MAX_FILE_SIZE = 10 * 1024 * 1024;
5
+ const ACCEPTED_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
6
+ export default function MediaUploader({ pluginContext, onUploadComplete }) {
7
+ const [dragOver, setDragOver] = useState(false);
8
+ const [uploading, setUploading] = useState(false);
9
+ const [progress, setProgress] = useState("");
10
+ const [error, setError] = useState("");
11
+ const inputRef = useRef(null);
12
+ const uploadFile = useCallback(async (file) => {
13
+ setError("");
14
+ if (!ACCEPTED_TYPES.includes(file.type)) {
15
+ setError(`Unsupported file type: ${file.type}. Accepted: JPEG, PNG, GIF, WebP.`);
16
+ return;
17
+ }
18
+ if (file.size > MAX_FILE_SIZE) {
19
+ setError(`File exceeds 10 MB limit (${(file.size / (1024 * 1024)).toFixed(1)} MB).`);
20
+ return;
21
+ }
22
+ setUploading(true);
23
+ setProgress(`Uploading ${file.name}...`);
24
+ try {
25
+ const formData = new FormData();
26
+ formData.append("file", file);
27
+ const token = pluginContext.getAccessToken();
28
+ const headers = {};
29
+ if (token)
30
+ headers.Authorization = `Bearer ${token}`;
31
+ if (pluginContext.orgId)
32
+ headers["x-cedros-org-id"] = pluginContext.orgId;
33
+ const baseUrl = pluginContext.serverUrl.replace(/\/+$/, "");
34
+ const response = await fetch(`${baseUrl}/admin/media/upload`, {
35
+ method: "POST",
36
+ headers,
37
+ body: formData
38
+ });
39
+ if (!response.ok) {
40
+ const text = await response.text();
41
+ throw new Error(text || `Upload failed (${response.status})`);
42
+ }
43
+ setProgress("");
44
+ onUploadComplete();
45
+ }
46
+ catch (err) {
47
+ setError(err.message);
48
+ setProgress("");
49
+ }
50
+ finally {
51
+ setUploading(false);
52
+ }
53
+ }, [pluginContext, onUploadComplete]);
54
+ const handleDrop = useCallback((e) => {
55
+ e.preventDefault();
56
+ setDragOver(false);
57
+ const file = e.dataTransfer.files[0];
58
+ if (file)
59
+ void uploadFile(file);
60
+ }, [uploadFile]);
61
+ const handleFileInput = useCallback((e) => {
62
+ const file = e.target.files?.[0];
63
+ if (file)
64
+ void uploadFile(file);
65
+ if (inputRef.current)
66
+ inputRef.current.value = "";
67
+ }, [uploadFile]);
68
+ return (_jsxs("div", { className: "cedros-data-card", children: [_jsxs("div", { className: `cedros-media-dropzone ${dragOver ? "cedros-media-dropzone--active" : ""}`, onDragOver: (e) => {
69
+ e.preventDefault();
70
+ setDragOver(true);
71
+ }, onDragLeave: () => setDragOver(false), onDrop: handleDrop, children: [_jsx("p", { className: "cedros-media-dropzone__text", children: uploading ? progress : "Drag & drop an image here, or click to browse" }), _jsx(AdminButton, { variant: "secondary", size: "sm", disabled: uploading, onClick: () => inputRef.current?.click(), children: "Choose File" }), _jsx("input", { ref: inputRef, type: "file", accept: ACCEPTED_TYPES.join(","), style: { display: "none" }, onChange: handleFileInput }), _jsx("p", { className: "cedros-media-dropzone__hint", children: "JPEG, PNG, GIF, WebP \u00B7 Max 10 MB" })] }), error && _jsx("div", { className: "cedros-data-card__body", children: _jsx(StatusNotice, { tone: "error", message: error }) })] }));
72
+ }
@@ -0,0 +1,33 @@
1
+ export interface AssetVariant {
2
+ suffix: string;
3
+ key: string;
4
+ url: string;
5
+ content_type: string;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ export interface Asset {
10
+ id: string;
11
+ filename: string;
12
+ original_key: string;
13
+ content_type: string;
14
+ size_bytes: number;
15
+ width: number | null;
16
+ height: number | null;
17
+ variants: AssetVariant[];
18
+ alt_text: string | null;
19
+ metadata: Record<string, unknown>;
20
+ created_at: string;
21
+ updated_at: string;
22
+ }
23
+ export interface StorageConfigResponse {
24
+ enabled: boolean;
25
+ config?: {
26
+ bucket: string;
27
+ region: string;
28
+ endpoint: string | null;
29
+ cdn_base_url: string | null;
30
+ path_prefix: string | null;
31
+ access_key: string;
32
+ };
33
+ }
@@ -0,0 +1 @@
1
+ export {};