@burdenoff/microfe-devportal 2026.1112.2 → 2026.1113.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.
- package/dist/devportal/components/AppCard.js +73 -70
- package/dist/devportal/components/AppCard.js.map +1 -1
- package/dist/devportal/components/ImageUploadField.js +82 -80
- package/dist/devportal/components/ImageUploadField.js.map +1 -1
- package/dist/devportal/components/StoreImage.d.ts +15 -0
- package/dist/devportal/components/StoreImage.js +30 -0
- package/dist/devportal/components/StoreImage.js.map +1 -0
- package/dist/devportal/components/index.d.ts +1 -0
- package/dist/devportal/components/index.js +17 -16
- package/dist/devportal/hooks/usePackageUpload.d.ts +46 -4
- package/dist/devportal/hooks/usePackageUpload.js +90 -36
- package/dist/devportal/hooks/usePackageUpload.js.map +1 -1
- package/dist/devportal/pages/AdminReviewPage.js +205 -202
- package/dist/devportal/pages/AdminReviewPage.js.map +1 -1
- package/dist/devportal/pages/ApplicationDetailPage.js +2 -2
- package/dist/devportal/pages/ApplicationDetailPage.js.map +1 -1
- package/dist/devportal/pages/ApplicationsPage.js +201 -199
- package/dist/devportal/pages/ApplicationsPage.js.map +1 -1
- package/dist/devportal/pages/CreatePhysicalItemPage.js +2 -2
- package/dist/devportal/pages/CreatePhysicalItemPage.js.map +1 -1
- package/dist/devportal/pages/EditPhysicalItemPage.js +312 -307
- package/dist/devportal/pages/EditPhysicalItemPage.js.map +1 -1
- package/dist/devportal/pages/ListingPreviewPage.js +175 -171
- package/dist/devportal/pages/ListingPreviewPage.js.map +1 -1
- package/dist/devportal/pages/SellerDashboardPage.js +118 -116
- package/dist/devportal/pages/SellerDashboardPage.js.map +1 -1
- package/dist/devportal/pages/SellerItemsPage.js +202 -199
- package/dist/devportal/pages/SellerItemsPage.js.map +1 -1
- package/dist/devportal/pages/StoreSubmissionPage.js +133 -131
- package/dist/devportal/pages/StoreSubmissionPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,144 +3,147 @@ import { FeaturedBadge as t, StatusBadge as n, VerifiedBadge as r } from "./Stat
|
|
|
3
3
|
import { APP_TYPE_LABELS as i } from "../utils/constants.js";
|
|
4
4
|
import { useDevportal as a } from "../providers/DevportalProvider.js";
|
|
5
5
|
import { SelectionCheckbox as o } from "./SelectionCheckbox.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { StoreImage as s } from "./StoreImage.js";
|
|
7
|
+
import { AppWindow as c, Download as l, ExternalLink as u, Star as d } from "lucide-react";
|
|
8
|
+
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
9
|
+
import { useI18n as m } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
10
|
+
import { GlassCard as h } from "@burdenoff/fe-libs/ui";
|
|
10
11
|
// src/devportal/components/AppCard.tsx
|
|
11
|
-
var
|
|
12
|
-
let { navigate:
|
|
13
|
-
let n =
|
|
12
|
+
var g = ({ app: g, onClick: _, className: v, showActions: y = !0, selected: b = !1, onToggleSelect: x }) => {
|
|
13
|
+
let { navigate: S, basePath: C } = a(), { t: w } = m(), T = (e, t) => {
|
|
14
|
+
let n = w(e);
|
|
14
15
|
return n === e ? t : n;
|
|
15
|
-
},
|
|
16
|
-
|
|
16
|
+
}, E = () => {
|
|
17
|
+
_ ? _() : S && S(`${C}/apps/${g.id}`);
|
|
17
18
|
};
|
|
18
|
-
return /* @__PURE__ */
|
|
19
|
+
return /* @__PURE__ */ p(h, {
|
|
19
20
|
treatment: "insight",
|
|
20
21
|
role: "button",
|
|
21
22
|
tabIndex: 0,
|
|
22
|
-
className: e("group cursor-pointer border-border-seam p-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
23
|
-
onClick:
|
|
23
|
+
className: e("group cursor-pointer border-border-seam p-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", b && "border-primary bg-action-primary-bg/5", v),
|
|
24
|
+
onClick: E,
|
|
24
25
|
onKeyDown: (e) => {
|
|
25
|
-
(e.key === "Enter" || e.key === " ") && (e.preventDefault(),
|
|
26
|
+
(e.key === "Enter" || e.key === " ") && (e.preventDefault(), E());
|
|
26
27
|
},
|
|
27
28
|
children: [
|
|
28
|
-
/* @__PURE__ */
|
|
29
|
+
/* @__PURE__ */ p("div", {
|
|
29
30
|
className: "flex items-start gap-3",
|
|
30
31
|
children: [
|
|
31
|
-
|
|
32
|
+
x && /* @__PURE__ */ f("div", {
|
|
32
33
|
onClick: (e) => e.stopPropagation(),
|
|
33
|
-
children: /* @__PURE__ */
|
|
34
|
-
checked:
|
|
35
|
-
onChange:
|
|
36
|
-
ariaLabel: `Select application ${
|
|
34
|
+
children: /* @__PURE__ */ f(o, {
|
|
35
|
+
checked: b,
|
|
36
|
+
onChange: x,
|
|
37
|
+
ariaLabel: `Select application ${g.name}`,
|
|
37
38
|
className: "mt-1"
|
|
38
39
|
})
|
|
39
40
|
}),
|
|
40
|
-
|
|
41
|
-
src:
|
|
42
|
-
alt:
|
|
43
|
-
className: "h-12 w-12 rounded-lg object-cover"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
/* @__PURE__ */ f(s, {
|
|
42
|
+
src: g.iconUrl,
|
|
43
|
+
alt: g.name,
|
|
44
|
+
className: "h-12 w-12 rounded-lg object-cover",
|
|
45
|
+
fallback: /* @__PURE__ */ f("div", {
|
|
46
|
+
className: "flex h-12 w-12 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary",
|
|
47
|
+
children: /* @__PURE__ */ f(c, { size: 24 })
|
|
48
|
+
})
|
|
47
49
|
}),
|
|
48
|
-
/* @__PURE__ */
|
|
50
|
+
/* @__PURE__ */ p("div", {
|
|
49
51
|
className: "flex-1 min-w-0",
|
|
50
|
-
children: [/* @__PURE__ */
|
|
52
|
+
children: [/* @__PURE__ */ p("div", {
|
|
51
53
|
className: "flex items-center gap-2",
|
|
52
|
-
children: [/* @__PURE__ */
|
|
54
|
+
children: [/* @__PURE__ */ f("h3", {
|
|
53
55
|
className: "font-semibold truncate",
|
|
54
|
-
children:
|
|
55
|
-
}),
|
|
56
|
-
}), /* @__PURE__ */
|
|
56
|
+
children: g.name
|
|
57
|
+
}), g.isVerified && /* @__PURE__ */ f(r, { verified: !0 })]
|
|
58
|
+
}), /* @__PURE__ */ f("p", {
|
|
57
59
|
className: "text-sm text-text-secondary",
|
|
58
|
-
children: i[
|
|
60
|
+
children: i[g.appType] || g.appType
|
|
59
61
|
})]
|
|
60
62
|
})
|
|
61
63
|
]
|
|
62
64
|
}),
|
|
63
|
-
|
|
65
|
+
g.shortDescription && /* @__PURE__ */ f("p", {
|
|
64
66
|
className: "mt-3 text-sm text-text-secondary line-clamp-2",
|
|
65
|
-
children:
|
|
67
|
+
children: g.shortDescription
|
|
66
68
|
}),
|
|
67
|
-
/* @__PURE__ */
|
|
69
|
+
/* @__PURE__ */ p("div", {
|
|
68
70
|
className: "mt-3 flex flex-wrap items-center gap-2",
|
|
69
|
-
children: [/* @__PURE__ */
|
|
71
|
+
children: [/* @__PURE__ */ f(n, { status: g.status }), g.isFeatured && /* @__PURE__ */ f(t, {})]
|
|
70
72
|
}),
|
|
71
|
-
/* @__PURE__ */
|
|
73
|
+
/* @__PURE__ */ p("div", {
|
|
72
74
|
className: "mt-4 flex items-center gap-4 text-sm text-text-secondary",
|
|
73
|
-
children: [
|
|
75
|
+
children: [g.averageRating !== void 0 && g.averageRating > 0 && /* @__PURE__ */ p("span", {
|
|
74
76
|
className: "flex items-center gap-1",
|
|
75
|
-
children: [/* @__PURE__ */ d
|
|
77
|
+
children: [/* @__PURE__ */ f(d, {
|
|
76
78
|
size: 14,
|
|
77
79
|
className: "text-status-warning-text fill-status-warning-text"
|
|
78
|
-
}),
|
|
79
|
-
}), /* @__PURE__ */
|
|
80
|
+
}), g.averageRating.toFixed(1)]
|
|
81
|
+
}), /* @__PURE__ */ p("span", {
|
|
80
82
|
className: "flex items-center gap-1",
|
|
81
83
|
children: [
|
|
82
|
-
/* @__PURE__ */
|
|
83
|
-
|
|
84
|
+
/* @__PURE__ */ f(l, { size: 14 }),
|
|
85
|
+
g.installCount.toLocaleString(),
|
|
84
86
|
" ",
|
|
85
|
-
|
|
87
|
+
T("devportal.installs", "installs")
|
|
86
88
|
]
|
|
87
89
|
})]
|
|
88
90
|
}),
|
|
89
|
-
|
|
91
|
+
y && /* @__PURE__ */ f("div", {
|
|
90
92
|
className: "mt-4 flex items-center justify-between opacity-0 group-hover:opacity-100 transition-opacity",
|
|
91
|
-
children: /* @__PURE__ */
|
|
93
|
+
children: /* @__PURE__ */ p("button", {
|
|
92
94
|
className: "text-sm text-primary hover:underline flex items-center gap-1",
|
|
93
95
|
onClick: (e) => {
|
|
94
|
-
e.stopPropagation(),
|
|
96
|
+
e.stopPropagation(), E();
|
|
95
97
|
},
|
|
96
|
-
children: [
|
|
98
|
+
children: [T("devportal.viewDetails", "View Details"), /* @__PURE__ */ f(u, { size: 14 })]
|
|
97
99
|
})
|
|
98
100
|
})
|
|
99
101
|
]
|
|
100
102
|
});
|
|
101
|
-
},
|
|
102
|
-
let { navigate:
|
|
103
|
-
r ? r() :
|
|
103
|
+
}, _ = ({ app: t, onClick: r, className: l, selected: u = !1, onToggleSelect: d }) => {
|
|
104
|
+
let { navigate: m, basePath: h } = a(), g = () => {
|
|
105
|
+
r ? r() : m && m(`${h}/apps/${t.id}`);
|
|
104
106
|
};
|
|
105
|
-
return /* @__PURE__ */
|
|
107
|
+
return /* @__PURE__ */ p("div", {
|
|
106
108
|
role: "button",
|
|
107
109
|
tabIndex: 0,
|
|
108
|
-
className: e("flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3 cursor-pointer transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
109
|
-
onClick:
|
|
110
|
+
className: e("flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3 cursor-pointer transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", u && "border-primary bg-action-primary-bg/5", l),
|
|
111
|
+
onClick: g,
|
|
110
112
|
onKeyDown: (e) => {
|
|
111
|
-
(e.key === "Enter" || e.key === " ") && (e.preventDefault(),
|
|
113
|
+
(e.key === "Enter" || e.key === " ") && (e.preventDefault(), g());
|
|
112
114
|
},
|
|
113
115
|
children: [
|
|
114
|
-
|
|
116
|
+
d && /* @__PURE__ */ f("div", {
|
|
115
117
|
onClick: (e) => e.stopPropagation(),
|
|
116
|
-
children: /* @__PURE__ */
|
|
117
|
-
checked:
|
|
118
|
-
onChange:
|
|
118
|
+
children: /* @__PURE__ */ f(o, {
|
|
119
|
+
checked: u,
|
|
120
|
+
onChange: d,
|
|
119
121
|
ariaLabel: `Select application ${t.name}`
|
|
120
122
|
})
|
|
121
123
|
}),
|
|
122
|
-
|
|
124
|
+
/* @__PURE__ */ f(s, {
|
|
123
125
|
src: t.iconUrl,
|
|
124
126
|
alt: t.name,
|
|
125
|
-
className: "h-10 w-10 rounded-lg object-cover"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
className: "h-10 w-10 rounded-lg object-cover",
|
|
128
|
+
fallback: /* @__PURE__ */ f("div", {
|
|
129
|
+
className: "flex h-10 w-10 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary",
|
|
130
|
+
children: /* @__PURE__ */ f(c, { size: 20 })
|
|
131
|
+
})
|
|
129
132
|
}),
|
|
130
|
-
/* @__PURE__ */
|
|
133
|
+
/* @__PURE__ */ p("div", {
|
|
131
134
|
className: "flex-1 min-w-0",
|
|
132
|
-
children: [/* @__PURE__ */
|
|
135
|
+
children: [/* @__PURE__ */ f("p", {
|
|
133
136
|
className: "font-medium truncate",
|
|
134
137
|
children: t.name
|
|
135
|
-
}), /* @__PURE__ */
|
|
138
|
+
}), /* @__PURE__ */ f("p", {
|
|
136
139
|
className: "text-sm text-text-secondary truncate",
|
|
137
140
|
children: t.shortDescription || i[t.appType]
|
|
138
141
|
})]
|
|
139
142
|
}),
|
|
140
|
-
/* @__PURE__ */
|
|
143
|
+
/* @__PURE__ */ f(n, { status: t.status })
|
|
141
144
|
]
|
|
142
145
|
});
|
|
143
146
|
};
|
|
144
|
-
export {
|
|
147
|
+
export { g as AppCard, _ as AppListItem };
|
|
145
148
|
|
|
146
149
|
//# sourceMappingURL=AppCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCard.js","names":["AppCard","useDevportal","useI18n","tr","value","t","key","fallback","handleClick","onClick","navigate","basePath","app","GlassCard","cn","selected","className","event","onToggleSelect","SelectionCheckbox","AppWindow","VerifiedBadge","APP_TYPE_LABELS","StatusBadge","FeaturedBadge","Star","Download","showActions","ExternalLink","AppListItem"],"sources":["../../../src/devportal/components/AppCard.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { AppWindow, Star, Download, ExternalLink } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { cn } from \"../utils/cn\";\nimport { StatusBadge, VerifiedBadge, FeaturedBadge } from \"./StatusBadge\";\nimport type { Application } from \"../types\";\nimport { APP_TYPE_LABELS } from \"../utils/constants\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { SelectionCheckbox } from \"./SelectionCheckbox\";\n\ninterface AppCardProps {\n app: Application;\n onClick?: () => void;\n className?: string;\n showActions?: boolean;\n selected?: boolean;\n onToggleSelect?: (checked: boolean) => void;\n}\n\n/**\n * Application card for displaying app information\n */\nexport const AppCard: FC<AppCardProps> = ({\n app,\n onClick,\n className,\n showActions = true,\n selected = false,\n onToggleSelect,\n}) => {\n const { navigate, basePath } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const handleClick = () => {\n if (onClick) {\n onClick();\n } else if (navigate) {\n navigate(`${basePath}/apps/${app.id}`);\n }\n };\n\n return (\n <GlassCard\n treatment=\"insight\"\n role=\"button\"\n tabIndex={0}\n className={cn(\n \"group cursor-pointer border-border-seam p-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n selected && \"border-primary bg-action-primary-bg/5\",\n className,\n )}\n onClick={handleClick}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Header */}\n <div className=\"flex items-start gap-3\">\n {onToggleSelect && (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selected}\n onChange={onToggleSelect}\n ariaLabel={`Select application ${app.name}`}\n className=\"mt-1\"\n />\n </div>\n )}\n {app.iconUrl ? (\n <img\n src={app.iconUrl}\n alt={app.name}\n className=\"h-12 w-12 rounded-lg object-cover\"\n />\n ) : (\n <div className=\"flex h-12 w-12 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <AppWindow size={24} />\n </div>\n )}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold truncate\">{app.name}</h3>\n {app.isVerified && <VerifiedBadge verified />}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {APP_TYPE_LABELS[app.appType] || app.appType}\n </p>\n </div>\n </div>\n\n {/* Description */}\n {app.shortDescription && (\n <p className=\"mt-3 text-sm text-text-secondary line-clamp-2\">\n {app.shortDescription}\n </p>\n )}\n\n {/* Badges */}\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <StatusBadge status={app.status} />\n {app.isFeatured && <FeaturedBadge />}\n </div>\n\n {/* Stats */}\n <div className=\"mt-4 flex items-center gap-4 text-sm text-text-secondary\">\n {app.averageRating !== undefined && app.averageRating > 0 && (\n <span className=\"flex items-center gap-1\">\n <Star\n size={14}\n className=\"text-status-warning-text fill-status-warning-text\"\n />\n {app.averageRating.toFixed(1)}\n </span>\n )}\n <span className=\"flex items-center gap-1\">\n <Download size={14} />\n {app.installCount.toLocaleString()}{\" \"}\n {tr(\"devportal.installs\", \"installs\")}\n </span>\n </div>\n\n {/* Actions */}\n {showActions && (\n <div className=\"mt-4 flex items-center justify-between opacity-0 group-hover:opacity-100 transition-opacity\">\n <button\n className=\"text-sm text-primary hover:underline flex items-center gap-1\"\n onClick={(e) => {\n e.stopPropagation();\n handleClick();\n }}\n >\n {tr(\"devportal.viewDetails\", \"View Details\")}\n <ExternalLink size={14} />\n </button>\n </div>\n )}\n </GlassCard>\n );\n};\n\n/**\n * Grid of app cards\n */\nexport const AppCardGrid: FC<{\n apps: Application[];\n onAppClick?: (app: Application) => void;\n className?: string;\n}> = ({ apps, onAppClick, className }) => (\n <div\n className={cn(\n \"grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\",\n className,\n )}\n >\n {apps.map((app) => (\n <AppCard\n key={app.id}\n app={app}\n onClick={onAppClick ? () => onAppClick(app) : undefined}\n />\n ))}\n </div>\n);\n\n/**\n * Compact app list item\n */\nexport const AppListItem: FC<{\n app: Application;\n onClick?: () => void;\n className?: string;\n selected?: boolean;\n onToggleSelect?: (checked: boolean) => void;\n}> = ({ app, onClick, className, selected = false, onToggleSelect }) => {\n const { navigate, basePath } = useDevportal();\n\n const handleClick = () => {\n if (onClick) {\n onClick();\n } else if (navigate) {\n navigate(`${basePath}/apps/${app.id}`);\n }\n };\n\n return (\n <div\n role=\"button\"\n tabIndex={0}\n className={cn(\n \"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3 cursor-pointer transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n selected && \"border-primary bg-action-primary-bg/5\",\n className,\n )}\n onClick={handleClick}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n handleClick();\n }\n }}\n >\n {onToggleSelect && (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selected}\n onChange={onToggleSelect}\n ariaLabel={`Select application ${app.name}`}\n />\n </div>\n )}\n {app.iconUrl ? (\n <img\n src={app.iconUrl}\n alt={app.name}\n className=\"h-10 w-10 rounded-lg object-cover\"\n />\n ) : (\n <div className=\"flex h-10 w-10 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <AppWindow size={20} />\n </div>\n )}\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium truncate\">{app.name}</p>\n <p className=\"text-sm text-text-secondary truncate\">\n {app.shortDescription || APP_TYPE_LABELS[app.appType]}\n </p>\n </div>\n <StatusBadge status={app.status} />\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AppCard.js","names":["AppCard","useDevportal","useI18n","tr","value","t","key","fallback","handleClick","onClick","navigate","basePath","app","GlassCard","cn","selected","className","event","onToggleSelect","SelectionCheckbox","StoreImage","AppWindow","VerifiedBadge","APP_TYPE_LABELS","StatusBadge","FeaturedBadge","Star","Download","showActions","ExternalLink","AppListItem"],"sources":["../../../src/devportal/components/AppCard.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { AppWindow, Star, Download, ExternalLink } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { cn } from \"../utils/cn\";\nimport { StatusBadge, VerifiedBadge, FeaturedBadge } from \"./StatusBadge\";\nimport type { Application } from \"../types\";\nimport { APP_TYPE_LABELS } from \"../utils/constants\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { SelectionCheckbox } from \"./SelectionCheckbox\";\nimport { StoreImage } from \"./StoreImage\";\n\ninterface AppCardProps {\n app: Application;\n onClick?: () => void;\n className?: string;\n showActions?: boolean;\n selected?: boolean;\n onToggleSelect?: (checked: boolean) => void;\n}\n\n/**\n * Application card for displaying app information\n */\nexport const AppCard: FC<AppCardProps> = ({\n app,\n onClick,\n className,\n showActions = true,\n selected = false,\n onToggleSelect,\n}) => {\n const { navigate, basePath } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const handleClick = () => {\n if (onClick) {\n onClick();\n } else if (navigate) {\n navigate(`${basePath}/apps/${app.id}`);\n }\n };\n\n return (\n <GlassCard\n treatment=\"insight\"\n role=\"button\"\n tabIndex={0}\n className={cn(\n \"group cursor-pointer border-border-seam p-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n selected && \"border-primary bg-action-primary-bg/5\",\n className,\n )}\n onClick={handleClick}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Header */}\n <div className=\"flex items-start gap-3\">\n {onToggleSelect && (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selected}\n onChange={onToggleSelect}\n ariaLabel={`Select application ${app.name}`}\n className=\"mt-1\"\n />\n </div>\n )}\n <StoreImage\n src={app.iconUrl}\n alt={app.name}\n className=\"h-12 w-12 rounded-lg object-cover\"\n fallback={\n <div className=\"flex h-12 w-12 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <AppWindow size={24} />\n </div>\n }\n />\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold truncate\">{app.name}</h3>\n {app.isVerified && <VerifiedBadge verified />}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {APP_TYPE_LABELS[app.appType] || app.appType}\n </p>\n </div>\n </div>\n\n {/* Description */}\n {app.shortDescription && (\n <p className=\"mt-3 text-sm text-text-secondary line-clamp-2\">\n {app.shortDescription}\n </p>\n )}\n\n {/* Badges */}\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <StatusBadge status={app.status} />\n {app.isFeatured && <FeaturedBadge />}\n </div>\n\n {/* Stats */}\n <div className=\"mt-4 flex items-center gap-4 text-sm text-text-secondary\">\n {app.averageRating !== undefined && app.averageRating > 0 && (\n <span className=\"flex items-center gap-1\">\n <Star\n size={14}\n className=\"text-status-warning-text fill-status-warning-text\"\n />\n {app.averageRating.toFixed(1)}\n </span>\n )}\n <span className=\"flex items-center gap-1\">\n <Download size={14} />\n {app.installCount.toLocaleString()}{\" \"}\n {tr(\"devportal.installs\", \"installs\")}\n </span>\n </div>\n\n {/* Actions */}\n {showActions && (\n <div className=\"mt-4 flex items-center justify-between opacity-0 group-hover:opacity-100 transition-opacity\">\n <button\n className=\"text-sm text-primary hover:underline flex items-center gap-1\"\n onClick={(e) => {\n e.stopPropagation();\n handleClick();\n }}\n >\n {tr(\"devportal.viewDetails\", \"View Details\")}\n <ExternalLink size={14} />\n </button>\n </div>\n )}\n </GlassCard>\n );\n};\n\n/**\n * Grid of app cards\n */\nexport const AppCardGrid: FC<{\n apps: Application[];\n onAppClick?: (app: Application) => void;\n className?: string;\n}> = ({ apps, onAppClick, className }) => (\n <div\n className={cn(\n \"grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\",\n className,\n )}\n >\n {apps.map((app) => (\n <AppCard\n key={app.id}\n app={app}\n onClick={onAppClick ? () => onAppClick(app) : undefined}\n />\n ))}\n </div>\n);\n\n/**\n * Compact app list item\n */\nexport const AppListItem: FC<{\n app: Application;\n onClick?: () => void;\n className?: string;\n selected?: boolean;\n onToggleSelect?: (checked: boolean) => void;\n}> = ({ app, onClick, className, selected = false, onToggleSelect }) => {\n const { navigate, basePath } = useDevportal();\n\n const handleClick = () => {\n if (onClick) {\n onClick();\n } else if (navigate) {\n navigate(`${basePath}/apps/${app.id}`);\n }\n };\n\n return (\n <div\n role=\"button\"\n tabIndex={0}\n className={cn(\n \"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3 cursor-pointer transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n selected && \"border-primary bg-action-primary-bg/5\",\n className,\n )}\n onClick={handleClick}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n handleClick();\n }\n }}\n >\n {onToggleSelect && (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selected}\n onChange={onToggleSelect}\n ariaLabel={`Select application ${app.name}`}\n />\n </div>\n )}\n <StoreImage\n src={app.iconUrl}\n alt={app.name}\n className=\"h-10 w-10 rounded-lg object-cover\"\n fallback={\n <div className=\"flex h-10 w-10 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <AppWindow size={20} />\n </div>\n }\n />\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium truncate\">{app.name}</p>\n <p className=\"text-sm text-text-secondary truncate\">\n {app.shortDescription || APP_TYPE_LABELS[app.appType]}\n </p>\n </div>\n <StatusBadge status={app.status} />\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,56 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { StoreImage as e, StoreImagePlaceholder as t } from "./StoreImage.js";
|
|
2
|
+
import { cn as n } from "../utils/cn.js";
|
|
3
|
+
import { useCallback as r, useRef as i, useState as a } from "react";
|
|
4
|
+
import { Loader2 as o, Upload as s, X as c } from "lucide-react";
|
|
5
|
+
import { Fragment as l, jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
5
6
|
// src/devportal/components/ImageUploadField.tsx
|
|
6
|
-
var
|
|
7
|
-
let v =
|
|
8
|
-
x.current !==
|
|
9
|
-
let S =
|
|
7
|
+
var f = ({ label: e, value: t, onChange: c, hint: l, accept: f = "image/png,image/jpeg,image/webp,image/gif,image/svg+xml", aspectClass: p = "aspect-square", upload: m, uploading: h, progress: g, error: _ }) => {
|
|
8
|
+
let v = i(null), [y, b] = a(!1), x = i(t);
|
|
9
|
+
x.current !== t && (x.current = t, y && b(!1));
|
|
10
|
+
let S = r(async (e) => {
|
|
10
11
|
try {
|
|
11
|
-
|
|
12
|
+
c((await m(e)).downloadUrl);
|
|
12
13
|
} catch {}
|
|
13
|
-
}, [m,
|
|
14
|
-
return /* @__PURE__ */
|
|
14
|
+
}, [m, c]);
|
|
15
|
+
return /* @__PURE__ */ d("div", {
|
|
15
16
|
className: "space-y-1.5",
|
|
16
17
|
children: [
|
|
17
|
-
/* @__PURE__ */
|
|
18
|
+
/* @__PURE__ */ u("label", {
|
|
18
19
|
className: "text-sm font-medium",
|
|
19
|
-
children:
|
|
20
|
+
children: e
|
|
20
21
|
}),
|
|
21
|
-
/* @__PURE__ */
|
|
22
|
+
/* @__PURE__ */ d("div", {
|
|
22
23
|
className: "flex gap-3 items-start",
|
|
23
|
-
children: [/* @__PURE__ */
|
|
24
|
-
className:
|
|
25
|
-
children: [
|
|
26
|
-
src:
|
|
27
|
-
alt:
|
|
24
|
+
children: [/* @__PURE__ */ d("div", {
|
|
25
|
+
className: n("relative shrink-0 w-20 rounded-lg border border-border-subtle bg-bg-sunken overflow-hidden flex items-center justify-center", p),
|
|
26
|
+
children: [t && !y ? /* @__PURE__ */ u("img", {
|
|
27
|
+
src: t,
|
|
28
|
+
alt: e,
|
|
28
29
|
className: "w-full h-full object-cover",
|
|
29
30
|
onError: () => b(!0)
|
|
30
|
-
}) : /* @__PURE__ */
|
|
31
|
+
}) : /* @__PURE__ */ u(s, {
|
|
31
32
|
size: 18,
|
|
32
33
|
className: "text-text-secondary opacity-40"
|
|
33
|
-
}), h && /* @__PURE__ */
|
|
34
|
+
}), h && /* @__PURE__ */ d("div", {
|
|
34
35
|
className: "absolute inset-0 bg-bg-overlay flex flex-col items-center justify-center gap-1",
|
|
35
|
-
children: [/* @__PURE__ */
|
|
36
|
+
children: [/* @__PURE__ */ u(o, {
|
|
36
37
|
size: 14,
|
|
37
38
|
className: "animate-spin text-white"
|
|
38
|
-
}), /* @__PURE__ */
|
|
39
|
+
}), /* @__PURE__ */ d("span", {
|
|
39
40
|
className: "text-white text-xs",
|
|
40
41
|
children: [g, "%"]
|
|
41
42
|
})]
|
|
42
43
|
})]
|
|
43
|
-
}), /* @__PURE__ */
|
|
44
|
+
}), /* @__PURE__ */ d("div", {
|
|
44
45
|
className: "flex-1 space-y-1.5",
|
|
45
46
|
children: [
|
|
46
|
-
/* @__PURE__ */
|
|
47
|
+
/* @__PURE__ */ u("input", {
|
|
47
48
|
type: "url",
|
|
48
|
-
value:
|
|
49
|
-
onChange: (e) =>
|
|
49
|
+
value: t,
|
|
50
|
+
onChange: (e) => c(e.target.value),
|
|
50
51
|
placeholder: "https://...",
|
|
51
52
|
className: "w-full rounded-md border border-border-subtle bg-bg-input px-3 py-1.5 text-sm placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
52
53
|
}),
|
|
53
|
-
/* @__PURE__ */
|
|
54
|
+
/* @__PURE__ */ u("input", {
|
|
54
55
|
ref: v,
|
|
55
56
|
type: "file",
|
|
56
57
|
accept: f,
|
|
@@ -60,122 +61,123 @@ var u = ({ label: o, value: s, onChange: u, hint: d, accept: f = "image/png,imag
|
|
|
60
61
|
t && S(t), e.target.value = "";
|
|
61
62
|
}
|
|
62
63
|
}),
|
|
63
|
-
/* @__PURE__ */
|
|
64
|
+
/* @__PURE__ */ d("button", {
|
|
64
65
|
type: "button",
|
|
65
66
|
disabled: h,
|
|
66
67
|
onClick: () => v.current?.click(),
|
|
67
68
|
className: "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium hover:bg-bg-sunken transition-colors disabled:opacity-50",
|
|
68
|
-
children: [/* @__PURE__ */
|
|
69
|
+
children: [/* @__PURE__ */ u(s, { size: 11 }), h ? `Uploading… ${g}%` : "Upload image"]
|
|
69
70
|
}),
|
|
70
|
-
_ && /* @__PURE__ */
|
|
71
|
+
_ && /* @__PURE__ */ u("p", {
|
|
71
72
|
className: "text-xs text-status-error-text",
|
|
72
73
|
children: _
|
|
73
74
|
})
|
|
74
75
|
]
|
|
75
76
|
})]
|
|
76
77
|
}),
|
|
77
|
-
|
|
78
|
+
l && /* @__PURE__ */ u("p", {
|
|
78
79
|
className: "text-xs text-text-secondary",
|
|
79
|
-
children:
|
|
80
|
+
children: l
|
|
80
81
|
})
|
|
81
82
|
]
|
|
82
83
|
});
|
|
83
|
-
},
|
|
84
|
-
let
|
|
84
|
+
}, p = ({ urls: f, onChange: p, upload: m, uploading: h, progress: g, error: _ }) => {
|
|
85
|
+
let v = i(null), [y, b] = a(""), x = r(async (e) => {
|
|
85
86
|
try {
|
|
86
|
-
let t = await
|
|
87
|
-
|
|
87
|
+
let t = await m(e);
|
|
88
|
+
p([...f, t.downloadUrl]);
|
|
88
89
|
} catch {}
|
|
89
90
|
}, [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
]),
|
|
94
|
-
let e =
|
|
95
|
-
e && !
|
|
96
|
-
},
|
|
97
|
-
|
|
91
|
+
m,
|
|
92
|
+
p,
|
|
93
|
+
f
|
|
94
|
+
]), S = () => {
|
|
95
|
+
let e = y.trim();
|
|
96
|
+
e && !f.includes(e) && (p([...f, e]), b(""));
|
|
97
|
+
}, C = (e) => {
|
|
98
|
+
p(f.filter((t, n) => n !== e));
|
|
98
99
|
};
|
|
99
|
-
return /* @__PURE__ */
|
|
100
|
+
return /* @__PURE__ */ d("div", {
|
|
100
101
|
className: "space-y-2",
|
|
101
102
|
children: [
|
|
102
|
-
/* @__PURE__ */
|
|
103
|
+
/* @__PURE__ */ d("div", {
|
|
103
104
|
className: "flex flex-col gap-0.5",
|
|
104
|
-
children: [/* @__PURE__ */
|
|
105
|
+
children: [/* @__PURE__ */ u("label", {
|
|
105
106
|
className: "text-sm font-medium",
|
|
106
107
|
children: "Screenshots"
|
|
107
|
-
}), /* @__PURE__ */
|
|
108
|
+
}), /* @__PURE__ */ u("p", {
|
|
108
109
|
className: "text-xs text-text-muted",
|
|
109
110
|
children: "Shown in the horizontal preview strip below the hero — wide landscape proportions work best (16:9)"
|
|
110
111
|
})]
|
|
111
112
|
}),
|
|
112
|
-
/* @__PURE__ */
|
|
113
|
+
/* @__PURE__ */ d("div", {
|
|
113
114
|
className: "flex flex-wrap gap-3",
|
|
114
|
-
children: [
|
|
115
|
+
children: [f.map((n, r) => /* @__PURE__ */ d("div", {
|
|
115
116
|
className: "relative group w-36 aspect-video rounded-lg border border-border-subtle overflow-hidden bg-bg-sunken",
|
|
116
|
-
children: [/* @__PURE__ */
|
|
117
|
-
src:
|
|
118
|
-
alt: `Screenshot ${
|
|
119
|
-
className: "w-full h-full object-cover"
|
|
120
|
-
|
|
117
|
+
children: [/* @__PURE__ */ u(e, {
|
|
118
|
+
src: n,
|
|
119
|
+
alt: `Screenshot ${r + 1}`,
|
|
120
|
+
className: "w-full h-full object-cover",
|
|
121
|
+
fallback: /* @__PURE__ */ u(t, { className: "h-full w-full rounded-none border-0" })
|
|
122
|
+
}), /* @__PURE__ */ u("button", {
|
|
121
123
|
type: "button",
|
|
122
|
-
onClick: () =>
|
|
124
|
+
onClick: () => C(r),
|
|
123
125
|
className: "absolute top-1 right-1 rounded-full bg-bg-overlay text-white p-0.5 opacity-0 group-hover:opacity-100 transition-opacity",
|
|
124
|
-
children: /* @__PURE__ */ c
|
|
126
|
+
children: /* @__PURE__ */ u(c, { size: 12 })
|
|
125
127
|
})]
|
|
126
|
-
},
|
|
128
|
+
}, r)), /* @__PURE__ */ u("button", {
|
|
127
129
|
type: "button",
|
|
128
|
-
disabled:
|
|
129
|
-
onClick: () =>
|
|
130
|
-
className:
|
|
131
|
-
children:
|
|
130
|
+
disabled: h,
|
|
131
|
+
onClick: () => v.current?.click(),
|
|
132
|
+
className: n("w-36 aspect-video rounded-lg border-2 border-dashed border-border-subtle flex flex-col items-center justify-center gap-1.5 text-text-secondary hover:border-border-strong hover:bg-bg-sunken transition-colors disabled:opacity-50"),
|
|
133
|
+
children: h ? /* @__PURE__ */ d(l, { children: [/* @__PURE__ */ u(o, {
|
|
132
134
|
size: 18,
|
|
133
135
|
className: "animate-spin text-primary"
|
|
134
|
-
}), /* @__PURE__ */
|
|
136
|
+
}), /* @__PURE__ */ d("span", {
|
|
135
137
|
className: "text-xs",
|
|
136
|
-
children: [
|
|
137
|
-
})] }) : /* @__PURE__ */ l
|
|
138
|
+
children: [g, "%"]
|
|
139
|
+
})] }) : /* @__PURE__ */ d(l, { children: [/* @__PURE__ */ u(s, { size: 18 }), /* @__PURE__ */ u("span", {
|
|
138
140
|
className: "text-xs",
|
|
139
141
|
children: "Add screenshot"
|
|
140
142
|
})] })
|
|
141
143
|
})]
|
|
142
144
|
}),
|
|
143
|
-
/* @__PURE__ */
|
|
145
|
+
/* @__PURE__ */ d("div", {
|
|
144
146
|
className: "flex gap-2",
|
|
145
|
-
children: [/* @__PURE__ */
|
|
147
|
+
children: [/* @__PURE__ */ u("input", {
|
|
146
148
|
type: "url",
|
|
147
|
-
value:
|
|
148
|
-
onChange: (e) =>
|
|
149
|
+
value: y,
|
|
150
|
+
onChange: (e) => b(e.target.value),
|
|
149
151
|
onKeyDown: (e) => {
|
|
150
|
-
e.key === "Enter" && (e.preventDefault(),
|
|
152
|
+
e.key === "Enter" && (e.preventDefault(), S());
|
|
151
153
|
},
|
|
152
154
|
placeholder: "https://... paste a screenshot URL",
|
|
153
155
|
className: "flex-1 rounded-md border border-border-subtle bg-bg-input px-3 py-1.5 text-sm placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
154
|
-
}), /* @__PURE__ */
|
|
156
|
+
}), /* @__PURE__ */ u("button", {
|
|
155
157
|
type: "button",
|
|
156
|
-
onClick:
|
|
157
|
-
disabled: !
|
|
158
|
+
onClick: S,
|
|
159
|
+
disabled: !y.trim(),
|
|
158
160
|
className: "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium hover:bg-bg-sunken transition-colors disabled:opacity-50",
|
|
159
161
|
children: "Add"
|
|
160
162
|
})]
|
|
161
163
|
}),
|
|
162
|
-
/* @__PURE__ */
|
|
163
|
-
ref:
|
|
164
|
+
/* @__PURE__ */ u("input", {
|
|
165
|
+
ref: v,
|
|
164
166
|
type: "file",
|
|
165
167
|
accept: "image/png,image/jpeg,image/webp,image/gif",
|
|
166
168
|
className: "hidden",
|
|
167
169
|
onChange: (e) => {
|
|
168
170
|
let t = e.target.files?.[0];
|
|
169
|
-
t &&
|
|
171
|
+
t && x(t), e.target.value = "";
|
|
170
172
|
}
|
|
171
173
|
}),
|
|
172
|
-
|
|
174
|
+
_ && /* @__PURE__ */ u("p", {
|
|
173
175
|
className: "text-xs text-status-error-text",
|
|
174
|
-
children:
|
|
176
|
+
children: _
|
|
175
177
|
})
|
|
176
178
|
]
|
|
177
179
|
});
|
|
178
180
|
};
|
|
179
|
-
export {
|
|
181
|
+
export { f as ImageUploadField, p as ScreenshotGallery };
|
|
180
182
|
|
|
181
183
|
//# sourceMappingURL=ImageUploadField.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageUploadField.js","names":["_Fragment","ImageUploadField","inputRef","useRef","imgBroken","setImgBroken","useState","prevValue","value","handleFile","useCallback","upload","file","onChange","label","cn","aspectClass","Upload","uploading","Loader2","progress","f","error","hint","ScreenshotGallery","urlInput","setUrlInput","result","urls","addUrl","trimmed","remove","i","idx","url","X","Fragment"],"sources":["../../../src/devportal/components/ImageUploadField.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { Loader2, Upload, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\n\n// =============================================================================\n// IMAGE UPLOAD COMPONENTS\n//\n// Shared by the devportal App pages (Store Listing tab) and the physical\n// item create/edit pages — a single icon/banner upload field plus a\n// screenshot gallery, both backed by usePackageUpload's\n// upload(file) => Promise<{ downloadUrl }> shape.\n// =============================================================================\n\ninterface ImageUploadFieldProps {\n label: string;\n value: string;\n onChange: (url: string) => void;\n hint?: string;\n accept?: string;\n aspectClass?: string; // tailwind class for the preview container ratio\n upload: (file: File) => Promise<{ downloadUrl: string }>;\n uploading: boolean;\n progress: number;\n error: string | null;\n}\n\nexport const ImageUploadField: FC<ImageUploadFieldProps> = ({\n label,\n value,\n onChange,\n hint,\n accept = \"image/png,image/jpeg,image/webp,image/gif,image/svg+xml\",\n aspectClass = \"aspect-square\",\n upload,\n uploading,\n progress,\n error,\n}) => {\n const inputRef = useRef<HTMLInputElement>(null);\n const [imgBroken, setImgBroken] = useState(false);\n const prevValue = useRef(value);\n if (prevValue.current !== value) {\n prevValue.current = value;\n if (imgBroken) setImgBroken(false);\n }\n\n const handleFile = useCallback(\n async (file: File) => {\n try {\n const result = await upload(file);\n onChange(result.downloadUrl);\n } catch {\n // error shown via error prop\n }\n },\n [upload, onChange],\n );\n\n return (\n <div className=\"space-y-1.5\">\n <label className=\"text-sm font-medium\">{label}</label>\n <div className=\"flex gap-3 items-start\">\n {/* Preview */}\n <div\n className={cn(\n \"relative shrink-0 w-20 rounded-lg border border-border-subtle bg-bg-sunken overflow-hidden flex items-center justify-center\",\n aspectClass,\n )}\n >\n {value && !imgBroken ? (\n <img\n src={value}\n alt={label}\n className=\"w-full h-full object-cover\"\n onError={() => setImgBroken(true)}\n />\n ) : (\n <Upload size={18} className=\"text-text-secondary opacity-40\" />\n )}\n {uploading && (\n <div className=\"absolute inset-0 bg-bg-overlay flex flex-col items-center justify-center gap-1\">\n <Loader2 size={14} className=\"animate-spin text-white\" />\n <span className=\"text-white text-xs\">{progress}%</span>\n </div>\n )}\n </div>\n {/* URL input + upload button */}\n <div className=\"flex-1 space-y-1.5\">\n <input\n type=\"url\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder=\"https://...\"\n className=\"w-full rounded-md border border-border-subtle bg-bg-input px-3 py-1.5 text-sm placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary/30\"\n />\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n className=\"hidden\"\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void handleFile(f);\n e.target.value = \"\";\n }}\n />\n <button\n type=\"button\"\n disabled={uploading}\n onClick={() => inputRef.current?.click()}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n <Upload size={11} />\n {uploading ? `Uploading… ${progress}%` : \"Upload image\"}\n </button>\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n </div>\n {hint && <p className=\"text-xs text-text-secondary\">{hint}</p>}\n </div>\n );\n};\n\ninterface ScreenshotGalleryProps {\n urls: string[];\n onChange: (urls: string[]) => void;\n upload: (file: File) => Promise<{ downloadUrl: string }>;\n uploading: boolean;\n progress: number;\n error: string | null;\n}\n\nexport const ScreenshotGallery: FC<ScreenshotGalleryProps> = ({\n urls,\n onChange,\n upload,\n uploading,\n progress,\n error,\n}) => {\n const inputRef = useRef<HTMLInputElement>(null);\n const [urlInput, setUrlInput] = useState(\"\");\n\n const handleFile = useCallback(\n async (file: File) => {\n try {\n const result = await upload(file);\n onChange([...urls, result.downloadUrl]);\n } catch {\n // error shown via error prop\n }\n },\n [upload, onChange, urls],\n );\n\n const addUrl = () => {\n const trimmed = urlInput.trim();\n if (trimmed && !urls.includes(trimmed)) {\n onChange([...urls, trimmed]);\n setUrlInput(\"\");\n }\n };\n\n const remove = (idx: number) => {\n onChange(urls.filter((_, i) => i !== idx));\n };\n\n return (\n <div className=\"space-y-2\">\n <div className=\"flex flex-col gap-0.5\">\n <label className=\"text-sm font-medium\">Screenshots</label>\n <p className=\"text-xs text-text-muted\">\n Shown in the horizontal preview strip below the hero — wide landscape\n proportions work best (16:9)\n </p>\n </div>\n <div className=\"flex flex-wrap gap-3\">\n {urls.map((url, idx) => (\n <div\n key={idx}\n className=\"relative group w-36 aspect-video rounded-lg border border-border-subtle overflow-hidden bg-bg-sunken\"\n >\n <img\n src={url}\n alt={`Screenshot ${idx + 1}`}\n className=\"w-full h-full object-cover\"\n />\n <button\n type=\"button\"\n onClick={() => remove(idx)}\n className=\"absolute top-1 right-1 rounded-full bg-bg-overlay text-white p-0.5 opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n <X size={12} />\n </button>\n </div>\n ))}\n {/* Upload slot */}\n <button\n type=\"button\"\n disabled={uploading}\n onClick={() => inputRef.current?.click()}\n className={cn(\n \"w-36 aspect-video rounded-lg border-2 border-dashed border-border-subtle flex flex-col items-center justify-center gap-1.5 text-text-secondary hover:border-border-strong hover:bg-bg-sunken transition-colors disabled:opacity-50\",\n )}\n >\n {uploading ? (\n <>\n <Loader2 size={18} className=\"animate-spin text-primary\" />\n <span className=\"text-xs\">{progress}%</span>\n </>\n ) : (\n <>\n <Upload size={18} />\n <span className=\"text-xs\">Add screenshot</span>\n </>\n )}\n </button>\n </div>\n {/* URL paste row */}\n <div className=\"flex gap-2\">\n <input\n type=\"url\"\n value={urlInput}\n onChange={(e) => setUrlInput(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n addUrl();\n }\n }}\n placeholder=\"https://... paste a screenshot URL\"\n className=\"flex-1 rounded-md border border-border-subtle bg-bg-input px-3 py-1.5 text-sm placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary/30\"\n />\n <button\n type=\"button\"\n onClick={addUrl}\n disabled={!urlInput.trim()}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n Add\n </button>\n </div>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/png,image/jpeg,image/webp,image/gif\"\n className=\"hidden\"\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void handleFile(f);\n e.target.value = \"\";\n }}\n />\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ImageUploadField.js","names":["_Fragment","ImageUploadField","inputRef","useRef","imgBroken","setImgBroken","useState","prevValue","value","handleFile","useCallback","upload","file","onChange","label","cn","aspectClass","Upload","uploading","Loader2","progress","f","error","hint","ScreenshotGallery","urlInput","setUrlInput","result","urls","addUrl","trimmed","remove","i","idx","StoreImage","url","StoreImagePlaceholder","X","Fragment"],"sources":["../../../src/devportal/components/ImageUploadField.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { Loader2, Upload, X } from \"lucide-react\";\nimport { StoreImage, StoreImagePlaceholder } from \"./StoreImage\";\nimport { cn } from \"../utils/cn\";\n\n// =============================================================================\n// IMAGE UPLOAD COMPONENTS\n//\n// Shared by the devportal App pages (Store Listing tab) and the physical\n// item create/edit pages — a single icon/banner upload field plus a\n// screenshot gallery, both backed by usePackageUpload's\n// upload(file) => Promise<{ downloadUrl }> shape.\n// =============================================================================\n\ninterface ImageUploadFieldProps {\n label: string;\n value: string;\n onChange: (url: string) => void;\n hint?: string;\n accept?: string;\n aspectClass?: string; // tailwind class for the preview container ratio\n upload: (file: File) => Promise<{ downloadUrl: string }>;\n uploading: boolean;\n progress: number;\n error: string | null;\n}\n\nexport const ImageUploadField: FC<ImageUploadFieldProps> = ({\n label,\n value,\n onChange,\n hint,\n accept = \"image/png,image/jpeg,image/webp,image/gif,image/svg+xml\",\n aspectClass = \"aspect-square\",\n upload,\n uploading,\n progress,\n error,\n}) => {\n const inputRef = useRef<HTMLInputElement>(null);\n const [imgBroken, setImgBroken] = useState(false);\n const prevValue = useRef(value);\n if (prevValue.current !== value) {\n prevValue.current = value;\n if (imgBroken) setImgBroken(false);\n }\n\n const handleFile = useCallback(\n async (file: File) => {\n try {\n const result = await upload(file);\n onChange(result.downloadUrl);\n } catch {\n // error shown via error prop\n }\n },\n [upload, onChange],\n );\n\n return (\n <div className=\"space-y-1.5\">\n <label className=\"text-sm font-medium\">{label}</label>\n <div className=\"flex gap-3 items-start\">\n {/* Preview */}\n <div\n className={cn(\n \"relative shrink-0 w-20 rounded-lg border border-border-subtle bg-bg-sunken overflow-hidden flex items-center justify-center\",\n aspectClass,\n )}\n >\n {value && !imgBroken ? (\n <img\n src={value}\n alt={label}\n className=\"w-full h-full object-cover\"\n onError={() => setImgBroken(true)}\n />\n ) : (\n <Upload size={18} className=\"text-text-secondary opacity-40\" />\n )}\n {uploading && (\n <div className=\"absolute inset-0 bg-bg-overlay flex flex-col items-center justify-center gap-1\">\n <Loader2 size={14} className=\"animate-spin text-white\" />\n <span className=\"text-white text-xs\">{progress}%</span>\n </div>\n )}\n </div>\n {/* URL input + upload button */}\n <div className=\"flex-1 space-y-1.5\">\n <input\n type=\"url\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder=\"https://...\"\n className=\"w-full rounded-md border border-border-subtle bg-bg-input px-3 py-1.5 text-sm placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary/30\"\n />\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n className=\"hidden\"\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void handleFile(f);\n e.target.value = \"\";\n }}\n />\n <button\n type=\"button\"\n disabled={uploading}\n onClick={() => inputRef.current?.click()}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n <Upload size={11} />\n {uploading ? `Uploading… ${progress}%` : \"Upload image\"}\n </button>\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n </div>\n {hint && <p className=\"text-xs text-text-secondary\">{hint}</p>}\n </div>\n );\n};\n\ninterface ScreenshotGalleryProps {\n urls: string[];\n onChange: (urls: string[]) => void;\n upload: (file: File) => Promise<{ downloadUrl: string }>;\n uploading: boolean;\n progress: number;\n error: string | null;\n}\n\nexport const ScreenshotGallery: FC<ScreenshotGalleryProps> = ({\n urls,\n onChange,\n upload,\n uploading,\n progress,\n error,\n}) => {\n const inputRef = useRef<HTMLInputElement>(null);\n const [urlInput, setUrlInput] = useState(\"\");\n\n const handleFile = useCallback(\n async (file: File) => {\n try {\n const result = await upload(file);\n onChange([...urls, result.downloadUrl]);\n } catch {\n // error shown via error prop\n }\n },\n [upload, onChange, urls],\n );\n\n const addUrl = () => {\n const trimmed = urlInput.trim();\n if (trimmed && !urls.includes(trimmed)) {\n onChange([...urls, trimmed]);\n setUrlInput(\"\");\n }\n };\n\n const remove = (idx: number) => {\n onChange(urls.filter((_, i) => i !== idx));\n };\n\n return (\n <div className=\"space-y-2\">\n <div className=\"flex flex-col gap-0.5\">\n <label className=\"text-sm font-medium\">Screenshots</label>\n <p className=\"text-xs text-text-muted\">\n Shown in the horizontal preview strip below the hero — wide landscape\n proportions work best (16:9)\n </p>\n </div>\n <div className=\"flex flex-wrap gap-3\">\n {urls.map((url, idx) => (\n <div\n key={idx}\n className=\"relative group w-36 aspect-video rounded-lg border border-border-subtle overflow-hidden bg-bg-sunken\"\n >\n <StoreImage\n src={url}\n alt={`Screenshot ${idx + 1}`}\n className=\"w-full h-full object-cover\"\n fallback={\n <StoreImagePlaceholder className=\"h-full w-full rounded-none border-0\" />\n }\n />\n <button\n type=\"button\"\n onClick={() => remove(idx)}\n className=\"absolute top-1 right-1 rounded-full bg-bg-overlay text-white p-0.5 opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n <X size={12} />\n </button>\n </div>\n ))}\n {/* Upload slot */}\n <button\n type=\"button\"\n disabled={uploading}\n onClick={() => inputRef.current?.click()}\n className={cn(\n \"w-36 aspect-video rounded-lg border-2 border-dashed border-border-subtle flex flex-col items-center justify-center gap-1.5 text-text-secondary hover:border-border-strong hover:bg-bg-sunken transition-colors disabled:opacity-50\",\n )}\n >\n {uploading ? (\n <>\n <Loader2 size={18} className=\"animate-spin text-primary\" />\n <span className=\"text-xs\">{progress}%</span>\n </>\n ) : (\n <>\n <Upload size={18} />\n <span className=\"text-xs\">Add screenshot</span>\n </>\n )}\n </button>\n </div>\n {/* URL paste row */}\n <div className=\"flex gap-2\">\n <input\n type=\"url\"\n value={urlInput}\n onChange={(e) => setUrlInput(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n addUrl();\n }\n }}\n placeholder=\"https://... paste a screenshot URL\"\n className=\"flex-1 rounded-md border border-border-subtle bg-bg-input px-3 py-1.5 text-sm placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary/30\"\n />\n <button\n type=\"button\"\n onClick={addUrl}\n disabled={!urlInput.trim()}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n Add\n </button>\n </div>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/png,image/jpeg,image/webp,image/gif\"\n className=\"hidden\"\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void handleFile(f);\n e.target.value = \"\";\n }}\n />\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FC, ImgHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
/** True when `src` is a value the browser can load as an image. */
|
|
3
|
+
export declare function isRenderableImageSrc(src: string | null | undefined): src is string;
|
|
4
|
+
export interface StoreImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "onError"> {
|
|
5
|
+
src: string | null | undefined;
|
|
6
|
+
alt: string;
|
|
7
|
+
/** Shown instead of the image when it is missing or fails to load. */
|
|
8
|
+
fallback?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export declare const StoreImage: FC<StoreImageProps>;
|
|
11
|
+
/** Neutral tile for a store image that can't be shown. Size it via className. */
|
|
12
|
+
export declare const StoreImagePlaceholder: FC<{
|
|
13
|
+
className?: string;
|
|
14
|
+
iconSize?: number;
|
|
15
|
+
}>;
|