@burdenoff/microfe-devportal 2026.1112.1 → 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.
Files changed (42) hide show
  1. package/dist/devportal/components/AppCard.js +73 -70
  2. package/dist/devportal/components/AppCard.js.map +1 -1
  3. package/dist/devportal/components/ImageUploadField.js +82 -80
  4. package/dist/devportal/components/ImageUploadField.js.map +1 -1
  5. package/dist/devportal/components/StoreImage.d.ts +15 -0
  6. package/dist/devportal/components/StoreImage.js +30 -0
  7. package/dist/devportal/components/StoreImage.js.map +1 -0
  8. package/dist/devportal/components/index.d.ts +1 -0
  9. package/dist/devportal/components/index.js +17 -16
  10. package/dist/devportal/hooks/usePackageUpload.d.ts +46 -4
  11. package/dist/devportal/hooks/usePackageUpload.js +90 -36
  12. package/dist/devportal/hooks/usePackageUpload.js.map +1 -1
  13. package/dist/devportal/pages/AdminReviewPage.js +205 -202
  14. package/dist/devportal/pages/AdminReviewPage.js.map +1 -1
  15. package/dist/devportal/pages/ApplicationDetailPage.js +2 -2
  16. package/dist/devportal/pages/ApplicationDetailPage.js.map +1 -1
  17. package/dist/devportal/pages/ApplicationsPage.js +201 -199
  18. package/dist/devportal/pages/ApplicationsPage.js.map +1 -1
  19. package/dist/devportal/pages/CreateOrganizationPage.js +120 -114
  20. package/dist/devportal/pages/CreateOrganizationPage.js.map +1 -1
  21. package/dist/devportal/pages/CreatePhysicalItemPage.js +2 -2
  22. package/dist/devportal/pages/CreatePhysicalItemPage.js.map +1 -1
  23. package/dist/devportal/pages/EditApplicationPage.js +84 -73
  24. package/dist/devportal/pages/EditApplicationPage.js.map +1 -1
  25. package/dist/devportal/pages/EditPhysicalItemPage.js +312 -307
  26. package/dist/devportal/pages/EditPhysicalItemPage.js.map +1 -1
  27. package/dist/devportal/pages/ListingPreviewPage.js +175 -171
  28. package/dist/devportal/pages/ListingPreviewPage.js.map +1 -1
  29. package/dist/devportal/pages/OAuthClientDetailPage.js +134 -115
  30. package/dist/devportal/pages/OAuthClientDetailPage.js.map +1 -1
  31. package/dist/devportal/pages/SellerDashboardPage.js +118 -116
  32. package/dist/devportal/pages/SellerDashboardPage.js.map +1 -1
  33. package/dist/devportal/pages/SellerItemsPage.js +202 -199
  34. package/dist/devportal/pages/SellerItemsPage.js.map +1 -1
  35. package/dist/devportal/pages/SetupPage.js +141 -143
  36. package/dist/devportal/pages/SetupPage.js.map +1 -1
  37. package/dist/devportal/pages/StoreSubmissionPage.js +133 -131
  38. package/dist/devportal/pages/StoreSubmissionPage.js.map +1 -1
  39. package/dist/devportal/utils/errorMessage.d.ts +2 -0
  40. package/dist/devportal/utils/errorMessage.js +7 -0
  41. package/dist/devportal/utils/errorMessage.js.map +1 -0
  42. 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 { AppWindow as s, Download as c, ExternalLink as l, Star as u } from "lucide-react";
7
- import { jsx as d, jsxs as f } from "react/jsx-runtime";
8
- import { useI18n as p } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
9
- import { GlassCard as m } from "@burdenoff/fe-libs/ui";
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 h = ({ app: h, onClick: g, className: _, showActions: v = !0, selected: y = !1, onToggleSelect: b }) => {
12
- let { navigate: x, basePath: S } = a(), { t: C } = p(), w = (e, t) => {
13
- let n = C(e);
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
- }, T = () => {
16
- g ? g() : x && x(`${S}/apps/${h.id}`);
16
+ }, E = () => {
17
+ _ ? _() : S && S(`${C}/apps/${g.id}`);
17
18
  };
18
- return /* @__PURE__ */ f(m, {
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", y && "border-primary bg-action-primary-bg/5", _),
23
- onClick: T,
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(), T());
26
+ (e.key === "Enter" || e.key === " ") && (e.preventDefault(), E());
26
27
  },
27
28
  children: [
28
- /* @__PURE__ */ f("div", {
29
+ /* @__PURE__ */ p("div", {
29
30
  className: "flex items-start gap-3",
30
31
  children: [
31
- b && /* @__PURE__ */ d("div", {
32
+ x && /* @__PURE__ */ f("div", {
32
33
  onClick: (e) => e.stopPropagation(),
33
- children: /* @__PURE__ */ d(o, {
34
- checked: y,
35
- onChange: b,
36
- ariaLabel: `Select application ${h.name}`,
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
- h.iconUrl ? /* @__PURE__ */ d("img", {
41
- src: h.iconUrl,
42
- alt: h.name,
43
- className: "h-12 w-12 rounded-lg object-cover"
44
- }) : /* @__PURE__ */ d("div", {
45
- className: "flex h-12 w-12 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary",
46
- children: /* @__PURE__ */ d(s, { size: 24 })
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__ */ f("div", {
50
+ /* @__PURE__ */ p("div", {
49
51
  className: "flex-1 min-w-0",
50
- children: [/* @__PURE__ */ f("div", {
52
+ children: [/* @__PURE__ */ p("div", {
51
53
  className: "flex items-center gap-2",
52
- children: [/* @__PURE__ */ d("h3", {
54
+ children: [/* @__PURE__ */ f("h3", {
53
55
  className: "font-semibold truncate",
54
- children: h.name
55
- }), h.isVerified && /* @__PURE__ */ d(r, { verified: !0 })]
56
- }), /* @__PURE__ */ d("p", {
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[h.appType] || h.appType
60
+ children: i[g.appType] || g.appType
59
61
  })]
60
62
  })
61
63
  ]
62
64
  }),
63
- h.shortDescription && /* @__PURE__ */ d("p", {
65
+ g.shortDescription && /* @__PURE__ */ f("p", {
64
66
  className: "mt-3 text-sm text-text-secondary line-clamp-2",
65
- children: h.shortDescription
67
+ children: g.shortDescription
66
68
  }),
67
- /* @__PURE__ */ f("div", {
69
+ /* @__PURE__ */ p("div", {
68
70
  className: "mt-3 flex flex-wrap items-center gap-2",
69
- children: [/* @__PURE__ */ d(n, { status: h.status }), h.isFeatured && /* @__PURE__ */ d(t, {})]
71
+ children: [/* @__PURE__ */ f(n, { status: g.status }), g.isFeatured && /* @__PURE__ */ f(t, {})]
70
72
  }),
71
- /* @__PURE__ */ f("div", {
73
+ /* @__PURE__ */ p("div", {
72
74
  className: "mt-4 flex items-center gap-4 text-sm text-text-secondary",
73
- children: [h.averageRating !== void 0 && h.averageRating > 0 && /* @__PURE__ */ f("span", {
75
+ children: [g.averageRating !== void 0 && g.averageRating > 0 && /* @__PURE__ */ p("span", {
74
76
  className: "flex items-center gap-1",
75
- children: [/* @__PURE__ */ d(u, {
77
+ children: [/* @__PURE__ */ f(d, {
76
78
  size: 14,
77
79
  className: "text-status-warning-text fill-status-warning-text"
78
- }), h.averageRating.toFixed(1)]
79
- }), /* @__PURE__ */ f("span", {
80
+ }), g.averageRating.toFixed(1)]
81
+ }), /* @__PURE__ */ p("span", {
80
82
  className: "flex items-center gap-1",
81
83
  children: [
82
- /* @__PURE__ */ d(c, { size: 14 }),
83
- h.installCount.toLocaleString(),
84
+ /* @__PURE__ */ f(l, { size: 14 }),
85
+ g.installCount.toLocaleString(),
84
86
  " ",
85
- w("devportal.installs", "installs")
87
+ T("devportal.installs", "installs")
86
88
  ]
87
89
  })]
88
90
  }),
89
- v && /* @__PURE__ */ d("div", {
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__ */ f("button", {
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(), T();
96
+ e.stopPropagation(), E();
95
97
  },
96
- children: [w("devportal.viewDetails", "View Details"), /* @__PURE__ */ d(l, { size: 14 })]
98
+ children: [T("devportal.viewDetails", "View Details"), /* @__PURE__ */ f(u, { size: 14 })]
97
99
  })
98
100
  })
99
101
  ]
100
102
  });
101
- }, g = ({ app: t, onClick: r, className: c, selected: l = !1, onToggleSelect: u }) => {
102
- let { navigate: p, basePath: m } = a(), h = () => {
103
- r ? r() : p && p(`${m}/apps/${t.id}`);
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__ */ f("div", {
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", l && "border-primary bg-action-primary-bg/5", c),
109
- onClick: h,
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(), h());
113
+ (e.key === "Enter" || e.key === " ") && (e.preventDefault(), g());
112
114
  },
113
115
  children: [
114
- u && /* @__PURE__ */ d("div", {
116
+ d && /* @__PURE__ */ f("div", {
115
117
  onClick: (e) => e.stopPropagation(),
116
- children: /* @__PURE__ */ d(o, {
117
- checked: l,
118
- onChange: u,
118
+ children: /* @__PURE__ */ f(o, {
119
+ checked: u,
120
+ onChange: d,
119
121
  ariaLabel: `Select application ${t.name}`
120
122
  })
121
123
  }),
122
- t.iconUrl ? /* @__PURE__ */ d("img", {
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
- }) : /* @__PURE__ */ d("div", {
127
- className: "flex h-10 w-10 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary",
128
- children: /* @__PURE__ */ d(s, { size: 20 })
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__ */ f("div", {
133
+ /* @__PURE__ */ p("div", {
131
134
  className: "flex-1 min-w-0",
132
- children: [/* @__PURE__ */ d("p", {
135
+ children: [/* @__PURE__ */ f("p", {
133
136
  className: "font-medium truncate",
134
137
  children: t.name
135
- }), /* @__PURE__ */ d("p", {
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__ */ d(n, { status: t.status })
143
+ /* @__PURE__ */ f(n, { status: t.status })
141
144
  ]
142
145
  });
143
146
  };
144
- export { h as AppCard, g as AppListItem };
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 { cn as e } from "../utils/cn.js";
2
- import { useCallback as t, useRef as n, useState as r } from "react";
3
- import { Loader2 as i, Upload as a, X as o } from "lucide-react";
4
- import { Fragment as s, jsx as c, jsxs as l } from "react/jsx-runtime";
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 u = ({ label: o, value: s, onChange: u, hint: d, accept: f = "image/png,image/jpeg,image/webp,image/gif,image/svg+xml", aspectClass: p = "aspect-square", upload: m, uploading: h, progress: g, error: _ }) => {
7
- let v = n(null), [y, b] = r(!1), x = n(s);
8
- x.current !== s && (x.current = s, y && b(!1));
9
- let S = t(async (e) => {
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
- u((await m(e)).downloadUrl);
12
+ c((await m(e)).downloadUrl);
12
13
  } catch {}
13
- }, [m, u]);
14
- return /* @__PURE__ */ l("div", {
14
+ }, [m, c]);
15
+ return /* @__PURE__ */ d("div", {
15
16
  className: "space-y-1.5",
16
17
  children: [
17
- /* @__PURE__ */ c("label", {
18
+ /* @__PURE__ */ u("label", {
18
19
  className: "text-sm font-medium",
19
- children: o
20
+ children: e
20
21
  }),
21
- /* @__PURE__ */ l("div", {
22
+ /* @__PURE__ */ d("div", {
22
23
  className: "flex gap-3 items-start",
23
- children: [/* @__PURE__ */ l("div", {
24
- className: e("relative shrink-0 w-20 rounded-lg border border-border-subtle bg-bg-sunken overflow-hidden flex items-center justify-center", p),
25
- children: [s && !y ? /* @__PURE__ */ c("img", {
26
- src: s,
27
- alt: o,
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__ */ c(a, {
31
+ }) : /* @__PURE__ */ u(s, {
31
32
  size: 18,
32
33
  className: "text-text-secondary opacity-40"
33
- }), h && /* @__PURE__ */ l("div", {
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__ */ c(i, {
36
+ children: [/* @__PURE__ */ u(o, {
36
37
  size: 14,
37
38
  className: "animate-spin text-white"
38
- }), /* @__PURE__ */ l("span", {
39
+ }), /* @__PURE__ */ d("span", {
39
40
  className: "text-white text-xs",
40
41
  children: [g, "%"]
41
42
  })]
42
43
  })]
43
- }), /* @__PURE__ */ l("div", {
44
+ }), /* @__PURE__ */ d("div", {
44
45
  className: "flex-1 space-y-1.5",
45
46
  children: [
46
- /* @__PURE__ */ c("input", {
47
+ /* @__PURE__ */ u("input", {
47
48
  type: "url",
48
- value: s,
49
- onChange: (e) => u(e.target.value),
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__ */ c("input", {
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__ */ l("button", {
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__ */ c(a, { size: 11 }), h ? `Uploading… ${g}%` : "Upload image"]
69
+ children: [/* @__PURE__ */ u(s, { size: 11 }), h ? `Uploading… ${g}%` : "Upload image"]
69
70
  }),
70
- _ && /* @__PURE__ */ c("p", {
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
- d && /* @__PURE__ */ c("p", {
78
+ l && /* @__PURE__ */ u("p", {
78
79
  className: "text-xs text-text-secondary",
79
- children: d
80
+ children: l
80
81
  })
81
82
  ]
82
83
  });
83
- }, d = ({ urls: u, onChange: d, upload: f, uploading: p, progress: m, error: h }) => {
84
- let g = n(null), [_, v] = r(""), y = t(async (e) => {
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 f(e);
87
- d([...u, t.downloadUrl]);
87
+ let t = await m(e);
88
+ p([...f, t.downloadUrl]);
88
89
  } catch {}
89
90
  }, [
90
- f,
91
- d,
92
- u
93
- ]), b = () => {
94
- let e = _.trim();
95
- e && !u.includes(e) && (d([...u, e]), v(""));
96
- }, x = (e) => {
97
- d(u.filter((t, n) => n !== e));
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__ */ l("div", {
100
+ return /* @__PURE__ */ d("div", {
100
101
  className: "space-y-2",
101
102
  children: [
102
- /* @__PURE__ */ l("div", {
103
+ /* @__PURE__ */ d("div", {
103
104
  className: "flex flex-col gap-0.5",
104
- children: [/* @__PURE__ */ c("label", {
105
+ children: [/* @__PURE__ */ u("label", {
105
106
  className: "text-sm font-medium",
106
107
  children: "Screenshots"
107
- }), /* @__PURE__ */ c("p", {
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__ */ l("div", {
113
+ /* @__PURE__ */ d("div", {
113
114
  className: "flex flex-wrap gap-3",
114
- children: [u.map((e, t) => /* @__PURE__ */ l("div", {
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__ */ c("img", {
117
- src: e,
118
- alt: `Screenshot ${t + 1}`,
119
- className: "w-full h-full object-cover"
120
- }), /* @__PURE__ */ c("button", {
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: () => x(t),
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(o, { size: 12 })
126
+ children: /* @__PURE__ */ u(c, { size: 12 })
125
127
  })]
126
- }, t)), /* @__PURE__ */ c("button", {
128
+ }, r)), /* @__PURE__ */ u("button", {
127
129
  type: "button",
128
- disabled: p,
129
- onClick: () => g.current?.click(),
130
- className: e("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"),
131
- children: p ? /* @__PURE__ */ l(s, { children: [/* @__PURE__ */ c(i, {
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__ */ l("span", {
136
+ }), /* @__PURE__ */ d("span", {
135
137
  className: "text-xs",
136
- children: [m, "%"]
137
- })] }) : /* @__PURE__ */ l(s, { children: [/* @__PURE__ */ c(a, { size: 18 }), /* @__PURE__ */ c("span", {
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__ */ l("div", {
145
+ /* @__PURE__ */ d("div", {
144
146
  className: "flex gap-2",
145
- children: [/* @__PURE__ */ c("input", {
147
+ children: [/* @__PURE__ */ u("input", {
146
148
  type: "url",
147
- value: _,
148
- onChange: (e) => v(e.target.value),
149
+ value: y,
150
+ onChange: (e) => b(e.target.value),
149
151
  onKeyDown: (e) => {
150
- e.key === "Enter" && (e.preventDefault(), b());
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__ */ c("button", {
156
+ }), /* @__PURE__ */ u("button", {
155
157
  type: "button",
156
- onClick: b,
157
- disabled: !_.trim(),
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__ */ c("input", {
163
- ref: g,
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 && y(t), e.target.value = "";
171
+ t && x(t), e.target.value = "";
170
172
  }
171
173
  }),
172
- h && /* @__PURE__ */ c("p", {
174
+ _ && /* @__PURE__ */ u("p", {
173
175
  className: "text-xs text-status-error-text",
174
- children: h
176
+ children: _
175
177
  })
176
178
  ]
177
179
  });
178
180
  };
179
- export { u as ImageUploadField, d as ScreenshotGallery };
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
+ }>;