@aglyn/plugins-marketplace 1.0.0-beta.143

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 (165) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +51 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
  8. package/src/lib/components/artifact-update-dialog.component.js +244 -0
  9. package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
  10. package/src/lib/components/host-plugins-card.component.d.ts +23 -0
  11. package/src/lib/components/host-plugins-card.component.js +566 -0
  12. package/src/lib/components/host-plugins-card.component.js.map +1 -0
  13. package/src/lib/components/listing-content.component.d.ts +57 -0
  14. package/src/lib/components/listing-content.component.js +1880 -0
  15. package/src/lib/components/listing-content.component.js.map +1 -0
  16. package/src/lib/components/listing-image.component.d.ts +46 -0
  17. package/src/lib/components/listing-image.component.js +38 -0
  18. package/src/lib/components/listing-image.component.js.map +1 -0
  19. package/src/lib/components/listing-reviews.component.d.ts +22 -0
  20. package/src/lib/components/listing-reviews.component.js +419 -0
  21. package/src/lib/components/listing-reviews.component.js.map +1 -0
  22. package/src/lib/components/marketplace-browse.component.d.ts +38 -0
  23. package/src/lib/components/marketplace-browse.component.js +802 -0
  24. package/src/lib/components/marketplace-browse.component.js.map +1 -0
  25. package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
  26. package/src/lib/components/plugin-site-set-panel.component.js +137 -0
  27. package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
  28. package/src/lib/components/plugin-site-set.component.d.ts +43 -0
  29. package/src/lib/components/plugin-site-set.component.js +425 -0
  30. package/src/lib/components/plugin-site-set.component.js.map +1 -0
  31. package/src/lib/components/rating-input.component.d.ts +3 -0
  32. package/src/lib/components/rating-input.component.js +47 -0
  33. package/src/lib/components/rating-input.component.js.map +1 -0
  34. package/src/lib/components/report-target.component.d.ts +36 -0
  35. package/src/lib/components/report-target.component.js +181 -0
  36. package/src/lib/components/report-target.component.js.map +1 -0
  37. package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
  38. package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
  39. package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
  40. package/src/lib/constants/bundle-common.d.ts +8 -0
  41. package/src/lib/constants/bundle-common.js +9 -0
  42. package/src/lib/constants/bundle-common.js.map +1 -0
  43. package/src/lib/hooks/use-artifact-update.d.ts +41 -0
  44. package/src/lib/hooks/use-artifact-update.js +169 -0
  45. package/src/lib/hooks/use-artifact-update.js.map +1 -0
  46. package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
  47. package/src/lib/hooks/use-marketplace-actions.js +523 -0
  48. package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
  49. package/src/lib/model/artifact-merge.d.ts +154 -0
  50. package/src/lib/model/artifact-merge.js +228 -0
  51. package/src/lib/model/artifact-merge.js.map +1 -0
  52. package/src/lib/model/index.d.ts +43 -0
  53. package/src/lib/model/index.js +41 -0
  54. package/src/lib/model/index.js.map +1 -0
  55. package/src/lib/model/marketplace-props.d.ts +57 -0
  56. package/src/lib/model/marketplace-props.js +441 -0
  57. package/src/lib/model/marketplace-props.js.map +1 -0
  58. package/src/lib/model/marketplace.d.ts +833 -0
  59. package/src/lib/model/marketplace.js +929 -0
  60. package/src/lib/model/marketplace.js.map +1 -0
  61. package/src/lib/model/rating-field.d.ts +24 -0
  62. package/src/lib/model/rating-field.js +33 -0
  63. package/src/lib/model/rating-field.js.map +1 -0
  64. package/src/lib/plugin.d.ts +33 -0
  65. package/src/lib/plugin.js +86 -0
  66. package/src/lib/plugin.js.map +1 -0
  67. package/src/lib/server/billing-webhook.d.ts +25 -0
  68. package/src/lib/server/billing-webhook.js +1062 -0
  69. package/src/lib/server/billing-webhook.js.map +1 -0
  70. package/src/lib/server/checkout.d.ts +76 -0
  71. package/src/lib/server/checkout.js +477 -0
  72. package/src/lib/server/checkout.js.map +1 -0
  73. package/src/lib/server/connect.d.ts +24 -0
  74. package/src/lib/server/connect.js +205 -0
  75. package/src/lib/server/connect.js.map +1 -0
  76. package/src/lib/server/install-dataset-schema.d.ts +32 -0
  77. package/src/lib/server/install-dataset-schema.js +255 -0
  78. package/src/lib/server/install-dataset-schema.js.map +1 -0
  79. package/src/lib/server/install-email-starter.d.ts +65 -0
  80. package/src/lib/server/install-email-starter.js +279 -0
  81. package/src/lib/server/install-email-starter.js.map +1 -0
  82. package/src/lib/server/install-email-template.d.ts +34 -0
  83. package/src/lib/server/install-email-template.js +252 -0
  84. package/src/lib/server/install-email-template.js.map +1 -0
  85. package/src/lib/server/install-layout.d.ts +29 -0
  86. package/src/lib/server/install-layout.js +323 -0
  87. package/src/lib/server/install-layout.js.map +1 -0
  88. package/src/lib/server/install-pin-counts.d.ts +173 -0
  89. package/src/lib/server/install-pin-counts.js +248 -0
  90. package/src/lib/server/install-pin-counts.js.map +1 -0
  91. package/src/lib/server/install-plugin.d.ts +18 -0
  92. package/src/lib/server/install-plugin.js +405 -0
  93. package/src/lib/server/install-plugin.js.map +1 -0
  94. package/src/lib/server/install-template.d.ts +37 -0
  95. package/src/lib/server/install-template.js +348 -0
  96. package/src/lib/server/install-template.js.map +1 -0
  97. package/src/lib/server/install-theme.d.ts +42 -0
  98. package/src/lib/server/install-theme.js +313 -0
  99. package/src/lib/server/install-theme.js.map +1 -0
  100. package/src/lib/server/install.d.ts +27 -0
  101. package/src/lib/server/install.js +276 -0
  102. package/src/lib/server/install.js.map +1 -0
  103. package/src/lib/server/listing-versions.d.ts +26 -0
  104. package/src/lib/server/listing-versions.js +378 -0
  105. package/src/lib/server/listing-versions.js.map +1 -0
  106. package/src/lib/server/preview-image.d.ts +37 -0
  107. package/src/lib/server/preview-image.js +216 -0
  108. package/src/lib/server/preview-image.js.map +1 -0
  109. package/src/lib/server/provenance.d.ts +100 -0
  110. package/src/lib/server/provenance.js +118 -0
  111. package/src/lib/server/provenance.js.map +1 -0
  112. package/src/lib/server/publish-dataset-schema.d.ts +31 -0
  113. package/src/lib/server/publish-dataset-schema.js +202 -0
  114. package/src/lib/server/publish-dataset-schema.js.map +1 -0
  115. package/src/lib/server/publish-email-starter.d.ts +44 -0
  116. package/src/lib/server/publish-email-starter.js +231 -0
  117. package/src/lib/server/publish-email-starter.js.map +1 -0
  118. package/src/lib/server/publish-email-template.d.ts +33 -0
  119. package/src/lib/server/publish-email-template.js +211 -0
  120. package/src/lib/server/publish-email-template.js.map +1 -0
  121. package/src/lib/server/publish-layout.d.ts +33 -0
  122. package/src/lib/server/publish-layout.js +243 -0
  123. package/src/lib/server/publish-layout.js.map +1 -0
  124. package/src/lib/server/publish-plugin.d.ts +18 -0
  125. package/src/lib/server/publish-plugin.js +485 -0
  126. package/src/lib/server/publish-plugin.js.map +1 -0
  127. package/src/lib/server/publish-preconditions.d.ts +86 -0
  128. package/src/lib/server/publish-preconditions.js +160 -0
  129. package/src/lib/server/publish-preconditions.js.map +1 -0
  130. package/src/lib/server/publish-template.d.ts +27 -0
  131. package/src/lib/server/publish-template.js +215 -0
  132. package/src/lib/server/publish-template.js.map +1 -0
  133. package/src/lib/server/publish-theme.d.ts +38 -0
  134. package/src/lib/server/publish-theme.js +186 -0
  135. package/src/lib/server/publish-theme.js.map +1 -0
  136. package/src/lib/server/publish.d.ts +26 -0
  137. package/src/lib/server/publish.js +204 -0
  138. package/src/lib/server/publish.js.map +1 -0
  139. package/src/lib/server/publisher-profile-save.d.ts +30 -0
  140. package/src/lib/server/publisher-profile-save.js +226 -0
  141. package/src/lib/server/publisher-profile-save.js.map +1 -0
  142. package/src/lib/server/publisher-profile.d.ts +86 -0
  143. package/src/lib/server/publisher-profile.js +116 -0
  144. package/src/lib/server/publisher-profile.js.map +1 -0
  145. package/src/lib/server/purchase-entitlement.d.ts +83 -0
  146. package/src/lib/server/purchase-entitlement.js +118 -0
  147. package/src/lib/server/purchase-entitlement.js.map +1 -0
  148. package/src/lib/server/report.d.ts +32 -0
  149. package/src/lib/server/report.js +101 -0
  150. package/src/lib/server/report.js.map +1 -0
  151. package/src/lib/server/reviews.d.ts +41 -0
  152. package/src/lib/server/reviews.js +240 -0
  153. package/src/lib/server/reviews.js.map +1 -0
  154. package/src/lib/server/update-artifact.d.ts +19 -0
  155. package/src/lib/server/update-artifact.js +569 -0
  156. package/src/lib/server/update-artifact.js.map +1 -0
  157. package/src/lib/server/verification-request.d.ts +31 -0
  158. package/src/lib/server/verification-request.js +173 -0
  159. package/src/lib/server/verification-request.js.map +1 -0
  160. package/src/lib/server/version-stats.d.ts +202 -0
  161. package/src/lib/server/version-stats.js +305 -0
  162. package/src/lib/server/version-stats.js.map +1 -0
  163. package/src/lib/server.d.ts +30 -0
  164. package/src/lib/server.js +95 -0
  165. package/src/lib/server.js.map +1 -0
@@ -0,0 +1,802 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ import { mdiCheckDecagram } from "@aglyn/shared-data-mdi";
20
+ import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
21
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
22
+ import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
23
+ import { Alert, Chip, Tooltip, Grid, MenuItem, Rating, Stack, TextField, Typography } from "@mui/material";
24
+ import { collection, doc, documentId, getDoc, limit, orderBy, query, where } from "firebase/firestore";
25
+ import { useEffect, useMemo, useRef, useState } from "react";
26
+ import { buildRoute, PLATFORM_BRAND_NAME, pluginDocsHelp, Route } from "@aglyn/aglyn";
27
+ import { useConsoleHostRoute, useFirestore, useFirestoreCollection, useFirestoreDoc, useHostOrgId, useUser, useScopeTokens } from "@aglyn/tenant-feature-instance";
28
+ /*
29
+ * The MODULE, not the barrel, for the two PURE helpers — the specs that render
30
+ * this grid mock `@aglyn/tenant-feature-instance` wholesale to stage their
31
+ * Firestore hooks, and a query builder imported through that barrel disappears
32
+ * under the mock. Neither of these is a hook.
33
+ */ import { ceilingedWindow, collectionCeiling } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
34
+ import { isListingBrowsable, isListingDeleted, isPrivateListing, listingArtifactType, listingArtifactLabel, resolvePluginInstallState } from "../model/marketplace.js";
35
+ import { ListingImage } from "./listing-image.component.js";
36
+ // The console route table is shared (AGL-685), so these go through
37
+ // buildRoute rather than being reassembled from a base string — the shape
38
+ // of `/[orgSlug]/hosts/[host]/marketplace/…` is not this plugin's to know.
39
+ /**
40
+ * How many install records the shelf reads to decide what it already runs.
41
+ *
42
+ * One number for the five collections that answer that question, because the
43
+ * card reads them together and a reader comparing a plugin's chip to a
44
+ * dataset's should not have to know which window was twice the other. It
45
+ * bounds an install-state LOOKUP, not the shelf: the listings query has its
46
+ * own cap, and the grid pages what survives the filters.
47
+ */ const INSTALL_STATE_CEILING = 100;
48
+ /**
49
+ * Marketplace components browse (AGL-44). A read-only catalogue: each card links
50
+ * to the listing's detail page, which is the only place an install happens
51
+ * (AGL-867) — installing from a grid card was too easy and skipped the
52
+ * site-targeting choice. The card still shows install STATE (installed,
53
+ * org-wide) so the shelf reads honestly, but carries no install action.
54
+ */ export function MarketplaceBrowse(props) {
55
+ const { hostId } = props;
56
+ const firestore = useFirestore();
57
+ const { data: user } = useUser();
58
+ const { orgScoped, orgSlug: orgSlugProp, publisherId } = props;
59
+ const [handles, setHandles] = useState({});
60
+ // Listings are org-owned (AGL-652), so "is this mine" is an org comparison.
61
+ // Resolved from the routing mirror rather than a new prop so the component
62
+ // stays self-contained; hostIndex is signed-in readable.
63
+ const [viewerOrgId, setViewerOrgId] = useState(null);
64
+ useEffect(()=>{
65
+ let active = true;
66
+ void getDoc(doc(firestore, 'hostIndex', hostId)).then((snapshot)=>{
67
+ var _snapshot_get;
68
+ if (active) setViewerOrgId((_snapshot_get = snapshot.get('orgId')) != null ? _snapshot_get : null);
69
+ }).catch(()=>undefined);
70
+ return ()=>{
71
+ active = false;
72
+ };
73
+ }, [
74
+ firestore,
75
+ hostId
76
+ ]);
77
+ // Card links were built as `/{hostDocId}/marketplace/…`, a shape that has
78
+ // not resolved since AGL-621/622 — every listing and publisher link on
79
+ // this grid 404'd. One shared resolution (AGL-673); null renders plain
80
+ // text rather than a link to nowhere.
81
+ const { orgSlug: resolvedOrgSlug, subdomain } = useConsoleHostRoute(hostId);
82
+ // Prefer the URL-supplied slug (AGL-867) — synchronous and always present at
83
+ // org scope — over the async host resolution, which the per-site route still
84
+ // relies on.
85
+ const orgSlug = orgSlugProp != null ? orgSlugProp : resolvedOrgSlug;
86
+ // Link targets differ by surface (AGL-772): the org marketplace resolves
87
+ // to the org route (`/[orgSlug]/marketplace/[listingId]`), the per-site tab
88
+ // to the host route. Null → plain text, never a link to nowhere. Publisher
89
+ // pages have no org route yet, so they stay text at org scope for now.
90
+ const listingHref = (listingId)=>orgScoped ? orgSlug ? buildRoute(Route.ORG_MARKETPLACE_LISTING, {
91
+ orgSlug,
92
+ listingId
93
+ }) : undefined : orgSlug && subdomain ? buildRoute(Route.ORG_MARKETPLACE_LISTING, {
94
+ orgSlug,
95
+ listingId
96
+ }) : undefined;
97
+ // Storefront links carry the publisher's HANDLE (AGL-1001), the identity
98
+ // shown right beside them on the card. The id remains a valid segment on
99
+ // the page itself, so a publisher whose handle hasn't loaded yet still
100
+ // links somewhere real rather than nowhere.
101
+ const publisherHref = (profileId)=>{
102
+ var _handles_profileId, _handles_profileId1;
103
+ return orgScoped ? // listings. Needs the URL slug, which is passed in at org scope.
104
+ orgSlug ? buildRoute(Route.ORG_MARKETPLACE_PUBLISHER, {
105
+ orgSlug,
106
+ handle: (_handles_profileId = handles[profileId]) != null ? _handles_profileId : profileId
107
+ }) : undefined : orgSlug && subdomain ? buildRoute(Route.ORG_MARKETPLACE_PUBLISHER, {
108
+ orgSlug,
109
+ handle: (_handles_profileId1 = handles[profileId]) != null ? _handles_profileId1 : profileId
110
+ }) : undefined;
111
+ };
112
+ /**
113
+ * NO `where('deletedAt','==',null)` HERE — soft-deleted listings are dropped
114
+ * in `items` below instead (AGL-1196).
115
+ *
116
+ * `deletedAt` flips every time a publisher unpublishes or republishes. A
117
+ * document that stops matching a live query leaves the query target, and the
118
+ * client can cache a `noDocument` tombstone at its own path — which is then
119
+ * served to every reader of that path, including the detail page, which
120
+ * reads `marketplaceListings/{id}` BY ID. So a browse session open across an
121
+ * unpublish could 404 a listing that exists, and republishing would not
122
+ * clear it: a resumed listen only pulls deltas, so an otherwise-unchanged
123
+ * document is never re-sent (AGL-827, AGL-929).
124
+ *
125
+ * Dropping the predicate removes the mechanism rather than healing it: with
126
+ * no `where`, no document can stop matching. The rule permits it —
127
+ * `marketplaceListings` is `allow read: if true`, with no `resource.data`
128
+ * term, so an unconstrained list is not denied. (That is NOT true of the
129
+ * scoped `datasets` query below, which is why this fix does not generalise.)
130
+ *
131
+ * It also fixes a quieter bug: `== null` matches only an EXPLICIT null, and
132
+ * Firestore cannot express "field is absent". Every publish path stamps
133
+ * `deletedAt: null` today, so this happens to be exact — but any future
134
+ * write path that omits the field would make a live listing silently
135
+ * invisible. An in-memory falsy check treats absent as live, which is what
136
+ * the dataset and email-template filters in this file already do.
137
+ *
138
+ * The cap counts CANDIDATES, not results — private and unreviewed listings
139
+ * already consume slots and get filtered below, so this is the same class of
140
+ * consumption, nudged up to leave room for soft-deleted rows. Measured on
141
+ * production 2026-08-03: 7 listings, 0 soft-deleted, 0 missing the field.
142
+ *
143
+ * ## The cap DOES take an `orderBy`, and it is not the same decision
144
+ *
145
+ * A `limit` with no ordering is answered in document-id order, and listing
146
+ * ids are random — so the ninety this loaded were an arbitrary ninety, and
147
+ * the client sort below then arranged that sample by date and called it
148
+ * "Newest". The window is now the newest ninety, which is what the default
149
+ * sort already claimed it was.
150
+ *
151
+ * `orderBy` carries the drop the `where` above was removed to avoid — it
152
+ * matches only documents that HAVE the field, so a listing without
153
+ * `createdAt` would not be mis-ordered, it would be invisible. That is safe
154
+ * here for a reason the `deletedAt` predicate could not offer: a listing is
155
+ * created by exactly two paths, `publish.ts` and `publish-plugin.ts`, and
156
+ * both stamp `createdAt` in the same `set` that brings the document into
157
+ * existence. Every other writer resolves an EXISTING listing by id and
158
+ * refuses when it is absent, so none of them can produce one without the
159
+ * field.
160
+ *
161
+ * It also cannot reproduce the tombstone (AGL-827/929) that made a mutable
162
+ * `where` unsafe: `createdAt` is written under `existing.empty` and never
163
+ * rewritten, so no document can stop matching mid-session and leave a
164
+ * `noDocument` cached at a path the detail page reads by id.
165
+ */ const { data: listings } = useFirestoreCollection(()=>query(collection(firestore, 'marketplaceListings'), orderBy('createdAt', 'desc'), limit(90)), [
166
+ firestore
167
+ ], {
168
+ idField: '$id'
169
+ });
170
+ /*
171
+ * ## The install-state lookups are ORDERED too, and they are not lists
172
+ *
173
+ * The five queries below (this one, the two pin collections, the org
174
+ * datasets and the site's email templates) answer one question per card:
175
+ * has this listing already been installed here. None of them is rendered —
176
+ * each is folded into a map keyed by listing id — so the fix that suits the
177
+ * shelf above does not suit them: there is no page to walk and no footer to
178
+ * put a control in.
179
+ *
180
+ * What they share with it is the defect. A bare `limit` is answered in
181
+ * DOCUMENT-ID order, so an unnamed window is an arbitrary slice of its
182
+ * collection, and a lookup MISS is indistinguishable from a genuine
183
+ * not-installed: a workspace past such a window is shown "Add to this site"
184
+ * beside something it already runs, and the detail page — which reads its
185
+ * own pins by id — disagrees with the grid it was reached from.
186
+ *
187
+ * `collectionCeiling` names the order and asks for one row past the
188
+ * ceiling, which is what turns "the window may have bitten" into a fact the
189
+ * shelf can state rather than a silence it cannot.
190
+ */ const { data: installedRead } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'components'), INSTALL_STATE_CEILING), [
191
+ firestore,
192
+ hostId
193
+ ], {
194
+ idField: '$id'
195
+ });
196
+ const { rows: installedDocs, truncated: installedTruncated } = useMemo(()=>ceilingedWindow(installedRead, INSTALL_STATE_CEILING), [
197
+ installedRead
198
+ ]);
199
+ // listingId → installed component doc (deleted installs don't count).
200
+ const installed = useMemo(()=>{
201
+ const map = {};
202
+ for (const definition of installedDocs != null ? installedDocs : []){
203
+ var _definition_marketplace;
204
+ const listingId = definition == null ? void 0 : (_definition_marketplace = definition.marketplace) == null ? void 0 : _definition_marketplace.listingId;
205
+ if (listingId && !definition.deletedAt) map[listingId] = definition;
206
+ }
207
+ return map;
208
+ }, [
209
+ installedDocs
210
+ ]);
211
+ // Plugin installs are version PINS, not component snapshots (AGL-656): the
212
+ // `components` map above never holds one, so a plugin already installed —
213
+ // at host or org scope — showed "Add to this site". These two pin
214
+ // collections are what the loader honors (a host pin shadows an org pin).
215
+ const orgId = useHostOrgId(hostId);
216
+ const { data: hostPinRead } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'installs'), INSTALL_STATE_CEILING), [
217
+ firestore,
218
+ hostId
219
+ ], {
220
+ idField: '$id'
221
+ });
222
+ const { rows: hostPinDocs, truncated: hostPinsTruncated } = useMemo(()=>ceilingedWindow(hostPinRead, INSTALL_STATE_CEILING), [
223
+ hostPinRead
224
+ ]);
225
+ // Held at null while `useHostOrgId` is in flight, never `orgs/-pending-`
226
+ // (AGL-1440): the AGL-1047 comment seven lines below records the same
227
+ // denial-every-mount shape for `useScopeTokens` — this listen had it too.
228
+ const { data: orgPinRead } = useFirestoreCollection(()=>orgId ? collectionCeiling(collection(firestore, 'orgs', orgId, 'installs'), INSTALL_STATE_CEILING) : null, [
229
+ firestore,
230
+ orgId
231
+ ], {
232
+ idField: '$id'
233
+ });
234
+ const { rows: orgPinDocs, truncated: orgPinsTruncated } = useMemo(()=>ceilingedWindow(orgPinRead, INSTALL_STATE_CEILING), [
235
+ orgPinRead
236
+ ]);
237
+ // Scoped (AGL-1044): an unfiltered list is REJECTED for a scoped member,
238
+ // not filtered, so this would error without the constraint.
239
+ //
240
+ // Wait for the member doc before listing (AGL-1047). `useScopeTokens`
241
+ // reports `orgWide: true` while loading, so without `scopeLoaded` a scoped
242
+ // collaborator's first render computes `needsScope: false` and sends an
243
+ // UNFILTERED list that the AGL-1041 rules deny per document. It recovers
244
+ // on the next render, which is what makes it easy to miss: the page looks
245
+ // right and logs a denial every mount.
246
+ const { tokens: scopeTokens, orgWide: viewerOrgWide, loaded: scopeLoaded } = useScopeTokens(orgId != null ? orgId : undefined);
247
+ const needsScope = Boolean(orgId) && !viewerOrgWide;
248
+ // The AGL-657 types land in neither the components collection nor a pin
249
+ // (AGL-789): a dataset schema becomes an org dataset, an email template a
250
+ // draft version. Both installers stamp the source listing, so read those.
251
+ /*
252
+ * `documentId()` rather than a field, for the reason the audience sweep in
253
+ * `campaign-send.ts` gives: Firestore's automatic single-field index for an
254
+ * array member is keyed on the value and the document name, so
255
+ * `array-contains-any` plus `orderBy(__name__)` is served by it. Ordering on
256
+ * anything else would need a composite index per scope shape.
257
+ *
258
+ * The ceiling is the shared one. It is not sized for the dataset
259
+ * collection: it is sized for the number of LISTINGS whose install state a
260
+ * shelf of ninety cards can describe, which is the same question its four
261
+ * neighbours answer, so it is the same number.
262
+ */ const { data: datasetRead } = useFirestoreCollection(()=>orgId && scopeLoaded ? query(collection(firestore, 'orgs', orgId, 'datasets'), ...needsScope ? [
263
+ where('visibleTo', 'array-contains-any', scopeTokens)
264
+ ] : [], orderBy(documentId()), limit(INSTALL_STATE_CEILING + 1)) : null, [
265
+ firestore,
266
+ orgId,
267
+ scopeLoaded,
268
+ needsScope,
269
+ scopeTokens
270
+ ], // `visibleTo` is MUTABLE — every scope edit rewrites it — so this query
271
+ // can tombstone a dataset the way AGL-827 tombstoned a host. The rule
272
+ // requires the constraint for anyone who is not org-wide, so unlike the
273
+ // listings query above the predicate cannot simply be dropped (AGL-1196).
274
+ {
275
+ idField: '$id',
276
+ confirmDisappearances: true
277
+ });
278
+ const { rows: datasetDocs, truncated: datasetsTruncated } = useMemo(()=>ceilingedWindow(datasetRead, INSTALL_STATE_CEILING), [
279
+ datasetRead
280
+ ]);
281
+ const { data: emailRead } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'emailTemplates'), INSTALL_STATE_CEILING), [
282
+ firestore,
283
+ hostId
284
+ ], {
285
+ idField: '$id'
286
+ });
287
+ const { rows: emailDocs, truncated: emailsTruncated } = useMemo(()=>ceilingedWindow(emailRead, INSTALL_STATE_CEILING), [
288
+ emailRead
289
+ ]);
290
+ // A theme is not a document in a collection — it is a field on the site
291
+ // (AGL-1020) — so its install is read from the host doc rather than from a
292
+ // per-artifact query like the two above.
293
+ const { data: hostDoc } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId), [
294
+ firestore,
295
+ hostId
296
+ ], {
297
+ idField: '$id'
298
+ });
299
+ // listingId → the newest install of it. A schema install deliberately makes
300
+ // a NEW dataset every time, so this is genuinely one-to-many; the highest
301
+ // installed version is what the card should speak for.
302
+ const artifactInstalls = useMemo(()=>{
303
+ var _hostDoc_themeInstalledFrom, _hostDoc_themeInstalledFrom1;
304
+ const map = {};
305
+ const note = (listingId, version)=>{
306
+ var _map_id;
307
+ const id = listingId ? String(listingId) : '';
308
+ if (!id) return;
309
+ const next = version != null ? String(version) : null;
310
+ const seen = (_map_id = map[id]) == null ? void 0 : _map_id.version;
311
+ if (!seen || next && next > seen) map[id] = {
312
+ version: next
313
+ };
314
+ };
315
+ for (const dataset of datasetDocs != null ? datasetDocs : []){
316
+ var _dataset_source, _dataset_source1;
317
+ if (dataset.deletedAt) continue;
318
+ note((_dataset_source = dataset.source) == null ? void 0 : _dataset_source.listingId, (_dataset_source1 = dataset.source) == null ? void 0 : _dataset_source1.version);
319
+ }
320
+ for (const template of emailDocs != null ? emailDocs : []){
321
+ var _template_installedFrom, _template_installedFrom1;
322
+ if (template.deletedAt) continue;
323
+ note((_template_installedFrom = template.installedFrom) == null ? void 0 : _template_installedFrom.listingId, (_template_installedFrom1 = template.installedFrom) == null ? void 0 : _template_installedFrom1.version);
324
+ }
325
+ note(hostDoc == null ? void 0 : (_hostDoc_themeInstalledFrom = hostDoc.themeInstalledFrom) == null ? void 0 : _hostDoc_themeInstalledFrom.listingId, hostDoc == null ? void 0 : (_hostDoc_themeInstalledFrom1 = hostDoc.themeInstalledFrom) == null ? void 0 : _hostDoc_themeInstalledFrom1.version);
326
+ return map;
327
+ }, [
328
+ datasetDocs,
329
+ emailDocs,
330
+ hostDoc
331
+ ]);
332
+ const hostPins = useMemo(()=>{
333
+ const map = {};
334
+ for (const pin of hostPinDocs != null ? hostPinDocs : [])map[pin.$id] = pin;
335
+ return map;
336
+ }, [
337
+ hostPinDocs
338
+ ]);
339
+ const orgPins = useMemo(()=>{
340
+ const map = {};
341
+ for (const pin of orgPinDocs != null ? orgPinDocs : [])map[pin.$id] = pin;
342
+ return map;
343
+ }, [
344
+ orgPinDocs
345
+ ]);
346
+ // Resolve each listing's publisher handle once. `handles` must NOT be a
347
+ // dependency here: the effect writes `handles`, so listing it would make the
348
+ // effect re-run on its own output. During the post-load window, when
349
+ // `listings` arrives while every other subscription on this always-mounted
350
+ // grid is also settling, that self-retrigger adds render+effect cycles to an
351
+ // already dense flurry — enough that a concurrent update elsewhere can trip
352
+ // React's nested-update limit (AGL-785). A ref of already-requested ids
353
+ // dedupes instead, so the effect depends only on `listings`.
354
+ const requestedHandles = useRef(new Set());
355
+ useEffect(()=>{
356
+ const profileIds = [
357
+ ...new Set((listings != null ? listings : []).map((listing)=>listing.profileId))
358
+ ].filter((profileId)=>profileId && !requestedHandles.current.has(String(profileId)));
359
+ if (!profileIds.length) return;
360
+ for (const profileId of profileIds)requestedHandles.current.add(String(profileId));
361
+ let cancelled = false;
362
+ Promise.all(profileIds.map(async (profileId)=>{
363
+ var _ref;
364
+ const snapshot = await getDoc(doc(firestore, 'publisherProfiles', String(profileId))).catch(()=>null);
365
+ return [
366
+ profileId,
367
+ (_ref = snapshot == null ? void 0 : snapshot.get('handle')) != null ? _ref : ''
368
+ ];
369
+ })).then((entries)=>{
370
+ if (!cancelled) {
371
+ setHandles((prev)=>_extends({}, prev, Object.fromEntries(entries)));
372
+ }
373
+ });
374
+ return ()=>{
375
+ cancelled = true;
376
+ };
377
+ }, [
378
+ listings,
379
+ firestore
380
+ ]);
381
+ // Browse controls (AGL-95): client-side search/filter/sort over the
382
+ // fetched page of listings.
383
+ const [search, setSearch] = useState('');
384
+ const [category, setCategory] = useState(null);
385
+ const [sort, setSort] = useState('newest');
386
+ const categories = useMemo(()=>[
387
+ ...new Set((listings != null ? listings : []).map((listing)=>listing.category).filter(Boolean))
388
+ ].sort(), [
389
+ listings
390
+ ]);
391
+ const items = useMemo(()=>{
392
+ const needle = search.trim().toLowerCase();
393
+ const filtered = (listings != null ? listings : []).filter((listing)=>{
394
+ // Private plugins never appear here, not even for the org that owns
395
+ // them (AGL-968/993). The owner exemption below exists so a publisher
396
+ // can watch their own SUBMISSION move through review — but a private
397
+ // listing is not waiting to be listed, it is deliberately not for the
398
+ // marketplace, and showing it in this grid is the one thing "private"
399
+ // promises will not happen. Owners reach theirs from
400
+ // Marketplace › Listings, whose View opens the same detail page
401
+ // installs happen on.
402
+ // Soft-deleted listings used to be excluded by the query itself; they
403
+ // are dropped here now so no mutable field sits in a `where`
404
+ // (AGL-1196). Unconditional — unlike the review gate below, deletion
405
+ // has no owner exemption.
406
+ if (isListingDeleted(listing)) return false;
407
+ if (isPrivateListing(listing)) return false;
408
+ // Review queue gate (AGL-432): unreviewed/rejected plugin listings
409
+ // stay off the public browse; the owner still sees their own (the
410
+ // detail page shows them the status). UX-level only — the docs are
411
+ // public-readable by design.
412
+ if (!isListingBrowsable(listing) && listing.profileId !== viewerOrgId) {
413
+ return false;
414
+ }
415
+ // Publisher page (AGL-869): only this publisher's listings.
416
+ if (publisherId && listing.profileId !== publisherId) return false;
417
+ if (category && listing.category !== category) return false;
418
+ if (!needle) return true;
419
+ return [
420
+ listing.displayName,
421
+ listing.description,
422
+ listing.category
423
+ ].filter(Boolean).some((value)=>value.toLowerCase().includes(needle));
424
+ });
425
+ return [
426
+ ...filtered
427
+ ].sort((a, b)=>{
428
+ var _ref, _ref1;
429
+ var _b_createdAt, _a_createdAt;
430
+ if (sort === 'installed') {
431
+ var _b_installCount, _a_installCount;
432
+ return ((_b_installCount = b.installCount) != null ? _b_installCount : 0) - ((_a_installCount = a.installCount) != null ? _a_installCount : 0);
433
+ }
434
+ if (sort === 'rated') {
435
+ var _b_ratingAverage, _a_ratingAverage, _b_ratingCount, _a_ratingCount;
436
+ // Unrated listings sort last rather than as zero-stars — a new
437
+ // listing has not been judged badly, it has not been judged.
438
+ const byAverage = ((_b_ratingAverage = b.ratingAverage) != null ? _b_ratingAverage : -1) - ((_a_ratingAverage = a.ratingAverage) != null ? _a_ratingAverage : -1);
439
+ return byAverage || ((_b_ratingCount = b.ratingCount) != null ? _b_ratingCount : 0) - ((_a_ratingCount = a.ratingCount) != null ? _a_ratingCount : 0);
440
+ }
441
+ return ((_ref = (_b_createdAt = b.createdAt) == null ? void 0 : _b_createdAt.seconds) != null ? _ref : 0) - ((_ref1 = (_a_createdAt = a.createdAt) == null ? void 0 : _a_createdAt.seconds) != null ? _ref1 : 0);
442
+ });
443
+ }, [
444
+ listings,
445
+ search,
446
+ category,
447
+ sort,
448
+ publisherId,
449
+ user == null ? void 0 : user.uid
450
+ ]);
451
+ /*
452
+ * The shelf PAGES (AGL-2501). It used to render every card the window held
453
+ * in one wall, so the only control over how much arrived at once was the
454
+ * cap in the query — a number the reader cannot see and cannot change.
455
+ *
456
+ * Client-side, deliberately, and this is the case `ListPagination`'s
457
+ * `count` prop exists for. The search box, the category chips and the
458
+ * owner/review gates are all resolved in memory, so a server page would be
459
+ * a page of CANDIDATES: a reader filtering to one category would get pages
460
+ * that were mostly empty and a count that meant nothing. Paging what is
461
+ * left after the filter is the only version whose numbers are about what is
462
+ * on screen.
463
+ */ /**
464
+ * The install-state lookups did not read everything they were asked about.
465
+ *
466
+ * Said on the SHELF rather than on the card that is wrong, because there is
467
+ * no way to know which card that is: a miss and a genuine not-installed are
468
+ * the same absence from the same map. What the reader can act on is that the
469
+ * chips are a floor — anything marked installed is installed, and something
470
+ * unmarked may still be.
471
+ */ const installStateTruncated = installedTruncated || hostPinsTruncated || orgPinsTruncated || datasetsTruncated || emailsTruncated;
472
+ const [page, setPage] = useState(0);
473
+ const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
474
+ /*
475
+ * A new query starts at page one. Page four of a category with two pages
476
+ * does not exist, and an out-of-range page renders as an empty grid with
477
+ * nothing saying why — which reads as the filter having broken.
478
+ */ useEffect(()=>{
479
+ setPage(0);
480
+ }, [
481
+ search,
482
+ category,
483
+ sort,
484
+ pageSize
485
+ ]);
486
+ const shown = useMemo(()=>items.slice(page * pageSize, (page + 1) * pageSize), [
487
+ items,
488
+ page,
489
+ pageSize
490
+ ]);
491
+ return /*#__PURE__*/ _jsxs(CardDisplay, {
492
+ header: 'Marketplace components',
493
+ help: pluginDocsHelp('plugins', {
494
+ anchor: '#install--upgrade'
495
+ }),
496
+ contentGutterX: true,
497
+ contentGutterY: true,
498
+ children: [
499
+ /*#__PURE__*/ _jsxs(Stack, {
500
+ direction: "row",
501
+ spacing: 1,
502
+ sx: {
503
+ mb: 2,
504
+ alignItems: 'center',
505
+ flexWrap: 'wrap',
506
+ rowGap: 1
507
+ },
508
+ children: [
509
+ /*#__PURE__*/ _jsx(TextField, {
510
+ placeholder: "Search components…",
511
+ value: search,
512
+ onChange: (event)=>setSearch(event.target.value),
513
+ size: "small",
514
+ sx: {
515
+ minWidth: 200
516
+ }
517
+ }),
518
+ categories.map((value)=>/*#__PURE__*/ _jsx(Chip, {
519
+ label: value,
520
+ variant: category === value ? 'filled' : 'outlined',
521
+ color: category === value ? 'primary' : 'default',
522
+ onClick: ()=>setCategory((previous)=>previous === value ? null : value)
523
+ }, value)),
524
+ /*#__PURE__*/ _jsxs(TextField, {
525
+ value: sort,
526
+ onChange: (event)=>setSort(event.target.value),
527
+ size: "small",
528
+ select: true,
529
+ sx: {
530
+ ml: 'auto',
531
+ minWidth: 150
532
+ },
533
+ children: [
534
+ /*#__PURE__*/ _jsx(MenuItem, {
535
+ value: "newest",
536
+ children: 'Newest'
537
+ }),
538
+ /*#__PURE__*/ _jsx(MenuItem, {
539
+ value: "installed",
540
+ children: 'Most installed'
541
+ }),
542
+ /*#__PURE__*/ _jsx(MenuItem, {
543
+ value: "rated",
544
+ children: 'Highest rated'
545
+ })
546
+ ]
547
+ })
548
+ ]
549
+ }),
550
+ installStateTruncated ? /*#__PURE__*/ _jsx(Alert, {
551
+ severity: "info",
552
+ sx: {
553
+ mb: 2
554
+ },
555
+ children: `Installed state is resolved from the first ${INSTALL_STATE_CEILING} ` + 'install records in this workspace, ordered by id. There are ' + 'more, so a card may not be marked installed even though it is. ' + 'Open a listing to see what this workspace actually runs.'
556
+ }) : null,
557
+ items.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
558
+ variant: "body2",
559
+ color: "text.secondary",
560
+ children: 'No marketplace components published yet — publish one of your ' + 'reusable components from the Setup page to be the first.'
561
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
562
+ children: [
563
+ /*#__PURE__*/ _jsx(Grid, {
564
+ container: true,
565
+ spacing: 2,
566
+ children: shown.map((listing)=>{
567
+ var _ref, _listing_priceUsd, _listing_ratingAverage, _listing_ratingAverage1;
568
+ var _componentInstall_marketplace;
569
+ const artifactType = listingArtifactType(listing);
570
+ const isPlugin = artifactType === 'plugin';
571
+ const pluginState = resolvePluginInstallState(listing.latestVersion, isPlugin ? hostPins[listing.$id] : null, isPlugin ? orgPins[listing.$id] : null);
572
+ const componentInstall = installed[listing.$id];
573
+ // datasetSchema/emailTemplate installs are tracked by the source
574
+ // listing stamped on what they created (AGL-789).
575
+ //
576
+ // `emailStarter` is deliberately absent. Every other type has ONE
577
+ // install per site to point at — a pin, a dataset, a draft on a
578
+ // fixed catalog key, the site's theme — so "Installed" names
579
+ // something. A starter installs as a NEW email each time, and a
580
+ // site may keep five of them; there is no single copy for a chip
581
+ // to describe, and marking the listing installed would grey out
582
+ // the button that makes the sixth.
583
+ const artifactInstall = artifactType === 'datasetSchema' || artifactType === 'emailTemplate' || artifactType === 'theme' ? artifactInstalls[listing.$id] : undefined;
584
+ const isInstalled = isPlugin ? pluginState.scope != null : Boolean(componentInstall != null ? componentInstall : artifactInstall);
585
+ const installedVersion = isPlugin ? pluginState.installedVersion : (_ref = componentInstall == null ? void 0 : (_componentInstall_marketplace = componentInstall.marketplace) == null ? void 0 : _componentInstall_marketplace.version) != null ? _ref : artifactInstall == null ? void 0 : artifactInstall.version;
586
+ const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
587
+ const detailHref = listingHref(listing.$id);
588
+ return /*#__PURE__*/ _jsx(Grid, {
589
+ size: {
590
+ xs: 12,
591
+ sm: 6,
592
+ md: 4
593
+ },
594
+ children: /*#__PURE__*/ _jsxs(Stack, {
595
+ spacing: 1,
596
+ sx: {
597
+ border: 1,
598
+ borderColor: 'divider',
599
+ borderRadius: 1,
600
+ p: 2,
601
+ height: '100%'
602
+ },
603
+ children: [
604
+ /*#__PURE__*/ _jsx(ListingImage, {
605
+ src: listing.previewImageUrl,
606
+ alt: `${listing.displayName} preview`,
607
+ sx: {
608
+ width: '100%',
609
+ height: 120,
610
+ objectFit: 'cover',
611
+ borderRadius: 1
612
+ }
613
+ }),
614
+ /*#__PURE__*/ _jsx(AppLink, {
615
+ href: listingHref(listing.$id),
616
+ color: "inherit",
617
+ underline: "hover",
618
+ variant: "subtitle2",
619
+ children: listing.displayName
620
+ }),
621
+ /*#__PURE__*/ _jsxs(Stack, {
622
+ direction: "row",
623
+ spacing: 1,
624
+ sx: {
625
+ alignItems: 'center',
626
+ flexWrap: 'wrap',
627
+ rowGap: 1
628
+ },
629
+ children: [
630
+ /*#__PURE__*/ _jsx(Chip, {
631
+ size: "small",
632
+ color: "primary",
633
+ label: listingArtifactLabel(listing)
634
+ }),
635
+ listing.category ? /*#__PURE__*/ _jsx(Chip, {
636
+ size: "small",
637
+ label: listing.category
638
+ }) : null,
639
+ listing.reviewStatus === 'verified' ? /*#__PURE__*/ _jsx(Tooltip, {
640
+ title: `A human at ${PLATFORM_BRAND_NAME} confirmed who ` + 'this publisher is, ' + 'and that the listing describes what the code does. ' + 'It is a claim about the publisher, not about this ' + 'release — it survives a version bump.',
641
+ children: /*#__PURE__*/ _jsx(Chip, {
642
+ size: "small",
643
+ color: "info",
644
+ label: "Verified publisher",
645
+ // Carries more weight than the neighbouring
646
+ // classification chips on purpose: it is the only one
647
+ // that says a human vouched for the code, and the
648
+ // rest are just taxonomy. The icon is what makes it
649
+ // findable when scanning a grid rather than reading
650
+ // one card.
651
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
652
+ path: mdiCheckDecagram.path,
653
+ sx: {
654
+ fontSize: 16
655
+ }
656
+ }),
657
+ sx: {
658
+ fontWeight: 600
659
+ }
660
+ })
661
+ }) : null,
662
+ listing.latestVersionReviewState === 'approved' ? /*#__PURE__*/ _jsx(Tooltip, {
663
+ title: `A human at ${PLATFORM_BRAND_NAME} read these exact ` + 'bytes — the ' + 'version on offer — against a required checklist. ' + 'Re-earned per version, so a new release starts ' + 'without it. Not a security guarantee: every plugin ' + 'runs in the same sandbox either way.',
664
+ children: /*#__PURE__*/ _jsx(Chip, {
665
+ size: "small",
666
+ color: "success",
667
+ label: "Reviewed"
668
+ })
669
+ }) : null,
670
+ /*#__PURE__*/ _jsx(Chip, {
671
+ size: "small",
672
+ color: priceUsd > 0 ? 'primary' : 'success',
673
+ variant: priceUsd > 0 ? 'filled' : 'outlined',
674
+ label: priceUsd > 0 ? `$${priceUsd}` : 'Free'
675
+ })
676
+ ]
677
+ }),
678
+ /*#__PURE__*/ _jsxs(Typography, {
679
+ variant: "caption",
680
+ color: "text.secondary",
681
+ children: [
682
+ `v${listing.latestVersion}`,
683
+ handles[listing.profileId] ? /*#__PURE__*/ _jsxs(_Fragment, {
684
+ children: [
685
+ ' · by ',
686
+ /*#__PURE__*/ _jsx(AppLink, {
687
+ href: publisherHref(listing.profileId),
688
+ color: "primary",
689
+ underline: "hover",
690
+ children: `@${handles[listing.profileId]}`
691
+ })
692
+ ]
693
+ }) : '',
694
+ listing.installCount ? ` · ${listing.installCount} install${listing.installCount === 1 ? '' : 's'}` : ''
695
+ ]
696
+ }),
697
+ /*#__PURE__*/ _jsx(Stack, {
698
+ direction: "row",
699
+ spacing: 0.5,
700
+ sx: {
701
+ alignItems: 'center'
702
+ },
703
+ children: listing.ratingCount ? /*#__PURE__*/ _jsxs(_Fragment, {
704
+ children: [
705
+ /*#__PURE__*/ _jsx(Rating, {
706
+ size: "small",
707
+ readOnly: true,
708
+ precision: 0.1,
709
+ value: Number((_listing_ratingAverage = listing.ratingAverage) != null ? _listing_ratingAverage : 0)
710
+ }),
711
+ /*#__PURE__*/ _jsx(Typography, {
712
+ variant: "caption",
713
+ color: "text.secondary",
714
+ children: `${(_listing_ratingAverage1 = listing.ratingAverage) != null ? _listing_ratingAverage1 : 0} (${listing.ratingCount})`
715
+ })
716
+ ]
717
+ }) : /*#__PURE__*/ _jsx(Typography, {
718
+ variant: "caption",
719
+ color: "text.secondary",
720
+ children: 'Not yet rated'
721
+ })
722
+ }),
723
+ listing.description ? /*#__PURE__*/ _jsx(Typography, {
724
+ variant: "body2",
725
+ color: "text.secondary",
726
+ sx: {
727
+ flex: 1
728
+ },
729
+ // Four lines, then ellipsis (AGL-1002). Grid rows
730
+ // stretch to their tallest card, so one publisher's
731
+ // essay used to leave every card beside it mostly white
732
+ // space; the full text is one click away.
733
+ //
734
+ // Plain `style`, not `sx`: emotion drops the
735
+ // `-webkit-box` display value on the way through, and
736
+ // without it the other three properties clamp nothing
737
+ // (measured — `display` computed to `flow-root` while
738
+ // line-clamp and box-orient came through fine). Nothing
739
+ // here reads the theme, so there is nothing to lose.
740
+ style: {
741
+ display: '-webkit-box',
742
+ WebkitBoxOrient: 'vertical',
743
+ WebkitLineClamp: 4,
744
+ overflow: 'hidden'
745
+ },
746
+ children: listing.description
747
+ }) : /*#__PURE__*/ _jsx("span", {
748
+ style: {
749
+ flex: 1
750
+ }
751
+ }),
752
+ isInstalled ? /*#__PURE__*/ _jsxs(Stack, {
753
+ direction: "row",
754
+ spacing: 0.75,
755
+ sx: {
756
+ alignItems: 'center',
757
+ flexWrap: 'wrap'
758
+ },
759
+ children: [
760
+ /*#__PURE__*/ _jsx(Typography, {
761
+ variant: "caption",
762
+ color: "success.main",
763
+ children: `Installed${installedVersion ? ` (v${installedVersion})` : ''}`
764
+ }),
765
+ isPlugin && pluginState.scope === 'org' ? /*#__PURE__*/ _jsx(Chip, {
766
+ size: "small",
767
+ variant: "outlined",
768
+ label: pluginState.shadowed ? 'Org-wide (shadowed)' : 'Org-wide'
769
+ }) : null
770
+ ]
771
+ }) : null,
772
+ /*#__PURE__*/ _jsx(AppLink, {
773
+ componentVariant: "button",
774
+ size: "small",
775
+ variant: "outlined",
776
+ color: "primary",
777
+ href: detailHref != null ? detailHref : '',
778
+ disabled: !detailHref,
779
+ children: priceUsd > 0 && !isInstalled ? `View details · $${priceUsd}` : 'View details'
780
+ })
781
+ ]
782
+ })
783
+ }, listing.$id);
784
+ })
785
+ }),
786
+ /*#__PURE__*/ _jsx(ListPagination, {
787
+ page: page,
788
+ pageSize: pageSize,
789
+ rowCount: shown.length,
790
+ count: items.length,
791
+ onPageChange: setPage,
792
+ onPageSizeChange: setPageSize
793
+ })
794
+ ]
795
+ })
796
+ ]
797
+ });
798
+ }
799
+ MarketplaceBrowse.displayName = 'MarketplaceBrowse';
800
+ export default MarketplaceBrowse;
801
+
802
+ //# sourceMappingURL=marketplace-browse.component.js.map