@mohasinac/appkit 4.69.1 → 4.70.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 +3 -3
- package/dist/features/about/index.d.ts +0 -2
- package/dist/features/about/index.js +0 -1
- package/dist/features/admin/schemas/firestore.js +0 -1
- package/dist/features/homepage/components/FAQSection.js +1 -1
- package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -4
- package/dist/styles.css +20 -0
- package/dist/ui/components/Tabs.d.ts +9 -1
- package/dist/ui/components/Tabs.js +20 -3
- package/dist/ui/components/Tabs.style.css +20 -0
- package/dist/ui/rich-text/RichText.d.ts +0 -12
- package/dist/ui/rich-text/RichText.js +52 -1
- package/package.json +1 -1
- package/dist/features/about/components/AboutView.d.ts +0 -30
- package/dist/features/about/components/AboutView.js +0 -14
package/dist/.build-stamp.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"stampVersion": 1,
|
|
3
|
-
"version": "4.
|
|
4
|
-
"head": "
|
|
3
|
+
"version": "4.70.0",
|
|
4
|
+
"head": "2818ecbd04176961cc2184dc89a3bf319efb92d4",
|
|
5
5
|
"branch": "main",
|
|
6
6
|
"dirty": false,
|
|
7
|
-
"builtAt": "2026-10-
|
|
7
|
+
"builtAt": "2026-10-28T02:31:30.990Z"
|
|
8
8
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export { AboutView } from "./components/AboutView";
|
|
2
|
-
export type { AboutViewProps, AboutHowItem, AboutValueItem, AboutMilestone, AboutTeamMember, } from "./components/AboutView";
|
|
3
1
|
export type { AboutContentDocument } from "./schemas/firestore";
|
|
4
2
|
export { TrackOrderView } from "./components/TrackOrderView";
|
|
5
3
|
export type { TrackOrderViewProps } from "./components/TrackOrderView";
|
|
@@ -442,7 +442,6 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
|
|
|
442
442
|
String(ROUTES.PUBLIC.ITEM_REQUESTS),
|
|
443
443
|
String(ROUTES.PUBLIC.LOTTERIES),
|
|
444
444
|
String(ROUTES.PUBLIC.PARTS),
|
|
445
|
-
String(ROUTES.PUBLIC.PROMOTIONS),
|
|
446
445
|
String(ROUTES.PUBLIC.REPORT_ENTITY),
|
|
447
446
|
String(ROUTES.PUBLIC.SCAMS),
|
|
448
447
|
/* No `ROUTES.PUBLIC.SELL` constant exists, so `/sell` — the public
|
|
@@ -65,7 +65,7 @@ export function FAQSection({ title, subtitle, items, tabs: tabsProp, showCategor
|
|
|
65
65
|
return items;
|
|
66
66
|
return items.filter((i) => (i.category ?? "general") === activeTab);
|
|
67
67
|
}, [items, showTabs, activeTab]);
|
|
68
|
-
return (_jsx(Section, { paddingX: "x-md-2xl", className: className, surface: "subtle", paddingY: "y-4xl", children: _jsxs(Div, { className: "w-full max-w-4xl mx-auto", children: [_jsxs(Div, { className: "text-left mb-10", children: [_jsx(Heading, { level: 2, variant: "none", gradient: "brand", className: "mb-3", mdSize: "4xl", size: "3xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { className: `max-w-xl ${themed.textSecondary}`, size: "base", children: subtitle }))] }), showTabs && (_jsx(Tabs, { value: activeTab, onChange: setActiveTab, className: "mb-8", children: _jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "all", children: "All" }), derivedTabs.map((tab) => (_jsx(TabsTrigger, { value: tab.value, children: tab.label }, tab.value)))] }) })), _jsxs(Stack, { gap: "none", rounded: "xl", className: `${themed.bgPrimary} ${__O.hidden} border ${themed.border}`, children: [visibleItems.length === 0 && (_jsx(Text, { paddingY: "3xl", align: "start", className: themed.textSecondary, children: "No FAQs in this category yet." })), visibleItems.map((faq, index) => {
|
|
68
|
+
return (_jsx(Section, { paddingX: "x-md-2xl", className: className, surface: "subtle", paddingY: "y-4xl", children: _jsxs(Div, { className: "w-full max-w-4xl mx-auto", children: [_jsxs(Div, { className: "text-left mb-10", children: [_jsx(Heading, { level: 2, variant: "none", gradient: "brand", className: "mb-3", mdSize: "4xl", size: "3xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { className: `max-w-xl ${themed.textSecondary}`, size: "base", children: subtitle }))] }), showTabs && (_jsx(Tabs, { value: activeTab, onChange: setActiveTab, className: "mb-8", children: _jsxs(TabsList, { wrap: true, children: [_jsx(TabsTrigger, { value: "all", children: "All" }), derivedTabs.map((tab) => (_jsx(TabsTrigger, { value: tab.value, children: tab.label }, tab.value)))] }) })), _jsxs(Stack, { gap: "none", rounded: "xl", className: `${themed.bgPrimary} ${__O.hidden} border ${themed.border}`, children: [visibleItems.length === 0 && (_jsx(Text, { paddingY: "3xl", align: "start", className: themed.textSecondary, children: "No FAQs in this category yet." })), visibleItems.map((faq, index) => {
|
|
69
69
|
const isOpen = openIds.has(faq.id);
|
|
70
70
|
return (_jsxs(Div, { className: `transition-colors duration-200 ${index < visibleItems.length - 1 ? "border-b border-[var(--appkit-color-border)]" : ""} ${isOpen ? "bg-[var(--appkit-color-surface-elevated)]" : ""}`, children: [_jsxs(Button, { variant: "ghost", rounded: "none", className: `w-full text-left px-[1.25rem] py-[1rem] ${flex.between} gap-[1rem] hover:${themed.bgTertiary} transition-colors`, onClick: () => toggle(faq.id), "aria-expanded": isOpen, children: [_jsx(Span, { size: "sm", weight: "medium", className: `flex-1 leading-snug ${isOpen ? "text-primary" : themed.textPrimary}`, children: faq.question }), _jsx(ChevronDown, { className: `w-4 h-4 flex-shrink-0 transition-transform duration-250 ${isOpen ? "rotate-180 text-primary" : themed.textSecondary}` })] }), _jsx(Div, { layout: "grid", className: `transition-all duration-300 ease-out ${isOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"}`, children: _jsx(Div, { className: `${__O.hidden}`, children: _jsx(Div, { paddingY: "b-md-lg", padding: "x-md", children: isHtml(faq.answer) ? (_jsx(RichText, { html: faq.answer, proseClass: "prose prose-sm max-w-none dark:prose-invert" })) : (_jsx(Text, { className: `leading-relaxed ${themed.textSecondary}`, size: "sm", children: faq.answer })) }) }) })] }, faq.id));
|
|
71
71
|
})] }), viewMoreHref && (_jsx(Div, { className: "text-left mt-10", children: _jsxs(TextLink, { rounded: "full", paddingX: "xl", paddingY: "sm", href: viewMoreHref, layout: "inline-flex", align: "center", className: "gap-[0.5rem] bg-primary/10 text-primary hover:bg-primary/20 dark:bg-primary/15 transition-colors", size: "sm", weight: "medium", children: [viewMoreLabel, hasMore && moreCount > 0 && (_jsxs(Span, { color: "inverse", size: "xs", className: "bg-primary", rounded: "full", padding: "pill-xs", children: ["+", moreCount] }))] }) }))] }) }));
|
|
@@ -22,6 +22,7 @@ import { gateCarouselSlides } from "../lib/slide-gating.js";
|
|
|
22
22
|
import { gateHomepageSections } from "../lib/section-gating.js";
|
|
23
23
|
import { closedSurfaceContext, isClosedHref } from "../../../_internal/shared/listing-types/route-gating.js";
|
|
24
24
|
import { getSurfaceCounts } from "../../../_internal/server/features/products/surface-counts.js";
|
|
25
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
25
26
|
import { SECTION_COPY } from "../constants/section-copy.js";
|
|
26
27
|
import { homepageSectionsRepository } from "../repository/homepage-sections.repository.js";
|
|
27
28
|
import { isListingTypeEnabled, isCategoryTypeEnabled, isBrowsePageEnabled, } from "../../../_internal/shared/listing-types/feature-flags.js";
|
|
@@ -75,6 +76,36 @@ const HOMEPAGE_SECTION_BROWSE_PAGE = {
|
|
|
75
76
|
brands: "brands",
|
|
76
77
|
stores: "stores",
|
|
77
78
|
};
|
|
79
|
+
/**
|
|
80
|
+
* The FOURTH axis — a section that funnels into a route closed by
|
|
81
|
+
* `closedPaths` or `disabledRoutes`, neither of which the three maps above
|
|
82
|
+
* can see.
|
|
83
|
+
*
|
|
84
|
+
* 🛑 THIS ONE EXISTS BECAUSE A SEED LOAD RE-ENABLED A SECTION. The blog rail
|
|
85
|
+
* was switched off per-document, `appkit-seed load` writes `set(merge: true)`
|
|
86
|
+
* over `enabled`, and the seed still said `true` — so the rail came back and
|
|
87
|
+
* the homepage shipped FIVE dead links (`/blog` plus four post URLs) into a
|
|
88
|
+
* section closed via `disabledRoutes`.
|
|
89
|
+
*
|
|
90
|
+
* A per-document `enabled: false` is a value a reseed can overwrite. A gate
|
|
91
|
+
* keyed on whether the destination is OPEN cannot be: the section stays dark
|
|
92
|
+
* for exactly as long as the route is shut, however the flag got flipped.
|
|
93
|
+
* That is the difference between fixing this instance and fixing the class.
|
|
94
|
+
*/
|
|
95
|
+
/*
|
|
96
|
+
* 🛑 `coupons` is NOT here. Its "View all offers" button points at
|
|
97
|
+
* `/promotions`, which is a real offers page with a live coupon behind it —
|
|
98
|
+
* so the fix was to REOPEN that route, not to hide a working rail. Closing a
|
|
99
|
+
* page and then hiding everything that links to it is the wrong direction
|
|
100
|
+
* when the page is one a shop wants.
|
|
101
|
+
*/
|
|
102
|
+
const HOMEPAGE_SECTION_ROUTE = {
|
|
103
|
+
"blog-articles": String(ROUTES.BLOG.LIST),
|
|
104
|
+
events: String(ROUTES.PUBLIC.EVENTS),
|
|
105
|
+
"event-raffles": String(ROUTES.PUBLIC.EVENTS),
|
|
106
|
+
reviews: String(ROUTES.PUBLIC.REVIEWS),
|
|
107
|
+
"featured-bundles": String(ROUTES.PUBLIC.BUNDLES),
|
|
108
|
+
};
|
|
78
109
|
import { getFeaturedProducts, getFeaturedAuctions, getFeaturedPreOrders } from "../../products/actions/product-actions.js";
|
|
79
110
|
import { listTopLevelCategories, listBrandCategories } from "../../categories/actions/category-actions.js";
|
|
80
111
|
import { listFeaturedBundles } from "../../../_internal/server/features/bundles/data.js";
|
|
@@ -189,6 +220,9 @@ export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onB
|
|
|
189
220
|
const gatedBrowse = HOMEPAGE_SECTION_BROWSE_PAGE[type];
|
|
190
221
|
if (gatedBrowse && !isBrowsePageEnabled(gatedBrowse, siteSettings))
|
|
191
222
|
return false;
|
|
223
|
+
const gatedRoute = HOMEPAGE_SECTION_ROUTE[type];
|
|
224
|
+
if (gatedRoute && isClosedHref(gatedRoute, closedSurfaces))
|
|
225
|
+
return false;
|
|
192
226
|
return true;
|
|
193
227
|
});
|
|
194
228
|
/*
|
package/dist/index.d.ts
CHANGED
|
@@ -753,7 +753,6 @@ export { validateFileSize } from "./providers/storage-firebase/client";
|
|
|
753
753
|
export { validateFileType } from "./providers/storage-firebase/client";
|
|
754
754
|
export { validateImage } from "./providers/storage-firebase/client";
|
|
755
755
|
export type { StorageHelpers } from "./providers/storage-firebase/client";
|
|
756
|
-
export { AboutView } from "./features/about/components/AboutView";
|
|
757
756
|
export { FAQPageView } from "./features/about/components/FAQPageView";
|
|
758
757
|
export { FeesView } from "./features/about/components/FeesView";
|
|
759
758
|
export { HelpPageView } from "./features/about/components/HelpPageView";
|
|
@@ -774,7 +773,6 @@ export { TrackOrderView } from "./features/about/components/TrackOrderView";
|
|
|
774
773
|
export type { AboutHowItem } from "./features/about/schemas/firestore";
|
|
775
774
|
export type { AboutMilestone } from "./features/about/schemas/firestore";
|
|
776
775
|
export type { AboutValueItem } from "./features/about/schemas/firestore";
|
|
777
|
-
export type { AboutViewProps } from "./features/about/components/AboutView";
|
|
778
776
|
export type { AboutTeamMember } from "./features/about/schemas/firestore";
|
|
779
777
|
export type { AboutContentDocument } from "./features/about/schemas/firestore";
|
|
780
778
|
export type { FAQPageViewProps } from "./features/about/components/FAQPageView";
|
package/dist/index.js
CHANGED
|
@@ -1580,10 +1580,6 @@ export { validateFileType } from "./providers/storage-firebase/client.js";
|
|
|
1580
1580
|
// [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
|
|
1581
1581
|
// validateImage - Helper for validate image.
|
|
1582
1582
|
export { validateImage } from "./providers/storage-firebase/client.js";
|
|
1583
|
-
// ./features/about/index
|
|
1584
|
-
// [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
|
|
1585
|
-
// AboutView - Component for about view.
|
|
1586
|
-
export { AboutView } from "./features/about/components/AboutView.js";
|
|
1587
1583
|
// [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
|
|
1588
1584
|
// FAQPageView - Component for faqpage view.
|
|
1589
1585
|
export { FAQPageView } from "./features/about/components/FAQPageView.js";
|
package/dist/styles.css
CHANGED
|
@@ -7444,6 +7444,26 @@
|
|
|
7444
7444
|
color: var(--appkit-tabs-ink-active);
|
|
7445
7445
|
}
|
|
7446
7446
|
|
|
7447
|
+
/*
|
|
7448
|
+
* Wrapping tab bar — `<TabsList wrap>`.
|
|
7449
|
+
*
|
|
7450
|
+
* 🛑 For CONTENT tabs inside a page section, never for navigation. The
|
|
7451
|
+
* default list scrolls horizontally and collapses into a dropdown once it
|
|
7452
|
+
* overflows, which is right for a nav bar on a phone and wrong in the middle
|
|
7453
|
+
* of a homepage section: there it renders as a stray form control, and the
|
|
7454
|
+
* visitor cannot see what the section contains without opening it.
|
|
7455
|
+
*
|
|
7456
|
+
* Wrapping keeps every tab visible on a second line. `overflow: visible` is
|
|
7457
|
+
* required alongside `flex-wrap` — the base rule's `overflow-x: auto` would
|
|
7458
|
+
* otherwise still clip the wrapped rows and reintroduce a scrollbar.
|
|
7459
|
+
*/
|
|
7460
|
+
.appkit-tabs-list--wrap {
|
|
7461
|
+
flex-wrap: wrap;
|
|
7462
|
+
overflow-x: visible;
|
|
7463
|
+
overflow-y: visible;
|
|
7464
|
+
scrollbar-width: auto;
|
|
7465
|
+
}
|
|
7466
|
+
|
|
7447
7467
|
/* Radio — toggle and classic radio group variants */
|
|
7448
7468
|
|
|
7449
7469
|
.appkit-radio-group {
|
|
@@ -7,6 +7,14 @@ export interface TabsProps {
|
|
|
7
7
|
children?: React.ReactNode;
|
|
8
8
|
}
|
|
9
9
|
export interface TabsListProps {
|
|
10
|
+
/**
|
|
11
|
+
* Never collapse into a dropdown; let the tabs wrap onto a second line.
|
|
12
|
+
*
|
|
13
|
+
* For CONTENT tabs inside a section, where a dropdown reads as a stray form
|
|
14
|
+
* control. Navigation tab bars should leave this `false` — see the note in
|
|
15
|
+
* `TabsList`.
|
|
16
|
+
*/
|
|
17
|
+
wrap?: boolean;
|
|
10
18
|
className?: string;
|
|
11
19
|
children?: React.ReactNode;
|
|
12
20
|
}
|
|
@@ -30,7 +38,7 @@ export interface TabsContentProps {
|
|
|
30
38
|
children?: React.ReactNode;
|
|
31
39
|
}
|
|
32
40
|
export declare function Tabs({ defaultValue, value, onChange, className, children, }: TabsProps): React.JSX.Element;
|
|
33
|
-
export declare function TabsList({ className, children }: TabsListProps): React.JSX.Element;
|
|
41
|
+
export declare function TabsList({ className, children, wrap }: TabsListProps): React.JSX.Element;
|
|
34
42
|
export declare function TabsTrigger({ value, disabled, className, children, badge, }: TabsTriggerProps): React.JSX.Element;
|
|
35
43
|
export declare function TabsContent({ value, className, children, }: TabsContentProps): React.JSX.Element | null;
|
|
36
44
|
export default Tabs;
|
|
@@ -27,7 +27,7 @@ export function Tabs({ defaultValue = "", value, onChange, className = "", child
|
|
|
27
27
|
}), [active, onChange, value]);
|
|
28
28
|
return (_jsx(TabsContext.Provider, { value: contextValue, children: _jsx("div", { className: className, "data-section": "tabs-div-614", children: children }) }));
|
|
29
29
|
}
|
|
30
|
-
export function TabsList({ className = "", children }) {
|
|
30
|
+
export function TabsList({ className = "", children, wrap = false }) {
|
|
31
31
|
const { active, setActive } = useContext(TabsContext);
|
|
32
32
|
const shellRef = useRef(null);
|
|
33
33
|
const rowRef = useRef(null);
|
|
@@ -47,7 +47,24 @@ export function TabsList({ className = "", children }) {
|
|
|
47
47
|
onSelect: () => setActive(item.props.value),
|
|
48
48
|
}));
|
|
49
49
|
const signature = overflowItems.map((i) => `${i.id}:${i.label}:${i.count ?? ""}`).join("|");
|
|
50
|
-
const
|
|
50
|
+
const measured = useRowCollapse(shellRef, rowRef, signature);
|
|
51
|
+
/*
|
|
52
|
+
* 🛑 `wrap` OPTS OUT OF COLLAPSING ENTIRELY, and it exists because
|
|
53
|
+
* collapsing is only right for NAVIGATION.
|
|
54
|
+
*
|
|
55
|
+
* Owner, 2026-10-27: *"it makes sense for forms or navigations, but no
|
|
56
|
+
* sense for a homepage section's tabs to be coming down."* That is the
|
|
57
|
+
* correct distinction. A nav bar that collapses hides destinations behind
|
|
58
|
+
* one more tap, which is a fair trade on a phone. A content filter inside a
|
|
59
|
+
* section — the homepage FAQ's category tabs — collapses into a dropdown
|
|
60
|
+
* that looks like a form control sitting in the middle of a page, and the
|
|
61
|
+
* visitor cannot see at a glance what the section even contains.
|
|
62
|
+
*
|
|
63
|
+
* Wrapping is the right failure mode here: the tabs take a second line and
|
|
64
|
+
* stay visible. Default stays `false`, so every existing nav tab bar is
|
|
65
|
+
* untouched.
|
|
66
|
+
*/
|
|
67
|
+
const collapsed = wrap ? false : measured;
|
|
51
68
|
/*
|
|
52
69
|
* Collapsed renders `OverflowRow` in its already-collapsed state rather than
|
|
53
70
|
* the old `TabsNavSelect`, which was a native `<select>` — so the collapsed
|
|
@@ -62,7 +79,7 @@ export function TabsList({ className = "", children }) {
|
|
|
62
79
|
if (collapsed) {
|
|
63
80
|
return (_jsx("div", { ref: shellRef, className: className, children: _jsx(OverflowRow, { items: overflowItems, ariaLabel: "Tabs", activeId: active, alwaysCollapsed: true, searchPlaceholder: "Search tabs\u2026" }) }));
|
|
64
81
|
}
|
|
65
|
-
return (_jsx("div", { ref: shellRef, children: _jsx(TabBarShell, { className: className, ariaLabel: "Tabs", activeKey: active, listRef: rowRef, children: children }) }));
|
|
82
|
+
return (_jsx("div", { ref: shellRef, children: _jsx(TabBarShell, { className: [className, wrap ? "appkit-tabs-list--wrap" : ""].filter(Boolean).join(" "), ariaLabel: "Tabs", activeKey: active, listRef: rowRef, children: children }) }));
|
|
66
83
|
}
|
|
67
84
|
export function TabsTrigger({ value, disabled, className = "", children, badge, }) {
|
|
68
85
|
const { active, setActive } = useContext(TabsContext);
|
|
@@ -290,3 +290,23 @@
|
|
|
290
290
|
border-color: transparent;
|
|
291
291
|
color: var(--appkit-tabs-ink-active);
|
|
292
292
|
}
|
|
293
|
+
|
|
294
|
+
/*
|
|
295
|
+
* Wrapping tab bar — `<TabsList wrap>`.
|
|
296
|
+
*
|
|
297
|
+
* 🛑 For CONTENT tabs inside a page section, never for navigation. The
|
|
298
|
+
* default list scrolls horizontally and collapses into a dropdown once it
|
|
299
|
+
* overflows, which is right for a nav bar on a phone and wrong in the middle
|
|
300
|
+
* of a homepage section: there it renders as a stray form control, and the
|
|
301
|
+
* visitor cannot see what the section contains without opening it.
|
|
302
|
+
*
|
|
303
|
+
* Wrapping keeps every tab visible on a second line. `overflow: visible` is
|
|
304
|
+
* required alongside `flex-wrap` — the base rule's `overflow-x: auto` would
|
|
305
|
+
* otherwise still clip the wrapped rows and reintroduce a scrollbar.
|
|
306
|
+
*/
|
|
307
|
+
.appkit-tabs-list--wrap {
|
|
308
|
+
flex-wrap: wrap;
|
|
309
|
+
overflow-x: visible;
|
|
310
|
+
overflow-y: visible;
|
|
311
|
+
scrollbar-width: auto;
|
|
312
|
+
}
|
|
@@ -39,16 +39,4 @@ export interface RichTextProps {
|
|
|
39
39
|
*/
|
|
40
40
|
charts?: boolean;
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
43
|
-
* RichText renders sanitised HTML from a CMS / ProseMirror output.
|
|
44
|
-
*
|
|
45
|
-
* Security: client-side DOM-based sanitiser strips disallowed tags/attrs.
|
|
46
|
-
* For defence-in-depth, also sanitise on the server before storing.
|
|
47
|
-
*
|
|
48
|
-
* Usage:
|
|
49
|
-
* ```tsx
|
|
50
|
-
* <RichText html={blogPost.body} className="max-w-2xl" />
|
|
51
|
-
* <RichText html={desc} copyableCode />
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
42
|
export declare function RichText({ html, className, proseClass, copyableCode, highlightCode, charts, }: RichTextProps): import("react").JSX.Element;
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
import { Div } from "../components/Div.js";
|
|
5
5
|
import { normalizeRichTextHtml } from "../../utils/string.formatter.js";
|
|
6
|
+
import { useSiteSettings } from "../../core/hooks/useSiteSettings.js";
|
|
6
7
|
import { splitChartBlocks } from "./chart-blocks.js";
|
|
7
8
|
import { ChartFromJson } from "../components/Chart.js";
|
|
8
9
|
// --- Allowed HTML tags (safe subset — no script/object; iframe allowed for YouTube only) -------
|
|
@@ -182,9 +183,59 @@ function attachCopyButtons(container) {
|
|
|
182
183
|
* <RichText html={desc} copyableCode />
|
|
183
184
|
* ```
|
|
184
185
|
*/
|
|
186
|
+
/**
|
|
187
|
+
* Unwrap every `<a>` whose destination the site has closed, keeping its text.
|
|
188
|
+
*
|
|
189
|
+
* 🛑 THIS IS WHERE THE LAST DEAD LINKS WERE, and why six page-level sweeps
|
|
190
|
+
* kept reporting "clean". Stored rich text is authored content that no page
|
|
191
|
+
* template owns: a crawl of the live site on 2026-10-27 found `/scams`,
|
|
192
|
+
* `/tools/authenticity` and `/blog` linked from the HOMEPAGE, and every one
|
|
193
|
+
* of them was inside an FAQ ANSWER. Grepping a page for marketplace words
|
|
194
|
+
* cannot see an href, and gating a section cannot see inside its prose.
|
|
195
|
+
*
|
|
196
|
+
* `RichText` is the single renderer for every stored HTML field on the site —
|
|
197
|
+
* FAQ answers, legal pages, blog bodies, product descriptions — so one filter
|
|
198
|
+
* here covers all of them.
|
|
199
|
+
*
|
|
200
|
+
* 🛑 UNWRAPPED, NOT DELETED. The sentence still reads: "the scam registry is
|
|
201
|
+
* the place to report it" keeps its words and loses its link. Removing the
|
|
202
|
+
* whole anchor would leave a gap mid-sentence, and removing the paragraph
|
|
203
|
+
* would silently delete an admin's copy.
|
|
204
|
+
*
|
|
205
|
+
* Prefix-matched on a `/`-delimited boundary, the same rule as every other
|
|
206
|
+
* gate — `/store` must not match `/stores`.
|
|
207
|
+
*/
|
|
208
|
+
function unwrapClosedLinks(html, closed) {
|
|
209
|
+
if (closed.length === 0 || typeof document === "undefined")
|
|
210
|
+
return html;
|
|
211
|
+
const container = document.createElement("div");
|
|
212
|
+
container.innerHTML = html;
|
|
213
|
+
let changed = false;
|
|
214
|
+
for (const a of Array.from(container.querySelectorAll("a[href]"))) {
|
|
215
|
+
const raw = a.getAttribute("href") ?? "";
|
|
216
|
+
if (!raw.startsWith("/"))
|
|
217
|
+
continue;
|
|
218
|
+
const path = raw.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
|
|
219
|
+
// `/en/blog` -> `/blog`: an admin pasting out of the address bar stores
|
|
220
|
+
// the locale-prefixed form, which a raw comparison would miss.
|
|
221
|
+
const bare = path.replace(/^\/[a-z]{2}(?=\/)/, "");
|
|
222
|
+
const hit = closed.some((p) => bare === p || bare.startsWith(`${p}/`) || path === p || path.startsWith(`${p}/`));
|
|
223
|
+
if (!hit)
|
|
224
|
+
continue;
|
|
225
|
+
a.replaceWith(...Array.from(a.childNodes));
|
|
226
|
+
changed = true;
|
|
227
|
+
}
|
|
228
|
+
return changed ? container.innerHTML : html;
|
|
229
|
+
}
|
|
185
230
|
export function RichText({ html, className = "", proseClass = "prose max-w-none dark:prose-invert", copyableCode = false, highlightCode, charts = true, }) {
|
|
231
|
+
/*
|
|
232
|
+
* Fails OPEN: no settings yet, or a failed fetch, leaves every link intact.
|
|
233
|
+
* A transient error must not strip working links out of an admin's copy.
|
|
234
|
+
*/
|
|
235
|
+
const { data: settings } = useSiteSettings();
|
|
236
|
+
const closedPrefixes = settings?.hiddenSurfacePrefixes ?? [];
|
|
186
237
|
const safe = useMemo(() => {
|
|
187
|
-
const sanitized = sanitiseHtml(normalizeRichTextHtml(html));
|
|
238
|
+
const sanitized = unwrapClosedLinks(sanitiseHtml(normalizeRichTextHtml(html)), closedPrefixes);
|
|
188
239
|
if (!highlightCode || typeof document === "undefined")
|
|
189
240
|
return sanitized;
|
|
190
241
|
const container = document.createElement("div");
|
package/package.json
CHANGED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import type { AboutHowItem, AboutValueItem, AboutMilestone, AboutTeamMember } from "../schemas/firestore";
|
|
3
|
-
export type { AboutHowItem, AboutValueItem, AboutMilestone, AboutTeamMember } from "../schemas/firestore";
|
|
4
|
-
export interface AboutViewProps {
|
|
5
|
-
labels?: {
|
|
6
|
-
title?: string;
|
|
7
|
-
subtitle?: string;
|
|
8
|
-
missionTitle?: string;
|
|
9
|
-
missionText?: string;
|
|
10
|
-
howItWorksTitle?: string;
|
|
11
|
-
valuesTitle?: string;
|
|
12
|
-
valuesSubtitle?: string;
|
|
13
|
-
/** When set, renders a link from the values section through to `/ethics`. */
|
|
14
|
-
valuesLinkLabel?: string;
|
|
15
|
-
milestonesTitle?: string;
|
|
16
|
-
teamTitle?: string;
|
|
17
|
-
teamSubtitle?: string;
|
|
18
|
-
ctaTitle?: string;
|
|
19
|
-
ctaSell?: string;
|
|
20
|
-
ctaShop?: string;
|
|
21
|
-
};
|
|
22
|
-
howItems?: AboutHowItem[];
|
|
23
|
-
valueItems?: AboutValueItem[];
|
|
24
|
-
milestones?: AboutMilestone[];
|
|
25
|
-
teamMembers?: AboutTeamMember[];
|
|
26
|
-
ctaBannerClass?: string;
|
|
27
|
-
/** Render the CTA action buttons */
|
|
28
|
-
renderCtaButtons?: () => React.ReactNode;
|
|
29
|
-
}
|
|
30
|
-
export declare function AboutView({ labels, howItems, valueItems, milestones, teamMembers, ctaBannerClass, renderCtaButtons, }: AboutViewProps): React.JSX.Element;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Anchor } from "../../../ui/components/Anchor.js";
|
|
3
|
-
import { Avatar } from "../../../ui/components/Avatar.js";
|
|
4
|
-
import { Badge } from "../../../ui/components/Badge.js";
|
|
5
|
-
import { Card } from "../../../ui/components/Card.js";
|
|
6
|
-
import { Div } from "../../../ui/components/Div.js";
|
|
7
|
-
import { Heading, Span, Text } from "../../../ui/components/Typography.js";
|
|
8
|
-
import { Row, Stack } from "../../../ui/components/Layout.js";
|
|
9
|
-
import { Section } from "../../../ui/components/Semantic.js";
|
|
10
|
-
import { TextLink } from "../../../ui/components/TextLink.js";
|
|
11
|
-
import { ROUTES } from "../../../constants/index.js";
|
|
12
|
-
export function AboutView({ labels = {}, howItems = [], valueItems = [], milestones = [], teamMembers = [], ctaBannerClass = "", renderCtaButtons, }) {
|
|
13
|
-
return (_jsxs(Div, { className: "-mx-4 md:-mx-6 lg:-mx-8 -mt-6 sm:-mt-8 lg:-mt-10", children: [_jsx(Section, { color: "inverse", tone: "accent-banner", padding: "y-4xl", children: _jsxs(Div, { paddingX: "x-page", className: "max-w-4xl mx-auto text-center", children: [_jsx(Heading, { color: "inverse", level: 1, variant: "none", className: "mb-6", mdSize: "5xl", size: "4xl", weight: "bold", children: labels.title }), _jsx(Text, { color: "inverse-muted", variant: "none", className: "max-w-2xl mx-auto", size: "xl", children: labels.subtitle })] }) }), _jsxs(Stack, { gap: "section", paddingX: "x-page", className: "max-w-5xl mx-auto", padding: "y-4xl", children: [_jsxs(Section, { className: "text-center max-w-3xl mx-auto", children: [_jsx(Heading, { level: 2, className: "mb-4", children: labels.missionTitle }), _jsx(Text, { size: "lg", variant: "secondary", className: "leading-relaxed", children: labels.missionText })] }), _jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-12", align: "center", children: labels.howItWorksTitle }), _jsx(Div, { className: "grid md:grid-cols-3", gap: "6", children: howItems.map(({ title, text, icon, tone }) => (_jsxs(Card, { variant: `gradient-${tone ?? "indigo"}`, padding: "md", spacing: "sm", children: [_jsx(Div, { children: _jsx(Span, { size: "4xl", children: icon }) }), _jsx(Heading, { level: 3, color: "inverse", variant: "none", children: title }), _jsx(Text, { size: "sm", color: "inverse-muted", variant: "none", className: "leading-relaxed", children: text })] }, title))) })] }), _jsxs(Section, { children: [_jsx(Heading, { level: 2, className: labels.valuesSubtitle ? "mb-3" : "mb-12", align: "center", children: labels.valuesTitle }), labels.valuesSubtitle && (_jsx(Text, { size: "base", variant: "secondary", align: "center", className: "mb-12 max-w-2xl mx-auto leading-relaxed", children: labels.valuesSubtitle })), _jsx(Div, { className: "grid md:grid-cols-3", gap: "5", children: valueItems.map(({ title, text, icon, detail }) => (_jsxs(Stack, { gap: "3", surface: "default", padding: "lg", rounded: "xl", border: "default", children: [_jsx(Div, { children: _jsx(Span, { size: "3xl", children: icon }) }), _jsx(Heading, { level: 3, children: title }), _jsx(Text, { size: "sm", variant: "secondary", className: "leading-relaxed", children: text }), detail && (_jsx(Text, { size: "sm", color: "muted", className: "leading-relaxed", children: detail }))] }, title))) }), labels.valuesLinkLabel && (_jsx(Div, { className: "mt-8 text-center", children: _jsx(TextLink, { href: String(ROUTES.PUBLIC.ETHICS), size: "sm", children: labels.valuesLinkLabel }) }))] }), _jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-10", align: "center", children: labels.milestonesTitle }), _jsx(Stack, { className: "relative border-l-2 border-primary/30 max-w-2xl mx-auto pl-[2rem]", gap: "xl", children: milestones.map(({ year, text }) => (_jsxs(Div, { className: "relative", children: [_jsx(Div, { className: "absolute -left-10 top-1 w-4 h-4 bg-primary border-2 border-white", rounded: "full" }), _jsx(Span, { size: "xs", weight: "bold", className: "text-primary tracking-wide", transform: "uppercase", children: year }), _jsx(Text, { className: "mt-1", children: text })] }, year))) })] }), teamMembers.length > 0 && (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-2", align: "center", children: labels.teamTitle }), labels.teamSubtitle && (_jsx(Text, { size: "lg", variant: "secondary", align: "center", className: "mb-12 max-w-2xl mx-auto", children: labels.teamSubtitle })), _jsx(Div, { className: "grid md:grid-cols-3", gap: "6", children: teamMembers.map((member) => (_jsxs(Stack, { gap: "3", surface: "default", padding: "lg", rounded: "xl", border: "default", align: "center", className: "text-center", children: [_jsx(Avatar, { src: member.photoUrl, name: member.name, alt: member.name, size: "lg" }), _jsx(Heading, { level: 3, children: member.name }), _jsxs(Row, { gap: "xs", wrap: true, justify: "center", children: [_jsx(Span, { size: "xs", weight: "bold", className: "text-primary tracking-wide", transform: "uppercase", children: member.role }), member.isFounder && _jsx(Badge, { variant: "primary", size: "xs", children: "Founder" }), member.isDeveloper && _jsx(Badge, { variant: "secondary", size: "xs", children: "Developer" })] }), _jsx(Text, { size: "sm", variant: "secondary", className: "leading-relaxed", children: member.bio }), member.githubUrl && (_jsx(Anchor, { href: member.githubUrl, tone: "muted", size: "sm", underline: "hover", children: "GitHub \u2197" }))] }, member.name))) })] })), _jsxs(Section, { color: "inverse", tone: "accent-banner", className: `text-center ${ctaBannerClass}`, padding: "xl", rounded: "2xl", children: [_jsx(Heading, { color: "inverse", level: 2, variant: "none", className: "mb-8", size: "3xl", weight: "bold", children: labels.ctaTitle }), renderCtaButtons ? (renderCtaButtons()) : (_jsx(Row, { justify: "center", gap: "3", wrap: true, children: _jsxs(Span, { color: "inverse-muted", size: "sm", children: [labels.ctaSell, " \u00B7 ", labels.ctaShop] }) }))] })] })] }));
|
|
14
|
-
}
|