@mohasinac/appkit 4.46.0 → 4.48.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.
- package/dist/.build-stamp.json +4 -4
- package/dist/_internal/server/features/checkout/expand-order-items.js +3 -0
- package/dist/_internal/server/features/messages/actions.d.ts +68 -0
- package/dist/_internal/server/features/messages/actions.js +249 -0
- package/dist/_internal/server/features/parts/buildable.d.ts +47 -0
- package/dist/_internal/server/features/parts/buildable.js +159 -0
- package/dist/_internal/server/features/products/list-public.d.ts +17 -0
- package/dist/_internal/server/features/products/list-public.js +92 -5
- package/dist/_internal/server/features/products/og.d.ts +7 -0
- package/dist/_internal/server/features/products/og.js +38 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +6 -0
- package/dist/_internal/server/jobs/core/cleanupRtdbEvents.js +28 -0
- package/dist/_internal/server/jobs/core/notificationPrune.js +87 -3
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +36 -1
- package/dist/_internal/shared/actions/action-registry.js +8 -0
- package/dist/_internal/shared/features/notifications/action-url.js +8 -0
- package/dist/_internal/shared/features/notifications/email-eligibility.js +3 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +4 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +8 -0
- package/dist/constants/api-endpoints.d.ts +48 -0
- package/dist/constants/api-endpoints.js +30 -0
- package/dist/constants/notification-types.d.ts +1 -1
- package/dist/features/about/components/GstExplainerView.d.ts +28 -0
- package/dist/features/about/components/GstExplainerView.js +59 -0
- package/dist/features/about/components/GuidesHubView.d.ts +28 -0
- package/dist/features/about/components/GuidesHubView.js +58 -0
- package/dist/features/about/components/PublicProfileView.js +5 -0
- package/dist/features/about/components/SpotAFakeView.d.ts +29 -0
- package/dist/features/about/components/SpotAFakeView.js +49 -0
- package/dist/features/admin/actions/notification-actions.js +4 -0
- package/dist/features/admin/schemas/firestore.d.ts +13 -3
- package/dist/features/admin/schemas/firestore.js +8 -0
- package/dist/features/blog/repository/blog.repository.js +21 -7
- package/dist/features/categories/api/route.js +41 -1
- package/dist/features/categories/components/BreadcrumbTrail.d.ts +58 -13
- package/dist/features/categories/components/BreadcrumbTrail.js +63 -26
- package/dist/features/categories/components/CategoryDetailPageView.js +92 -38
- package/dist/features/categories/components/ContextualTrail.d.ts +62 -0
- package/dist/features/categories/components/ContextualTrail.js +81 -0
- package/dist/features/categories/components/LastCategoryBeacon.d.ts +18 -0
- package/dist/features/categories/components/LastCategoryBeacon.js +33 -0
- package/dist/features/categories/repository/categories.repository.d.ts +21 -0
- package/dist/features/categories/repository/categories.repository.js +36 -3
- package/dist/features/categories/repository/price-index.repository.d.ts +25 -0
- package/dist/features/email/notification-templates.js +4 -0
- package/dist/features/events/repository/events.repository.js +10 -3
- package/dist/features/faq/constants/read-caps.d.ts +29 -0
- package/dist/features/faq/constants/read-caps.js +29 -0
- package/dist/features/faq/repository/faqs.repository.d.ts +0 -2
- package/dist/features/faq/repository/faqs.repository.js +19 -16
- package/dist/features/homepage/components/AdSlot.js +10 -4
- package/dist/features/homepage/hooks/useActiveAd.d.ts +18 -1
- package/dist/features/homepage/hooks/useActiveAd.js +42 -2
- package/dist/features/homepage/lib/section-renderer.js +12 -2
- package/dist/features/layout/AppLayoutShell.d.ts +4 -1
- package/dist/features/layout/AppLayoutShell.js +2 -2
- package/dist/features/layout/TitleBarLayout.d.ts +4 -1
- package/dist/features/layout/TitleBarLayout.js +8 -4
- package/dist/features/media/AvatarUpload.js +8 -1
- package/dist/features/media/MediaAvatar.js +1 -1
- package/dist/features/media/MediaImage.js +3 -1
- package/dist/features/media/MediaLightbox.js +4 -1
- package/dist/features/media/MediaPickerModal.js +6 -1
- package/dist/features/media/MediaVideo.js +3 -1
- package/dist/features/messages/components/ConversationThreadView.d.ts +3 -0
- package/dist/features/messages/components/ConversationThreadView.js +75 -0
- package/dist/features/messages/components/ConversationsInboxView.d.ts +1 -0
- package/dist/features/messages/components/ConversationsInboxView.js +39 -0
- package/dist/features/messages/components/MessageSellerButton.d.ts +17 -0
- package/dist/features/messages/components/MessageSellerButton.js +33 -0
- package/dist/features/messages/hooks/useConversationChannel.d.ts +24 -0
- package/dist/features/messages/hooks/useConversationChannel.js +89 -0
- package/dist/features/messages/hooks/useMessages.d.ts +347 -0
- package/dist/features/messages/hooks/useMessages.js +113 -0
- package/dist/features/messages/repository/conversations.repository.d.ts +44 -0
- package/dist/features/messages/repository/conversations.repository.js +173 -0
- package/dist/features/messages/repository/messages.repository.d.ts +42 -0
- package/dist/features/messages/repository/messages.repository.js +142 -0
- package/dist/features/messages/schemas/firestore.d.ts +99 -0
- package/dist/features/messages/schemas/firestore.js +46 -0
- package/dist/features/orders/repository/orders.repository.js +21 -7
- package/dist/features/orders/schemas/firestore.d.ts +11 -0
- package/dist/features/products/components/CityCollectiblesView.d.ts +31 -0
- package/dist/features/products/components/CityCollectiblesView.js +82 -0
- package/dist/features/products/components/MarketPriceAnchor.d.ts +45 -0
- package/dist/features/products/components/MarketPriceAnchor.js +62 -0
- package/dist/features/products/components/ProductDetailPageView.js +71 -5
- package/dist/features/products/constants/meetup-cities.d.ts +47 -0
- package/dist/features/products/constants/meetup-cities.js +49 -0
- package/dist/features/products/repository/category-assignment.d.ts +82 -11
- package/dist/features/products/repository/category-assignment.js +93 -12
- package/dist/features/products/repository/products.repository.d.ts +12 -1
- package/dist/features/products/repository/products.repository.js +66 -34
- package/dist/features/products/schemas/firestore.d.ts +24 -2
- package/dist/features/products/schemas/firestore.js +13 -1
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/promotions/repository/coupons.repository.js +11 -4
- package/dist/features/reviews/repository/reviews.repository.js +11 -4
- package/dist/features/scams/repository/scammer.repository.js +21 -7
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +17 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +14 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +27 -2
- package/dist/features/seller/components/SellerProductShell.js +32 -3
- package/dist/features/seller/repository/offer.repository.js +21 -6
- package/dist/features/seller/schemas/product-form.d.ts +3 -3
- package/dist/features/seller/schemas/product-form.js +1 -1
- package/dist/features/server-errors/repository/server-errors.repository.js +11 -0
- package/dist/features/server-errors/schemas/firestore.d.ts +17 -0
- package/dist/features/server-errors/schemas/firestore.js +2 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +17 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +31 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +1 -1
- package/dist/features/store-extensions/schemas/firestore.js +4 -0
- package/dist/features/stores/repository/store.repository.js +11 -3
- package/dist/features/tester/seed-data/authored/community-support__messages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/community-support__messages.js +200 -0
- package/dist/features/tester/seed-data/authored/index.js +2 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +64 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +1 -1
- package/dist/next/routing/route-map.d.ts +14 -0
- package/dist/next/routing/route-map.js +26 -0
- package/dist/providers/db-firebase/rtdb-paths.d.ts +8 -0
- package/dist/providers/db-firebase/rtdb-paths.js +8 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +4 -0
- package/dist/schemas/registry.d.ts +3 -3
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-schemas.d.ts +17 -0
- package/dist/security/pii-schemas.js +15 -7
- package/dist/seed/_helpers/category-content-generated.d.ts +24 -0
- package/dist/seed/_helpers/category-content-generated.js +1853 -0
- package/dist/seed/_helpers/category-models.d.ts +1 -1
- package/dist/seed/_helpers/category-models.js +25 -1
- package/dist/seed/_helpers/category-names.d.ts +54 -0
- package/dist/seed/_helpers/category-names.js +118 -0
- package/dist/seed/_helpers/character-features.d.ts +1 -1
- package/dist/seed/_helpers/character-features.js +5 -4
- package/dist/seed/_helpers/final-sale-fixtures.js +1 -1
- package/dist/seed/categories-seed-data.js +55 -1
- package/dist/seed/grouped-listings-seed-data.js +2 -2
- package/dist/seed/products-art-seed-data.js +2 -1
- package/dist/seed/products-auctions-seed-data.js +42 -22
- package/dist/seed/products-classifieds-seed-data.js +21 -3
- package/dist/seed/products-digital-codes-seed-data.js +2 -1
- package/dist/seed/products-live-items-seed-data.js +2 -1
- package/dist/seed/products-preorders-seed-data.js +26 -11
- package/dist/seed/products-prize-draws-seed-data.js +8 -1
- package/dist/seed/products-standard-seed-data.js +26 -25
- package/dist/seed/products-stickers-seed-data.js +12 -11
- package/dist/server.d.ts +8 -0
- package/dist/server.js +15 -0
- package/dist/styles.css +113 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/sunset.js +17 -0
- package/dist/tokens/tokens.css +5 -0
- package/dist/ui/components/Breadcrumb.d.ts +8 -1
- package/dist/ui/components/Breadcrumb.js +2 -2
- package/dist/ui/components/Breadcrumb.style.css +37 -0
- package/dist/ui/components/Details.d.ts +11 -1
- package/dist/ui/components/Details.js +2 -2
- package/dist/ui/components/ImageMosaic.d.ts +43 -0
- package/dist/ui/components/ImageMosaic.js +46 -0
- package/dist/ui/components/ImageMosaic.style.css +69 -0
- package/dist/ui/components/SplitTile.d.ts +34 -0
- package/dist/ui/components/SplitTile.js +28 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +4 -0
- package/dist/utils/search-txt-query.d.ts +77 -0
- package/dist/utils/search-txt-query.js +50 -28
- package/package.json +1 -1
|
@@ -24,6 +24,23 @@ export const SUNSET_THEME = {
|
|
|
24
24
|
"appkit-color-primary": "#f59e0b",
|
|
25
25
|
"appkit-color-primary-500": "#f59e0b",
|
|
26
26
|
"appkit-color-primary-600": "#d97706",
|
|
27
|
+
/*
|
|
28
|
+
* 🛑 D7 — DARK ink on this primary, declared rather than inherited.
|
|
29
|
+
*
|
|
30
|
+
* The light base's `text-on-primary` is `#ffffff`, and white on amber
|
|
31
|
+
* `#f59e0b` measures **2.15:1** — under the 3:1 AA threshold for
|
|
32
|
+
* large/UI text, so every primary button, badge and pill in this theme was
|
|
33
|
+
* unreadable. It survived because the pair was INHERITED: `audit-a11y`
|
|
34
|
+
* treated a token this theme does not declare as "nothing to check", which
|
|
35
|
+
* is exactly the pairing a brand theme is most likely to break (a new
|
|
36
|
+
* brand colour under somebody else's ink).
|
|
37
|
+
*
|
|
38
|
+
* `#431407` is this theme's own `text` token, so the fix introduces no new
|
|
39
|
+
* colour: **7.29:1** on `primary` and **4.91:1** on `primary-600`, which is
|
|
40
|
+
* the darker shade a hover state uses — both clear AA for normal text, not
|
|
41
|
+
* just the 3:1 this pair is held to.
|
|
42
|
+
*/
|
|
43
|
+
"appkit-color-text-on-primary": "#431407",
|
|
27
44
|
"appkit-color-secondary": "#f43f5e",
|
|
28
45
|
"appkit-color-secondary-500": "#f43f5e",
|
|
29
46
|
"appkit-color-bg": "#fff7ed",
|
package/dist/tokens/tokens.css
CHANGED
|
@@ -608,6 +608,11 @@
|
|
|
608
608
|
--appkit-color-text: #431407; /* orange-950 */
|
|
609
609
|
--appkit-color-text-muted: #9a3412; /* orange-800 */
|
|
610
610
|
--appkit-color-text-faint: #c2410c; /* orange-700 */
|
|
611
|
+
/* D7: dark ink on the amber primary. Inherited white measured 2.15:1 and
|
|
612
|
+
failed AA for UI text; orange-950 is 7.29:1 on primary and 4.91:1 on
|
|
613
|
+
primary-600. Declared in BOTH places or the first paint disagrees with
|
|
614
|
+
the hydrated theme (audit-theme-drift). */
|
|
615
|
+
--appkit-color-text-on-primary: #431407;
|
|
611
616
|
|
|
612
617
|
--appkit-color-success: #15803d;
|
|
613
618
|
--appkit-color-success-surface: #f0fdf4;
|
|
@@ -8,8 +8,15 @@ export interface BreadcrumbItem {
|
|
|
8
8
|
label: string;
|
|
9
9
|
href?: string;
|
|
10
10
|
}
|
|
11
|
+
export type BreadcrumbTone = "default" | "on-media";
|
|
11
12
|
export interface BreadcrumbProps {
|
|
12
13
|
items: BreadcrumbItem[];
|
|
13
14
|
className?: string;
|
|
15
|
+
/**
|
|
16
|
+
* `"on-media"` for a trail drawn over a cover photo — fixed white ink on the
|
|
17
|
+
* scrim, in both themes. See the tone block in `Breadcrumb.style.css` for why
|
|
18
|
+
* it is not a theme token.
|
|
19
|
+
*/
|
|
20
|
+
tone?: BreadcrumbTone;
|
|
14
21
|
}
|
|
15
|
-
export declare function Breadcrumb({ items, className }: BreadcrumbProps): import("react").JSX.Element;
|
|
22
|
+
export declare function Breadcrumb({ items, className, tone }: BreadcrumbProps): import("react").JSX.Element;
|
|
@@ -3,8 +3,8 @@ import { ChevronRight } from "lucide-react";
|
|
|
3
3
|
import { Nav, Ol, Li } from "./Semantic.js";
|
|
4
4
|
import { Span } from "./Typography.js";
|
|
5
5
|
import { TextLink } from "./TextLink.js";
|
|
6
|
-
export function Breadcrumb({ items, className = "" }) {
|
|
7
|
-
return (_jsx(Nav, { "aria-label": "Breadcrumb", className: `appkit-breadcrumb ${className}`, children: _jsx(Ol, { className: "appkit-breadcrumb__list", children: items.map((item, i) => {
|
|
6
|
+
export function Breadcrumb({ items, className = "", tone = "default" }) {
|
|
7
|
+
return (_jsx(Nav, { "aria-label": "Breadcrumb", className: `appkit-breadcrumb ${tone === "on-media" ? "appkit-breadcrumb--on-media" : ""} ${className}`, children: _jsx(Ol, { className: "appkit-breadcrumb__list", children: items.map((item, i) => {
|
|
8
8
|
const isLast = i === items.length - 1;
|
|
9
9
|
return (_jsxs(Li, { className: "appkit-breadcrumb__item", children: [i > 0 && (_jsx(ChevronRight, { className: "appkit-breadcrumb__separator", "aria-hidden": "true" })), isLast || !item.href ? (_jsx(Span, { className: isLast
|
|
10
10
|
? "appkit-breadcrumb__current"
|
|
@@ -58,3 +58,40 @@
|
|
|
58
58
|
.dark .appkit-breadcrumb__link:hover {
|
|
59
59
|
color: var(--appkit-color-primary-400);
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* ── tone="on-media" ─────────────────────────────────────────────────────────
|
|
64
|
+
*
|
|
65
|
+
* A trail rendered ON a cover photo. The category hero does exactly this, and
|
|
66
|
+
* before this tone existed that page hand-rolled its own `<Nav>` with
|
|
67
|
+
* `text-white/70` conditionals purely to stay readable — which is why the
|
|
68
|
+
* shared component had no caller there, and why its visible trail (three
|
|
69
|
+
* crumbs) disagreed with its own JSON-LD (eight).
|
|
70
|
+
*
|
|
71
|
+
* 🛑 Fixed white rather than a theme token, deliberately. The backdrop is a
|
|
72
|
+
* photograph with a dark scrim over it in BOTH themes, so a theme-inverting
|
|
73
|
+
* token would be invisible in one of them — the trap Root Cause #67 is about.
|
|
74
|
+
* An alpha on white is the spelling that rule prescribes for a control on a
|
|
75
|
+
* fixed dark scrim.
|
|
76
|
+
*/
|
|
77
|
+
.appkit-breadcrumb--on-media .appkit-breadcrumb__list,
|
|
78
|
+
.appkit-breadcrumb--on-media .appkit-breadcrumb__text,
|
|
79
|
+
.dark .appkit-breadcrumb--on-media .appkit-breadcrumb__list,
|
|
80
|
+
.dark .appkit-breadcrumb--on-media .appkit-breadcrumb__text {
|
|
81
|
+
color: rgb(255 255 255 / 0.7);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.appkit-breadcrumb--on-media .appkit-breadcrumb__separator,
|
|
85
|
+
.dark .appkit-breadcrumb--on-media .appkit-breadcrumb__separator {
|
|
86
|
+
color: rgb(255 255 255 / 0.4);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.appkit-breadcrumb--on-media .appkit-breadcrumb__current,
|
|
90
|
+
.dark .appkit-breadcrumb--on-media .appkit-breadcrumb__current {
|
|
91
|
+
color: rgb(255 255 255);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.appkit-breadcrumb--on-media .appkit-breadcrumb__link:hover,
|
|
95
|
+
.dark .appkit-breadcrumb--on-media .appkit-breadcrumb__link:hover {
|
|
96
|
+
color: rgb(255 255 255);
|
|
97
|
+
}
|
|
@@ -54,7 +54,17 @@ export interface SummaryProps {
|
|
|
54
54
|
align?: SummaryAlign;
|
|
55
55
|
/** Main-axis distribution — takes effect when `layout` is set. */
|
|
56
56
|
justify?: SummaryJustify;
|
|
57
|
+
/**
|
|
58
|
+
* Accessible name for the disclosure control.
|
|
59
|
+
*
|
|
60
|
+
* A summary whose visible label is a shorthand ("… 3 more") reads as nothing
|
|
61
|
+
* useful to a screen reader, and the alternative — padding the visible label
|
|
62
|
+
* — costs the horizontal space the collapse exists to save. A prop rather
|
|
63
|
+
* than a caller's raw attribute, because a raw `aria-label` on a primitive is
|
|
64
|
+
* the className escape hatch by another name (Rule #21).
|
|
65
|
+
*/
|
|
66
|
+
ariaLabel?: string;
|
|
57
67
|
}
|
|
58
68
|
export declare function Details({ children, defaultOpen, open, onToggle, tone, padding, rounded, name, className, style, }: DetailsProps): import("react").JSX.Element;
|
|
59
|
-
export declare function Summary({ children, className, size, weight, paddingX, paddingY, color, layout, align, justify }: SummaryProps): import("react").JSX.Element;
|
|
69
|
+
export declare function Summary({ children, className, size, weight, paddingX, paddingY, color, layout, align, justify, ariaLabel }: SummaryProps): import("react").JSX.Element;
|
|
60
70
|
export {};
|
|
@@ -75,8 +75,8 @@ export function Details({ children, defaultOpen, open, onToggle, tone = "default
|
|
|
75
75
|
className ?? "",
|
|
76
76
|
].filter(Boolean).join(" ") || undefined, style: style, children: children }));
|
|
77
77
|
}
|
|
78
|
-
export function Summary({ children, className, size, weight, paddingX, paddingY, color, layout, align, justify }) {
|
|
79
|
-
return (_jsx("summary", { className: [
|
|
78
|
+
export function Summary({ children, className, size, weight, paddingX, paddingY, color, layout, align, justify, ariaLabel }) {
|
|
79
|
+
return (_jsx("summary", { "aria-label": ariaLabel, className: [
|
|
80
80
|
"cursor-pointer select-none font-medium text-[var(--appkit-color-text)] list-none [&::-webkit-details-marker]:hidden focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--appkit-color-focus-ring)] rounded",
|
|
81
81
|
size ? SUMMARY_SIZE_MAP[size] : "",
|
|
82
82
|
weight ? SUMMARY_WEIGHT_MAP[weight] : "",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/** Below this there is nothing to arrange, so the caller should render nothing. */
|
|
3
|
+
export declare const MOSAIC_MIN = 3;
|
|
4
|
+
export interface ImageMosaicProps {
|
|
5
|
+
/** Image URLs, most important first. Fewer than `MOSAIC_MIN` renders null. */
|
|
6
|
+
images: readonly string[];
|
|
7
|
+
/** Describes the SUBJECT, not the images — one alt for a decorative group. */
|
|
8
|
+
alt: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* D3 — a mosaic of a collection's own images, for a hero that has no cover
|
|
13
|
+
* photo of its own.
|
|
14
|
+
*
|
|
15
|
+
* ## 🛑 Why this exists and what it replaces
|
|
16
|
+
*
|
|
17
|
+
* A category with no `display.coverImage` rendered a flat
|
|
18
|
+
* `bg-[var(--appkit-color-bg)]` band behind its title — the same empty strip on
|
|
19
|
+
* every one of the ~1,100 categories that has no photographer. The images were
|
|
20
|
+
* already on the page (the product grid below it), so the hero was blank beside
|
|
21
|
+
* a screen full of pictures.
|
|
22
|
+
*
|
|
23
|
+
* ## 🛑 It is DECORATIVE, and that decides three things
|
|
24
|
+
*
|
|
25
|
+
* 1. **One `alt` for the group, describing the subject** — "Beyblade Burst
|
|
26
|
+
* parts", not five filenames. Five alts for five thumbnails of the same
|
|
27
|
+
* subject is noise to a screen reader, and `alt=""` on each would leave the
|
|
28
|
+
* band unlabelled entirely.
|
|
29
|
+
* 2. **`aria-hidden` is NOT used**, because the group does carry meaning: it is
|
|
30
|
+
* what this category looks like. Hidden decoration plus a labelled group are
|
|
31
|
+
* different claims and the second is true here.
|
|
32
|
+
* 3. **No links.** Every one of these products is linked in the grid below; a
|
|
33
|
+
* second set of hit targets over a title is a tap hazard on a phone and
|
|
34
|
+
* gives a crawler duplicate links to the same URLs.
|
|
35
|
+
*
|
|
36
|
+
* ## 🛑 Deterministic emphasis, never random
|
|
37
|
+
*
|
|
38
|
+
* The first image takes the large cell and the rest fill the small ones in
|
|
39
|
+
* order. `Math.random()` here would make the hero change on every render, which
|
|
40
|
+
* breaks SSR/hydration agreement and makes the page feel unstable — and it is
|
|
41
|
+
* the shape Root Cause #25 is about on the seed side.
|
|
42
|
+
*/
|
|
43
|
+
export declare function ImageMosaic({ images, alt, className }: ImageMosaicProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MediaImage } from "../../features/media/MediaImage.js";
|
|
3
|
+
import { Div } from "./Div.js";
|
|
4
|
+
/** How many tiles the mosaic draws. More than five reads as a grid, not a hero. */
|
|
5
|
+
const MOSAIC_MAX = 5;
|
|
6
|
+
/** Below this there is nothing to arrange, so the caller should render nothing. */
|
|
7
|
+
export const MOSAIC_MIN = 3;
|
|
8
|
+
/**
|
|
9
|
+
* D3 — a mosaic of a collection's own images, for a hero that has no cover
|
|
10
|
+
* photo of its own.
|
|
11
|
+
*
|
|
12
|
+
* ## 🛑 Why this exists and what it replaces
|
|
13
|
+
*
|
|
14
|
+
* A category with no `display.coverImage` rendered a flat
|
|
15
|
+
* `bg-[var(--appkit-color-bg)]` band behind its title — the same empty strip on
|
|
16
|
+
* every one of the ~1,100 categories that has no photographer. The images were
|
|
17
|
+
* already on the page (the product grid below it), so the hero was blank beside
|
|
18
|
+
* a screen full of pictures.
|
|
19
|
+
*
|
|
20
|
+
* ## 🛑 It is DECORATIVE, and that decides three things
|
|
21
|
+
*
|
|
22
|
+
* 1. **One `alt` for the group, describing the subject** — "Beyblade Burst
|
|
23
|
+
* parts", not five filenames. Five alts for five thumbnails of the same
|
|
24
|
+
* subject is noise to a screen reader, and `alt=""` on each would leave the
|
|
25
|
+
* band unlabelled entirely.
|
|
26
|
+
* 2. **`aria-hidden` is NOT used**, because the group does carry meaning: it is
|
|
27
|
+
* what this category looks like. Hidden decoration plus a labelled group are
|
|
28
|
+
* different claims and the second is true here.
|
|
29
|
+
* 3. **No links.** Every one of these products is linked in the grid below; a
|
|
30
|
+
* second set of hit targets over a title is a tap hazard on a phone and
|
|
31
|
+
* gives a crawler duplicate links to the same URLs.
|
|
32
|
+
*
|
|
33
|
+
* ## 🛑 Deterministic emphasis, never random
|
|
34
|
+
*
|
|
35
|
+
* The first image takes the large cell and the rest fill the small ones in
|
|
36
|
+
* order. `Math.random()` here would make the hero change on every render, which
|
|
37
|
+
* breaks SSR/hydration agreement and makes the page feel unstable — and it is
|
|
38
|
+
* the shape Root Cause #25 is about on the seed side.
|
|
39
|
+
*/
|
|
40
|
+
export function ImageMosaic({ images, alt, className }) {
|
|
41
|
+
const tiles = images.filter(Boolean).slice(0, MOSAIC_MAX);
|
|
42
|
+
if (tiles.length < MOSAIC_MIN)
|
|
43
|
+
return null;
|
|
44
|
+
const [lead, ...rest] = tiles;
|
|
45
|
+
return (_jsxs(Div, { className: `appkit-image-mosaic ${className ?? ""}`, role: "img", "aria-label": alt, children: [_jsx(Div, { className: "appkit-image-mosaic__lead", children: _jsx(MediaImage, { src: lead, alt: "", size: "card" }) }), _jsx(Div, { className: "appkit-image-mosaic__rest", children: rest.map((src, i) => (_jsx(Div, { className: "appkit-image-mosaic__tile", children: _jsx(MediaImage, { src: src, alt: "", size: "thumbnail" }) }, `${src}-${i}`))) })] }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* D3 — the collection mosaic.
|
|
3
|
+
*
|
|
4
|
+
* 🛑 A fixed HEIGHT, not an aspect ratio. This band sits behind a hero title
|
|
5
|
+
* whose length varies with the category name, and an aspect-ratio box would
|
|
6
|
+
* change the title's vertical position per category — so the page would jump
|
|
7
|
+
* around as a visitor moves through the tree. A fixed height per breakpoint is
|
|
8
|
+
* the one shape that keeps the hero stable.
|
|
9
|
+
*
|
|
10
|
+
* 🛑 `overflow: hidden` on the grid and `object-fit: cover` on the images, so a
|
|
11
|
+
* portrait product photo cannot stretch the band. Every image here is a
|
|
12
|
+
* thumbnail of unknown aspect ratio; cropping is the only option that does not
|
|
13
|
+
* distort the subject.
|
|
14
|
+
*/
|
|
15
|
+
.appkit-image-mosaic {
|
|
16
|
+
display: grid;
|
|
17
|
+
grid-template-columns: 2fr 1fr;
|
|
18
|
+
gap: var(--appkit-space-2);
|
|
19
|
+
height: 180px;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
border-radius: var(--appkit-radius-xl);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@media (min-width: 768px) {
|
|
25
|
+
.appkit-image-mosaic {
|
|
26
|
+
height: 260px;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.appkit-image-mosaic__lead,
|
|
31
|
+
.appkit-image-mosaic__tile {
|
|
32
|
+
position: relative;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
border-radius: var(--appkit-radius-lg);
|
|
35
|
+
background: var(--appkit-color-surface);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.appkit-image-mosaic__lead img,
|
|
39
|
+
.appkit-image-mosaic__tile img {
|
|
40
|
+
width: 100%;
|
|
41
|
+
height: 100%;
|
|
42
|
+
object-fit: cover;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.appkit-image-mosaic__rest {
|
|
46
|
+
display: grid;
|
|
47
|
+
grid-template-rows: repeat(2, 1fr);
|
|
48
|
+
gap: var(--appkit-space-2);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* The small column holds at most two tiles on a phone — four 60px squares beside
|
|
53
|
+
* a title is visual noise rather than a preview. The remaining tiles are
|
|
54
|
+
* dropped by CSS rather than by a second render path, so SSR output is one
|
|
55
|
+
* shape at every width (no layout shift on hydration).
|
|
56
|
+
*/
|
|
57
|
+
.appkit-image-mosaic__rest .appkit-image-mosaic__tile:nth-child(n + 3) {
|
|
58
|
+
display: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@media (min-width: 768px) {
|
|
62
|
+
.appkit-image-mosaic__rest {
|
|
63
|
+
grid-template-rows: repeat(2, 1fr);
|
|
64
|
+
grid-template-columns: repeat(2, 1fr);
|
|
65
|
+
}
|
|
66
|
+
.appkit-image-mosaic__rest .appkit-image-mosaic__tile:nth-child(n + 3) {
|
|
67
|
+
display: block;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface SplitTilePane {
|
|
2
|
+
href: string;
|
|
3
|
+
label: string;
|
|
4
|
+
/** A real photo for this pane. Absent panes are never padded with a placeholder. */
|
|
5
|
+
imageUrl: string;
|
|
6
|
+
/** A real, already-counted figure — "128 listings", never invented. */
|
|
7
|
+
countLabel?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SplitTileProps {
|
|
10
|
+
left: SplitTilePane;
|
|
11
|
+
right: SplitTilePane;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* D3 — two real options, side by side, each a real photo and a real count.
|
|
16
|
+
*
|
|
17
|
+
* ## 🛑 Why exactly two, and why both need a photo
|
|
18
|
+
*
|
|
19
|
+
* This exists for the shape a root category with two genuinely distinct
|
|
20
|
+
* branches has — Beyblade vs Battle Gear under Spinning Tops, Companion
|
|
21
|
+
* Animals vs Live Plants under Living Collectibles — where "pick one" is a
|
|
22
|
+
* more honest hero than a uniform grid that treats six branches as
|
|
23
|
+
* interchangeable. It is NOT a general-purpose N-up component: three or more
|
|
24
|
+
* options is what the existing sub-category scroller already does, and
|
|
25
|
+
* `ImageMosaic` is the pattern for "we have no single cover photo but we have
|
|
26
|
+
* many product photos".
|
|
27
|
+
*
|
|
28
|
+
* 🛑 THE CALLER DECIDES WHETHER TO RENDER THIS AT ALL. `SplitTile` takes two
|
|
29
|
+
* required panes and renders both — it has no "only one pane" fallback and no
|
|
30
|
+
* placeholder image, because a tile with an invented or missing side is worse
|
|
31
|
+
* than not showing the pattern. A category with only one photographed branch
|
|
32
|
+
* falls through to the ordinary sub-category scroller instead.
|
|
33
|
+
*/
|
|
34
|
+
export declare function SplitTile({ left, right, className }: SplitTileProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Anchor } from "./Anchor.js";
|
|
3
|
+
import { Div } from "./Div.js";
|
|
4
|
+
import { Heading, Span } from "./Typography.js";
|
|
5
|
+
import { MediaImage } from "../../features/media/MediaImage.js";
|
|
6
|
+
/**
|
|
7
|
+
* D3 — two real options, side by side, each a real photo and a real count.
|
|
8
|
+
*
|
|
9
|
+
* ## 🛑 Why exactly two, and why both need a photo
|
|
10
|
+
*
|
|
11
|
+
* This exists for the shape a root category with two genuinely distinct
|
|
12
|
+
* branches has — Beyblade vs Battle Gear under Spinning Tops, Companion
|
|
13
|
+
* Animals vs Live Plants under Living Collectibles — where "pick one" is a
|
|
14
|
+
* more honest hero than a uniform grid that treats six branches as
|
|
15
|
+
* interchangeable. It is NOT a general-purpose N-up component: three or more
|
|
16
|
+
* options is what the existing sub-category scroller already does, and
|
|
17
|
+
* `ImageMosaic` is the pattern for "we have no single cover photo but we have
|
|
18
|
+
* many product photos".
|
|
19
|
+
*
|
|
20
|
+
* 🛑 THE CALLER DECIDES WHETHER TO RENDER THIS AT ALL. `SplitTile` takes two
|
|
21
|
+
* required panes and renders both — it has no "only one pane" fallback and no
|
|
22
|
+
* placeholder image, because a tile with an invented or missing side is worse
|
|
23
|
+
* than not showing the pattern. A category with only one photographed branch
|
|
24
|
+
* falls through to the ordinary sub-category scroller instead.
|
|
25
|
+
*/
|
|
26
|
+
export function SplitTile({ left, right, className }) {
|
|
27
|
+
return (_jsx(Div, { className: `grid grid-cols-1 md:grid-cols-2 gap-[var(--appkit-space-3)] ${className ?? ""}`, children: [left, right].map((pane) => (_jsxs(Anchor, { href: pane.href, underline: "none", className: "group relative block overflow-hidden rounded-xl h-[160px] md:h-[220px]", children: [_jsx(MediaImage, { src: pane.imageUrl, alt: "", size: "banner", className: "absolute inset-0 h-full w-full object-cover transition-transform group-hover:scale-105" }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" }), _jsxs(Div, { className: "absolute inset-0 flex flex-col justify-end p-[var(--appkit-space-4)]", children: [_jsx(Heading, { level: 3, color: "inverse", size: "lg", weight: "bold", children: pane.label }), pane.countLabel && (_jsx(Span, { color: "inverse", size: "xs", className: "mt-[var(--appkit-space-1)] opacity-80", children: pane.countLabel }))] })] }, pane.href))) }));
|
|
28
|
+
}
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -103,6 +103,10 @@ export type { StarRatingProps } from "./components/StarRating";
|
|
|
103
103
|
export { StarRating } from "./components/StarRating";
|
|
104
104
|
export type { BreadcrumbProps, BreadcrumbItem } from "./components/Breadcrumb";
|
|
105
105
|
export { Breadcrumb } from "./components/Breadcrumb";
|
|
106
|
+
export { ImageMosaic, MOSAIC_MIN } from "./components/ImageMosaic";
|
|
107
|
+
export type { ImageMosaicProps } from "./components/ImageMosaic";
|
|
108
|
+
export { SplitTile } from "./components/SplitTile";
|
|
109
|
+
export type { SplitTileProps, SplitTilePane } from "./components/SplitTile";
|
|
106
110
|
export type { ImageLightboxProps, LightboxImage, } from "./components/ImageLightbox";
|
|
107
111
|
export { ImageLightbox } from "./components/ImageLightbox";
|
|
108
112
|
export type { TagInputProps } from "./components/TagInput";
|
package/dist/ui/index.js
CHANGED
|
@@ -81,6 +81,10 @@ export { QuantityStepper } from "./components/QuantityStepper.js";
|
|
|
81
81
|
export { ToastProvider, useToast } from "./components/Toast.js";
|
|
82
82
|
export { StarRating } from "./components/StarRating.js";
|
|
83
83
|
export { Breadcrumb } from "./components/Breadcrumb.js";
|
|
84
|
+
/* D3 — the collection mosaic, for a hero with no cover photo of its own. */
|
|
85
|
+
export { ImageMosaic, MOSAIC_MIN } from "./components/ImageMosaic.js";
|
|
86
|
+
/* D3 — two real branches, side by side with real photos and real counts. */
|
|
87
|
+
export { SplitTile } from "./components/SplitTile.js";
|
|
84
88
|
export { ImageLightbox } from "./components/ImageLightbox.js";
|
|
85
89
|
export { TagInput } from "./components/TagInput.js";
|
|
86
90
|
export { StepperNav } from "./components/StepperNav.js";
|
|
@@ -67,6 +67,69 @@ export declare const SEARCH_REFINE_WINDOW = 250;
|
|
|
67
67
|
* false last one. Reporting a saturated window as an exact total is the defect
|
|
68
68
|
* above with a bigger number.
|
|
69
69
|
*/
|
|
70
|
+
/**
|
|
71
|
+
* The page fields `beginSearchWindow` overrides.
|
|
72
|
+
*
|
|
73
|
+
* Structural rather than importing `SieveModel`, so `utils/` keeps no
|
|
74
|
+
* dependency on `providers/db-firebase` (`audit-import-cycles`), and NOT
|
|
75
|
+
* `unknown` — the shapes are the same `number | string` Sieve itself accepts,
|
|
76
|
+
* and `unknown` here would be a type-level shrug that `audit-unknown-leakage`
|
|
77
|
+
* correctly refuses.
|
|
78
|
+
*/
|
|
79
|
+
export interface SearchWindowModel {
|
|
80
|
+
page?: number | string;
|
|
81
|
+
pageSize?: number | string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The three coupled decisions a refined search has to make, in ONE place.
|
|
85
|
+
*
|
|
86
|
+
* ## 🛑 Why this exists rather than 13 hand-written copies
|
|
87
|
+
*
|
|
88
|
+
* `refineSearchTxt` grew a real-pagination mode, and `products` was the only
|
|
89
|
+
* caller converted — deliberately, so the other eleven stayed byte-identical.
|
|
90
|
+
* The conversion is mechanical and has THREE parts that must all happen:
|
|
91
|
+
*
|
|
92
|
+
* 1. fetch a bounded WINDOW instead of the requested page;
|
|
93
|
+
* 2. raise `maxPageSize` to that window, or Sieve clamps the request back to
|
|
94
|
+
* its own limit and the window silently becomes a page again — the
|
|
95
|
+
* original bug with a larger constant;
|
|
96
|
+
* 3. pass `{page, pageSize, windowSaturated}` so the refine slices the
|
|
97
|
+
* requested page out and reports `truncated`.
|
|
98
|
+
*
|
|
99
|
+
* Doing 1 and 3 without 2 reads exactly like a working conversion and is the
|
|
100
|
+
* half that no-ops. So the three move together here, and a caller cannot have
|
|
101
|
+
* two of them.
|
|
102
|
+
*
|
|
103
|
+
* ```ts
|
|
104
|
+
* const win = beginSearchWindow(model, plan);
|
|
105
|
+
* const result = await this.sieveQuery(win.model, FIELDS, {
|
|
106
|
+
* baseQuery, defaultPageSize: 20, maxPageSize: win.maxPageSize(100),
|
|
107
|
+
* });
|
|
108
|
+
* return win.finish(result);
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export declare function beginSearchWindow<M extends SearchWindowModel>(model: M, plan: SearchTxtPlan,
|
|
112
|
+
/**
|
|
113
|
+
* True when something OTHER than the extra search terms also has to be
|
|
114
|
+
* refined in memory — a filter Firestore could not express beside the
|
|
115
|
+
* search clause. Then the window is needed even for a single-term query.
|
|
116
|
+
*/
|
|
117
|
+
opts?: {
|
|
118
|
+
alsoRefining?: boolean;
|
|
119
|
+
}): {
|
|
120
|
+
model: M;
|
|
121
|
+
maxPageSize(normal: number): number;
|
|
122
|
+
finish<T extends {
|
|
123
|
+
searchTxt?: string[];
|
|
124
|
+
}, R extends {
|
|
125
|
+
items: T[];
|
|
126
|
+
total: number;
|
|
127
|
+
totalPages: number;
|
|
128
|
+
hasMore: boolean;
|
|
129
|
+
}>(result: R, also?: (row: T) => boolean): R & {
|
|
130
|
+
truncated?: boolean;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
70
133
|
export declare function refineSearchTxt<T extends {
|
|
71
134
|
searchTxt?: string[];
|
|
72
135
|
}, R extends {
|
|
@@ -85,6 +148,20 @@ opts?: {
|
|
|
85
148
|
pageSize: number;
|
|
86
149
|
/** True when the fetch returned a full window, so more may exist. */
|
|
87
150
|
windowSaturated: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* One more AND-condition to apply over the same window, for a filter
|
|
153
|
+
* Firestore could not express alongside the search clause.
|
|
154
|
+
*
|
|
155
|
+
* 🛑 It exists so there is ONE refine-and-paginate implementation rather
|
|
156
|
+
* than a second copy per filter. Firestore permits a single array operator
|
|
157
|
+
* per query, so when a search and a category filter arrive together the
|
|
158
|
+
* search takes the clause — and the category then HAS to be refined here
|
|
159
|
+
* or it is simply not applied (which is exactly what used to happen:
|
|
160
|
+
* searching inside a category returned matches from the whole catalogue).
|
|
161
|
+
* The window bookkeeping, the `total`-is-a-floor rule and `truncated` are
|
|
162
|
+
* identical for both conditions, which is why they share this function.
|
|
163
|
+
*/
|
|
164
|
+
also?: (row: T) => boolean;
|
|
88
165
|
}): R & {
|
|
89
166
|
truncated?: boolean;
|
|
90
167
|
};
|
|
@@ -50,38 +50,58 @@ export function emptySearchResult() {
|
|
|
50
50
|
*/
|
|
51
51
|
export const SEARCH_REFINE_WINDOW = 250;
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
53
|
+
* The three coupled decisions a refined search has to make, in ONE place.
|
|
54
54
|
*
|
|
55
|
-
* ## 🛑
|
|
55
|
+
* ## 🛑 Why this exists rather than 13 hand-written copies
|
|
56
56
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* `refineSearchTxt` grew a real-pagination mode, and `products` was the only
|
|
58
|
+
* caller converted — deliberately, so the other eleven stayed byte-identical.
|
|
59
|
+
* The conversion is mechanical and has THREE parts that must all happen:
|
|
60
60
|
*
|
|
61
|
-
* 1.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
61
|
+
* 1. fetch a bounded WINDOW instead of the requested page;
|
|
62
|
+
* 2. raise `maxPageSize` to that window, or Sieve clamps the request back to
|
|
63
|
+
* its own limit and the window silently becomes a page again — the
|
|
64
|
+
* original bug with a larger constant;
|
|
65
|
+
* 3. pass `{page, pageSize, windowSaturated}` so the refine slices the
|
|
66
|
+
* requested page out and reports `truncated`.
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
68
|
+
* Doing 1 and 3 without 2 reads exactly like a working conversion and is the
|
|
69
|
+
* half that no-ops. So the three move together here, and a caller cannot have
|
|
70
|
+
* two of them.
|
|
70
71
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
72
|
+
* ```ts
|
|
73
|
+
* const win = beginSearchWindow(model, plan);
|
|
74
|
+
* const result = await this.sieveQuery(win.model, FIELDS, {
|
|
75
|
+
* baseQuery, defaultPageSize: 20, maxPageSize: win.maxPageSize(100),
|
|
76
|
+
* });
|
|
77
|
+
* return win.finish(result);
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function beginSearchWindow(model, plan,
|
|
81
|
+
/**
|
|
82
|
+
* True when something OTHER than the extra search terms also has to be
|
|
83
|
+
* refined in memory — a filter Firestore could not express beside the
|
|
84
|
+
* search clause. Then the window is needed even for a single-term query.
|
|
84
85
|
*/
|
|
86
|
+
opts) {
|
|
87
|
+
const refining = plan.rest.length > 0 || opts?.alsoRefining === true;
|
|
88
|
+
const page = Math.max(1, Number(model?.page ?? 1) || 1);
|
|
89
|
+
const pageSize = Math.max(1, Number(model?.pageSize ?? 20) || 20);
|
|
90
|
+
return {
|
|
91
|
+
model: refining ? { ...model, page: 1, pageSize: SEARCH_REFINE_WINDOW } : model,
|
|
92
|
+
maxPageSize: (normal) => (refining ? SEARCH_REFINE_WINDOW : normal),
|
|
93
|
+
finish: (result, also) => refineSearchTxt(result, plan.rest, refining
|
|
94
|
+
? {
|
|
95
|
+
page,
|
|
96
|
+
pageSize,
|
|
97
|
+
// Saturated means more may exist beyond the window, so the
|
|
98
|
+
// refined `total` is a FLOOR and `truncated` says so.
|
|
99
|
+
windowSaturated: (result.items?.length ?? 0) >= SEARCH_REFINE_WINDOW,
|
|
100
|
+
...(also ? { also } : {}),
|
|
101
|
+
}
|
|
102
|
+
: undefined),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
85
105
|
export function refineSearchTxt(result, rest,
|
|
86
106
|
/**
|
|
87
107
|
* Supply these to get real pagination. Omitted, the behaviour is the old
|
|
@@ -89,9 +109,11 @@ export function refineSearchTxt(result, rest,
|
|
|
89
109
|
* until each is converted, rather than all of them changing at once.
|
|
90
110
|
*/
|
|
91
111
|
opts) {
|
|
92
|
-
|
|
112
|
+
const also = opts?.also;
|
|
113
|
+
if (rest.length === 0 && !also)
|
|
93
114
|
return result;
|
|
94
|
-
const matched = result.items.filter((row) => matchesAllSearchTerms(row.searchTxt, rest))
|
|
115
|
+
const matched = result.items.filter((row) => (rest.length === 0 || matchesAllSearchTerms(row.searchTxt, rest)) &&
|
|
116
|
+
(!also || also(row)));
|
|
95
117
|
if (!opts) {
|
|
96
118
|
/*
|
|
97
119
|
* The legacy shape. `total` is this page's count — a floor — and
|