@mohasinac/appkit 4.66.0 → 4.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/maintenance/index.d.ts +1 -0
  3. package/dist/_internal/client/features/maintenance/index.js +1 -0
  4. package/dist/_internal/client/features/maintenance/views/CacheInvalidationPanel.d.ts +33 -0
  5. package/dist/_internal/client/features/maintenance/views/CacheInvalidationPanel.js +96 -0
  6. package/dist/_internal/client/features/maintenance/views/MaintenanceDashboardView.js +2 -1
  7. package/dist/_internal/server/features/products/surface-counts.d.ts +26 -0
  8. package/dist/_internal/server/features/products/surface-counts.js +60 -0
  9. package/dist/_internal/server/features/seo/sitemap.js +31 -2
  10. package/dist/_internal/server/features/site-settings/adapters.d.ts +16 -0
  11. package/dist/_internal/server/features/site-settings/adapters.js +21 -0
  12. package/dist/_internal/server/functions/firestore.d.ts +18 -1
  13. package/dist/_internal/server/functions/firestore.js +25 -1
  14. package/dist/_internal/server/jobs/core/onProductWrite.js +33 -0
  15. package/dist/_internal/server/jobs/core/surfaceCountsRollup.d.ts +55 -0
  16. package/dist/_internal/server/jobs/core/surfaceCountsRollup.js +128 -0
  17. package/dist/_internal/server/jobs/handlers/countersReconcile.js +54 -1
  18. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  19. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  20. package/dist/_internal/server/jobs/handlers/onSiteSettingsWrite.d.ts +53 -0
  21. package/dist/_internal/server/jobs/handlers/onSiteSettingsWrite.js +63 -0
  22. package/dist/_internal/shared/actions/action-registry.js +28 -0
  23. package/dist/_internal/shared/cache/revalidate-targets.js +66 -47
  24. package/dist/_internal/shared/features/site-settings/types.d.ts +39 -0
  25. package/dist/_internal/shared/listing-types/feature-flags.d.ts +45 -0
  26. package/dist/_internal/shared/listing-types/feature-flags.js +51 -0
  27. package/dist/_internal/shared/listing-types/hidden-surfaces.d.ts +43 -0
  28. package/dist/_internal/shared/listing-types/hidden-surfaces.js +57 -0
  29. package/dist/client.d.ts +4 -2
  30. package/dist/client.js +10 -1
  31. package/dist/constants/api-endpoints.d.ts +3 -0
  32. package/dist/constants/api-endpoints.js +11 -0
  33. package/dist/features/products/schemas/surface-counts.d.ts +83 -0
  34. package/dist/features/products/schemas/surface-counts.js +58 -0
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +3 -1
  37. package/dist/react/hooks/useListingSurfaceVisibility.d.ts +44 -0
  38. package/dist/react/hooks/useListingSurfaceVisibility.js +20 -0
  39. package/dist/server.d.ts +2 -0
  40. package/dist/server.js +12 -0
  41. package/package.json +33 -1
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Which public BROWSE ROUTES must not be served, given the admin flags and the
3
+ * measured counts.
4
+ *
5
+ * ## Why this is one function and not two copies
6
+ *
7
+ * Two places need this answer and they must never disagree:
8
+ *
9
+ * · `buildSitemap` — decides which browse URLs are advertised.
10
+ * · `toPublicSiteSettings` → `src/proxy.ts` — decides which browse URLs 404.
11
+ *
12
+ * A disagreement is not cosmetic: the sitemap advertising a URL the edge 404s
13
+ * is a crawl error on a page we told Google to fetch, and the reverse serves a
14
+ * surface we have decided is closed. They were two hand-written copies of the
15
+ * same two loops when this was extracted.
16
+ *
17
+ * ## 🛑 EXACT PATHS, never prefixes
18
+ *
19
+ * `art`, `stickers`, `spares` and `standard` all detail at `/products/{slug}`,
20
+ * so a prefix rule would delist — or 404 — the entire standard catalogue the
21
+ * day `spares` emptied. An empty type contributes no detail URLs anyway, since
22
+ * its own fetcher returns none, so the exact-path rule loses nothing real.
23
+ *
24
+ * ## 🛑 A SHARED route survives while ANY of its types is visible
25
+ *
26
+ * `/art` spans `art` and `stickers`; `/products` spans `standard` plus several
27
+ * others. Without the second pass, hiding `stickers` alone would close a page
28
+ * `art` still fills — and hiding `spares` alone would close `/products`.
29
+ *
30
+ * @tag domain:products
31
+ * @tag layer:shared
32
+ * @tag access:isomorphic
33
+ * @tag sideEffects:none
34
+ */
35
+ import { type SurfaceCountSnapshot } from "./feature-flags";
36
+ /** The snapshot shape `isListingSurfaceVisible` reads its admin flags from. */
37
+ interface SettingsSnapshot {
38
+ listings?: {
39
+ listingTypes?: Partial<Record<string, boolean>>;
40
+ };
41
+ }
42
+ export declare function hiddenSurfaceRoutes(settings: SettingsSnapshot | null | undefined, counts: SurfaceCountSnapshot | null | undefined): string[];
43
+ export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Which public BROWSE ROUTES must not be served, given the admin flags and the
3
+ * measured counts.
4
+ *
5
+ * ## Why this is one function and not two copies
6
+ *
7
+ * Two places need this answer and they must never disagree:
8
+ *
9
+ * · `buildSitemap` — decides which browse URLs are advertised.
10
+ * · `toPublicSiteSettings` → `src/proxy.ts` — decides which browse URLs 404.
11
+ *
12
+ * A disagreement is not cosmetic: the sitemap advertising a URL the edge 404s
13
+ * is a crawl error on a page we told Google to fetch, and the reverse serves a
14
+ * surface we have decided is closed. They were two hand-written copies of the
15
+ * same two loops when this was extracted.
16
+ *
17
+ * ## 🛑 EXACT PATHS, never prefixes
18
+ *
19
+ * `art`, `stickers`, `spares` and `standard` all detail at `/products/{slug}`,
20
+ * so a prefix rule would delist — or 404 — the entire standard catalogue the
21
+ * day `spares` emptied. An empty type contributes no detail URLs anyway, since
22
+ * its own fetcher returns none, so the exact-path rule loses nothing real.
23
+ *
24
+ * ## 🛑 A SHARED route survives while ANY of its types is visible
25
+ *
26
+ * `/art` spans `art` and `stickers`; `/products` spans `standard` plus several
27
+ * others. Without the second pass, hiding `stickers` alone would close a page
28
+ * `art` still fills — and hiding `spares` alone would close `/products`.
29
+ *
30
+ * @tag domain:products
31
+ * @tag layer:shared
32
+ * @tag access:isomorphic
33
+ * @tag sideEffects:none
34
+ */
35
+ import { ALL_LISTING_TYPES, isListingSurfaceVisible, } from "./feature-flags.js";
36
+ import { pluginFor } from "./_registry.js";
37
+ export function hiddenSurfaceRoutes(settings, counts) {
38
+ const hidden = new Set();
39
+ const routeOf = (type) => String(pluginFor(type).browseRoute ?? "");
40
+ for (const type of ALL_LISTING_TYPES) {
41
+ if (isListingSurfaceVisible(type, settings, counts))
42
+ continue;
43
+ const route = routeOf(type);
44
+ if (route)
45
+ hidden.add(route);
46
+ }
47
+ /*
48
+ * Second pass, deliberately not folded into the first: a route is only
49
+ * hidden once EVERY type that browses there is hidden, and the first pass
50
+ * cannot know that until it has seen them all.
51
+ */
52
+ for (const type of ALL_LISTING_TYPES) {
53
+ if (isListingSurfaceVisible(type, settings, counts))
54
+ hidden.delete(routeOf(type));
55
+ }
56
+ return [...hidden];
57
+ }
package/dist/client.d.ts CHANGED
@@ -422,6 +422,8 @@ export { useListingTypeFlags } from "./react/hooks/useListingTypeFlags";
422
422
  export type { ListingTypeFlags, ListingTypeFlagsShape, } from "./react/hooks/useListingTypeFlags";
423
423
  export { useBrowsePageFlags } from "./react/hooks/useBrowsePageFlags";
424
424
  export type { BrowsePageFlags, BrowsePageFlagsShape, } from "./react/hooks/useBrowsePageFlags";
425
+ export { useListingSurfaceVisibility } from "./react/hooks/useListingSurfaceVisibility";
426
+ export type { ListingSurfaceVisibility } from "./react/hooks/useListingSurfaceVisibility";
425
427
  export { useActionDispatch } from "./react/hooks/use-action-dispatch";
426
428
  export type { DispatchAction, UseActionDispatchOptions } from "./react/hooks/use-action-dispatch";
427
429
  export { usePanelStore } from "./stores/panel-store";
@@ -535,8 +537,8 @@ export type { TrackArgs as TrackHistoryArgs } from "./features/history/hooks/use
535
537
  export { WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, } from "./constants/limits";
536
538
  export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/utils/listing-type";
537
539
  export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
538
- export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType, isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey, } from "./_internal/shared/listing-types/feature-flags";
539
- export type { BrowsePageKey } from "./_internal/shared/listing-types/feature-flags";
540
+ export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType, isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey, isListingSurfaceVisible, isCountedSurfaceVisible, visibleListingTypes, } from "./_internal/shared/listing-types/feature-flags";
541
+ export type { BrowsePageKey, SurfaceCountSnapshot, } from "./_internal/shared/listing-types/feature-flags";
540
542
  export { ADVANCED_LISTING_TYPE_CAPABILITY, ADVANCED_LISTING_TYPES, BASIC_LISTING_TYPES, LISTING_TYPE_TIER, hasAdvancedListingTypes, isAdvancedListingType, isListingTypeAllowedFor, listingTypesAllowedFor, } from "./_internal/shared/listing-types/tiers";
541
543
  export type { ListingTypeAccessContext, ListingTypeTier, } from "./_internal/shared/listing-types/tiers";
542
544
  export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type ActionEvent, type ActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
package/dist/client.js CHANGED
@@ -448,6 +448,11 @@ export { useListingTypeFlags } from "./react/hooks/useListingTypeFlags.js";
448
448
  // [CLIENT-ONLY] useBrowsePageFlags: read listings.browsePages from
449
449
  // siteSettings; drop nav/sidebar/footer links into a closed browse section.
450
450
  export { useBrowsePageFlags } from "./react/hooks/useBrowsePageFlags.js";
451
+ // [CLIENT-ONLY] useListingSurfaceVisibility: the admin flag MINUS the surfaces
452
+ // measured as empty. For BROWSING links only — authoring surfaces (the seller
453
+ // type dropdown) must keep using useListingTypeFlags, or the first listing of
454
+ // an emptied type becomes impossible to create.
455
+ export { useListingSurfaceVisibility } from "./react/hooks/useListingSurfaceVisibility.js";
451
456
  // Action dispatch hook + panel store
452
457
  export { useActionDispatch } from "./react/hooks/use-action-dispatch.js";
453
458
  export { usePanelStore } from "./stores/panel-store.js";
@@ -542,7 +547,11 @@ export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabl
542
547
  // Browse-section gates. Client-safe and genuinely needed here: the nav,
543
548
  // sidebar, mobile sheet and footer all filter on them, and a link into a
544
549
  // closed section is a 404 on every page of the site.
545
- isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey, } from "./_internal/shared/listing-types/feature-flags.js";
550
+ isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey,
551
+ // Auto-hide: a surface measured as having nothing in it. Client-safe and
552
+ // needed here for the same reason as the browse-section gates above — the
553
+ // nav, sidebar, mobile sheet and footer all filter on them.
554
+ isListingSurfaceVisible, isCountedSurfaceVisible, visibleListingTypes, } from "./_internal/shared/listing-types/feature-flags.js";
546
555
  // Basic/advanced listing-type split. Client-safe: the nav filter and any
547
556
  // seller-facing picker need the same rule the server enforces, and deriving it
548
557
  // twice is how the ten hand-written copies of the union happened.
@@ -264,6 +264,7 @@ export declare const ADMIN_ENDPOINTS: {
264
264
  readonly CHECKOUT_BYPASS: "/api/admin/checkout-bypass";
265
265
  readonly MAINTENANCE_ANALYSIS: "/api/admin/maintenance/analysis";
266
266
  readonly MAINTENANCE_CLOUD_LOGS: "/api/admin/maintenance/cloud-logs";
267
+ readonly CACHE_REVALIDATE: "/api/admin/cache/revalidate";
267
268
  readonly MEDIA_LIST: "/api/admin/media";
268
269
  };
269
270
  export declare const AUCTION_ENDPOINTS: {
@@ -898,6 +899,7 @@ export declare const API_ENDPOINTS: {
898
899
  readonly CHECKOUT_BYPASS: "/api/admin/checkout-bypass";
899
900
  readonly MAINTENANCE_ANALYSIS: "/api/admin/maintenance/analysis";
900
901
  readonly MAINTENANCE_CLOUD_LOGS: "/api/admin/maintenance/cloud-logs";
902
+ readonly CACHE_REVALIDATE: "/api/admin/cache/revalidate";
901
903
  readonly MEDIA_LIST: "/api/admin/media";
902
904
  };
903
905
  readonly AUCTIONS: {
@@ -1497,6 +1499,7 @@ export declare const API_ROUTES: {
1497
1499
  readonly CHECKOUT_BYPASS: "/api/admin/checkout-bypass";
1498
1500
  readonly MAINTENANCE_ANALYSIS: "/api/admin/maintenance/analysis";
1499
1501
  readonly MAINTENANCE_CLOUD_LOGS: "/api/admin/maintenance/cloud-logs";
1502
+ readonly CACHE_REVALIDATE: "/api/admin/cache/revalidate";
1500
1503
  readonly MEDIA_LIST: "/api/admin/media";
1501
1504
  };
1502
1505
  readonly AUCTIONS: {
@@ -313,6 +313,17 @@ export const ADMIN_ENDPOINTS = {
313
313
  CHECKOUT_BYPASS: "/api/admin/checkout-bypass",
314
314
  MAINTENANCE_ANALYSIS: "/api/admin/maintenance/analysis",
315
315
  MAINTENANCE_CLOUD_LOGS: "/api/admin/maintenance/cloud-logs",
316
+ /*
317
+ * Admin-session-gated cache invalidation.
318
+ *
319
+ * 🛑 Deliberately NOT `/api/cache/revalidate`, which authenticates on an
320
+ * `x-api-key` matching `CACHE_REVALIDATION_SECRET`. That secret belongs to
321
+ * the Firestore triggers and the seed CLI; shipping it to a browser so a
322
+ * button could call that endpoint would put a server secret in client code.
323
+ * This one carries no secret and is gated on the admin session like every
324
+ * other `/api/admin/*` route.
325
+ */
326
+ CACHE_REVALIDATE: "/api/admin/cache/revalidate",
316
327
  MEDIA_LIST: "/api/admin/media",
317
328
  };
318
329
  // ---------------------------------------------------------------------------
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Site-wide "does this browse surface have anything in it" counters.
3
+ *
4
+ * ## Why a sibling singleton and not a field on `siteSettings`
5
+ *
6
+ * `siteSettings` is admin-authored CONFIG and is in the tester's PRESERVE tier.
7
+ * This is DERIVED measurement, rewritten by a job. Mixing the two means a
8
+ * reconciler writing into the document that holds live API credentials, and it
9
+ * means a reseed or a settings restore silently reverting a count. Same split
10
+ * `categoryCounts` already makes against `categories`, and for the same
11
+ * Root Cause #92 reason: nothing triggers on this collection, so writing it
12
+ * can never re-enter the trigger that produced it.
13
+ *
14
+ * ## 🛑 ABSENT MEANS UNKNOWN, NEVER ZERO
15
+ *
16
+ * This is the single rule the whole file exists to enforce, and getting it
17
+ * backwards hides the entire catalogue.
18
+ *
19
+ * A key missing from either map means "not measured yet" — a fresh install, a
20
+ * failed job, a type added since the last run. Every reader must treat that as
21
+ * **visible**, exactly as `getLiveCategoryCounts` returns `undefined` rather
22
+ * than 0 so its callers fall back to `metrics`. Root Cause #72 is this
23
+ * project's record of the opposite choice: a "hide it when it's empty" rule
24
+ * whose count map did not cover an option, so "unknown" read as "zero" and the
25
+ * section vanished.
26
+ *
27
+ * `Partial` is therefore load-bearing typing, not convenience — an explicit
28
+ * zero and an absent key MUST stay distinguishable.
29
+ *
30
+ * ## 🛑 These are AVAILABLE rows, which is a time-dependent question
31
+ *
32
+ * The counts come from `listPublicProducts`, the one function every public
33
+ * browse page already queries through — so a surface can never be hidden while
34
+ * its own page would have rendered rows, which is the correctness property
35
+ * that matters here and the reason this is not a bespoke `count()` query.
36
+ *
37
+ * It inherits that function's time-dependence: an auction becomes unavailable
38
+ * by the clock with no product write, so a type whose last row just ended stays
39
+ * "non-empty" until the next recompute. Erring toward VISIBLE is the right
40
+ * direction for this feature — showing an empty list is a cosmetic problem,
41
+ * hiding a live one is a lost sale.
42
+ *
43
+ * @tag domain:products
44
+ * @tag layer:schema
45
+ * @tag access:isomorphic
46
+ */
47
+ import type { ListingType } from "../types/index";
48
+ export declare const SURFACE_COUNTS_COLLECTION = "surfaceCounts";
49
+ /** Singleton — these are site-wide totals, so there is exactly one row. */
50
+ export declare const SURFACE_COUNTS_DOC_ID = "global";
51
+ /**
52
+ * Public browse surfaces that are NOT a listing type and so have no
53
+ * `listings.listingTypes` flag to hang off.
54
+ *
55
+ * Deliberately just `reviews` today. A key here must have a real consumer —
56
+ * `/stores` and `/categories` already have their own `browsePages` flag and
57
+ * adding them here would be a second mechanism deciding one thing.
58
+ */
59
+ export type CountedSurfaceKey = "reviews";
60
+ export interface SurfaceCountsDocument {
61
+ id: string;
62
+ /**
63
+ * Available listings per type. Absent key = UNKNOWN = show.
64
+ *
65
+ * A map rather than scalar fields: the listing-type union has grown twice
66
+ * (nine → ten with `spares`), and a map needs no schema change for the next.
67
+ */
68
+ listingTypes: Partial<Record<ListingType, number>>;
69
+ /** Non-listing-type surfaces. Absent key = UNKNOWN = show. */
70
+ surfaces: Partial<Record<CountedSurfaceKey, number>>;
71
+ /**
72
+ * When the figures were taken. Read by the admin panel so an operator can
73
+ * tell "measured as empty an hour ago" from "never measured".
74
+ */
75
+ updatedAt: Date;
76
+ }
77
+ /**
78
+ * Is this surface known to be empty?
79
+ *
80
+ * Returns `false` for an unmeasured surface — the caller must not be able to
81
+ * express "hide it" without a measurement that says so.
82
+ */
83
+ export declare function isKnownEmpty(count: number | undefined): boolean;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Site-wide "does this browse surface have anything in it" counters.
3
+ *
4
+ * ## Why a sibling singleton and not a field on `siteSettings`
5
+ *
6
+ * `siteSettings` is admin-authored CONFIG and is in the tester's PRESERVE tier.
7
+ * This is DERIVED measurement, rewritten by a job. Mixing the two means a
8
+ * reconciler writing into the document that holds live API credentials, and it
9
+ * means a reseed or a settings restore silently reverting a count. Same split
10
+ * `categoryCounts` already makes against `categories`, and for the same
11
+ * Root Cause #92 reason: nothing triggers on this collection, so writing it
12
+ * can never re-enter the trigger that produced it.
13
+ *
14
+ * ## 🛑 ABSENT MEANS UNKNOWN, NEVER ZERO
15
+ *
16
+ * This is the single rule the whole file exists to enforce, and getting it
17
+ * backwards hides the entire catalogue.
18
+ *
19
+ * A key missing from either map means "not measured yet" — a fresh install, a
20
+ * failed job, a type added since the last run. Every reader must treat that as
21
+ * **visible**, exactly as `getLiveCategoryCounts` returns `undefined` rather
22
+ * than 0 so its callers fall back to `metrics`. Root Cause #72 is this
23
+ * project's record of the opposite choice: a "hide it when it's empty" rule
24
+ * whose count map did not cover an option, so "unknown" read as "zero" and the
25
+ * section vanished.
26
+ *
27
+ * `Partial` is therefore load-bearing typing, not convenience — an explicit
28
+ * zero and an absent key MUST stay distinguishable.
29
+ *
30
+ * ## 🛑 These are AVAILABLE rows, which is a time-dependent question
31
+ *
32
+ * The counts come from `listPublicProducts`, the one function every public
33
+ * browse page already queries through — so a surface can never be hidden while
34
+ * its own page would have rendered rows, which is the correctness property
35
+ * that matters here and the reason this is not a bespoke `count()` query.
36
+ *
37
+ * It inherits that function's time-dependence: an auction becomes unavailable
38
+ * by the clock with no product write, so a type whose last row just ended stays
39
+ * "non-empty" until the next recompute. Erring toward VISIBLE is the right
40
+ * direction for this feature — showing an empty list is a cosmetic problem,
41
+ * hiding a live one is a lost sale.
42
+ *
43
+ * @tag domain:products
44
+ * @tag layer:schema
45
+ * @tag access:isomorphic
46
+ */
47
+ export const SURFACE_COUNTS_COLLECTION = "surfaceCounts";
48
+ /** Singleton — these are site-wide totals, so there is exactly one row. */
49
+ export const SURFACE_COUNTS_DOC_ID = "global";
50
+ /**
51
+ * Is this surface known to be empty?
52
+ *
53
+ * Returns `false` for an unmeasured surface — the caller must not be able to
54
+ * express "hide it" without a measurement that says so.
55
+ */
56
+ export function isKnownEmpty(count) {
57
+ return count === 0;
58
+ }
package/dist/index.d.ts CHANGED
@@ -2190,8 +2190,8 @@ export { getProductSortOptions } from "./features/products/components/ProductFil
2190
2190
  export { getProductTableColumns } from "./features/products/columns/productTableColumns";
2191
2191
  export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/index";
2192
2192
  export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
2193
- export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType, isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey, } from "./_internal/shared/listing-types/feature-flags";
2194
- export type { BrowsePageKey } from "./_internal/shared/listing-types/feature-flags";
2193
+ export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType, isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey, isListingSurfaceVisible, isCountedSurfaceVisible, visibleListingTypes, } from "./_internal/shared/listing-types/feature-flags";
2194
+ export type { BrowsePageKey, SurfaceCountSnapshot, } from "./_internal/shared/listing-types/feature-flags";
2195
2195
  export { ADVANCED_LISTING_TYPE_CAPABILITY, ADVANCED_LISTING_TYPES, BASIC_LISTING_TYPES, LISTING_TYPE_TIER, assertListingTypeAllowed, hasAdvancedListingTypes, isAdvancedListingType, isListingTypeAllowedFor, listingTypesAllowedFor, } from "./_internal/shared/listing-types/tiers";
2196
2196
  export type { ListingTypeAccessContext, ListingTypeTier, } from "./_internal/shared/listing-types/tiers";
2197
2197
  export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type ActionEvent, type ActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
package/dist/index.js CHANGED
@@ -4083,7 +4083,9 @@ export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabl
4083
4083
  // file's ALL_LISTING_TYPES note: `server-entry.ts` re-exports index, so a
4084
4084
  // Server Component already reaches these from the bare specifier and a
4085
4085
  // second path would leak an `_internal/` symbol through a third barrel.
4086
- isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey, } from "./_internal/shared/listing-types/feature-flags.js";
4086
+ isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey,
4087
+ // Auto-hide gates — same barrel choice as the browse-section ones above.
4088
+ isListingSurfaceVisible, isCountedSurfaceVisible, visibleListingTypes, } from "./_internal/shared/listing-types/feature-flags.js";
4087
4089
  // Basic/advanced listing-type split — which types a store may create.
4088
4090
  // Pure (no firebase-admin in its import chain), so it is safe in index.ts.
4089
4091
  export { ADVANCED_LISTING_TYPE_CAPABILITY, ADVANCED_LISTING_TYPES, BASIC_LISTING_TYPES, LISTING_TYPE_TIER, assertListingTypeAllowed, hasAdvancedListingTypes, isAdvancedListingType, isListingTypeAllowedFor, listingTypesAllowedFor, } from "./_internal/shared/listing-types/tiers.js";
@@ -0,0 +1,44 @@
1
+ import type { ListingType } from "../../features/products/types/index";
2
+ /**
3
+ * Which public browse surfaces should be LINKED — the admin flag, minus the
4
+ * ones measured as having nothing in them.
5
+ *
6
+ * ## 🛑 Why this is not a change to `useListingTypeFlags`
7
+ *
8
+ * That hook answers a different question — *"does this marketplace sell X?"* —
9
+ * and its main consumer is the seller's type dropdown. A seller must still be
10
+ * able to list the first spare part when there are zero spares, so folding
11
+ * emptiness into it would make the catalogue unfillable the moment it emptied:
12
+ * the only control that could add stock would disappear for want of stock.
13
+ * Same split as `enabledListingTypes` vs `visibleListingTypes`.
14
+ *
15
+ * So: `useListingTypeFlags` for anything AUTHORING, this for anything
16
+ * BROWSING.
17
+ *
18
+ * ## 🛑 Defaults to VISIBLE while settings load
19
+ *
20
+ * Same reasoning as `useBrowsePageFlags`, and it bites harder here. This hook
21
+ * decides whether a nav entry renders, and `surfaceCounts` is absent from the
22
+ * payload until the rollup has run at least once — so a restrictive default
23
+ * would blank most of the navbar on every cold load, and would blank it
24
+ * permanently on an install whose job has never run.
25
+ *
26
+ * `isListingSurfaceVisible` encodes the rule (absent count → visible); this
27
+ * hook only supplies it with data. Do not re-derive the comparison here.
28
+ */
29
+ export type ListingSurfaceVisibility = Record<ListingType, boolean> & {
30
+ /** True when the type's browse surface should be linked. */
31
+ isVisible: (type: ListingType) => boolean;
32
+ /**
33
+ * True when a non-listing-type surface (`"reviews"`) should be linked.
34
+ * Absent measurement → true.
35
+ */
36
+ isSurfaceVisible: (surface: string) => boolean;
37
+ /**
38
+ * True when a link may be shown — it points at no listing-type surface, or
39
+ * at one that is visible. Pass an href straight from a nav/footer constant,
40
+ * together with the href→types map the caller owns.
41
+ */
42
+ allowsTypes: (types: readonly ListingType[] | undefined) => boolean;
43
+ };
44
+ export declare function useListingSurfaceVisibility(): ListingSurfaceVisibility;
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { useSiteSettings } from "../../core/hooks/useSiteSettings.js";
3
+ import { ALL_LISTING_TYPES, isListingSurfaceVisible, isCountedSurfaceVisible, } from "../../_internal/shared/listing-types/feature-flags.js";
4
+ export function useListingSurfaceVisibility() {
5
+ const { data } = useSiteSettings();
6
+ const settings = { listings: { listingTypes: data?.listings?.listingTypes } };
7
+ const counts = data?.surfaceCounts;
8
+ const flags = Object.fromEntries(ALL_LISTING_TYPES.map((t) => [t, isListingSurfaceVisible(t, settings, counts)]));
9
+ return {
10
+ ...flags,
11
+ isVisible: (type) => flags[type],
12
+ isSurfaceVisible: (surface) => isCountedSurfaceVisible(surface, counts),
13
+ /*
14
+ * ANY of the types, not every — matching the existing footer rule, whose
15
+ * comment states it: "/art stays reachable when stickers are off and art
16
+ * is on." A shared page is empty only when every type it spans is.
17
+ */
18
+ allowsTypes: (types) => !types || types.length === 0 || types.some((t) => flags[t]),
19
+ };
20
+ }
package/dist/server.d.ts CHANGED
@@ -662,3 +662,5 @@ export { AD_BOOKING_COLLECTION, AD_BOOKING_PAYMENT_STATES, AD_MODERATION_STATES,
662
662
  export type { AdBookingDocument, AdBookingCreative, AdBookingPaymentState, AdModerationState, AdPeriod, } from "./features/ads/schemas/firestore";
663
663
  export { getDashboardHubData } from "./_internal/server/features/dashboard/data";
664
664
  export type { DashboardHubData } from "./_internal/server/features/dashboard/data";
665
+ export { getSurfaceCounts } from "./_internal/server/features/products/surface-counts";
666
+ export type { PublicSurfaceCounts } from "./_internal/server/features/products/surface-counts";
package/dist/server.js CHANGED
@@ -1872,3 +1872,15 @@ export { AD_BOOKING_COLLECTION, AD_BOOKING_PAYMENT_STATES, AD_MODERATION_STATES,
1872
1872
  * above reaches a consumer route.
1873
1873
  */
1874
1874
  export { getDashboardHubData } from "./_internal/server/features/dashboard/data.js";
1875
+ // Auto-hide: the measured "does this browse surface hold anything" document.
1876
+ // Server-only (firebase-admin via getAdminDb); the PREDICATE that reads it is
1877
+ // client-safe and lives in client.ts/index.ts beside the other visibility gates.
1878
+ export { getSurfaceCounts } from "./_internal/server/features/products/surface-counts.js";
1879
+ /*
1880
+ * 🛑 `runSurfaceCountsRollup` / `computeSurfaceCounts` are deliberately NOT
1881
+ * exported. They live under `_internal/server/jobs/core/` and their only
1882
+ * caller is `countersReconcileHandler`, which imports them directly — so a
1883
+ * barrel entry would leak an `_internal/` symbol for no consumer, which
1884
+ * `audit-appkit-reexports` correctly blocks. Add one only if a consumer
1885
+ * genuinely needs to trigger a rollup from outside appkit.
1886
+ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.66.0",
3
+ "version": "4.68.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,6 +23,18 @@
23
23
  "types": "./dist/client.d.ts",
24
24
  "import": "./dist/client.js"
25
25
  },
26
+ "./client/*": {
27
+ "types": "./dist/client/*.d.ts",
28
+ "import": "./dist/client/*.js"
29
+ },
30
+ "./hand-mode": {
31
+ "types": "./dist/hand-mode/index.d.ts",
32
+ "import": "./dist/hand-mode/index.js"
33
+ },
34
+ "./theme": {
35
+ "types": "./dist/theme/index.d.ts",
36
+ "import": "./dist/theme/index.js"
37
+ },
26
38
  "./next": {
27
39
  "types": "./dist/next/index.d.ts",
28
40
  "import": "./dist/next/index.js"
@@ -281,6 +293,26 @@
281
293
  "./views/tour-provider": {
282
294
  "types": "./dist/_internal/client/features/tour/TourProvider.d.ts",
283
295
  "import": "./dist/_internal/client/features/tour/TourProvider.js"
296
+ },
297
+ "./shipping/quote": {
298
+ "types": "./dist/_internal/shared/shipping/quote.d.ts",
299
+ "import": "./dist/_internal/shared/shipping/quote.js"
300
+ },
301
+ "./fees/calculator": {
302
+ "types": "./dist/_internal/shared/fees/calculator.d.ts",
303
+ "import": "./dist/_internal/shared/fees/calculator.js"
304
+ },
305
+ "./emi/choice": {
306
+ "types": "./dist/_internal/shared/features/emi/choice.d.ts",
307
+ "import": "./dist/_internal/shared/features/emi/choice.js"
308
+ },
309
+ "./layout/nav-portal": {
310
+ "types": "./dist/_internal/shared/features/layout/types.d.ts",
311
+ "import": "./dist/_internal/shared/features/layout/types.js"
312
+ },
313
+ "./views/digital-content-pool": {
314
+ "types": "./dist/_internal/client/features/digital-code/DigitalContentPoolManager.d.ts",
315
+ "import": "./dist/_internal/client/features/digital-code/DigitalContentPoolManager.js"
284
316
  }
285
317
  },
286
318
  "files": [