diff --git a/packages/shared/src/components/FeedItemComponent.tsx b/packages/shared/src/components/FeedItemComponent.tsx
index 95d1321a2e8..ce6524321c3 100644
--- a/packages/shared/src/components/FeedItemComponent.tsx
+++ b/packages/shared/src/components/FeedItemComponent.tsx
@@ -17,6 +17,7 @@ import { useFeedLayout } from '../hooks';
import { CollectionList } from './cards/collection/CollectionList';
import { FeedItemType } from './cards/common/common';
import { AdGrid } from './cards/ad/AdGrid';
+import { PreferredSearchCard } from './post/preferredSources';
import { AdList } from './cards/ad/AdList';
import { SignalAdList } from './cards/ad/SignalAdList';
import type { AdCardProps } from './cards/ad/common/common';
@@ -489,6 +490,18 @@ function FeedItemComponent({
/>
);
}
+ case FeedItemType.Placeholder: {
+ // An ad position the ad server could not fill renders a grey card.
+ // Offer something of ours in that space instead — never in a slot real
+ // content would have taken.
+ const isAdSlot = typeof item.index === 'number';
+
+ return isAdSlot ? (
+
): the sanitized title can itself be wrapped in a
, and a
inside a
is invalid and gets auto-closed by the browser, breaking the layout. */} -
+onClick/onAdd callback for exactly this.
+ data-theme takes light (default) or{' '}
+ dark; data-lang overrides the auto-detected
+ language. There is no attribute for which domain to add — the
+ script reads the origin it is running on.
+ q parameter takes any eligible
+ host.
+ + Nothing sponsored today. Three things from us instead. +
++ Not sponsored. You can change the Google setting any time. +
+*.vercel.app preview is
+ CORS-blocked by the API and renders an empty shell.
+ + {children} +
+); + +/** Labels one rendered state. Used inside placement stories. */ +export const State = ({ + label, + tone = 'current', + children, +}: { + label: string; + tone?: 'current' | 'proposed'; + children: ReactNode; +}) => ( ++ Nothing sponsored today. This one is ours. +
++ A freeze covers one day you miss, automatically. You have{' '} + {freezesOwned} right now. +
++ Used automatically on the first day you miss. Nothing is charged again. +
++ Your {lostDays} day streak has been reset. +
++ {canRestore + ? 'Restore it today and the count carries on where it left off.' + : 'Read one post and day one starts again.'} +
++ {canRestore + ? 'Restores are available for two days after a streak ends.' + : 'The restore window has closed. Freezes still cover future days.'} +
++ No partner offer matched what you asked to see, so here is one of ours + instead. +
++ {count && count > 1 + ? 'Choose one of our partner offers below' + : 'A partner offer, on your streak'} +
++ Sponsored offers. No charge until a trial ends, cancel anytime. +
+); + +/** + * How the popup ends when the user does not want anything. + * + * On a phone the decline has to be a real target near the thumb, because the + * close button is a small X in a far corner. On desktop the X is already right + * there under the cursor, so a full-width button underneath it says the same + * thing twice. + */ +export enum DeclineStyle { + /** Desktop default: the X is the decline. Nothing at the bottom. */ + CloseOnly = 'closeOnly', + /** A quiet link that names what closing actually does. */ + SaveLink = 'saveLink', + /** Mobile: a real button at the end of the flow. */ + Button = 'button', +} + +/** The link version: a forward action rather than a refusal. */ +export const SaveForLater = ({ + onClick, + className, +}: { + onClick?: () => void; + className?: string; +}): ReactElement => ( + +); + +/** + * The single secondary action. Declining keeps the gift in the vault, so the + * word can stay this light. + */ +export const NoThanks = ({ + onClick, + className, +}: { + onClick?: () => void; + className?: string; +}): ReactElement => ( + +); + +/** + * The popup frame. Wide by default: these moments are landscape so the + * celebration and the gift sit side by side instead of stacking into a scroll. + * + * Not the app `Modal` because this is a prototype. Production would use + * `Modal` (Kind.FlexibleCenter, isDrawerOnMobile). + */ +export const MomentShell = ({ + onClose, + children, + className, + width = 'w-full max-w-[54rem]', +}: { + onClose?: () => void; + children: ReactNode; + className?: string; + width?: string; +}): ReactElement => ( + // The container has to be an ancestor of what it styles, so the query lives + // on this wrapper and the card inside it is what collapses. +| + {cell} + | + ))} +
|---|
| + {cell} + | + ))} +