@rovela-ai/sdk 0.16.3 → 0.17.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 (42) hide show
  1. package/dist/admin/api/branding.d.ts +79 -0
  2. package/dist/admin/api/branding.d.ts.map +1 -0
  3. package/dist/admin/api/branding.js +238 -0
  4. package/dist/admin/api/branding.js.map +1 -0
  5. package/dist/admin/components/AdminTopBar.d.ts +1 -1
  6. package/dist/admin/components/AdminTopBar.d.ts.map +1 -1
  7. package/dist/admin/components/AdminTopBar.js +19 -2
  8. package/dist/admin/components/AdminTopBar.js.map +1 -1
  9. package/dist/admin/components/BrandingAssetUpload.d.ts +14 -0
  10. package/dist/admin/components/BrandingAssetUpload.d.ts.map +1 -0
  11. package/dist/admin/components/BrandingAssetUpload.js +207 -0
  12. package/dist/admin/components/BrandingAssetUpload.js.map +1 -0
  13. package/dist/admin/components/LogoUpload.d.ts +11 -10
  14. package/dist/admin/components/LogoUpload.d.ts.map +1 -1
  15. package/dist/admin/components/LogoUpload.js +8 -192
  16. package/dist/admin/components/LogoUpload.js.map +1 -1
  17. package/dist/admin/components/index.d.ts +2 -0
  18. package/dist/admin/components/index.d.ts.map +1 -1
  19. package/dist/admin/components/index.js +1 -0
  20. package/dist/admin/components/index.js.map +1 -1
  21. package/dist/admin/index.d.ts +2 -2
  22. package/dist/admin/index.d.ts.map +1 -1
  23. package/dist/admin/index.js +1 -1
  24. package/dist/admin/index.js.map +1 -1
  25. package/dist/admin/styles/admin-theme.css +8 -0
  26. package/dist/core/StoreSettingsProvider.d.ts +4 -0
  27. package/dist/core/StoreSettingsProvider.d.ts.map +1 -1
  28. package/dist/core/StoreSettingsProvider.js +3 -0
  29. package/dist/core/StoreSettingsProvider.js.map +1 -1
  30. package/dist/core/api/settings.d.ts +1 -0
  31. package/dist/core/api/settings.d.ts.map +1 -1
  32. package/dist/core/api/settings.js +2 -0
  33. package/dist/core/api/settings.js.map +1 -1
  34. package/dist/core/db/queries.d.ts +33 -31
  35. package/dist/core/db/queries.d.ts.map +1 -1
  36. package/dist/core/db/queries.js +11 -1
  37. package/dist/core/db/queries.js.map +1 -1
  38. package/dist/core/db/schema.d.ts +20 -1
  39. package/dist/core/db/schema.d.ts.map +1 -1
  40. package/dist/core/db/schema.js +3 -0
  41. package/dist/core/db/schema.js.map +1 -1
  42. package/package.json +5 -1
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @rovela-ai/sdk/admin/api/branding
3
+ *
4
+ * Store-side branding asset management (logo + favicon) — the store admin's
5
+ * half of the branding architecture shared with the Rovela platform.
6
+ *
7
+ * ARCHITECTURE (lockstep with the platform's lib/branding-asset.ts): the ONE
8
+ * source of truth is the R2 object at the fixed key
9
+ * `stores/{STORE_ID}/branding/{logo|favicon}` (no extension — R2 serves the
10
+ * Content-Type from object metadata, so the URL never changes when the file
11
+ * format changes). Everything else is a pointer that only encodes existence:
12
+ *
13
+ * - store_settings.logo_url / favicon_url — the store-side RUNTIME mirror.
14
+ * StoreSettingsProvider, the storefront header, transactional emails and
15
+ * the admin top bar all read settings-first, so writes here are live
16
+ * immediately (no redeploy).
17
+ * - projects.blueprint.store.{logoUrl|faviconUrl} — the PLATFORM-side
18
+ * mirror (written by the platform branding routes; feeds the env vars at
19
+ * deploy time). This handler cannot reach it, and doesn't need to: with
20
+ * settings-first readers the env vars are a build-time fallback only.
21
+ * - NEXT_PUBLIC_STORE_{LOGO|FAVICON}_URL — build-time fallback.
22
+ *
23
+ * Contract for every writer: update R2 + every mirror you can reach. This
24
+ * handler writes R2 + store_settings. GET reads the truth directly (an R2
25
+ * HEAD on the fixed key) so the admin UI can never lie about whether an
26
+ * asset exists, regardless of mirror state.
27
+ */
28
+ import { NextRequest, NextResponse } from 'next/server';
29
+ export type BrandingKind = 'logo' | 'favicon';
30
+ interface BrandingApiError {
31
+ error: string;
32
+ code?: string;
33
+ }
34
+ export declare const logoGET: () => Promise<NextResponse<BrandingApiError> | NextResponse<{
35
+ success: boolean;
36
+ data: {
37
+ exists: boolean;
38
+ url: string | null;
39
+ };
40
+ }>>;
41
+ export declare const logoPOST: (req: NextRequest) => Promise<NextResponse<BrandingApiError> | NextResponse<{
42
+ success: boolean;
43
+ data: {
44
+ url: string;
45
+ key: string;
46
+ size: number;
47
+ type: string;
48
+ settingsUpdated: boolean;
49
+ };
50
+ }>>;
51
+ export declare const logoDELETE: () => Promise<NextResponse<BrandingApiError> | NextResponse<{
52
+ success: boolean;
53
+ deleted: boolean;
54
+ settingsUpdated: boolean;
55
+ }>>;
56
+ export declare const faviconGET: () => Promise<NextResponse<BrandingApiError> | NextResponse<{
57
+ success: boolean;
58
+ data: {
59
+ exists: boolean;
60
+ url: string | null;
61
+ };
62
+ }>>;
63
+ export declare const faviconPOST: (req: NextRequest) => Promise<NextResponse<BrandingApiError> | NextResponse<{
64
+ success: boolean;
65
+ data: {
66
+ url: string;
67
+ key: string;
68
+ size: number;
69
+ type: string;
70
+ settingsUpdated: boolean;
71
+ };
72
+ }>>;
73
+ export declare const faviconDELETE: () => Promise<NextResponse<BrandingApiError> | NextResponse<{
74
+ success: boolean;
75
+ deleted: boolean;
76
+ settingsUpdated: boolean;
77
+ }>>;
78
+ export {};
79
+ //# sourceMappingURL=branding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branding.d.ts","sourceRoot":"","sources":["../../../src/admin/api/branding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAcvD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,CAAA;AAE7C,UAAU,gBAAgB;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AA6QD,eAAO,MAAM,OAAO;;;;;;GAAmB,CAAA;AACvC,eAAO,MAAM,QAAQ,QA9MM,WAAW;;;;;;;;;GA8MG,CAAA;AACzC,eAAO,MAAM,UAAU;;;;GAAsB,CAAA;AAE7C,eAAO,MAAM,UAAU;;;;;;GAAsB,CAAA;AAC7C,eAAO,MAAM,WAAW,QAlNG,WAAW;;;;;;;;;GAkNS,CAAA;AAC/C,eAAO,MAAM,aAAa;;;;GAAyB,CAAA"}
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @rovela-ai/sdk/admin/api/branding
3
+ *
4
+ * Store-side branding asset management (logo + favicon) — the store admin's
5
+ * half of the branding architecture shared with the Rovela platform.
6
+ *
7
+ * ARCHITECTURE (lockstep with the platform's lib/branding-asset.ts): the ONE
8
+ * source of truth is the R2 object at the fixed key
9
+ * `stores/{STORE_ID}/branding/{logo|favicon}` (no extension — R2 serves the
10
+ * Content-Type from object metadata, so the URL never changes when the file
11
+ * format changes). Everything else is a pointer that only encodes existence:
12
+ *
13
+ * - store_settings.logo_url / favicon_url — the store-side RUNTIME mirror.
14
+ * StoreSettingsProvider, the storefront header, transactional emails and
15
+ * the admin top bar all read settings-first, so writes here are live
16
+ * immediately (no redeploy).
17
+ * - projects.blueprint.store.{logoUrl|faviconUrl} — the PLATFORM-side
18
+ * mirror (written by the platform branding routes; feeds the env vars at
19
+ * deploy time). This handler cannot reach it, and doesn't need to: with
20
+ * settings-first readers the env vars are a build-time fallback only.
21
+ * - NEXT_PUBLIC_STORE_{LOGO|FAVICON}_URL — build-time fallback.
22
+ *
23
+ * Contract for every writer: update R2 + every mirror you can reach. This
24
+ * handler writes R2 + store_settings. GET reads the truth directly (an R2
25
+ * HEAD on the fixed key) so the admin UI can never lie about whether an
26
+ * asset exists, regardless of mirror state.
27
+ */
28
+ import { NextResponse } from 'next/server';
29
+ import { PutObjectCommand, DeleteObjectCommand, HeadObjectCommand, } from '@aws-sdk/client-s3';
30
+ import { requireAdmin } from '../server/admin-session';
31
+ import { getR2Client, getBucketName, getPublicUrlBase, getStoreId } from '../../media/server';
32
+ import { findSettings, upsertSettings, setOnboardingFlag } from '../../core/db/queries';
33
+ const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
34
+ const BASE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp', 'image/svg+xml'];
35
+ const KINDS = {
36
+ logo: {
37
+ settingsField: 'logoUrl',
38
+ envVar: 'NEXT_PUBLIC_STORE_LOGO_URL',
39
+ allowedTypes: BASE_TYPES,
40
+ label: 'Logo',
41
+ },
42
+ favicon: {
43
+ settingsField: 'faviconUrl',
44
+ envVar: 'NEXT_PUBLIC_STORE_FAVICON_URL',
45
+ // Favicons additionally accept .ico (both MIME spellings browsers emit).
46
+ allowedTypes: [...BASE_TYPES, 'image/x-icon', 'image/vnd.microsoft.icon'],
47
+ label: 'Favicon',
48
+ },
49
+ };
50
+ /** Fixed R2 key — mirror of the platform's getBrandingR2Key. No extension. */
51
+ function brandingR2Key(storeId, kind) {
52
+ return `stores/${storeId}/branding/${kind}`;
53
+ }
54
+ /**
55
+ * Mirror the asset URL (or null on delete) into store_settings so every
56
+ * settings-first reader picks it up live. Awaited but non-fatal: R2 is the
57
+ * truth and GET reads R2 directly, so a mirror failure degrades to "shows
58
+ * after the next successful settings write" instead of failing the upload.
59
+ */
60
+ async function mirrorToSettings(kind, url) {
61
+ try {
62
+ await upsertSettings({ [KINDS[kind].settingsField]: url });
63
+ return true;
64
+ }
65
+ catch (err) {
66
+ console.error(`[Branding API] store_settings ${KINDS[kind].settingsField} mirror failed:`, err);
67
+ return false;
68
+ }
69
+ }
70
+ // =============================================================================
71
+ // Handler factory
72
+ // =============================================================================
73
+ function createBrandingHandlers(kind) {
74
+ const cfg = KINDS[kind];
75
+ async function POST(req) {
76
+ try {
77
+ // Branding is a settings write — owner + administrator only.
78
+ const guard = await requireAdmin({ permission: 'settings.write' });
79
+ if (!guard.ok)
80
+ return guard.response;
81
+ let storeId;
82
+ let bucketName;
83
+ let publicUrl;
84
+ try {
85
+ storeId = getStoreId();
86
+ bucketName = getBucketName();
87
+ publicUrl = getPublicUrlBase();
88
+ }
89
+ catch {
90
+ return NextResponse.json({ error: 'Media storage not configured', code: 'NOT_CONFIGURED' }, { status: 503 });
91
+ }
92
+ let formData;
93
+ try {
94
+ formData = await req.formData();
95
+ }
96
+ catch {
97
+ return NextResponse.json({ error: 'Invalid form data', code: 'INVALID_REQUEST' }, { status: 400 });
98
+ }
99
+ // Field name matches the kind ('logo' / 'favicon'); accept 'file' as a
100
+ // generic fallback so future callers don't have to know the kind.
101
+ const file = (formData.get(kind) ?? formData.get('file'));
102
+ if (!file) {
103
+ return NextResponse.json({ error: `No ${kind} file provided`, code: 'MISSING_FILE' }, { status: 400 });
104
+ }
105
+ if (file.size > MAX_FILE_SIZE) {
106
+ return NextResponse.json({ error: `${cfg.label} file exceeds maximum size of 2MB`, code: 'FILE_TOO_LARGE' }, { status: 400 });
107
+ }
108
+ if (!cfg.allowedTypes.includes(file.type)) {
109
+ return NextResponse.json({
110
+ error: `Invalid file type. Allowed: PNG, JPG, WebP, SVG${kind === 'favicon' ? ', ICO' : ''}`,
111
+ code: 'INVALID_TYPE',
112
+ }, { status: 400 });
113
+ }
114
+ const r2Key = brandingR2Key(storeId, kind);
115
+ try {
116
+ const arrayBuffer = await file.arrayBuffer();
117
+ await getR2Client().send(new PutObjectCommand({
118
+ Bucket: bucketName,
119
+ Key: r2Key,
120
+ Body: Buffer.from(arrayBuffer),
121
+ ContentType: file.type, // R2 serves this back — the fixed URL never changes
122
+ // Explicit cache policy: the key is fixed, bytes get replaced in
123
+ // place. Without this, browsers heuristic-cache a long-unchanged
124
+ // asset for days; max-age=300 caps staleness at 5 minutes.
125
+ CacheControl: 'public, max-age=300',
126
+ }));
127
+ }
128
+ catch (err) {
129
+ console.error(`[Branding API] Failed to upload ${kind}:`, err);
130
+ return NextResponse.json({ error: `Failed to upload ${kind}`, code: 'UPLOAD_FAILED' }, { status: 500 });
131
+ }
132
+ const url = `${publicUrl}/${r2Key}`;
133
+ const settingsUpdated = await mirrorToSettings(kind, url);
134
+ if (kind === 'logo') {
135
+ // Setup-guide writeback: mark "Upload your own logo" complete.
136
+ // Fire-and-forget — onboarding flag failures never block the upload.
137
+ setOnboardingFlag('logoUploaded', true).catch((err) => {
138
+ console.error('[Branding API] onboarding writeback failed:', err);
139
+ });
140
+ }
141
+ console.log(`[Branding API] Uploaded ${kind} for store ${storeId}: ${r2Key} (${file.size} bytes, ${file.type}, settings mirror: ${settingsUpdated})`);
142
+ return NextResponse.json({
143
+ success: true,
144
+ data: { url, key: r2Key, size: file.size, type: file.type, settingsUpdated },
145
+ });
146
+ }
147
+ catch (error) {
148
+ console.error(`[Branding API] ${kind} POST error:`, error);
149
+ return NextResponse.json({ error: `Failed to process ${kind} upload`, code: 'INTERNAL_ERROR' }, { status: 500 });
150
+ }
151
+ }
152
+ async function DELETE() {
153
+ try {
154
+ const guard = await requireAdmin({ permission: 'settings.write' });
155
+ if (!guard.ok)
156
+ return guard.response;
157
+ let storeId;
158
+ let bucketName;
159
+ try {
160
+ storeId = getStoreId();
161
+ bucketName = getBucketName();
162
+ }
163
+ catch {
164
+ return NextResponse.json({ error: 'Media storage not configured', code: 'NOT_CONFIGURED' }, { status: 503 });
165
+ }
166
+ const r2Key = brandingR2Key(storeId, kind);
167
+ let deleted = false;
168
+ try {
169
+ await getR2Client().send(new DeleteObjectCommand({ Bucket: bucketName, Key: r2Key }));
170
+ deleted = true;
171
+ console.log(`[Branding API] Deleted ${kind}: ${r2Key}`);
172
+ }
173
+ catch {
174
+ // Object didn't exist — that's fine, the mirror clear below still runs.
175
+ console.log(`[Branding API] No ${kind} found to delete: ${r2Key}`);
176
+ }
177
+ const settingsUpdated = await mirrorToSettings(kind, null);
178
+ return NextResponse.json({ success: true, deleted, settingsUpdated });
179
+ }
180
+ catch (error) {
181
+ console.error(`[Branding API] ${kind} DELETE error:`, error);
182
+ return NextResponse.json({ error: `Failed to delete ${kind}`, code: 'INTERNAL_ERROR' }, { status: 500 });
183
+ }
184
+ }
185
+ async function GET() {
186
+ try {
187
+ // Every authenticated admin can read (the UI renders the asset).
188
+ const guard = await requireAdmin({ permission: 'settings.read' });
189
+ if (!guard.ok)
190
+ return guard.response;
191
+ // Truth first: HEAD the fixed R2 key. The object either exists or it
192
+ // doesn't — no mirror (env var, settings row, blueprint) can lie here.
193
+ try {
194
+ const storeId = getStoreId();
195
+ const bucketName = getBucketName();
196
+ const publicUrl = getPublicUrlBase();
197
+ const r2Key = brandingR2Key(storeId, kind);
198
+ let exists = false;
199
+ try {
200
+ await getR2Client().send(new HeadObjectCommand({ Bucket: bucketName, Key: r2Key }));
201
+ exists = true;
202
+ }
203
+ catch {
204
+ exists = false; // 404 / NotFound — asset absent
205
+ }
206
+ return NextResponse.json({
207
+ success: true,
208
+ data: { exists, url: exists ? `${publicUrl}/${r2Key}` : null },
209
+ });
210
+ }
211
+ catch {
212
+ // R2 not configured — degrade to the pointer chain (settings → env).
213
+ const settings = await findSettings().catch(() => null);
214
+ const url = settings?.[cfg.settingsField] ||
215
+ process.env[cfg.envVar] ||
216
+ null;
217
+ return NextResponse.json({ success: true, data: { exists: !!url, url } });
218
+ }
219
+ }
220
+ catch (error) {
221
+ console.error(`[Branding API] ${kind} GET error:`, error);
222
+ return NextResponse.json({ error: `Failed to check ${kind}`, code: 'INTERNAL_ERROR' }, { status: 500 });
223
+ }
224
+ }
225
+ return { GET, POST, DELETE };
226
+ }
227
+ // =============================================================================
228
+ // Exported handlers — template routes re-export these
229
+ // =============================================================================
230
+ const logoHandlers = createBrandingHandlers('logo');
231
+ const faviconHandlers = createBrandingHandlers('favicon');
232
+ export const logoGET = logoHandlers.GET;
233
+ export const logoPOST = logoHandlers.POST;
234
+ export const logoDELETE = logoHandlers.DELETE;
235
+ export const faviconGET = faviconHandlers.GET;
236
+ export const faviconPOST = faviconHandlers.POST;
237
+ export const faviconDELETE = faviconHandlers.DELETE;
238
+ //# sourceMappingURL=branding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branding.js","sourceRoot":"","sources":["../../../src/admin/api/branding.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import { type ReactNode } from 'react';
2
2
  export interface AdminTopBarProps {
3
3
  /** Store name (falls back to StoreSettingsProvider, then 'Admin'). */
4
4
  storeName?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"AdminTopBar.d.ts","sourceRoot":"","sources":["../../../src/admin/components/AdminTopBar.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQtC,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAMD,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EAAE,aAAuB,EAClC,IAAI,EACJ,QAAc,EACd,SAAc,GACf,EAAE,gBAAgB,2CA4ElB"}
1
+ {"version":3,"file":"AdminTopBar.d.ts","sourceRoot":"","sources":["../../../src/admin/components/AdminTopBar.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAQ3D,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAMD,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EAAE,aAAuB,EAClC,IAAI,EACJ,QAAc,EACd,SAAc,GACf,EAAE,gBAAgB,2CA8FlB"}
@@ -17,6 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
17
  */
18
18
  import Link from 'next/link';
19
19
  import { ExternalLink, Pencil } from 'lucide-react';
20
+ import { useEffect, useState } from 'react';
20
21
  import { useStoreSettingsOptional } from '../../core';
21
22
  import { AdminUserMenu } from './AdminUserMenu';
22
23
  // =============================================================================
@@ -29,7 +30,23 @@ export function AdminTopBar({ storeName: storeNameProp = 'Admin', logo, storeUrl
29
30
  const storeSettings = useStoreSettingsOptional();
30
31
  const settingsLoading = storeNameProp === 'Admin' && !!storeSettings?.isLoading && !storeSettings?.settings;
31
32
  const storeName = storeNameProp !== 'Admin' ? storeNameProp : (storeSettings?.storeName || storeNameProp);
33
+ // Brand tile mark: favicon FIRST (a square mark belongs in a 32px tile;
34
+ // wide logo lockups read cramped), then logo, then monogram. Each source
35
+ // is settings-first (live store_settings via the provider) with the
36
+ // build-time env var as fallback — the branding write contract keeps the
37
+ // settings mirror fresh, so store-side changes show without a redeploy.
38
+ const faviconUrl = storeSettings?.faviconUrl || process.env.NEXT_PUBLIC_STORE_FAVICON_URL || null;
32
39
  const logoUrl = storeSettings?.logoUrl || process.env.NEXT_PUBLIC_STORE_LOGO_URL || null;
40
+ const tileUrl = faviconUrl || logoUrl;
41
+ // A stale pointer (e.g. asset deleted store-side while an env var still
42
+ // carries the fixed URL) must degrade to the monogram, never a broken img.
43
+ const [tileBroken, setTileBroken] = useState(false);
44
+ useEffect(() => {
45
+ // Pattern A — reset internal state on prop change (retry when the URL changes).
46
+ // eslint-disable-next-line react-hooks/set-state-in-effect
47
+ setTileBroken(false);
48
+ }, [tileUrl]);
49
+ const tileSrc = tileBroken ? null : tileUrl;
33
50
  // Deep-link back to the Rovela editor (/generate/[id]) so the merchant has
34
51
  // a clean Manage ↔ Edit loop. Only rendered when NEXT_PUBLIC_ROVELA_PROJECT_ID
35
52
  // is set (present on every store rebuilt since buildStoreEnvVars started
@@ -41,8 +58,8 @@ export function AdminTopBar({ storeName: storeNameProp = 'Admin', logo, storeUrl
41
58
  const editStoreUrl = rovelaProjectId
42
59
  ? `${rovelaBase.replace(/\/$/, '')}/generate/${rovelaProjectId}`
43
60
  : null;
44
- return (_jsxs("header", { className: `admin-topbar ${className}`.trim(), children: [_jsxs(Link, { href: "/admin", className: "admin-topbar-brand", children: [logo || (_jsx("span", { className: `admin-brand-tile ${logoUrl ? '' : 'admin-brand-tile-monogram'}`, children: logoUrl ? (
61
+ return (_jsxs("header", { className: `admin-topbar ${className}`.trim(), children: [_jsxs(Link, { href: "/admin", className: "admin-topbar-brand", children: [logo || (_jsx("span", { className: `admin-brand-tile ${tileSrc ? '' : 'admin-brand-tile-monogram'}`, children: tileSrc ? (
45
62
  // eslint-disable-next-line @next/next/no-img-element
46
- _jsx("img", { src: logoUrl, alt: "" })) : settingsLoading ? (_jsx("span", { className: "admin-skeleton admin-brand-tile-skeleton" })) : (storeName.charAt(0).toUpperCase()) })), settingsLoading ? (_jsx("span", { className: "admin-skeleton admin-topbar-name-skeleton" })) : (_jsx("span", { className: "admin-topbar-store", children: storeName })), _jsx("span", { className: "admin-topbar-tag", children: "Admin" })] }), _jsxs("div", { className: "admin-topbar-actions", children: [_jsxs("a", { href: storeUrl, target: "_blank", rel: "noopener noreferrer", className: "admin-topbar-btn", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), "View store"] }), editStoreUrl && (_jsxs("a", { href: editStoreUrl, target: "_blank", rel: "noopener noreferrer", className: "admin-topbar-btn", children: [_jsx(Pencil, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), "Edit store"] })), _jsx("span", { className: "admin-topbar-divider", "aria-hidden": "true" }), _jsx(AdminUserMenu, { compact: true })] })] }));
63
+ _jsx("img", { src: tileSrc, alt: "", onError: () => setTileBroken(true) })) : settingsLoading ? (_jsx("span", { className: "admin-skeleton admin-brand-tile-skeleton" })) : (storeName.charAt(0).toUpperCase()) })), settingsLoading ? (_jsx("span", { className: "admin-skeleton admin-topbar-name-skeleton" })) : (_jsx("span", { className: "admin-topbar-store", children: storeName })), _jsx("span", { className: "admin-topbar-tag", children: "Admin" })] }), _jsxs("div", { className: "admin-topbar-actions", children: [_jsxs("a", { href: storeUrl, target: "_blank", rel: "noopener noreferrer", className: "admin-topbar-btn", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), "View store"] }), editStoreUrl && (_jsxs("a", { href: editStoreUrl, target: "_blank", rel: "noopener noreferrer", className: "admin-topbar-btn", children: [_jsx(Pencil, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), "Edit store"] })), _jsx("span", { className: "admin-topbar-divider", "aria-hidden": "true" }), _jsx(AdminUserMenu, { compact: true })] })] }));
47
64
  }
48
65
  //# sourceMappingURL=AdminTopBar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AdminTopBar.js","sourceRoot":"","sources":["../../../src/admin/components/AdminTopBar.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AdminTopBar.js","sourceRoot":"","sources":["../../../src/admin/components/AdminTopBar.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;;;;;;;;;;;;GAcG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAA;AAC3D,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAiB/C,gFAAgF;AAChF,YAAY;AACZ,gFAAgF;AAEhF,MAAM,UAAU,WAAW,CAAC,EAC1B,SAAS,EAAE,aAAa,GAAG,OAAO,EAClC,IAAI,EACJ,QAAQ,GAAG,GAAG,EACd,SAAS,GAAG,EAAE,GACG;IACjB,uEAAuE;IACvE,kEAAkE;IAClE,4BAA4B;IAC5B,MAAM,aAAa,GAAG,wBAAwB,EAAE,CAAA;IAChD,MAAM,eAAe,GACnB,aAAa,KAAK,OAAO,IAAI,CAAC,CAAC,aAAa,EAAE,SAAS,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAA;IACrF,MAAM,SAAS,GACb,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,SAAS,IAAI,aAAa,CAAC,CAAA;IACzF,wEAAwE;IACxE,yEAAyE;IACzE,oEAAoE;IACpE,yEAAyE;IACzE,wEAAwE;IACxE,MAAM,UAAU,GACd,aAAa,EAAE,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,6BAA6B,IAAI,IAAI,CAAA;IAChF,MAAM,OAAO,GAAG,aAAa,EAAE,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,IAAI,CAAA;IACxF,MAAM,OAAO,GAAG,UAAU,IAAI,OAAO,CAAA;IAErC,wEAAwE;IACxE,2EAA2E;IAC3E,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACnD,SAAS,CAAC,GAAG,EAAE;QACb,gFAAgF;QAChF,2DAA2D;QAC3D,aAAa,CAAC,KAAK,CAAC,CAAA;IACtB,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IACb,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAA;IAE3C,2EAA2E;IAC3E,+EAA+E;IAC/E,yEAAyE;IACzE,iEAAiE;IACjE,2EAA2E;IAC3E,8DAA8D;IAC9D,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAA;IACjE,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,mBAAmB,CAAA;IAC5E,MAAM,YAAY,GAAG,eAAe;QAClC,CAAC,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,eAAe,EAAE;QAChE,CAAC,CAAC,IAAI,CAAA;IAER,OAAO,CACL,kBAAQ,SAAS,EAAE,gBAAgB,SAAS,EAAE,CAAC,IAAI,EAAE,aAEnD,MAAC,IAAI,IAAC,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,oBAAoB,aAC/C,IAAI,IAAI,CACP,eACE,SAAS,EAAE,oBAAoB,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,2BAA2B,EAAE,YAE1E,OAAO,CAAC,CAAC,CAAC;wBACT,qDAAqD;wBACrD,cAAK,GAAG,EAAE,OAAO,EAAE,GAAG,EAAC,EAAE,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,GAAI,CACjE,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CACpB,eAAM,SAAS,EAAC,0CAA0C,GAAG,CAC9D,CAAC,CAAC,CAAC,CACF,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAClC,GACI,CACR,EACA,eAAe,CAAC,CAAC,CAAC,CACjB,eAAM,SAAS,EAAC,2CAA2C,GAAG,CAC/D,CAAC,CAAC,CAAC,CACF,eAAM,SAAS,EAAC,oBAAoB,YAAE,SAAS,GAAQ,CACxD,EACD,eAAM,SAAS,EAAC,kBAAkB,sBAAa,IAC1C,EAGP,eAAK,SAAS,EAAC,sBAAsB,aACnC,aACE,IAAI,EAAE,QAAQ,EACd,MAAM,EAAC,QAAQ,EACf,GAAG,EAAC,qBAAqB,EACzB,SAAS,EAAC,kBAAkB,aAE5B,KAAC,YAAY,IAAC,SAAS,EAAC,aAAa,EAAC,WAAW,EAAE,GAAG,GAAI,kBAExD,EACH,YAAY,IAAI,CACf,aACE,IAAI,EAAE,YAAY,EAClB,MAAM,EAAC,QAAQ,EACf,GAAG,EAAC,qBAAqB,EACzB,SAAS,EAAC,kBAAkB,aAE5B,KAAC,MAAM,IAAC,SAAS,EAAC,aAAa,EAAC,WAAW,EAAE,GAAG,GAAI,kBAElD,CACL,EACD,eAAM,SAAS,EAAC,sBAAsB,iBAAa,MAAM,GAAG,EAC5D,KAAC,aAAa,IAAC,OAAO,SAAG,IACrB,IACC,CACV,CAAA;AACH,CAAC"}
@@ -0,0 +1,14 @@
1
+ export interface BrandingAssetUploadProps {
2
+ /** Callback when the asset is successfully uploaded */
3
+ onUpload?: (url: string) => void;
4
+ /** Callback when the asset is successfully deleted */
5
+ onDelete?: () => void;
6
+ /** Additional CSS classes */
7
+ className?: string;
8
+ }
9
+ export declare function BrandingAssetUpload({ kind, onUpload, onDelete, className, }: BrandingAssetUploadProps & {
10
+ kind: 'logo' | 'favicon';
11
+ }): import("react/jsx-runtime").JSX.Element;
12
+ export type FaviconUploadProps = BrandingAssetUploadProps;
13
+ export declare function FaviconUpload(props: FaviconUploadProps): import("react/jsx-runtime").JSX.Element;
14
+ //# sourceMappingURL=BrandingAssetUpload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BrandingAssetUpload.d.ts","sourceRoot":"","sources":["../../../src/admin/components/BrandingAssetUpload.tsx"],"names":[],"mappings":"AAuBA,MAAM,WAAW,wBAAwB;IACvC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA0DD,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,SAAc,GACf,EAAE,wBAAwB,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,2CAuTzD;AAMD,MAAM,MAAM,kBAAkB,GAAG,wBAAwB,CAAA;AAEzD,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,2CAEtD"}
@@ -0,0 +1,207 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * @rovela/sdk/admin/components/BrandingAssetUpload
5
+ *
6
+ * Kind-parameterized branding asset upload (logo + favicon) for the admin
7
+ * settings page. `LogoUpload` (LogoUpload.tsx) and `FaviconUpload` (below)
8
+ * are thin instantiations — one implementation, two assets, zero drift.
9
+ *
10
+ * Talks to /api/admin/branding/{kind} (SDK handlers — R2 fixed key +
11
+ * store_settings mirror + R2-HEAD GET, see admin/api/branding.ts). The
12
+ * client-side `?v=` cache-buster on previews covers the fixed-URL byte
13
+ * replacement within this surface; the server sets max-age=300 for
14
+ * everything else.
15
+ */
16
+ import { useState, useEffect, useRef, useCallback } from 'react';
17
+ import { fetchAdminApi } from '../hooks/fetchAdminApi';
18
+ const BASE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp', 'image/svg+xml'];
19
+ const BASE_ACCEPT = 'image/png,image/jpeg,image/jpg,image/webp,image/svg+xml';
20
+ const KIND_CONFIG = {
21
+ logo: {
22
+ endpoint: '/api/admin/branding/logo',
23
+ formField: 'logo',
24
+ label: 'Store Logo',
25
+ helper: 'Your logo appears in the header and emails. Recommended: 200x80px.',
26
+ allowedTypes: BASE_TYPES,
27
+ accept: BASE_ACCEPT,
28
+ invalidTypeMessage: 'Invalid file type. Use PNG, JPG, WebP, or SVG.',
29
+ preview: 'wide',
30
+ },
31
+ favicon: {
32
+ endpoint: '/api/admin/branding/favicon',
33
+ formField: 'favicon',
34
+ label: 'Favicon',
35
+ helper: 'The small square icon in the browser tab and the admin top bar. Recommended: a square PNG or SVG, at least 64x64px. Your logo is used when no favicon is set.',
36
+ allowedTypes: [...BASE_TYPES, 'image/x-icon', 'image/vnd.microsoft.icon'],
37
+ accept: `${BASE_ACCEPT},image/x-icon,image/vnd.microsoft.icon,.ico`,
38
+ invalidTypeMessage: 'Invalid file type. Use PNG, JPG, WebP, SVG, or ICO.',
39
+ preview: 'tile',
40
+ },
41
+ };
42
+ const MAX_SIZE = 2 * 1024 * 1024; // 2MB (mirrored server-side)
43
+ // =============================================================================
44
+ // Component
45
+ // =============================================================================
46
+ export function BrandingAssetUpload({ kind, onUpload, onDelete, className = '', }) {
47
+ const cfg = KIND_CONFIG[kind];
48
+ const [asset, setAsset] = useState({
49
+ exists: false,
50
+ url: null,
51
+ isLoading: true,
52
+ isUploading: false,
53
+ isDeleting: false,
54
+ error: null,
55
+ });
56
+ const [preview, setPreview] = useState(null);
57
+ const [isDragging, setIsDragging] = useState(false);
58
+ const [successMessage, setSuccessMessage] = useState(null);
59
+ const [cacheBuster, setCacheBuster] = useState(() => Date.now());
60
+ const fileInputRef = useRef(null);
61
+ // Fetch status on mount — the GET is an R2 HEAD, so this reflects the
62
+ // actual object, not a possibly-stale env var or settings row.
63
+ useEffect(() => {
64
+ let cancelled = false;
65
+ async function fetchStatus() {
66
+ const res = await fetchAdminApi(cfg.endpoint);
67
+ if (cancelled)
68
+ return;
69
+ if (!res.ok) {
70
+ setAsset((prev) => ({ ...prev, isLoading: false, error: res.error }));
71
+ return;
72
+ }
73
+ setAsset({
74
+ exists: res.data.data.exists,
75
+ url: res.data.data.url,
76
+ isLoading: false,
77
+ isUploading: false,
78
+ isDeleting: false,
79
+ error: null,
80
+ });
81
+ }
82
+ fetchStatus();
83
+ return () => {
84
+ cancelled = true;
85
+ };
86
+ }, [cfg.endpoint]);
87
+ const uploadAsset = useCallback(async (file) => {
88
+ setAsset((prev) => ({ ...prev, isUploading: true, error: null }));
89
+ const formData = new FormData();
90
+ formData.append(cfg.formField, file);
91
+ const res = await fetchAdminApi(cfg.endpoint, {
92
+ method: 'POST',
93
+ body: formData,
94
+ });
95
+ if (!res.ok) {
96
+ setAsset((prev) => ({ ...prev, isUploading: false, error: res.error }));
97
+ setPreview(null);
98
+ return;
99
+ }
100
+ setCacheBuster(Date.now());
101
+ setAsset({
102
+ exists: true,
103
+ url: res.data.data.url,
104
+ isLoading: false,
105
+ isUploading: false,
106
+ isDeleting: false,
107
+ error: null,
108
+ });
109
+ setPreview(null);
110
+ setSuccessMessage(`${cfg.label} uploaded successfully!`);
111
+ setTimeout(() => setSuccessMessage(null), 3000);
112
+ onUpload?.(res.data.data.url);
113
+ }, [cfg.endpoint, cfg.formField, cfg.label, onUpload]);
114
+ const handleFileSelect = useCallback((file) => {
115
+ setAsset((prev) => ({ ...prev, error: null }));
116
+ setSuccessMessage(null);
117
+ if (!cfg.allowedTypes.includes(file.type)) {
118
+ setAsset((prev) => ({ ...prev, error: cfg.invalidTypeMessage }));
119
+ return;
120
+ }
121
+ if (file.size > MAX_SIZE) {
122
+ setAsset((prev) => ({ ...prev, error: 'File too large. Maximum size is 2MB.' }));
123
+ return;
124
+ }
125
+ const reader = new FileReader();
126
+ reader.onload = (e) => {
127
+ setPreview(e.target?.result);
128
+ };
129
+ reader.readAsDataURL(file);
130
+ uploadAsset(file);
131
+ }, [cfg.allowedTypes, cfg.invalidTypeMessage, uploadAsset]);
132
+ const handleDelete = async () => {
133
+ if (!asset.exists)
134
+ return;
135
+ setAsset((prev) => ({ ...prev, isDeleting: true, error: null }));
136
+ setSuccessMessage(null);
137
+ const res = await fetchAdminApi(cfg.endpoint, { method: 'DELETE' });
138
+ if (!res.ok) {
139
+ setAsset((prev) => ({ ...prev, isDeleting: false, error: res.error }));
140
+ return;
141
+ }
142
+ setAsset({
143
+ exists: false,
144
+ url: null,
145
+ isLoading: false,
146
+ isUploading: false,
147
+ isDeleting: false,
148
+ error: null,
149
+ });
150
+ setSuccessMessage(`${cfg.label} removed successfully!`);
151
+ setTimeout(() => setSuccessMessage(null), 3000);
152
+ onDelete?.();
153
+ };
154
+ const handleFileInputChange = (e) => {
155
+ const file = e.target.files?.[0];
156
+ if (file) {
157
+ handleFileSelect(file);
158
+ }
159
+ if (fileInputRef.current) {
160
+ fileInputRef.current.value = '';
161
+ }
162
+ };
163
+ const handleDragOver = (e) => {
164
+ e.preventDefault();
165
+ setIsDragging(true);
166
+ };
167
+ const handleDragLeave = (e) => {
168
+ e.preventDefault();
169
+ setIsDragging(false);
170
+ };
171
+ const handleDrop = (e) => {
172
+ e.preventDefault();
173
+ setIsDragging(false);
174
+ const file = e.dataTransfer.files?.[0];
175
+ if (file) {
176
+ handleFileSelect(file);
177
+ }
178
+ };
179
+ const getDisplayUrl = () => {
180
+ if (preview)
181
+ return preview;
182
+ if (asset.url)
183
+ return `${asset.url}?v=${cacheBuster}`;
184
+ return null;
185
+ };
186
+ const displayUrl = getDisplayUrl();
187
+ const isWorking = asset.isLoading || asset.isUploading || asset.isDeleting;
188
+ return (_jsxs("div", { className: className, children: [_jsx("label", { className: "admin-label", children: cfg.label }), _jsx("p", { className: "admin-helper-text mb-3", children: cfg.helper }), asset.error && (_jsx("div", { className: "admin-alert admin-alert-error mb-3", children: _jsx("div", { className: "admin-alert-description text-sm", children: asset.error }) })), successMessage && (_jsx("div", { className: "admin-alert admin-alert-success mb-3", children: _jsx("div", { className: "admin-alert-description text-sm", children: successMessage }) })), displayUrl ? (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "relative rounded-lg border border-[hsl(var(--admin-border))] bg-[hsl(var(--admin-background-subtle))] p-4", children: [_jsx("div", { className: cfg.preview === 'tile'
189
+ ? 'flex items-center justify-center min-h-[56px]'
190
+ : 'flex items-center justify-center min-h-[80px]', children: _jsx("img", { src: displayUrl, alt: cfg.label, className: cfg.preview === 'tile'
191
+ ? 'h-12 w-12 rounded-md object-contain'
192
+ : 'max-h-[80px] max-w-full object-contain' }) }), asset.isUploading && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-[hsl(var(--admin-background))]/80 rounded-lg", children: _jsxs("div", { className: "flex items-center gap-2 text-sm admin-text-muted", children: [_jsx("div", { className: "admin-spinner h-4 w-4" }), "Uploading..."] }) }))] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { ref: fileInputRef, type: "file", accept: cfg.accept, onChange: handleFileInputChange, className: "hidden" }), _jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), disabled: isWorking, className: "admin-btn admin-btn-secondary admin-btn-sm disabled:opacity-50", children: "Replace" }), _jsx("button", { type: "button", onClick: handleDelete, disabled: isWorking, className: "admin-btn admin-btn-destructive admin-btn-sm disabled:opacity-50", children: asset.isDeleting ? 'Removing...' : 'Remove' })] })] })) : (_jsxs("div", { onClick: () => !isWorking && fileInputRef.current?.click(), onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, className: `
193
+ relative flex min-h-[120px] cursor-pointer flex-col items-center justify-center
194
+ rounded-lg border-2 border-dashed transition-all
195
+ ${isDragging
196
+ ? 'border-[hsl(var(--admin-primary))] bg-[hsl(var(--admin-primary)/0.05)]'
197
+ : 'border-[hsl(var(--admin-border))] hover:border-[hsl(var(--admin-primary)/0.5)]'}
198
+ ${isWorking ? 'opacity-50 cursor-not-allowed' : ''}
199
+ `, children: [_jsx("input", { ref: fileInputRef, type: "file", accept: cfg.accept, onChange: handleFileInputChange, className: "hidden" }), asset.isLoading ? (_jsxs("div", { className: "flex items-center gap-2 admin-text-muted", children: [_jsx("div", { className: "admin-spinner h-5 w-5" }), _jsx("span", { className: "text-sm", children: "Checking..." })] })) : (_jsxs("div", { className: "flex flex-col items-center gap-2 p-4 text-center", children: [_jsx("div", { className: `
200
+ flex h-10 w-10 items-center justify-center rounded-full transition-colors
201
+ ${isDragging ? 'bg-[hsl(var(--admin-primary)/0.2)]' : 'bg-[hsl(var(--admin-background-subtle))]'}
202
+ `, children: _jsx("svg", { className: `h-5 w-5 transition-colors ${isDragging ? 'text-[hsl(var(--admin-primary))]' : 'admin-icon-muted'}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" }) }) }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium admin-text", children: isDragging ? 'Drop here' : 'Click or drag to upload' }), _jsx("p", { className: "admin-helper-text mt-1", children: kind === 'favicon' ? 'PNG, JPG, WebP, SVG, ICO (max 2MB)' : 'PNG, JPG, WebP, SVG (max 2MB)' })] })] }))] }))] }));
203
+ }
204
+ export function FaviconUpload(props) {
205
+ return _jsx(BrandingAssetUpload, { kind: "favicon", ...props });
206
+ }
207
+ //# sourceMappingURL=BrandingAssetUpload.js.map