@tribe-nest/forge 1.13.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/package.json +42 -0
- package/src/client/createForgeClient.ts +54 -0
- package/src/content/ForgeContentProvider.tsx +118 -0
- package/src/content/ForgeEditBridge.tsx +134 -0
- package/src/content/editFlag.ts +23 -0
- package/src/content/index.ts +28 -0
- package/src/content/resolve.ts +67 -0
- package/src/content/types.ts +97 -0
- package/src/content/useField.ts +64 -0
- package/src/contexts/AppAuthContext.tsx +185 -0
- package/src/contexts/AudioPlayerContext.tsx +389 -0
- package/src/contexts/CartContext.tsx +131 -0
- package/src/contexts/PublicAuthContext.tsx +333 -0
- package/src/contexts/useAppAdminGuard.ts +56 -0
- package/src/css.d.ts +5 -0
- package/src/data/collectionParams.ts +57 -0
- package/src/data/queries/useAccount.ts +71 -0
- package/src/data/queries/useAccountSettings.ts +82 -0
- package/src/data/queries/useAnalytics.ts +86 -0
- package/src/data/queries/useAuthActions.ts +28 -0
- package/src/data/queries/useBlog.ts +103 -0
- package/src/data/queries/useBlogComments.ts +96 -0
- package/src/data/queries/useBroadcasts.ts +31 -0
- package/src/data/queries/useCertificates.ts +58 -0
- package/src/data/queries/useChat.ts +267 -0
- package/src/data/queries/useCoachingAvailability.ts +69 -0
- package/src/data/queries/useCoachingProducts.ts +67 -0
- package/src/data/queries/useCohortPage.ts +17 -0
- package/src/data/queries/useCollections.ts +211 -0
- package/src/data/queries/useCommunity.ts +794 -0
- package/src/data/queries/useCoupons.ts +20 -0
- package/src/data/queries/useCourseAccess.ts +50 -0
- package/src/data/queries/useCourses.ts +91 -0
- package/src/data/queries/useCurrencies.ts +33 -0
- package/src/data/queries/useDocuments.ts +208 -0
- package/src/data/queries/useDonations.ts +29 -0
- package/src/data/queries/useEngagement.ts +115 -0
- package/src/data/queries/useEvents.ts +69 -0
- package/src/data/queries/useFinalize.ts +109 -0
- package/src/data/queries/useForms.ts +31 -0
- package/src/data/queries/useInvoice.ts +65 -0
- package/src/data/queries/useLeadMagnet.ts +96 -0
- package/src/data/queries/useMembership.ts +20 -0
- package/src/data/queries/useNotifications.ts +267 -0
- package/src/data/queries/useOffers.ts +56 -0
- package/src/data/queries/useOrders.ts +71 -0
- package/src/data/queries/usePWA.ts +101 -0
- package/src/data/queries/usePageActions.ts +36 -0
- package/src/data/queries/usePaymentFlow.ts +80 -0
- package/src/data/queries/usePaymentLink.ts +47 -0
- package/src/data/queries/usePodcast.ts +85 -0
- package/src/data/queries/usePostCollections.ts +48 -0
- package/src/data/queries/usePosts.ts +94 -0
- package/src/data/queries/useProducts.ts +108 -0
- package/src/data/queries/useReplay.ts +19 -0
- package/src/data/queries/useReviews.ts +122 -0
- package/src/data/queries/useShipping.ts +109 -0
- package/src/data/queries/useSubscriptions.ts +66 -0
- package/src/data/queries/useWebsite.ts +128 -0
- package/src/data/queries/useWebsiteAgent.ts +100 -0
- package/src/index.ts +98 -0
- package/src/provider/ForgeAppProvider.tsx +149 -0
- package/src/provider/ForgeProvider.tsx +91 -0
- package/src/provider/SiteConfigProvider.tsx +27 -0
- package/src/server/_tests/buildPwaHead.spec.ts +182 -0
- package/src/server/_tests/buildWebManifest.spec.ts +313 -0
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +251 -0
- package/src/server/index.ts +260 -0
- package/src/server/jobs.ts +104 -0
- package/src/server/pwa.ts +332 -0
- package/src/types/index.ts +74 -0
- package/src/types/models.ts +1313 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +208 -0
- package/src/ui/analytics/PageMetaPixel.tsx +92 -0
- package/src/ui/content/Editable.tsx +90 -0
- package/src/ui/format/PriceDisplay.tsx +142 -0
- package/src/ui/format/useFormatCurrency.ts +92 -0
- package/src/ui/headless/agent/useAiAgent.ts +106 -0
- package/src/ui/headless/auth/useLoginFlow.ts +72 -0
- package/src/ui/headless/auth/useSignupForm.ts +85 -0
- package/src/ui/headless/broadcast/useBroadcastWatch.ts +59 -0
- package/src/ui/headless/chat/attachmentKind.ts +27 -0
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +132 -0
- package/src/ui/headless/chat/useChatRoom.ts +223 -0
- package/src/ui/headless/chat/useMessageThread.ts +60 -0
- package/src/ui/headless/checkout/useCheckout.ts +438 -0
- package/src/ui/headless/coaching/useCoachingBooking.ts +223 -0
- package/src/ui/headless/community/index.ts +20 -0
- package/src/ui/headless/community/lexical.ts +131 -0
- package/src/ui/headless/community/replyTree.ts +51 -0
- package/src/ui/headless/community/richLexical.ts +314 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +137 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +122 -0
- package/src/ui/headless/community/useCommunityThread.ts +95 -0
- package/src/ui/headless/community/useMentionComposer.ts +105 -0
- package/src/ui/headless/community/useRichMentionComposer.ts +193 -0
- package/src/ui/headless/consent/useCookieConsent.ts +81 -0
- package/src/ui/headless/course/useCourseCheckout.ts +99 -0
- package/src/ui/headless/dialog.tsx +14 -0
- package/src/ui/headless/document/index.ts +7 -0
- package/src/ui/headless/document/useContractSignature.ts +68 -0
- package/src/ui/headless/document/useDocumentSigningFlow.ts +92 -0
- package/src/ui/headless/document/useOtpCodeInput.ts +86 -0
- package/src/ui/headless/document/useSignaturePad.ts +180 -0
- package/src/ui/headless/donation/Donation.tsx +115 -0
- package/src/ui/headless/donation/DonationContext.tsx +223 -0
- package/src/ui/headless/donation/fees.ts +15 -0
- package/src/ui/headless/donation/index.ts +10 -0
- package/src/ui/headless/engagement/usePostEngagement.ts +119 -0
- package/src/ui/headless/event/useEventCheckout.ts +141 -0
- package/src/ui/headless/forms/useContactForm.ts +58 -0
- package/src/ui/headless/forms/useEmailListForm.ts +75 -0
- package/src/ui/headless/forms/useSectionedForm.ts +256 -0
- package/src/ui/headless/index.ts +60 -0
- package/src/ui/headless/invoice/useInvoicePayment.ts +48 -0
- package/src/ui/headless/memberHome/index.ts +7 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +82 -0
- package/src/ui/headless/membership/MembershipGate.tsx +39 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +121 -0
- package/src/ui/headless/offer/Offer.tsx +88 -0
- package/src/ui/headless/offer/OfferContext.tsx +162 -0
- package/src/ui/headless/offer/index.ts +8 -0
- package/src/ui/headless/paymentLink/usePaymentLinkPayment.ts +46 -0
- package/src/ui/headless/podcast/usePodcastPlayer.ts +96 -0
- package/src/ui/headless/posts/index.ts +13 -0
- package/src/ui/headless/posts/upsell.ts +41 -0
- package/src/ui/headless/posts/useMemberPostsFeed.ts +110 -0
- package/src/ui/headless/posts/usePostCollectionDetail.ts +56 -0
- package/src/ui/headless/posts/usePostCollectionsList.ts +90 -0
- package/src/ui/headless/posts/usePostItem.ts +81 -0
- package/src/ui/headless/reviews/index.ts +6 -0
- package/src/ui/headless/reviews/useHelpfulVote.ts +45 -0
- package/src/ui/headless/reviews/useReviewForm.ts +108 -0
- package/src/ui/headless/reviews/useReviewsList.ts +79 -0
- package/src/ui/headless/work/index.ts +25 -0
- package/src/ui/headless/work/useWorkPortal.ts +349 -0
- package/src/ui/index.ts +144 -0
- package/src/ui/payment/ForgePaymentProvider.tsx +45 -0
- package/src/ui/payment/ForgeStripePayment.tsx +171 -0
- package/src/ui/shell/TribeNestApp.tsx +121 -0
- package/src/ui/shell/shellGating.spec.ts +28 -0
- package/src/ui/shell/shellGating.ts +11 -0
- package/src/ui/styled/AccountDashboard.tsx +575 -0
- package/src/ui/styled/AiAgentWidget.tsx +152 -0
- package/src/ui/styled/AudioPlayer.tsx +123 -0
- package/src/ui/styled/BlogCategory.tsx +79 -0
- package/src/ui/styled/BlogComments.tsx +236 -0
- package/src/ui/styled/BlogList.tsx +101 -0
- package/src/ui/styled/BlogPost.tsx +160 -0
- package/src/ui/styled/BlogPostFooter.tsx +33 -0
- package/src/ui/styled/BotProtection.tsx +101 -0
- package/src/ui/styled/Button.tsx +88 -0
- package/src/ui/styled/Cart.tsx +276 -0
- package/src/ui/styled/ChatRoom.tsx +252 -0
- package/src/ui/styled/Checkout.tsx +988 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +452 -0
- package/src/ui/styled/CoachingBooking.tsx +605 -0
- package/src/ui/styled/CoachingConfirmation.tsx +151 -0
- package/src/ui/styled/CoachingDetail.tsx +99 -0
- package/src/ui/styled/CohortPage.tsx +82 -0
- package/src/ui/styled/ConfirmSubscription.tsx +134 -0
- package/src/ui/styled/Confirmation.tsx +171 -0
- package/src/ui/styled/ContactForm.tsx +81 -0
- package/src/ui/styled/CookieConsent.tsx +239 -0
- package/src/ui/styled/CourseAccess.tsx +169 -0
- package/src/ui/styled/CourseCheckout.tsx +425 -0
- package/src/ui/styled/CourseConfirmation.tsx +191 -0
- package/src/ui/styled/CourseDetail.tsx +140 -0
- package/src/ui/styled/CoursesGrid.tsx +85 -0
- package/src/ui/styled/CurrencySwitcher.tsx +51 -0
- package/src/ui/styled/DonationButton.tsx +247 -0
- package/src/ui/styled/DonationPage.tsx +29 -0
- package/src/ui/styled/EmailListForm.tsx +88 -0
- package/src/ui/styled/EventConfirmation.tsx +228 -0
- package/src/ui/styled/EventCountdown.tsx +98 -0
- package/src/ui/styled/EventDetail.tsx +141 -0
- package/src/ui/styled/EventTickets.tsx +548 -0
- package/src/ui/styled/EventsList.tsx +83 -0
- package/src/ui/styled/ForgotPasswordForm.tsx +105 -0
- package/src/ui/styled/FormRenderer.tsx +249 -0
- package/src/ui/styled/InstallBanner.tsx +114 -0
- package/src/ui/styled/InvoiceConfirmation.tsx +50 -0
- package/src/ui/styled/InvoicePayment.tsx +141 -0
- package/src/ui/styled/LeadMagnet.tsx +93 -0
- package/src/ui/styled/Loading.tsx +56 -0
- package/src/ui/styled/LoginForm.tsx +134 -0
- package/src/ui/styled/Markdown.tsx +103 -0
- package/src/ui/styled/MembershipCheckout.tsx +197 -0
- package/src/ui/styled/MembershipTiers.tsx +115 -0
- package/src/ui/styled/OfferButton.tsx +181 -0
- package/src/ui/styled/PageActions.tsx +124 -0
- package/src/ui/styled/PaymentLinkConfirmation.tsx +50 -0
- package/src/ui/styled/PaymentLinkPayment.tsx +85 -0
- package/src/ui/styled/Paywall.tsx +54 -0
- package/src/ui/styled/PlayButton.tsx +48 -0
- package/src/ui/styled/PodcastEpisode.tsx +137 -0
- package/src/ui/styled/PodcastList.tsx +58 -0
- package/src/ui/styled/PodcastShow.tsx +164 -0
- package/src/ui/styled/PostsFeed.tsx +63 -0
- package/src/ui/styled/ProductDetail.tsx +616 -0
- package/src/ui/styled/ProductGrid.tsx +82 -0
- package/src/ui/styled/PushOptIn.tsx +65 -0
- package/src/ui/styled/PwaRegistration.tsx +58 -0
- package/src/ui/styled/ReactionBar.tsx +93 -0
- package/src/ui/styled/ReplayList.tsx +134 -0
- package/src/ui/styled/ResetPasswordForm.tsx +127 -0
- package/src/ui/styled/ReviewForm.tsx +292 -0
- package/src/ui/styled/ReviewsSection.tsx +260 -0
- package/src/ui/styled/SectionedFormRenderer.tsx +146 -0
- package/src/ui/styled/SignupForm.tsx +109 -0
- package/src/ui/styled/UserMenu.tsx +166 -0
- package/src/ui/styled/chat/ChatAttachments.tsx +172 -0
- package/src/ui/styled/chat/ChatComposer.tsx +153 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +94 -0
- package/src/ui/styled/chat/MessageBubble.tsx +66 -0
- package/src/ui/styled/chat/NewDmDialog.tsx +78 -0
- package/src/ui/styled/community/CommunityComposer.tsx +358 -0
- package/src/ui/styled/community/CommunityFeed.tsx +483 -0
- package/src/ui/styled/community/CommunityLeaderboard.tsx +109 -0
- package/src/ui/styled/community/CommunityMediaGallery.tsx +116 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +285 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +222 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +165 -0
- package/src/ui/styled/community/CommunityPostDetail.tsx +337 -0
- package/src/ui/styled/community/CommunityReactionBar.tsx +107 -0
- package/src/ui/styled/community/CommunityRichText.tsx +157 -0
- package/src/ui/styled/community/CommunitySpaces.tsx +174 -0
- package/src/ui/styled/community/MemberAvatar.tsx +74 -0
- package/src/ui/styled/community/index.ts +23 -0
- package/src/ui/styled/community/util.ts +15 -0
- package/src/ui/styled/members/MemberHomeLayout.tsx +98 -0
- package/src/ui/styled/members/MemberPostCard.tsx +392 -0
- package/src/ui/styled/members/MemberPostsFeed.tsx +219 -0
- package/src/ui/styled/members/PostCollectionDetail.tsx +227 -0
- package/src/ui/styled/members/PostCollectionsGrid.tsx +287 -0
- package/src/ui/styled/members/index.ts +7 -0
- package/src/ui/styled/rich-text.css +150 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +222 -0
- package/src/ui/styled/work/WorkPortalProjects.tsx +163 -0
- package/src/ui/styled/work/WorkProjectInvoices.tsx +113 -0
- package/src/ui/styled/work/WorkProjectReport.tsx +66 -0
- package/src/ui/styled/work/WorkReportBody.tsx +327 -0
- package/src/ui/styled/work/WorkTaskAttachments.tsx +67 -0
- package/src/ui/styled/work/WorkTaskComments.tsx +151 -0
- package/src/ui/styled/work/WorkTaskDetail.tsx +92 -0
- package/src/ui/styled/work/WorkTokenReport.tsx +49 -0
- package/src/ui/styled/work/index.ts +10 -0
- package/src/ui/theme/ForgeThemeProvider.tsx +121 -0
- package/src/ui/theme/contrast.ts +22 -0
- package/src/utils/analyticsBus.ts +31 -0
- package/src/utils/attribution.ts +68 -0
- package/src/utils/cookieConsent.ts +83 -0
- package/src/utils/landing.ts +153 -0
- package/src/utils/metaPixel.ts +122 -0
- package/src/utils/structuredData.ts +126 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { ArrowLeft, Lock } from "lucide-react";
|
|
3
|
+
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
4
|
+
import {
|
|
5
|
+
usePostCollectionDetail,
|
|
6
|
+
type UsePostCollectionDetailOptions,
|
|
7
|
+
} from "../../headless/posts/usePostCollectionDetail";
|
|
8
|
+
import { Loading } from "../Loading";
|
|
9
|
+
import { MemberPostCard } from "./MemberPostCard";
|
|
10
|
+
|
|
11
|
+
export interface PostCollectionDetailProps extends UsePostCollectionDetailOptions {
|
|
12
|
+
/** A collection uuid OR its per-profile slug — the public detail endpoint resolves either. */
|
|
13
|
+
collectionId: string;
|
|
14
|
+
/** Where the back button returns to (default `/i/members/collections`). */
|
|
15
|
+
backHref?: string;
|
|
16
|
+
/** Shown as the audio track artist for audio posts inside the collection. */
|
|
17
|
+
artistName?: string;
|
|
18
|
+
/** SPA navigation adapter (back button + upsell CTA). Defaults to a full-page visit. */
|
|
19
|
+
navigate?: (href: string) => void;
|
|
20
|
+
/** Called when a logged-out visitor tries to like/save/comment (default: navigate to `loginPath`). */
|
|
21
|
+
onRequireAuth?: () => void;
|
|
22
|
+
/** Login path for the default require-auth redirect (site-starter passes `/i/login`). */
|
|
23
|
+
loginPath?: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
style?: CSSProperties;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One collection's drill-in view — parity with the craft members area's
|
|
30
|
+
* `CollectionsContent` detail: back link, cover + title + rich-text
|
|
31
|
+
* description header, then the collection's posts as full member post cards
|
|
32
|
+
* (engagement, audio queue scoped to the collection, per-post gating). A
|
|
33
|
+
* locked collection renders the upgrade CTA instead of posts — access is
|
|
34
|
+
* server-computed, never faked client-side. Built on the headless
|
|
35
|
+
* `usePostCollectionDetail`.
|
|
36
|
+
*/
|
|
37
|
+
export function PostCollectionDetail({
|
|
38
|
+
collectionId,
|
|
39
|
+
backHref = "/i/members/collections",
|
|
40
|
+
artistName,
|
|
41
|
+
navigate,
|
|
42
|
+
onRequireAuth,
|
|
43
|
+
loginPath = "/login",
|
|
44
|
+
className,
|
|
45
|
+
style,
|
|
46
|
+
...detailOptions
|
|
47
|
+
}: PostCollectionDetailProps) {
|
|
48
|
+
const t = useThemeTokens();
|
|
49
|
+
const detail = usePostCollectionDetail(collectionId, detailOptions);
|
|
50
|
+
const go = navigate ?? ((href: string) => window.location.assign(href));
|
|
51
|
+
const requireAuth =
|
|
52
|
+
onRequireAuth ??
|
|
53
|
+
(() => {
|
|
54
|
+
const redirect = typeof window !== "undefined" ? `${window.location.pathname}${window.location.search}` : "";
|
|
55
|
+
go(redirect ? `${loginPath}?redirect=${encodeURIComponent(redirect)}` : loginPath);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const backButton = (
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={() => go(backHref)}
|
|
62
|
+
style={{
|
|
63
|
+
display: "inline-flex",
|
|
64
|
+
alignItems: "center",
|
|
65
|
+
gap: 8,
|
|
66
|
+
alignSelf: "flex-start",
|
|
67
|
+
padding: "8px 14px",
|
|
68
|
+
borderRadius: t.cornerRadius,
|
|
69
|
+
border: `1px solid ${t.border}`,
|
|
70
|
+
background: t.surface,
|
|
71
|
+
color: t.text,
|
|
72
|
+
fontSize: 13,
|
|
73
|
+
fontWeight: 600,
|
|
74
|
+
cursor: "pointer",
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<ArrowLeft size={14} style={{ color: t.primary }} /> All collections
|
|
78
|
+
</button>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (detail.isLoading) {
|
|
82
|
+
return (
|
|
83
|
+
<div className={className} style={{ display: "flex", flexDirection: "column", gap: 16, ...style }}>
|
|
84
|
+
{backButton}
|
|
85
|
+
<Loading />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (detail.isError || !detail.collection) {
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
className={className}
|
|
94
|
+
style={{ display: "flex", flexDirection: "column", gap: 16, fontFamily: t.fontFamily, ...style }}
|
|
95
|
+
>
|
|
96
|
+
{backButton}
|
|
97
|
+
<p style={{ textAlign: "center", padding: "40px 20px", color: t.muted, fontSize: 15 }}>Collection not found</p>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const collection = detail.collection;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
className={className}
|
|
107
|
+
style={{ display: "flex", flexDirection: "column", gap: 16, color: t.text, fontFamily: t.fontFamily, ...style }}
|
|
108
|
+
>
|
|
109
|
+
{backButton}
|
|
110
|
+
|
|
111
|
+
{/* Collection header */}
|
|
112
|
+
<div
|
|
113
|
+
style={{
|
|
114
|
+
padding: 24,
|
|
115
|
+
borderRadius: t.cornerRadius,
|
|
116
|
+
background: t.surface,
|
|
117
|
+
border: `1px solid ${t.border}`,
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{detail.cover && (
|
|
121
|
+
<img
|
|
122
|
+
src={detail.cover.url}
|
|
123
|
+
alt={collection.title}
|
|
124
|
+
style={{
|
|
125
|
+
width: "100%",
|
|
126
|
+
aspectRatio: "1 / 1",
|
|
127
|
+
objectFit: "cover",
|
|
128
|
+
display: "block",
|
|
129
|
+
borderRadius: t.cornerRadius,
|
|
130
|
+
marginBottom: 16,
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
<h2 style={{ margin: 0, fontSize: 26, fontWeight: 700 }}>{collection.title}</h2>
|
|
135
|
+
{collection.description && (
|
|
136
|
+
<div
|
|
137
|
+
style={{ marginTop: 8, fontSize: 15, lineHeight: 1.6, color: t.muted }}
|
|
138
|
+
dangerouslySetInnerHTML={{ __html: collection.description }}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Locked collection → upgrade CTA (server-gated; posts are not shipped). */}
|
|
144
|
+
{!detail.hasAccess ? (
|
|
145
|
+
<div
|
|
146
|
+
style={{
|
|
147
|
+
display: "flex",
|
|
148
|
+
flexDirection: "column",
|
|
149
|
+
alignItems: "center",
|
|
150
|
+
gap: 12,
|
|
151
|
+
padding: "40px 16px",
|
|
152
|
+
borderRadius: t.cornerRadius,
|
|
153
|
+
background: `${t.primary}0d`,
|
|
154
|
+
textAlign: "center",
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<span
|
|
158
|
+
style={{
|
|
159
|
+
width: 56,
|
|
160
|
+
height: 56,
|
|
161
|
+
borderRadius: "50%",
|
|
162
|
+
background: `${t.primary}22`,
|
|
163
|
+
color: t.primary,
|
|
164
|
+
display: "flex",
|
|
165
|
+
alignItems: "center",
|
|
166
|
+
justifyContent: "center",
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<Lock size={24} />
|
|
170
|
+
</span>
|
|
171
|
+
<div>
|
|
172
|
+
<div style={{ fontSize: 16, fontWeight: 700 }}>Content locked</div>
|
|
173
|
+
<div style={{ fontSize: 13, color: t.muted, marginTop: 2 }}>
|
|
174
|
+
{detail.upsell?.tier?.name
|
|
175
|
+
? `Subscribe to ${detail.upsell.tier.name} to access this content`
|
|
176
|
+
: "Subscribe to access this content"}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
{detail.upsell && (
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
onClick={() => go(detail.upsell!.href)}
|
|
183
|
+
style={{
|
|
184
|
+
padding: "10px 18px",
|
|
185
|
+
borderRadius: t.cornerRadius,
|
|
186
|
+
border: "none",
|
|
187
|
+
background: t.primary,
|
|
188
|
+
color: t.textPrimary,
|
|
189
|
+
fontWeight: 600,
|
|
190
|
+
cursor: "pointer",
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
Subscribe
|
|
194
|
+
</button>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
) : (
|
|
198
|
+
<>
|
|
199
|
+
<div style={{ fontSize: 16, fontWeight: 700 }}>
|
|
200
|
+
Collection items: {detail.posts.length}
|
|
201
|
+
</div>
|
|
202
|
+
{detail.posts.length === 0 ? (
|
|
203
|
+
<p style={{ textAlign: "center", padding: "40px 20px", color: t.muted, fontSize: 15 }}>
|
|
204
|
+
No posts in this collection
|
|
205
|
+
</p>
|
|
206
|
+
) : (
|
|
207
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
208
|
+
{detail.posts.map((post) => (
|
|
209
|
+
<MemberPostCard
|
|
210
|
+
key={post.id}
|
|
211
|
+
post={post}
|
|
212
|
+
hideHeader
|
|
213
|
+
artistName={artistName}
|
|
214
|
+
otherAudioPosts={detail.audioPosts.filter((p) => p.id !== post.id)}
|
|
215
|
+
navigate={go}
|
|
216
|
+
onRequireAuth={requireAuth}
|
|
217
|
+
checkoutPath={detailOptions.checkoutPath}
|
|
218
|
+
signupPath={detailOptions.signupPath}
|
|
219
|
+
/>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
</>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { useState, type CSSProperties } from "react";
|
|
2
|
+
import { ChevronLeft, ChevronRight, FileText, Lock, Search, X } from "lucide-react";
|
|
3
|
+
import type { IPublicPostCollection } from "../../../types/models";
|
|
4
|
+
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
5
|
+
import {
|
|
6
|
+
usePostCollectionsList,
|
|
7
|
+
type UsePostCollectionsListOptions,
|
|
8
|
+
} from "../../headless/posts/usePostCollectionsList";
|
|
9
|
+
import type { PostUpsell } from "../../headless/posts/upsell";
|
|
10
|
+
import { Loading } from "../Loading";
|
|
11
|
+
|
|
12
|
+
export interface PostCollectionsGridProps extends UsePostCollectionsListOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Builds the drill-in href for an accessible collection. Receives the
|
|
15
|
+
* collection's slug when it has one, else its uuid — the public detail
|
|
16
|
+
* endpoint resolves either. Default `` (idOrSlug) => `/i/members/collections/${idOrSlug}` ``.
|
|
17
|
+
*/
|
|
18
|
+
getCollectionHref?: (idOrSlug: string) => string;
|
|
19
|
+
/** SPA navigation adapter (drill-in + locked-collection upsell CTA). Defaults to a full-page visit. */
|
|
20
|
+
navigate?: (href: string) => void;
|
|
21
|
+
className?: string;
|
|
22
|
+
style?: CSSProperties;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const stripHtml = (html: string, len = 100) => {
|
|
26
|
+
const text = html
|
|
27
|
+
.replace(/<[^>]+>/g, " ")
|
|
28
|
+
.replace(/\s+/g, " ")
|
|
29
|
+
.trim();
|
|
30
|
+
return text.length > len ? text.slice(0, len) + "…" : text;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The member collections grid — parity with the craft members area's
|
|
35
|
+
* `CollectionsContent` list: cover, title, description, post count per
|
|
36
|
+
* collection; locked collections render dimmed with a Subscribe CTA that
|
|
37
|
+
* deep-links membership checkout (`hasAccess` is server-computed — a locked
|
|
38
|
+
* collection never opens); accessible collections drill into the detail view.
|
|
39
|
+
* Debounced search + numbered paging. Built on the headless
|
|
40
|
+
* `usePostCollectionsList`.
|
|
41
|
+
*/
|
|
42
|
+
export function PostCollectionsGrid({
|
|
43
|
+
getCollectionHref = (idOrSlug) => `/i/members/collections/${idOrSlug}`,
|
|
44
|
+
navigate,
|
|
45
|
+
className,
|
|
46
|
+
style,
|
|
47
|
+
...listOptions
|
|
48
|
+
}: PostCollectionsGridProps) {
|
|
49
|
+
const t = useThemeTokens();
|
|
50
|
+
const list = usePostCollectionsList(listOptions);
|
|
51
|
+
const go = navigate ?? ((href: string) => window.location.assign(href));
|
|
52
|
+
const [searchFocused, setSearchFocused] = useState(false);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className={className}
|
|
57
|
+
style={{ display: "flex", flexDirection: "column", gap: 16, color: t.text, fontFamily: t.fontFamily, ...style }}
|
|
58
|
+
>
|
|
59
|
+
{/* Search */}
|
|
60
|
+
<div
|
|
61
|
+
style={{
|
|
62
|
+
display: "flex",
|
|
63
|
+
alignItems: "center",
|
|
64
|
+
gap: 8,
|
|
65
|
+
padding: "10px 14px",
|
|
66
|
+
borderRadius: t.cornerRadius,
|
|
67
|
+
border: `1px solid ${searchFocused ? t.primary : t.border}`,
|
|
68
|
+
background: t.surface,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<Search size={16} style={{ color: t.muted, flexShrink: 0 }} />
|
|
72
|
+
<input
|
|
73
|
+
value={list.search}
|
|
74
|
+
onChange={(e) => list.setSearch(e.target.value)}
|
|
75
|
+
onFocus={() => setSearchFocused(true)}
|
|
76
|
+
onBlur={() => setSearchFocused(false)}
|
|
77
|
+
placeholder="Search collections…"
|
|
78
|
+
style={{ flex: 1, border: "none", outline: "none", background: "transparent", color: t.text, fontSize: 14 }}
|
|
79
|
+
/>
|
|
80
|
+
{list.search && (
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
aria-label="Clear search"
|
|
84
|
+
onClick={() => list.clearFilters()}
|
|
85
|
+
style={{ border: "none", background: "transparent", color: t.muted, cursor: "pointer", display: "flex" }}
|
|
86
|
+
>
|
|
87
|
+
<X size={14} />
|
|
88
|
+
</button>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Grid */}
|
|
93
|
+
{list.isLoading ? (
|
|
94
|
+
<Loading />
|
|
95
|
+
) : list.collections.length === 0 ? (
|
|
96
|
+
<p style={{ textAlign: "center", padding: "40px 20px", color: t.muted, fontSize: 15 }}>
|
|
97
|
+
{list.hasActiveFilters ? "No collections found matching your filters" : "No collections yet"}
|
|
98
|
+
</p>
|
|
99
|
+
) : (
|
|
100
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
101
|
+
{list.collections.map((collection) => (
|
|
102
|
+
<CollectionCard
|
|
103
|
+
key={collection.id}
|
|
104
|
+
collection={collection}
|
|
105
|
+
upsell={list.getUpsell(collection)}
|
|
106
|
+
onOpen={() => go(getCollectionHref(collection.slug ?? collection.id))}
|
|
107
|
+
go={go}
|
|
108
|
+
/>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{/* Pagination */}
|
|
114
|
+
{list.totalPages > 1 && (
|
|
115
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12 }}>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={list.prevPage}
|
|
119
|
+
disabled={!list.hasPrevPage}
|
|
120
|
+
aria-label="Previous page"
|
|
121
|
+
style={{
|
|
122
|
+
display: "flex",
|
|
123
|
+
alignItems: "center",
|
|
124
|
+
gap: 4,
|
|
125
|
+
padding: "8px 14px",
|
|
126
|
+
borderRadius: t.cornerRadius,
|
|
127
|
+
border: `1px solid ${t.border}`,
|
|
128
|
+
background: "transparent",
|
|
129
|
+
color: t.text,
|
|
130
|
+
fontSize: 13,
|
|
131
|
+
cursor: list.hasPrevPage ? "pointer" : "not-allowed",
|
|
132
|
+
opacity: list.hasPrevPage ? 1 : 0.4,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<ChevronLeft size={14} /> Prev
|
|
136
|
+
</button>
|
|
137
|
+
<span style={{ fontSize: 13, color: t.muted }}>
|
|
138
|
+
Page {list.page} of {list.totalPages}
|
|
139
|
+
</span>
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
onClick={list.nextPage}
|
|
143
|
+
disabled={!list.hasNextPage}
|
|
144
|
+
aria-label="Next page"
|
|
145
|
+
style={{
|
|
146
|
+
display: "flex",
|
|
147
|
+
alignItems: "center",
|
|
148
|
+
gap: 4,
|
|
149
|
+
padding: "8px 14px",
|
|
150
|
+
borderRadius: t.cornerRadius,
|
|
151
|
+
border: `1px solid ${t.border}`,
|
|
152
|
+
background: "transparent",
|
|
153
|
+
color: t.text,
|
|
154
|
+
fontSize: 13,
|
|
155
|
+
cursor: list.hasNextPage ? "pointer" : "not-allowed",
|
|
156
|
+
opacity: list.hasNextPage ? 1 : 0.4,
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
Next <ChevronRight size={14} />
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function CollectionCard({
|
|
168
|
+
collection,
|
|
169
|
+
upsell,
|
|
170
|
+
onOpen,
|
|
171
|
+
go,
|
|
172
|
+
}: {
|
|
173
|
+
collection: IPublicPostCollection;
|
|
174
|
+
upsell: PostUpsell;
|
|
175
|
+
onOpen: () => void;
|
|
176
|
+
go: (href: string) => void;
|
|
177
|
+
}) {
|
|
178
|
+
const t = useThemeTokens();
|
|
179
|
+
const locked = !collection.hasAccess;
|
|
180
|
+
const cover = collection.media?.[0];
|
|
181
|
+
const description = collection.description ? stripHtml(collection.description) : "";
|
|
182
|
+
const tierName = upsell.tier?.name;
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div
|
|
186
|
+
role={locked ? undefined : "button"}
|
|
187
|
+
tabIndex={locked ? undefined : 0}
|
|
188
|
+
onClick={locked ? undefined : onOpen}
|
|
189
|
+
onKeyDown={
|
|
190
|
+
locked
|
|
191
|
+
? undefined
|
|
192
|
+
: (e) => {
|
|
193
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
onOpen();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
aria-label={locked ? `${collection.title} (locked)` : `Open collection ${collection.title}`}
|
|
200
|
+
style={{
|
|
201
|
+
opacity: locked ? 0.6 : 1,
|
|
202
|
+
borderRadius: t.cornerRadius,
|
|
203
|
+
background: t.surface,
|
|
204
|
+
border: `1px solid ${t.border}`,
|
|
205
|
+
overflow: "hidden",
|
|
206
|
+
cursor: locked ? "default" : "pointer",
|
|
207
|
+
color: t.text,
|
|
208
|
+
fontFamily: t.fontFamily,
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{cover && (
|
|
212
|
+
<img
|
|
213
|
+
src={cover.url}
|
|
214
|
+
alt={collection.title}
|
|
215
|
+
style={{ width: "100%", aspectRatio: "1 / 1", objectFit: "cover", display: "block" }}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
<div style={{ padding: 20 }}>
|
|
219
|
+
<h3 style={{ margin: 0, fontSize: 18, fontWeight: 700 }}>{collection.title}</h3>
|
|
220
|
+
{description && (
|
|
221
|
+
<p style={{ margin: "8px 0 0", fontSize: 13, lineHeight: 1.5, color: t.muted }}>{description}</p>
|
|
222
|
+
)}
|
|
223
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6, marginTop: 12, fontSize: 13, color: t.muted }}>
|
|
224
|
+
<FileText size={14} />
|
|
225
|
+
<span>
|
|
226
|
+
{collection.posts?.length || 0} {(collection.posts?.length || 0) === 1 ? "post" : "posts"}
|
|
227
|
+
</span>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{locked && (
|
|
231
|
+
<div
|
|
232
|
+
style={{
|
|
233
|
+
display: "flex",
|
|
234
|
+
flexDirection: "column",
|
|
235
|
+
alignItems: "center",
|
|
236
|
+
gap: 8,
|
|
237
|
+
marginTop: 16,
|
|
238
|
+
padding: "16px 12px",
|
|
239
|
+
borderRadius: t.cornerRadius,
|
|
240
|
+
background: `${t.primary}0d`,
|
|
241
|
+
textAlign: "center",
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<span
|
|
245
|
+
style={{
|
|
246
|
+
width: 40,
|
|
247
|
+
height: 40,
|
|
248
|
+
borderRadius: "50%",
|
|
249
|
+
background: `${t.primary}22`,
|
|
250
|
+
color: t.primary,
|
|
251
|
+
display: "flex",
|
|
252
|
+
alignItems: "center",
|
|
253
|
+
justifyContent: "center",
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
<Lock size={18} />
|
|
257
|
+
</span>
|
|
258
|
+
<div style={{ fontSize: 15, fontWeight: 700 }}>Content locked</div>
|
|
259
|
+
<div style={{ fontSize: 13, color: t.muted }}>
|
|
260
|
+
{tierName ? `Subscribe to ${tierName} to access this content` : "Subscribe to access this content"}
|
|
261
|
+
</div>
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
onClick={(e) => {
|
|
265
|
+
e.stopPropagation();
|
|
266
|
+
go(upsell.href);
|
|
267
|
+
}}
|
|
268
|
+
style={{
|
|
269
|
+
marginTop: 4,
|
|
270
|
+
padding: "8px 16px",
|
|
271
|
+
borderRadius: t.cornerRadius,
|
|
272
|
+
border: "none",
|
|
273
|
+
background: t.primary,
|
|
274
|
+
color: t.textPrimary,
|
|
275
|
+
fontWeight: 600,
|
|
276
|
+
fontSize: 13,
|
|
277
|
+
cursor: "pointer",
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
Subscribe
|
|
281
|
+
</button>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Unified member home styled blocks. NOTE: lane-local aggregator — the
|
|
2
|
+
// integrator re-exports it from `ui/index.ts` (`export * from "./styled/members"`).
|
|
3
|
+
export { MemberHomeLayout, type MemberHomeLayoutProps } from "./MemberHomeLayout";
|
|
4
|
+
export { MemberPostsFeed, type MemberPostsFeedProps } from "./MemberPostsFeed";
|
|
5
|
+
export { MemberPostCard, type MemberPostCardProps } from "./MemberPostCard";
|
|
6
|
+
export { PostCollectionsGrid, type PostCollectionsGridProps } from "./PostCollectionsGrid";
|
|
7
|
+
export { PostCollectionDetail, type PostCollectionDetailProps } from "./PostCollectionDetail";
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Baseline styles for rich-text content rendered by Forge's styled blocks
|
|
3
|
+
* (BlogPost, PodcastEpisode). Ported from
|
|
4
|
+
* packages/frontend-shared/src/rich-text.css — the SAME rules are applied to
|
|
5
|
+
* the admin editor's content area, so authoring matches the result (WYSIWYG).
|
|
6
|
+
* Keep the two files in sync when the editor baseline changes.
|
|
7
|
+
*
|
|
8
|
+
* Bundled with Forge: the styled blocks that render `.rich-text` content import
|
|
9
|
+
* this file directly, so consumers get correct rendering with no host-loaded
|
|
10
|
+
* stylesheet. Structure + spacing only — color and font are inherited from each
|
|
11
|
+
* surface, so it never fights per-tenant theming.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
.rich-text {
|
|
15
|
+
line-height: 1.6;
|
|
16
|
+
}
|
|
17
|
+
.rich-text > :first-child {
|
|
18
|
+
margin-top: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.rich-text p {
|
|
22
|
+
margin: 0 0 1rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.rich-text h1 {
|
|
26
|
+
font-size: 1.875rem;
|
|
27
|
+
font-weight: 800;
|
|
28
|
+
line-height: 1.2;
|
|
29
|
+
margin: 1.75rem 0 0.75rem;
|
|
30
|
+
}
|
|
31
|
+
.rich-text h2 {
|
|
32
|
+
font-size: 1.5rem;
|
|
33
|
+
font-weight: 700;
|
|
34
|
+
line-height: 1.25;
|
|
35
|
+
margin: 1.5rem 0 0.75rem;
|
|
36
|
+
}
|
|
37
|
+
.rich-text h3 {
|
|
38
|
+
font-size: 1.25rem;
|
|
39
|
+
font-weight: 700;
|
|
40
|
+
line-height: 1.3;
|
|
41
|
+
margin: 1.25rem 0 0.5rem;
|
|
42
|
+
}
|
|
43
|
+
.rich-text h4 {
|
|
44
|
+
font-size: 1.125rem;
|
|
45
|
+
font-weight: 700;
|
|
46
|
+
margin: 1rem 0 0.5rem;
|
|
47
|
+
}
|
|
48
|
+
.rich-text h5 {
|
|
49
|
+
font-size: 1rem;
|
|
50
|
+
font-weight: 700;
|
|
51
|
+
margin: 1rem 0 0.5rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* list-style-position defaults to `outside`, so wrapped lines hang-indent and
|
|
55
|
+
align under the text; full width (no max-width). */
|
|
56
|
+
.rich-text ul {
|
|
57
|
+
list-style: disc;
|
|
58
|
+
padding-left: 1.5rem;
|
|
59
|
+
margin: 0 0 1rem;
|
|
60
|
+
}
|
|
61
|
+
.rich-text ol {
|
|
62
|
+
list-style: decimal;
|
|
63
|
+
padding-left: 1.5rem;
|
|
64
|
+
margin: 0 0 1rem;
|
|
65
|
+
}
|
|
66
|
+
.rich-text li {
|
|
67
|
+
margin: 0.25rem 0;
|
|
68
|
+
}
|
|
69
|
+
.rich-text li > ul,
|
|
70
|
+
.rich-text li > ol {
|
|
71
|
+
margin: 0.25rem 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.rich-text blockquote {
|
|
75
|
+
border-left: 3px solid currentColor;
|
|
76
|
+
padding-left: 1rem;
|
|
77
|
+
margin: 0 0 1rem;
|
|
78
|
+
opacity: 0.85;
|
|
79
|
+
font-style: italic;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.rich-text a {
|
|
83
|
+
text-decoration: underline;
|
|
84
|
+
}
|
|
85
|
+
.rich-text strong,
|
|
86
|
+
.rich-text b {
|
|
87
|
+
font-weight: 700;
|
|
88
|
+
}
|
|
89
|
+
.rich-text em,
|
|
90
|
+
.rich-text i {
|
|
91
|
+
font-style: italic;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.rich-text hr {
|
|
95
|
+
margin: 1.5rem 0;
|
|
96
|
+
border: 0;
|
|
97
|
+
border-top: 1px solid currentColor;
|
|
98
|
+
opacity: 0.2;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.rich-text img {
|
|
102
|
+
max-width: 100%;
|
|
103
|
+
height: auto;
|
|
104
|
+
}
|
|
105
|
+
.rich-text figure {
|
|
106
|
+
margin: 1rem 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* CTA button (inserted from the editor). Colors / radius / font come from the
|
|
110
|
+
site theme via `--forge-*` on published sites, falling back to the admin
|
|
111
|
+
design tokens (`--primary`, `--radius`) in the editor. Alignment is inline
|
|
112
|
+
(text-align on the .rich-text-button-wrap), so it round-trips without CSS. */
|
|
113
|
+
.rich-text .rich-text-button-wrap {
|
|
114
|
+
margin: 1rem 0;
|
|
115
|
+
}
|
|
116
|
+
.rich-text .rich-text-button {
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
gap: 0.5rem;
|
|
121
|
+
padding: 0.625rem 1.25rem;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
line-height: 1.2;
|
|
124
|
+
text-decoration: none;
|
|
125
|
+
border: 1px solid transparent;
|
|
126
|
+
border-radius: var(--forge-radius, var(--radius, 0.5rem));
|
|
127
|
+
font-family: var(--forge-font, inherit);
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
transition: opacity 0.15s ease, background 0.15s ease;
|
|
130
|
+
}
|
|
131
|
+
.rich-text .rich-text-button:hover {
|
|
132
|
+
opacity: 0.9;
|
|
133
|
+
}
|
|
134
|
+
.rich-text .rich-text-button--filled {
|
|
135
|
+
background: var(--forge-primary, hsl(var(--primary, 262 83% 58%)));
|
|
136
|
+
color: var(--forge-text-primary, hsl(var(--primary-foreground, 0 0% 100%)));
|
|
137
|
+
}
|
|
138
|
+
.rich-text .rich-text-button--outline {
|
|
139
|
+
background: transparent;
|
|
140
|
+
color: var(--forge-primary, hsl(var(--primary, 262 83% 58%)));
|
|
141
|
+
border-color: var(--forge-primary, hsl(var(--primary, 262 83% 58%)));
|
|
142
|
+
}
|
|
143
|
+
.rich-text .rich-text-button--ghost {
|
|
144
|
+
background: transparent;
|
|
145
|
+
color: var(--forge-primary, hsl(var(--primary, 262 83% 58%)));
|
|
146
|
+
}
|
|
147
|
+
.rich-text .rich-text-button--ghost:hover {
|
|
148
|
+
opacity: 1;
|
|
149
|
+
background: color-mix(in srgb, var(--forge-primary, hsl(var(--primary, 262 83% 58%))) 10%, transparent);
|
|
150
|
+
}
|