@burdenoff/microfe-devportal 2026.1112.2 → 2026.1113.2

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 (78) hide show
  1. package/dist/devportal/DevportalRoot.js +65 -119
  2. package/dist/devportal/DevportalRoot.js.map +1 -1
  3. package/dist/devportal/components/AppCard.js +72 -69
  4. package/dist/devportal/components/AppCard.js.map +1 -1
  5. package/dist/devportal/components/DeveloperSettingsHub.d.ts +6 -0
  6. package/dist/devportal/components/DeveloperSettingsHub.js +59 -0
  7. package/dist/devportal/components/DeveloperSettingsHub.js.map +1 -0
  8. package/dist/devportal/components/ImageUploadField.js +82 -80
  9. package/dist/devportal/components/ImageUploadField.js.map +1 -1
  10. package/dist/devportal/components/SectionNav.d.ts +1 -1
  11. package/dist/devportal/components/SectionNav.js +60 -36
  12. package/dist/devportal/components/SectionNav.js.map +1 -1
  13. package/dist/devportal/components/StoreImage.d.ts +15 -0
  14. package/dist/devportal/components/StoreImage.js +30 -0
  15. package/dist/devportal/components/StoreImage.js.map +1 -0
  16. package/dist/devportal/components/index.d.ts +1 -0
  17. package/dist/devportal/components/index.js +17 -16
  18. package/dist/devportal/hooks/usePackageUpload.d.ts +46 -4
  19. package/dist/devportal/hooks/usePackageUpload.js +90 -36
  20. package/dist/devportal/hooks/usePackageUpload.js.map +1 -1
  21. package/dist/devportal/pages/AdminReviewPage.js +205 -202
  22. package/dist/devportal/pages/AdminReviewPage.js.map +1 -1
  23. package/dist/devportal/pages/AnalyticsDashboardPage.js +186 -179
  24. package/dist/devportal/pages/AnalyticsDashboardPage.js.map +1 -1
  25. package/dist/devportal/pages/ApiKeysPage.js +113 -111
  26. package/dist/devportal/pages/ApiKeysPage.js.map +1 -1
  27. package/dist/devportal/pages/AppLogsPage.js +80 -84
  28. package/dist/devportal/pages/AppLogsPage.js.map +1 -1
  29. package/dist/devportal/pages/ApplicationDetailPage.js +529 -504
  30. package/dist/devportal/pages/ApplicationDetailPage.js.map +1 -1
  31. package/dist/devportal/pages/ApplicationsPage.js +186 -183
  32. package/dist/devportal/pages/ApplicationsPage.js.map +1 -1
  33. package/dist/devportal/pages/CreateApiKeyPage.js +163 -166
  34. package/dist/devportal/pages/CreateApiKeyPage.js.map +1 -1
  35. package/dist/devportal/pages/CreateOAuthClientPage.js +134 -130
  36. package/dist/devportal/pages/CreateOAuthClientPage.js.map +1 -1
  37. package/dist/devportal/pages/CreatePhysicalItemPage.js +2 -2
  38. package/dist/devportal/pages/CreatePhysicalItemPage.js.map +1 -1
  39. package/dist/devportal/pages/DelegationsPage.js +11 -11
  40. package/dist/devportal/pages/DocumentationPage.js +100 -94
  41. package/dist/devportal/pages/DocumentationPage.js.map +1 -1
  42. package/dist/devportal/pages/EditApplicationPage.js +379 -377
  43. package/dist/devportal/pages/EditApplicationPage.js.map +1 -1
  44. package/dist/devportal/pages/EditPhysicalItemPage.js +312 -307
  45. package/dist/devportal/pages/EditPhysicalItemPage.js.map +1 -1
  46. package/dist/devportal/pages/InvitationDecisionPage.js +83 -79
  47. package/dist/devportal/pages/InvitationDecisionPage.js.map +1 -1
  48. package/dist/devportal/pages/ListingPreviewPage.js +175 -171
  49. package/dist/devportal/pages/ListingPreviewPage.js.map +1 -1
  50. package/dist/devportal/pages/OAuthClientDetailPage.js +76 -76
  51. package/dist/devportal/pages/OAuthClientDetailPage.js.map +1 -1
  52. package/dist/devportal/pages/OAuthClientsPage.js +126 -124
  53. package/dist/devportal/pages/OAuthClientsPage.js.map +1 -1
  54. package/dist/devportal/pages/OrganizationDetailPage.js +19 -19
  55. package/dist/devportal/pages/OrganizationMembersPage.js +223 -204
  56. package/dist/devportal/pages/OrganizationMembersPage.js.map +1 -1
  57. package/dist/devportal/pages/ResourcesPage.d.ts +6 -0
  58. package/dist/devportal/pages/ResourcesPage.js +171 -155
  59. package/dist/devportal/pages/ResourcesPage.js.map +1 -1
  60. package/dist/devportal/pages/SellerDashboardPage.js +117 -115
  61. package/dist/devportal/pages/SellerDashboardPage.js.map +1 -1
  62. package/dist/devportal/pages/SellerItemsPage.js +202 -199
  63. package/dist/devportal/pages/SellerItemsPage.js.map +1 -1
  64. package/dist/devportal/pages/SettingsPage.js +330 -315
  65. package/dist/devportal/pages/SettingsPage.js.map +1 -1
  66. package/dist/devportal/pages/SetupPage.js +5 -5
  67. package/dist/devportal/pages/StoreSubmissionPage.js +133 -131
  68. package/dist/devportal/pages/StoreSubmissionPage.js.map +1 -1
  69. package/dist/devportal/pages/WebhookDetailPage.js +6 -6
  70. package/dist/devportal/pages/WebhooksPage.js +39 -39
  71. package/dist/devportal/pages/WebhooksPage.js.map +1 -1
  72. package/dist/devportal/utils/appDetailTabs.d.ts +73 -0
  73. package/dist/devportal/utils/appDetailTabs.js +62 -0
  74. package/dist/devportal/utils/appDetailTabs.js.map +1 -0
  75. package/dist/devportal/utils/developerNav.d.ts +39 -0
  76. package/dist/devportal/utils/developerNav.js +304 -0
  77. package/dist/devportal/utils/developerNav.js.map +1 -0
  78. package/package.json +2 -2
@@ -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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"}
@@ -6,7 +6,7 @@ interface SectionDef {
6
6
  fallback: string;
7
7
  icon: typeof LayoutDashboard;
8
8
  /** Additional sub-paths that should mark this section as active. */
9
- matchPrefixes?: string[];
9
+ matchPrefixes?: readonly string[];
10
10
  }
11
11
  /**
12
12
  * Org context switcher — can be embedded standalone or inside SectionNav.
@@ -4,84 +4,108 @@ import "../hooks/useGraphQL.js";
4
4
  import { cn as r } from "../utils/cn.js";
5
5
  import { useGetMyOrganizationsQuery as i } from "../../generated/global-operations.js";
6
6
  import { useTr as a } from "../utils/i18n.js";
7
- import { useEffect as o, useRef as s, useState as c } from "react";
8
- import { Building2 as l, Check as u, ChevronDown as d, Plus as f } from "lucide-react";
7
+ import { getSectionEntries as o, getSectionMatchPrefixes as s } from "../utils/developerNav.js";
8
+ import { useEffect as c, useRef as l, useState as u } from "react";
9
+ import { AppWindow as d, BarChart3 as f, Blocks as p, BookOpen as m, Building2 as h, Check as g, ChevronDown as _, DollarSign as v, LayoutDashboard as y, Library as b, Plus as x, Settings as S, Share2 as C, User as w } from "lucide-react";
9
10
  import "react-router";
10
- import { jsx as p, jsxs as m } from "react/jsx-runtime";
11
+ import { jsx as T, jsxs as E } from "react/jsx-runtime";
11
12
  // src/devportal/components/SectionNav.tsx
12
- var h = () => {
13
- let { activeOrgId: h, setActiveOrgId: g, activePhysicalOrgId: _, setActivePhysicalOrgId: v, navigate: y, basePath: b } = e(), x = n(), S = x ? _ : h, C = x ? v : g, w = a(), [T, E] = c(!1), D = s(null), { data: O } = i({ fetchPolicy: "cache-first" }), k = (O?.myDeveloperOrganizations ?? []).filter((e) => x ? e.kind === "PHYSICAL" : e.kind !== "PHYSICAL");
14
- o(() => {
15
- !S && k.length === 1 && k[0]?.id && C(k[0].id);
13
+ var D = {
14
+ dashboard: y,
15
+ apps: d,
16
+ "timecampus-applets": p,
17
+ organizations: h,
18
+ delegations: C,
19
+ revenue: v,
20
+ docs: m,
21
+ resources: b,
22
+ publisher: w,
23
+ settings: S,
24
+ analytics: f
25
+ };
26
+ function O(e) {
27
+ return o(e).map((t) => ({
28
+ path: t.id === "publisher" ? "publisher/profile" : t.path,
29
+ i18nKey: t.i18nKey,
30
+ fallback: t.fallback,
31
+ icon: D[t.id] ?? y,
32
+ matchPrefixes: s(e, t.id)
33
+ }));
34
+ }
35
+ O(!1), O(!0);
36
+ var k = () => {
37
+ let { activeOrgId: o, setActiveOrgId: s, activePhysicalOrgId: d, setActivePhysicalOrgId: f, navigate: p, basePath: m } = e(), v = n(), y = v ? d : o, b = v ? f : s, S = a(), [C, w] = u(!1), D = l(null), { data: O } = i({ fetchPolicy: "cache-first" }), k = (O?.myDeveloperOrganizations ?? []).filter((e) => v ? e.kind === "PHYSICAL" : e.kind !== "PHYSICAL");
38
+ c(() => {
39
+ !y && k.length === 1 && k[0]?.id && b(k[0].id);
16
40
  }, [
17
41
  k,
18
- S,
19
- C
20
- ]), o(() => {
42
+ y,
43
+ b
44
+ ]), c(() => {
21
45
  let e = (e) => {
22
- D.current && !D.current.contains(e.target) && E(!1);
46
+ D.current && !D.current.contains(e.target) && w(!1);
23
47
  };
24
- return T && document.addEventListener("mousedown", e), () => document.removeEventListener("mousedown", e);
25
- }, [T]);
26
- let A = k.find((e) => e.id === S)?.name ?? (k.length === 0 ? w("devportal.nav.noOrg", "No organization") : w("devportal.nav.selectOrg", "Select org")), j = t(x, "new");
27
- return /* @__PURE__ */ m("div", {
48
+ return C && document.addEventListener("mousedown", e), () => document.removeEventListener("mousedown", e);
49
+ }, [C]);
50
+ let A = k.find((e) => e.id === y)?.name ?? (k.length === 0 ? S("devportal.nav.noOrg", "No organization") : S("devportal.nav.selectOrg", "Select org")), j = t(v, "new");
51
+ return /* @__PURE__ */ E("div", {
28
52
  ref: D,
29
53
  className: "relative flex-shrink-0",
30
- children: [/* @__PURE__ */ m("button", {
54
+ children: [/* @__PURE__ */ E("button", {
31
55
  type: "button",
32
- onClick: () => E((e) => !e),
56
+ onClick: () => w((e) => !e),
33
57
  className: r("inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors", "bg-bg-surface text-text-primary border-border-subtle hover:bg-accent"),
34
- "aria-label": w("devportal.nav.switchOrg", "Switch organization"),
58
+ "aria-label": S("devportal.nav.switchOrg", "Switch organization"),
35
59
  children: [
36
- /* @__PURE__ */ p(l, {
60
+ /* @__PURE__ */ T(h, {
37
61
  size: 13,
38
62
  className: "text-primary flex-shrink-0"
39
63
  }),
40
- /* @__PURE__ */ p("span", {
64
+ /* @__PURE__ */ T("span", {
41
65
  className: "max-w-[140px] truncate",
42
66
  children: A
43
67
  }),
44
- /* @__PURE__ */ p(d, {
68
+ /* @__PURE__ */ T(_, {
45
69
  size: 13,
46
- className: r("transition-transform", T && "rotate-180")
70
+ className: r("transition-transform", C && "rotate-180")
47
71
  })
48
72
  ]
49
- }), T && /* @__PURE__ */ m("div", {
73
+ }), C && /* @__PURE__ */ E("div", {
50
74
  className: "absolute right-0 top-full z-50 mt-1 min-w-[200px] rounded-md border border-border-subtle bg-popover shadow-lg py-1",
51
75
  children: [
52
- k.map((e) => /* @__PURE__ */ m("button", {
76
+ k.map((e) => /* @__PURE__ */ E("button", {
53
77
  type: "button",
54
78
  onClick: () => {
55
- C(e.id), E(!1);
79
+ b(e.id), w(!1);
56
80
  },
57
- className: r("flex w-full items-center gap-2 px-3 py-2 text-sm text-left transition-colors hover:bg-accent", e.id === S ? "text-primary font-medium" : "text-text-primary"),
58
- children: [/* @__PURE__ */ p(u, {
81
+ className: r("flex w-full items-center gap-2 px-3 py-2 text-sm text-left transition-colors hover:bg-accent", e.id === y ? "text-primary font-medium" : "text-text-primary"),
82
+ children: [/* @__PURE__ */ T(g, {
59
83
  size: 13,
60
- className: r("flex-shrink-0", e.id === S ? "opacity-100 text-primary" : "opacity-0")
61
- }), /* @__PURE__ */ p("span", {
84
+ className: r("flex-shrink-0", e.id === y ? "opacity-100 text-primary" : "opacity-0")
85
+ }), /* @__PURE__ */ T("span", {
62
86
  className: "truncate",
63
87
  children: e.name
64
88
  })]
65
89
  }, e.id)),
66
- k.length > 0 && /* @__PURE__ */ p("div", { className: "my-1 border-t border-border-subtle" }),
67
- /* @__PURE__ */ m("button", {
90
+ k.length > 0 && /* @__PURE__ */ T("div", { className: "my-1 border-t border-border-subtle" }),
91
+ /* @__PURE__ */ E("button", {
68
92
  type: "button",
69
93
  onClick: () => {
70
- E(!1), y?.(`${b ?? ""}${j}`);
94
+ w(!1), p?.(`${m ?? ""}${j}`);
71
95
  },
72
96
  className: "flex w-full items-center gap-2 px-3 py-2 text-sm text-left text-primary transition-colors hover:bg-accent",
73
- children: [/* @__PURE__ */ p(f, {
97
+ children: [/* @__PURE__ */ T(x, {
74
98
  size: 13,
75
99
  className: "flex-shrink-0"
76
- }), /* @__PURE__ */ p("span", {
100
+ }), /* @__PURE__ */ T("span", {
77
101
  className: "truncate",
78
- children: w("devportal.nav.newOrganization", "New organization")
102
+ children: S("devportal.nav.newOrganization", "New organization")
79
103
  })]
80
104
  })
81
105
  ]
82
106
  })]
83
107
  });
84
108
  };
85
- export { h as OrgSwitcher };
109
+ export { k as OrgSwitcher };
86
110
 
87
111
  //# sourceMappingURL=SectionNav.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SectionNav.js","names":["OrgSwitcher","useDevportal","isSeller","useIsSellerSection","currentOrgId","activePhysicalOrgId","activeOrgId","setCurrentOrgId","setActivePhysicalOrgId","setActiveOrgId","tr","useTr","open","setOpen","useState","ref","useRef","useGetMyOrganizationsQuery","orgs","data","org","useEffect","handler","label","o","createOrgPath","orgSectionPath","v","cn","Building2","ChevronDown","Check","navigate","basePath","Plus"],"sources":["../../../src/devportal/components/SectionNav.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useLocation } from \"react-router\";\nimport {\n LayoutDashboard,\n AppWindow,\n Blocks,\n Building2,\n Share2,\n DollarSign,\n BookOpen,\n Library,\n User,\n Settings,\n ChevronDown,\n Check,\n Plus,\n} from \"lucide-react\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport {\n useIsSellerSection,\n orgSectionPath,\n} from \"../hooks/useIsSellerSection\";\nimport {\n useGetMyOrganizationsQuery,\n type GetMyOrganizationsQuery,\n} from \"../hooks/useGraphQL\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\n\ntype OrgItem = GetMyOrganizationsQuery[\"myDeveloperOrganizations\"][number];\n\ninterface SectionDef {\n path: string;\n i18nKey: string;\n fallback: string;\n icon: typeof LayoutDashboard;\n /** Additional sub-paths that should mark this section as active. */\n matchPrefixes?: string[];\n}\n\nconst SECTIONS: ReadonlyArray<SectionDef> = [\n {\n path: \"\",\n i18nKey: \"devportal.nav.dashboard\",\n fallback: \"Dashboard\",\n icon: LayoutDashboard,\n },\n {\n path: \"apps\",\n i18nKey: \"devportal.nav.apps\",\n fallback: \"Apps\",\n icon: AppWindow,\n },\n {\n path: \"timecampus-applets\",\n i18nKey: \"devportal.nav.timecampusApplets\",\n fallback: \"TimeCampus Applets\",\n icon: Blocks,\n },\n {\n path: \"organizations\",\n i18nKey: \"devportal.nav.organizations\",\n fallback: \"Organizations\",\n icon: Building2,\n },\n {\n path: \"delegations\",\n i18nKey: \"devportal.nav.delegations\",\n fallback: \"Delegations\",\n icon: Share2,\n },\n {\n path: \"revenue\",\n i18nKey: \"devportal.nav.revenue\",\n fallback: \"Revenue\",\n icon: DollarSign,\n },\n {\n path: \"docs\",\n i18nKey: \"devportal.nav.docs\",\n fallback: \"Docs\",\n icon: BookOpen,\n },\n {\n path: \"resources\",\n i18nKey: \"devportal.nav.resources\",\n fallback: \"Resources\",\n icon: Library,\n },\n {\n path: \"publisher/profile\",\n i18nKey: \"devportal.nav.publisher\",\n fallback: \"Publisher\",\n icon: User,\n matchPrefixes: [\"publisher\"],\n },\n {\n path: \"settings\",\n i18nKey: \"devportal.nav.settings\",\n fallback: \"Settings\",\n icon: Settings,\n },\n];\n\nfunction joinBaseAndPath(basePath: string | undefined, path: string): string {\n const base = (basePath ?? \"\").replace(/\\/$/, \"\");\n if (!path) return base || \"/\";\n const suffix = path.startsWith(\"/\") ? path : `/${path}`;\n return `${base}${suffix}`;\n}\n\nfunction isSectionActive(\n pathname: string,\n basePath: string | undefined,\n section: SectionDef,\n): boolean {\n const sectionUrl = joinBaseAndPath(basePath, section.path);\n // Index/dashboard is active only when exactly matched (or with empty extra).\n if (section.path === \"\") {\n return pathname === sectionUrl || pathname === `${sectionUrl}/`;\n }\n const candidates = [section.path, ...(section.matchPrefixes ?? [])];\n return candidates.some((prefix) => {\n const url = joinBaseAndPath(basePath, prefix);\n return pathname === url || pathname.startsWith(`${url}/`);\n });\n}\n\n/**\n * Org context switcher — can be embedded standalone or inside SectionNav.\n *\n * Shared between the regular devportal and the seller portal: filters the\n * org list to PHYSICAL-kind orgs under /devportal/seller/** and DIGITAL-kind\n * everywhere else, and reads/writes the matching active-org slot\n * (activePhysicalOrgId vs activeOrgId) so this switcher and\n * useSellerOrganization (used by the item-creation flow) always agree on\n * which org is current. See DeveloperOrgKind (BOFF-7285).\n */\nexport const OrgSwitcher: FC = () => {\n const {\n activeOrgId,\n setActiveOrgId,\n activePhysicalOrgId,\n setActivePhysicalOrgId,\n navigate,\n basePath,\n } = useDevportal();\n const isSeller = useIsSellerSection();\n const currentOrgId = isSeller ? activePhysicalOrgId : activeOrgId;\n const setCurrentOrgId = isSeller ? setActivePhysicalOrgId : setActiveOrgId;\n const tr = useTr();\n const [open, setOpen] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n\n const { data } = useGetMyOrganizationsQuery({ fetchPolicy: \"cache-first\" });\n const allOrgs: OrgItem[] = data?.myDeveloperOrganizations ?? [];\n const orgs = allOrgs.filter((org) =>\n isSeller ? org.kind === \"PHYSICAL\" : org.kind !== \"PHYSICAL\",\n );\n\n // Auto-select when there's exactly one matching org and nothing is stored yet\n useEffect(() => {\n if (!currentOrgId && orgs.length === 1 && orgs[0]?.id) {\n setCurrentOrgId(orgs[0].id);\n }\n }, [orgs, currentOrgId, setCurrentOrgId]);\n\n // Close on outside click\n useEffect(() => {\n const handler = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n if (open) document.addEventListener(\"mousedown\", handler);\n return () => document.removeEventListener(\"mousedown\", handler);\n }, [open]);\n\n const activeOrg = orgs.find((o) => o.id === currentOrgId);\n const label =\n activeOrg?.name ??\n (orgs.length === 0\n ? tr(\"devportal.nav.noOrg\", \"No organization\")\n : tr(\"devportal.nav.selectOrg\", \"Select org\"));\n\n const createOrgPath = orgSectionPath(isSeller, \"new\");\n\n return (\n <div ref={ref} className=\"relative flex-shrink-0\">\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors\",\n \"bg-bg-surface text-text-primary border-border-subtle hover:bg-accent\",\n )}\n aria-label={tr(\"devportal.nav.switchOrg\", \"Switch organization\")}\n >\n <Building2 size={13} className=\"text-primary flex-shrink-0\" />\n <span className=\"max-w-[140px] truncate\">{label}</span>\n <ChevronDown\n size={13}\n className={cn(\"transition-transform\", open && \"rotate-180\")}\n />\n </button>\n\n {open && (\n <div className=\"absolute right-0 top-full z-50 mt-1 min-w-[200px] rounded-md border border-border-subtle bg-popover shadow-lg py-1\">\n {orgs.map((org) => (\n <button\n key={org.id}\n type=\"button\"\n onClick={() => {\n setCurrentOrgId(org.id);\n setOpen(false);\n }}\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2 text-sm text-left transition-colors hover:bg-accent\",\n org.id === currentOrgId\n ? \"text-primary font-medium\"\n : \"text-text-primary\",\n )}\n >\n <Check\n size={13}\n className={cn(\n \"flex-shrink-0\",\n org.id === currentOrgId\n ? \"opacity-100 text-primary\"\n : \"opacity-0\",\n )}\n />\n <span className=\"truncate\">{org.name}</span>\n </button>\n ))}\n {orgs.length > 0 && (\n <div className=\"my-1 border-t border-border-subtle\" />\n )}\n <button\n type=\"button\"\n onClick={() => {\n setOpen(false);\n navigate?.(`${basePath ?? \"\"}${createOrgPath}`);\n }}\n className=\"flex w-full items-center gap-2 px-3 py-2 text-sm text-left text-primary transition-colors hover:bg-accent\"\n >\n <Plus size={13} className=\"flex-shrink-0\" />\n <span className=\"truncate\">\n {tr(\"devportal.nav.newOrganization\", \"New organization\")}\n </span>\n </button>\n </div>\n )}\n </div>\n );\n};\n\n/**\n * Horizontal section nav rendered above the page content on every devportal\n * route. Gives developers a one-click jump between all top-level sections so\n * routes like OAuth, Delegations, Analytics, Revenue, Docs, Resources,\n * Publisher Profile, and Settings are reachable without typing URLs.\n *\n * Renders nothing when `basePath` is undefined (standalone/dev mode) so it\n * doesn't conflict with whatever harness wraps the MFE there.\n */\nexport const SectionNav: FC = () => {\n const { navigate, basePath } = useDevportal();\n const { pathname } = useLocation();\n const tr = useTr();\n\n if (!basePath || !navigate) return null;\n\n const handleClick = (path: string) => {\n navigate(joinBaseAndPath(basePath, path));\n };\n\n return (\n <nav\n aria-label={tr(\"devportal.nav.sections\", \"Developer portal sections\")}\n className=\"border-b border-border-subtle bg-bg-surface/40 px-4 py-2\"\n data-testid=\"devportal-section-nav\"\n >\n <div className=\"flex items-center gap-2\">\n <ul className=\"flex flex-1 flex-wrap gap-1\">\n {SECTIONS.map((section) => {\n const active = isSectionActive(pathname, basePath, section);\n const Icon = section.icon;\n return (\n <li key={section.path}>\n <button\n type=\"button\"\n onClick={() => handleClick(section.path)}\n aria-current={active ? \"page\" : undefined}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors\",\n active\n ? \"bg-action-primary-bg text-action-primary-text\"\n : \"text-text-secondary hover:bg-accent hover:text-text-primary\",\n )}\n >\n <Icon size={14} />\n <span>{tr(section.i18nKey, section.fallback)}</span>\n </button>\n </li>\n );\n })}\n </ul>\n <OrgSwitcher />\n </div>\n </nav>\n );\n};\n\n// Exported for unit tests that want to assert against the canonical list.\nexport const __SECTION_NAV_SECTIONS = SECTIONS;\n"],"mappings":";;;;;;;;;;;AA2IA,IAAaA,UAAwB;CACnC,IAAM,EACJ,gBACA,mBACA,wBACA,2BACA,aACA,gBACEC,EAAa,GACXC,IAAWC,EAAmB,GAC9BC,IAAeF,IAAWG,IAAsBC,GAChDC,IAAkBL,IAAWM,IAAyBC,GACtDC,IAAKC,EAAM,GACX,CAACC,GAAMC,KAAWC,EAAS,EAAK,GAChCC,IAAMC,EAAuB,IAAI,GAEjC,EAAE,YAASC,EAA2B,EAAE,aAAa,cAAc,CAAC,GAEpEC,KADqBC,GAAM,4BAA4B,CAAC,EAAA,CACzC,QAAQ,MAC3BjB,IAAWkB,EAAI,SAAS,aAAaA,EAAI,SAAS,UACpD;CAUA,AAPAC,QAAgB;EACd,AAAI,CAACjB,KAAgBc,EAAK,WAAW,KAAKA,EAAK,EAAE,EAAE,MACjDX,EAAgBW,EAAK,EAAE,CAAC,EAAE;CAE9B,GAAG;EAACA;EAAMd;EAAcG;CAAe,CAAC,GAGxCc,QAAgB;EACd,IAAMC,KAAW,MAAkB;GACjC,AAAIP,EAAI,WAAW,CAACA,EAAI,QAAQ,SAAS,EAAE,MAAc,KACvDF,EAAQ,EAAK;EAEjB;EAEA,OADID,KAAM,SAAS,iBAAiB,aAAaU,CAAO,SAC3C,SAAS,oBAAoB,aAAaA,CAAO;CAChE,GAAG,CAACV,CAAI,CAAC;CAGT,IAAMW,IADYL,EAAK,MAAM,MAAMM,EAAE,OAAOpB,CAE1C,CAAA,EAAW,SACVc,EAAK,WAAW,IACbR,EAAG,uBAAuB,iBAAiB,IAC3CA,EAAG,2BAA2B,YAAY,IAE1Ce,IAAgBC,EAAexB,GAAU,KAAK;CAEpD,OACE,kBAAC,OAAD;EAAU;EAAK,WAAU;EAAzB,UAAA,CACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAeW,GAAS,MAAM,CAACc,CAAC;GAChC,WAAWC,EACT,wGACA,sEACF;GACA,cAAYlB,EAAG,2BAA2B,qBAAqB;GAPjE,UAAA;IASE,kBAACmB,GAAD;KAAW,MAAM;KAAI,WAAU;IAA8B,CAAA;IAC7D,kBAAC,QAAD;KAAM,WAAU;KAA0B,UAAAN;IAAY,CAAA;IACtD,kBAACO,GAAD;KACE,MAAM;KACN,WAAWF,EAAG,wBAAwBhB,KAAQ,YAAY;IAC3D,CAAA;GACK;EAEP,CAAA,GAAAA,KACC,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IACGM,EAAK,KAAK,MACT,kBAAC,UAAD;KAEE,MAAK;KACL,eAAe;MAEb,AADAX,EAAgBa,EAAI,EAAE,GACtBP,EAAQ,EAAK;KACf;KACA,WAAWe,EACT,gGACAR,EAAI,OAAOhB,IACP,6BACA,mBACN;KAZF,UAAA,CAcE,kBAAC2B,GAAD;MACE,MAAM;MACN,WAAWH,EACT,iBACAR,EAAI,OAAOhB,IACP,6BACA,WACN;KACD,CAAA,GACD,kBAAC,QAAD;MAAM,WAAU;MAAY,UAAAgB,EAAI;KAAW,CAAA,CACrC;IAvBD,GAAAA,EAAI,EAuBH,CACT;IACAF,EAAK,SAAS,KACb,kBAAC,OAAD,EAAK,WAAU,qCAAsC,CAAA;IAEvD,kBAAC,UAAD;KACE,MAAK;KACL,eAAe;MAEb,AADAL,EAAQ,EAAK,GACbmB,IAAW,GAAGC,KAAY,KAAKR,GAAe;KAChD;KACA,WAAU;KANZ,UAAA,CAQE,kBAACS,GAAD;MAAM,MAAM;MAAI,WAAU;KAAiB,CAAA,GAC3C,kBAAC,QAAD;MAAM,WAAU;MACb,UAAAxB,EAAG,iCAAiC,kBAAkB;KACnD,CAAA,CACA;;GACL;EAEJ,CAAA,CAAA;;AAET"}
1
+ {"version":3,"file":"SectionNav.js","names":["SECTION_ICONS","LayoutDashboard","AppWindow","Blocks","Building2","Share2","DollarSign","BookOpen","Library","User","Settings","BarChart3","buildSections","getSectionEntries","isFluidgrids","entry","getSectionMatchPrefixes","OrgSwitcher","useDevportal","isSeller","useIsSellerSection","currentOrgId","activePhysicalOrgId","activeOrgId","setCurrentOrgId","setActivePhysicalOrgId","setActiveOrgId","tr","useTr","open","setOpen","useState","ref","useRef","useGetMyOrganizationsQuery","orgs","data","org","useEffect","handler","label","o","createOrgPath","orgSectionPath","v","cn","ChevronDown","Check","navigate","basePath","Plus"],"sources":["../../../src/devportal/components/SectionNav.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useLocation } from \"react-router\";\nimport {\n LayoutDashboard,\n AppWindow,\n Blocks,\n Building2,\n Share2,\n DollarSign,\n BookOpen,\n Library,\n User,\n Settings,\n ChevronDown,\n Check,\n Plus,\n BarChart3,\n} from \"lucide-react\";\nimport {\n useDevportal,\n useIsFluidgridsVariant,\n} from \"../providers/DevportalProvider\";\nimport {\n useIsSellerSection,\n orgSectionPath,\n} from \"../hooks/useIsSellerSection\";\nimport {\n useGetMyOrganizationsQuery,\n type GetMyOrganizationsQuery,\n} from \"../hooks/useGraphQL\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\nimport {\n getSectionEntries,\n getSectionMatchPrefixes,\n} from \"../utils/developerNav\";\n\ntype OrgItem = GetMyOrganizationsQuery[\"myDeveloperOrganizations\"][number];\n\ninterface SectionDef {\n path: string;\n i18nKey: string;\n fallback: string;\n icon: typeof LayoutDashboard;\n /** Additional sub-paths that should mark this section as active. */\n matchPrefixes?: readonly string[];\n}\n\nconst SECTION_ICONS: Record<string, typeof LayoutDashboard> = {\n dashboard: LayoutDashboard,\n apps: AppWindow,\n \"timecampus-applets\": Blocks,\n organizations: Building2,\n delegations: Share2,\n revenue: DollarSign,\n docs: BookOpen,\n resources: Library,\n publisher: User,\n settings: Settings,\n analytics: BarChart3,\n};\n\n/** Section list for a variant, built from the shared nav data. */\nfunction buildSections(isFluidgrids: boolean): ReadonlyArray<SectionDef> {\n return getSectionEntries(isFluidgrids).map((entry) => ({\n path: entry.id === \"publisher\" ? \"publisher/profile\" : entry.path,\n i18nKey: entry.i18nKey,\n fallback: entry.fallback,\n icon: SECTION_ICONS[entry.id] ?? LayoutDashboard,\n matchPrefixes: getSectionMatchPrefixes(isFluidgrids, entry.id),\n }));\n}\n\nconst SECTIONS: ReadonlyArray<SectionDef> = buildSections(false);\nconst FLUIDGRIDS_SECTIONS: ReadonlyArray<SectionDef> = buildSections(true);\n\nfunction joinBaseAndPath(basePath: string | undefined, path: string): string {\n const base = (basePath ?? \"\").replace(/\\/$/, \"\");\n if (!path) return base || \"/\";\n const suffix = path.startsWith(\"/\") ? path : `/${path}`;\n return `${base}${suffix}`;\n}\n\nfunction isSectionActive(\n pathname: string,\n basePath: string | undefined,\n section: SectionDef,\n): boolean {\n const sectionUrl = joinBaseAndPath(basePath, section.path);\n // Index/dashboard is active only when exactly matched (or with empty extra).\n if (section.path === \"\") {\n return pathname === sectionUrl || pathname === `${sectionUrl}/`;\n }\n const candidates = [section.path, ...(section.matchPrefixes ?? [])];\n return candidates.some((prefix) => {\n const url = joinBaseAndPath(basePath, prefix);\n return pathname === url || pathname.startsWith(`${url}/`);\n });\n}\n\n/**\n * Org context switcher — can be embedded standalone or inside SectionNav.\n *\n * Shared between the regular devportal and the seller portal: filters the\n * org list to PHYSICAL-kind orgs under /devportal/seller/** and DIGITAL-kind\n * everywhere else, and reads/writes the matching active-org slot\n * (activePhysicalOrgId vs activeOrgId) so this switcher and\n * useSellerOrganization (used by the item-creation flow) always agree on\n * which org is current. See DeveloperOrgKind (BOFF-7285).\n */\nexport const OrgSwitcher: FC = () => {\n const {\n activeOrgId,\n setActiveOrgId,\n activePhysicalOrgId,\n setActivePhysicalOrgId,\n navigate,\n basePath,\n } = useDevportal();\n const isSeller = useIsSellerSection();\n const currentOrgId = isSeller ? activePhysicalOrgId : activeOrgId;\n const setCurrentOrgId = isSeller ? setActivePhysicalOrgId : setActiveOrgId;\n const tr = useTr();\n const [open, setOpen] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n\n const { data } = useGetMyOrganizationsQuery({ fetchPolicy: \"cache-first\" });\n const allOrgs: OrgItem[] = data?.myDeveloperOrganizations ?? [];\n const orgs = allOrgs.filter((org) =>\n isSeller ? org.kind === \"PHYSICAL\" : org.kind !== \"PHYSICAL\",\n );\n\n // Auto-select when there's exactly one matching org and nothing is stored yet\n useEffect(() => {\n if (!currentOrgId && orgs.length === 1 && orgs[0]?.id) {\n setCurrentOrgId(orgs[0].id);\n }\n }, [orgs, currentOrgId, setCurrentOrgId]);\n\n // Close on outside click\n useEffect(() => {\n const handler = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n if (open) document.addEventListener(\"mousedown\", handler);\n return () => document.removeEventListener(\"mousedown\", handler);\n }, [open]);\n\n const activeOrg = orgs.find((o) => o.id === currentOrgId);\n const label =\n activeOrg?.name ??\n (orgs.length === 0\n ? tr(\"devportal.nav.noOrg\", \"No organization\")\n : tr(\"devportal.nav.selectOrg\", \"Select org\"));\n\n const createOrgPath = orgSectionPath(isSeller, \"new\");\n\n return (\n <div ref={ref} className=\"relative flex-shrink-0\">\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors\",\n \"bg-bg-surface text-text-primary border-border-subtle hover:bg-accent\",\n )}\n aria-label={tr(\"devportal.nav.switchOrg\", \"Switch organization\")}\n >\n <Building2 size={13} className=\"text-primary flex-shrink-0\" />\n <span className=\"max-w-[140px] truncate\">{label}</span>\n <ChevronDown\n size={13}\n className={cn(\"transition-transform\", open && \"rotate-180\")}\n />\n </button>\n\n {open && (\n <div className=\"absolute right-0 top-full z-50 mt-1 min-w-[200px] rounded-md border border-border-subtle bg-popover shadow-lg py-1\">\n {orgs.map((org) => (\n <button\n key={org.id}\n type=\"button\"\n onClick={() => {\n setCurrentOrgId(org.id);\n setOpen(false);\n }}\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2 text-sm text-left transition-colors hover:bg-accent\",\n org.id === currentOrgId\n ? \"text-primary font-medium\"\n : \"text-text-primary\",\n )}\n >\n <Check\n size={13}\n className={cn(\n \"flex-shrink-0\",\n org.id === currentOrgId\n ? \"opacity-100 text-primary\"\n : \"opacity-0\",\n )}\n />\n <span className=\"truncate\">{org.name}</span>\n </button>\n ))}\n {orgs.length > 0 && (\n <div className=\"my-1 border-t border-border-subtle\" />\n )}\n <button\n type=\"button\"\n onClick={() => {\n setOpen(false);\n navigate?.(`${basePath ?? \"\"}${createOrgPath}`);\n }}\n className=\"flex w-full items-center gap-2 px-3 py-2 text-sm text-left text-primary transition-colors hover:bg-accent\"\n >\n <Plus size={13} className=\"flex-shrink-0\" />\n <span className=\"truncate\">\n {tr(\"devportal.nav.newOrganization\", \"New organization\")}\n </span>\n </button>\n </div>\n )}\n </div>\n );\n};\n\n/**\n * Horizontal section nav rendered above the page content on every devportal\n * route. Gives developers a one-click jump between all top-level sections so\n * routes like OAuth, Delegations, Analytics, Revenue, Docs, Resources,\n * Publisher Profile, and Settings are reachable without typing URLs.\n *\n * Renders nothing when `basePath` is undefined (standalone/dev mode) so it\n * doesn't conflict with whatever harness wraps the MFE there.\n */\nexport const SectionNav: FC = () => {\n const { navigate, basePath } = useDevportal();\n const { pathname } = useLocation();\n const tr = useTr();\n const isFluidgrids = useIsFluidgridsVariant();\n const sections = isFluidgrids ? FLUIDGRIDS_SECTIONS : SECTIONS;\n\n if (!basePath || !navigate) return null;\n\n const handleClick = (path: string) => {\n navigate(joinBaseAndPath(basePath, path));\n };\n\n return (\n <nav\n aria-label={tr(\"devportal.nav.sections\", \"Developer portal sections\")}\n className=\"border-b border-border-subtle bg-bg-surface/40 px-4 py-2\"\n data-testid=\"devportal-section-nav\"\n >\n <div className=\"flex items-center gap-2\">\n <ul className=\"flex flex-1 flex-wrap gap-1\">\n {sections.map((section) => {\n const active = isSectionActive(pathname, basePath, section);\n const Icon = section.icon;\n return (\n <li key={section.path}>\n <button\n type=\"button\"\n onClick={() => handleClick(section.path)}\n aria-current={active ? \"page\" : undefined}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors\",\n active\n ? \"bg-action-primary-bg text-action-primary-text\"\n : \"text-text-secondary hover:bg-accent hover:text-text-primary\",\n )}\n >\n <Icon size={14} />\n <span>{tr(section.i18nKey, section.fallback)}</span>\n </button>\n </li>\n );\n })}\n </ul>\n <OrgSwitcher />\n </div>\n </nav>\n );\n};\n\n// Exported for unit tests that want to assert against the canonical list.\nexport const __SECTION_NAV_SECTIONS = SECTIONS;\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
+ }>;
@@ -0,0 +1,30 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { useState as t } from "react";
3
+ import { ImageOff as n } from "lucide-react";
4
+ import { Fragment as r, jsx as i } from "react/jsx-runtime";
5
+ // src/devportal/components/StoreImage.tsx
6
+ var a = /^(?:https?:\/\/|data:image\/|blob:|\/(?!\/))/i;
7
+ function o(e) {
8
+ return typeof e == "string" && a.test(e.trim());
9
+ }
10
+ var s = ({ src: e, alt: n, fallback: a = null, ...s }) => {
11
+ let [c, l] = t(null);
12
+ return !o(e) || c === e ? /* @__PURE__ */ i(r, { children: a }) : /* @__PURE__ */ i("img", {
13
+ ...s,
14
+ src: e,
15
+ alt: n,
16
+ onError: () => l(e)
17
+ });
18
+ }, c = ({ className: t, iconSize: r = 18 }) => /* @__PURE__ */ i("div", {
19
+ role: "img",
20
+ "aria-label": "Image unavailable",
21
+ className: e("flex shrink-0 items-center justify-center rounded-input border border-border-subtle bg-bg-sunken", t),
22
+ children: /* @__PURE__ */ i(n, {
23
+ size: r,
24
+ className: "text-text-muted",
25
+ "aria-hidden": "true"
26
+ })
27
+ });
28
+ export { s as StoreImage, c as StoreImagePlaceholder, o as isRenderableImageSrc };
29
+
30
+ //# sourceMappingURL=StoreImage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StoreImage.js","names":["_Fragment","RENDERABLE_SRC","isRenderableImageSrc","src","StoreImage","imgProps","failedSrc","setFailedSrc","useState","Fragment","fallback","StoreImagePlaceholder","cn","className","ImageOff","iconSize"],"sources":["../../../src/devportal/components/StoreImage.tsx"],"sourcesContent":["import type { FC, ImgHTMLAttributes, ReactNode } from \"react\";\nimport { useState } from \"react\";\nimport { ImageOff } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\n\n// =============================================================================\n// STORE IMAGE\n//\n// Renders a store listing image (app/item icon, banner, screenshot) and swaps\n// in a fallback when the value can't be shown: empty, not a browser-loadable\n// URL (e.g. a bare Iconify name like \"mdi:robot\"), or the request fails (an\n// expired presigned link, a 404, a rate-limited third-party host).\n// fe-libs has no image-with-fallback primitive, so this lives here.\n// =============================================================================\n\nconst RENDERABLE_SRC = /^(?:https?:\\/\\/|data:image\\/|blob:|\\/(?!\\/))/i;\n\n/** True when `src` is a value the browser can load as an image. */\nexport function isRenderableImageSrc(\n src: string | null | undefined,\n): src is string {\n return typeof src === \"string\" && RENDERABLE_SRC.test(src.trim());\n}\n\nexport interface StoreImageProps extends Omit<\n ImgHTMLAttributes<HTMLImageElement>,\n \"src\" | \"onError\"\n> {\n src: string | null | undefined;\n alt: string;\n /** Shown instead of the image when it is missing or fails to load. */\n fallback?: ReactNode;\n}\n\nexport const StoreImage: FC<StoreImageProps> = ({\n src,\n alt,\n fallback = null,\n ...imgProps\n}) => {\n // Remember which src failed rather than a boolean, so a new src (e.g.\n // after a re-upload) gets a fresh attempt without an effect.\n const [failedSrc, setFailedSrc] = useState<string | null>(null);\n\n if (!isRenderableImageSrc(src) || failedSrc === src) {\n return <>{fallback}</>;\n }\n\n return (\n <img {...imgProps} src={src} alt={alt} onError={() => setFailedSrc(src)} />\n );\n};\n\n/** Neutral tile for a store image that can't be shown. Size it via className. */\nexport const StoreImagePlaceholder: FC<{\n className?: string;\n iconSize?: number;\n}> = ({ className, iconSize = 18 }) => (\n <div\n role=\"img\"\n aria-label=\"Image unavailable\"\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-input border border-border-subtle bg-bg-sunken\",\n className,\n )}\n >\n <ImageOff size={iconSize} className=\"text-text-muted\" aria-hidden=\"true\" />\n </div>\n);\n"],"mappings":";;;;;AAeA,IAAMC,IAAiB;AAGvB,SAAgBC,EACd,GACe;CACf,OAAO,OAAOC,KAAQ,YAAYF,EAAe,KAAKE,EAAI,KAAK,CAAC;AAClE;AAYA,IAAaC,KAAmC,EAC9C,QACA,QACA,cAAW,MACX,GAAGC,QACC;CAGJ,IAAM,CAACC,GAAWC,KAAgBC,EAAwB,IAAI;CAM9D,OAJI,CAACN,EAAqBC,CAAG,KAAKG,MAAcH,IACvC,kBAAAH,GAAA,EAAA,UAAGU,EAAW,CAAA,IAIrB,kBAAC,OAAD;EAAK,GAAIL;EAAe;EAAU;EAAK,eAAeE,EAAaJ,CAAG;CAAI,CAAA;AAE9E,GAGaQ,KAGP,EAAE,cAAW,cAAW,SAC5B,kBAAC,OAAD;CACE,MAAK;CACL,cAAW;CACX,WAAWC,EACT,oGACAC,CACF;CAEA,UAAA,kBAACC,GAAD;EAAU,MAAMC;EAAU,WAAU;EAAkB,eAAY;CAAQ,CAAA;AACvE,CAAA"}