@openrails/contentkit-ui 0.98.1 → 0.98.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"card.js","names":[],"sources":["../../../src/components/content/card.tsx"],"sourcesContent":["import { FavouriteIcon, LockIcon, ThumbsUpIcon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useCallback, useContext, useState, type CSSProperties, type MouseEvent, type ReactNode } from \"react\";\nimport type { Card, CardAccess, Chip as EntityChip } from \"../../client/generated/wire.js\";\nimport { useLanguageName, useMessages } from \"../../i18n/context.js\";\nimport type { Translator } from \"../../i18n/messages.js\";\nimport { ContentKitContext } from \"../../react/context.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { HoverPreview } from \"../hover-preview.js\";\nimport { RenditionImg } from \"../rendition-img.js\";\nimport { formatDate, RelativeTime } from \"../social/parts.js\";\nimport { GroupAvatar, GroupLink, labelOf, type GroupLike, type Labels } from \"../groups/parts.js\";\nimport { priceLabel } from \"./paywall.js\";\n\nexport interface ContentCardProps {\n card: Card;\n /** The card's link; default its canonical path. */\n href?: (card: Card) => string | undefined;\n /** Before the card's link is followed: a click signal. */\n onOpen?: (card: Card) => void;\n /** Cover shape: \"portrait\" (3:4, readers) or \"landscape\" (16:9, players). Default portrait. */\n shape?: \"portrait\" | \"landscape\";\n /** A CSS aspect ratio overriding shape, such as \"182 / 260\". */\n aspectRatio?: CSSProperties[\"aspectRatio\"];\n /** Place the title below the cover (default), or over its bottom gradient. */\n titlePosition?: \"below\" | \"cover\";\n /** Mount images and previews. False keeps the card's text and reserved cover space. */\n loadMedia?: boolean;\n /** The group kinds whose chips show, in order; default every kind the card names. */\n groupKinds?: readonly string[];\n /** Chips per group kind. Default 3. */\n chips?: number;\n /** Select which entity chips appear, before the per-kind chip limit. */\n filterEntity?: (entity: EntityChip, kind: string) => boolean;\n /** Bordered chips (default) or plain linked text. */\n entityStyle?: \"chips\" | \"text\";\n /** Metadata fields to show; omitted fields keep their defaults. */\n metadata?: CardMetadata;\n /** Show like and favorite totals. Default true. */\n showCounts?: boolean;\n /** Tiers the site sells, so a lock names them; others show as \"for sale\". */\n tiers?: readonly string[];\n /** Display names of kinds, traits and tiers by identifier. */\n labels?: Labels;\n /** Show the owner group (a channel) when the card has one; default true, false on that group's own feed. */\n owner?: boolean;\n /** The owner group's link; default its canonical path. */\n ownerHref?: (g: GroupLike) => string | undefined;\n /** Over the cover: the host's badges for the card, placed in the cover's box; clicks reach the card. */\n decorate?: (card: Card) => ReactNode;\n /** Below the meta line: the host's own. */\n children?: ReactNode;\n className?: string;\n}\n\nexport interface CardMetadata {\n languages?: boolean;\n /** True shows all traits; an array selects specific trait identifiers. */\n traits?: boolean | readonly string[];\n pages?: boolean;\n duration?: boolean;\n}\n\n/** The host's badges over a cover. */\nfunction Decorations({ card, decorate, aboveTitle }: { card: Card; decorate?: (card: Card) => ReactNode; aboveTitle?: boolean }) {\n const badges = decorate?.(card);\n if (badges == null || badges === false) return null;\n return (\n <span className={cn(\"pointer-events-none absolute inset-0\", aboveTitle && \"z-20\")} data-ckui=\"card-decorations\">\n {badges}\n </span>\n );\n}\n\nconst compact = (n: number, language?: string) => new Intl.NumberFormat(language, { notation: \"compact\", maximumFractionDigits: 1 }).format(n);\n\n/** Why a card is locked, in words (its price when it is on sale); null when it opens. */\nexport function lockLabel(m: Translator, a: CardAccess, tiers: readonly string[] = [], labels?: Labels): string | null {\n if (a.allowed) return null;\n if (a.unknown) return m.t(\"contents.unknownAccess\");\n const offer = a.offers?.[0];\n if (offer) return offer.target.kind === \"group\" && Number(offer.terms.amount) === 0 ? m.t(\"contents.joinFree\") : priceLabel(m, offer.terms);\n const tier = a.sell?.find((k) => tiers.includes(k));\n if (tier) return labelOf(labels, tier);\n if (a.sell?.some((k) => k.includes(\":group:\"))) return m.t(\"contents.members\");\n if (a.sell?.length) return m.t(\"contents.forSale\");\n return m.t(\"contents.locked\");\n}\n\n/** A link followed in-app through the provider's navigate. */\nfunction useLink(url: string | undefined, before?: () => void) {\n const navigate = useContext(ContentKitContext)?.navigate;\n return useCallback(\n (e: MouseEvent<HTMLAnchorElement>) => {\n before?.();\n if (!navigate || !url || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n navigate(url);\n },\n [navigate, url, before],\n );\n}\n\n/** One content in a listing: cover (a video the reader opens previews over it on hover), title, version, groups, counts, the reader's progress and why it is locked; what its kind's locked view hides is absent. */\nexport function ContentCard({ card, href, onOpen, shape = \"portrait\", aspectRatio, titlePosition = \"below\", loadMedia = true, groupKinds, chips = 3, filterEntity, entityStyle = \"chips\", metadata, showCounts = true, tiers, labels, owner = true, ownerHref, decorate, children, className }: ContentCardProps) {\n const m = useMessages();\n const { t } = m;\n const languageName = useLanguageName();\n const url = href ? href(card) : card.path;\n const open = useCallback(() => onOpen?.(card), [onOpen, card]);\n const onClick = useLink(url, open);\n const lock = lockLabel(m, card.access, tiers, labels);\n const v = card.version;\n const progress = card.viewer && card.viewer.progress_max > 0 ? Math.min(1, card.viewer.progress / card.viewer.progress_max) : 0;\n const kinds = groupKinds ?? Object.keys(card.entities);\n const entities = kinds.flatMap((kind) => (card.entities[kind] ?? []).filter((entity) => !filterEntity || filterEntity(entity, kind)).slice(0, chips).map((entity) => ({ kind, entity })));\n const minutes = v.duration_ms ? Math.round(v.duration_ms / 60_000) : 0;\n const selectedTraits = metadata?.traits;\n const traits = selectedTraits === false ? [] : Array.isArray(selectedTraits) ? v.traits.filter((trait) => selectedTraits.includes(trait)) : v.traits;\n const meta = [\n ...(metadata?.languages === false ? [] : v.languages.map(languageName)),\n metadata?.pages !== false && v.pages ? m.plural(\"contents.pages\", v.pages) : null,\n metadata?.duration !== false && minutes ? m.plural(\"contents.minutes\", minutes) : null,\n ].filter(Boolean);\n const title = card.title || card.code;\n const entityList = entities.length > 0 ? <ul className={cn(\"flex flex-wrap\", entityStyle === \"text\" ? \"gap-x-2 gap-y-1\" : \"gap-1\")} data-ckui=\"card-groups\">\n {entities.map(({ kind, entity }) => <li key={`${kind}:${entity.id}`} data-kind={kind}><Chip url={entity.path} name={entity.name || entity.slug} variant={entityStyle} /></li>)}\n </ul> : null;\n const titleLink = <a href={url} onClick={onClick} className={cn(\"text-lg leading-snug font-medium break-words focus-visible:underline sm:text-xl\", titlePosition === \"cover\" ? \"absolute inset-x-0 bottom-0 z-10 bg-linear-to-t from-black/90 via-black/60 to-transparent px-3 pt-10 pb-3 text-white hover:underline\" : \"hover:underline\")} data-ckui=\"card-title\">\n <span className=\"line-clamp-2\">{title}</span>\n {lock && <span className=\"sr-only\"> ({lock})</span>}\n </a>;\n const [article, setArticle] = useState<HTMLElement | null>(null);\n return (\n <article ref={setArticle} className={cn(\"group/card grid content-start gap-2\", className)} data-ckui=\"content-card\" data-id={card.id} data-locked={lock ? \"\" : undefined} lang={card.language || undefined}>\n <div className=\"relative overflow-hidden rounded-lg\" data-ckui=\"card-cover\">\n <a href={url} onClick={onClick} className=\"relative block overflow-hidden rounded-lg bg-muted\" tabIndex={-1} aria-hidden>\n <div data-ckui=\"card-cover-frame\" className={aspectRatio === undefined ? shape === \"landscape\" ? \"aspect-video\" : \"aspect-[3/4]\" : undefined} style={aspectRatio === undefined ? undefined : { aspectRatio }}>\n {loadMedia ? <RenditionImg\n outputs={card.cover}\n alt=\"\"\n fit=\"cover\"\n skeleton\n loading=\"lazy\"\n className=\"size-full transition-transform group-hover/card:scale-[1.02]\"\n fallback={<span className=\"grid size-full place-items-center text-2xl font-semibold text-muted-foreground\">{title.charAt(0).toUpperCase()}</span>}\n /> : <span className=\"block size-full bg-muted\" aria-hidden />}\n </div>\n {loadMedia && v.hls && <HoverPreview item={{ kind: card.kind, id: card.id, version: v.id }} dir={v.hls} duration={v.duration_ms ? v.duration_ms / 1000 : undefined} target={article} />}\n {lock && (\n <span className=\"absolute top-2 left-2 inline-flex items-center gap-1 rounded-md bg-background/85 px-1.5 py-0.5 text-xs font-medium text-foreground shadow-sm\" data-ckui=\"card-lock\">\n <HugeiconsIcon icon={LockIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n {lock}\n </span>\n )}\n {progress > 0 && (\n <span className={cn(\"absolute inset-x-0 bottom-0 h-1 bg-background/60\", titlePosition === \"cover\" && \"z-20\")} data-ckui=\"card-progress\">\n <span className=\"block h-full bg-primary\" style={{ width: `${Math.round(progress * 100)}%` }} />\n </span>\n )}\n </a>\n {titlePosition === \"cover\" && titleLink}\n {loadMedia && <Decorations card={card} decorate={decorate} aboveTitle={titlePosition === \"cover\"} />}\n </div>\n <div className=\"grid gap-1\">\n {titlePosition === \"below\" && titleLink}\n {entityStyle === \"text\" && entityList}\n {owner && card.owner && <OwnerChip group={card.owner} href={ownerHref} loadMedia={loadMedia} />}\n {traits.length > 0 && <div className=\"flex flex-wrap gap-1\" data-ckui=\"card-traits\">\n {traits.map((trait) => <Badge key={trait} variant=\"outline\" className=\"h-auto rounded-md text-sm text-muted-foreground\" data-ckui=\"card-trait\" data-trait={trait}>{labelOf(labels, trait)}</Badge>)}\n </div>}\n {meta.length > 0 && <p className=\"text-xs text-muted-foreground\">{meta.join(\" · \")}</p>}\n {card.viewer?.completed && (\n <Badge variant=\"secondary\" className=\"justify-self-start\">\n {t(\"contents.completed\")}\n </Badge>\n )}\n {!card.access.allowed && card.access.free_after && <p className=\"text-xs text-muted-foreground\">{t(\"contents.freeFrom\", { date: formatDate(m, card.access.free_after, false) })}</p>}\n {entityStyle === \"chips\" && entityList}\n {showCounts && (card.counts.favorites !== undefined || card.counts.likes !== undefined) && (\n <p className=\"flex items-center gap-3 text-xs text-muted-foreground tabular-nums\" data-ckui=\"card-counts\">\n {card.counts.favorites !== undefined && (\n <span className=\"inline-flex items-center gap-1\" title={t(\"contents.favorites\")}>\n <HugeiconsIcon icon={FavouriteIcon} strokeWidth={2} className={cn(\"size-3.5\", card.viewer?.favorited && \"fill-current text-primary\")} aria-hidden />\n <span className=\"sr-only\">{t(\"contents.favorites\")}</span>\n {compact(card.counts.favorites, m.language)}\n </span>\n )}\n {card.counts.likes !== undefined && (\n <span className=\"inline-flex items-center gap-1\" title={t(\"contents.likes\")}>\n <HugeiconsIcon icon={ThumbsUpIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n <span className=\"sr-only\">{t(\"contents.likes\")}</span>\n {compact(card.counts.likes, m.language)}\n </span>\n )}\n </p>\n )}\n {children}\n </div>\n </article>\n );\n}\n\n/** The group that owns a content (a channel): its avatar and name, linking to its page. */\nexport function OwnerChip({ group, href, className, loadMedia = true }: { group: GroupLike; href?: (g: GroupLike) => string | undefined; className?: string; loadMedia?: boolean }) {\n return (\n <span className={cn(\"flex min-w-0 text-xs text-muted-foreground\", className)} data-ckui=\"card-owner\" data-id={group.id}>\n <GroupLink group={group} href={href} className=\"inline-flex min-w-0 items-center gap-1.5 hover:text-foreground\">\n {loadMedia ? <GroupAvatar group={group} className=\"size-5 shrink-0 text-[0.625rem]\" /> : <span className=\"size-5 shrink-0 rounded-full bg-muted\" aria-hidden />}\n <span className=\"truncate\">{group.name || group.slug}</span>\n </GroupLink>\n </span>\n );\n}\n\nfunction Chip({ url, name, variant = \"chips\" }: { url?: string; name: string; variant?: \"chips\" | \"text\" }) {\n const onClick = useLink(url);\n const cls = variant === \"text\" ? \"text-sm text-muted-foreground\" : \"inline-block rounded-md border border-border px-1.5 py-0.5 text-sm text-muted-foreground\";\n return url ? (\n <a href={url} onClick={onClick} className={cn(cls, \"hover:text-foreground\", variant === \"text\" ? \"hover:underline\" : \"hover:bg-muted\")}>\n {name}\n </a>\n ) : (\n <span className={cls}>{name}</span>\n );\n}\n\nexport interface PostCardProps extends Omit<ContentCardProps, \"shape\" | \"aspectRatio\" | \"titlePosition\" | \"chips\" | \"groupKinds\" | \"filterEntity\" | \"entityStyle\" | \"metadata\"> {\n /** Above the title: the post's channel or author; default its owner group's chip. */\n byline?: ReactNode;\n}\n\n/** A post in a feed: a wide cover, its title, when it was published and what unlocking it costs. */\nexport function PostCard({ card, href, onOpen, tiers, labels, byline, owner = true, ownerHref, loadMedia = true, showCounts = true, decorate, children, className }: PostCardProps) {\n const m = useMessages();\n const url = href ? href(card) : card.path;\n const open = useCallback(() => onOpen?.(card), [onOpen, card]);\n const onClick = useLink(url, open);\n const lock = lockLabel(m, card.access, tiers, labels);\n const title = card.title || card.code;\n return (\n <article className={cn(\"grid gap-3 rounded-lg border border-border p-3\", className)} data-ckui=\"post-card\" data-id={card.id} data-locked={lock ? \"\" : undefined} lang={card.language || undefined}>\n {byline ?? (owner && card.owner && <OwnerChip group={card.owner} href={ownerHref} loadMedia={loadMedia} className=\"text-sm\" />)}\n <a href={url} onClick={onClick} className=\"grid gap-2\">\n <span className=\"font-medium break-words hover:underline\" data-ckui=\"card-title\">\n {title}\n </span>\n {card.cover.length > 0 && (\n <span className=\"relative block aspect-video overflow-hidden rounded-md bg-muted\">\n {loadMedia && <RenditionImg outputs={card.cover} alt=\"\" fit=\"cover\" skeleton loading=\"lazy\" className={cn(\"size-full\", lock && \"blur-md\")} />}\n {loadMedia && <Decorations card={card} decorate={decorate} />}\n {lock && (\n <span className=\"absolute inset-0 grid place-items-center\">\n <span className=\"inline-flex items-center gap-1 rounded-md bg-background/85 px-2 py-1 text-xs font-medium shadow-sm\" data-ckui=\"card-lock\">\n <HugeiconsIcon icon={LockIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n {lock}\n </span>\n </span>\n )}\n </span>\n )}\n </a>\n <p className=\"flex flex-wrap items-center gap-3 text-xs text-muted-foreground\">\n {card.published_at && <RelativeTime at={card.published_at} />}\n {!card.cover.length && lock && <span data-ckui=\"card-lock\">{lock}</span>}\n {showCounts && card.counts.favorites !== undefined && (\n <span className=\"inline-flex items-center gap-1 tabular-nums\" data-ckui=\"card-counts\">\n <HugeiconsIcon icon={FavouriteIcon} strokeWidth={2} className={cn(\"size-3.5\", card.viewer?.favorited && \"fill-current text-primary\")} aria-hidden />\n <span className=\"sr-only\">{m.t(\"contents.favorites\")}</span>\n {compact(card.counts.favorites, m.language)}\n </span>\n )}\n </p>\n {children}\n </article>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"card.js","names":[],"sources":["../../../src/components/content/card.tsx"],"sourcesContent":["import { FavouriteIcon, LockIcon, ThumbsUpIcon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useCallback, useContext, useState, type CSSProperties, type MouseEvent, type ReactNode } from \"react\";\nimport type { Card, CardAccess, Chip as EntityChip } from \"../../client/generated/wire.js\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { ratio } from \"../../client/aspect.js\";\nimport { useLanguageName, useMessages } from \"../../i18n/context.js\";\nimport type { Translator } from \"../../i18n/messages.js\";\nimport { ContentKitContext } from \"../../react/context.js\";\nimport { usePresets } from \"../../react/public.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { HoverPreview } from \"../hover-preview.js\";\nimport { RenditionImg } from \"../rendition-img.js\";\nimport { formatDate, RelativeTime } from \"../social/parts.js\";\nimport { GroupAvatar, GroupLink, labelOf, type GroupLike, type Labels } from \"../groups/parts.js\";\nimport { priceLabel } from \"./paywall.js\";\n\nexport interface ContentCardProps {\n card: Card;\n /** Overrides the provider's client for this card's public media configuration. */\n client?: ContentKitClient;\n /** The card's link; default its canonical path. */\n href?: (card: Card) => string | undefined;\n /** Before the card's link is followed: a click signal. */\n onOpen?: (card: Card) => void;\n /** Fallback cover shape when the server declares no preset aspect. Default portrait (3:4). */\n shape?: \"portrait\" | \"landscape\";\n /** A CSS aspect ratio overriding shape, such as \"182 / 260\". */\n aspectRatio?: CSSProperties[\"aspectRatio\"];\n /** Place the title below the cover (default), or over its bottom gradient. */\n titlePosition?: \"below\" | \"cover\";\n /** Mount images and previews. False keeps the card's text and reserved cover space. */\n loadMedia?: boolean;\n /** The group kinds whose chips show, in order; default every kind the card names. */\n groupKinds?: readonly string[];\n /** Chips per group kind. Default 3. */\n chips?: number;\n /** Select which entity chips appear, before the per-kind chip limit. */\n filterEntity?: (entity: EntityChip, kind: string) => boolean;\n /** Bordered chips (default) or plain linked text. */\n entityStyle?: \"chips\" | \"text\";\n /** One inline list (default), or a separate row for each group kind. */\n entityLayout?: \"inline\" | \"rows\";\n /** Metadata fields to show; omitted fields keep their defaults. */\n metadata?: CardMetadata;\n /** Show like and favorite totals. Default true. */\n showCounts?: boolean;\n /** Tiers the site sells, so a lock names them; others show as \"for sale\". */\n tiers?: readonly string[];\n /** Display names of kinds, traits and tiers by identifier. */\n labels?: Labels;\n /** Show the owner group (a channel) when the card has one; default true, false on that group's own feed. */\n owner?: boolean;\n /** The owner group's link; default its canonical path. */\n ownerHref?: (g: GroupLike) => string | undefined;\n /** Over the cover: the host's badges for the card, placed in the cover's box; clicks reach the card. */\n decorate?: (card: Card) => ReactNode;\n /** Below the meta line: the host's own. */\n children?: ReactNode;\n className?: string;\n}\n\nexport interface CardMetadata {\n languages?: boolean;\n /** True shows all traits; an array selects specific trait identifiers. */\n traits?: boolean | readonly string[];\n pages?: boolean;\n duration?: boolean;\n}\n\n/** The host's badges over a cover. */\nfunction Decorations({ card, decorate, aboveTitle }: { card: Card; decorate?: (card: Card) => ReactNode; aboveTitle?: boolean }) {\n const badges = decorate?.(card);\n if (badges == null || badges === false) return null;\n return (\n <span className={cn(\"pointer-events-none absolute inset-0\", aboveTitle && \"z-20\")} data-ckui=\"card-decorations\">\n {badges}\n </span>\n );\n}\n\nconst compact = (n: number, language?: string) => new Intl.NumberFormat(language, { notation: \"compact\", maximumFractionDigits: 1 }).format(n);\n\n/** Why a card is locked, in words (its price when it is on sale); null when it opens. */\nexport function lockLabel(m: Translator, a: CardAccess, tiers: readonly string[] = [], labels?: Labels): string | null {\n if (a.allowed) return null;\n if (a.unknown) return m.t(\"contents.unknownAccess\");\n const offer = a.offers?.[0];\n if (offer) return offer.target.kind === \"group\" && Number(offer.terms.amount) === 0 ? m.t(\"contents.joinFree\") : priceLabel(m, offer.terms);\n const tier = a.sell?.find((k) => tiers.includes(k));\n if (tier) return labelOf(labels, tier);\n if (a.sell?.some((k) => k.includes(\":group:\"))) return m.t(\"contents.members\");\n if (a.sell?.length) return m.t(\"contents.forSale\");\n return m.t(\"contents.locked\");\n}\n\n/** A link followed in-app through the provider's navigate. */\nfunction useLink(url: string | undefined, before?: () => void) {\n const navigate = useContext(ContentKitContext)?.navigate;\n return useCallback(\n (e: MouseEvent<HTMLAnchorElement>) => {\n before?.();\n if (!navigate || !url || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n navigate(url);\n },\n [navigate, url, before],\n );\n}\n\n/** One content in a listing: cover (a video the reader opens previews over it on hover), title, version, groups, counts, the reader's progress and why it is locked; what its kind's locked view hides is absent. */\nexport function ContentCard({ card, client, href, onOpen, shape = \"portrait\", aspectRatio, titlePosition = \"below\", loadMedia = true, groupKinds, chips = 3, filterEntity, entityStyle = \"chips\", entityLayout = \"inline\", metadata, showCounts = true, tiers, labels, owner = true, ownerHref, decorate, children, className }: ContentCardProps) {\n const m = useMessages();\n const { presets } = usePresets({ client });\n const coverAspect = aspectRatio ?? ratio(presets.find((p) => p.kind === card.kind && p.name === \"cover\")?.aspect);\n const { t } = m;\n const languageName = useLanguageName();\n const url = href ? href(card) : card.path;\n const open = useCallback(() => onOpen?.(card), [onOpen, card]);\n const onClick = useLink(url, open);\n const lock = lockLabel(m, card.access, tiers, labels);\n const v = card.version;\n const progress = card.viewer && card.viewer.progress_max > 0 ? Math.min(1, card.viewer.progress / card.viewer.progress_max) : 0;\n const kinds = groupKinds ?? Object.keys(card.entities);\n const entityGroups = kinds.map((kind) => ({ kind, entities: (card.entities[kind] ?? []).filter((entity) => !filterEntity || filterEntity(entity, kind)).slice(0, chips) })).filter(({ entities }) => entities.length > 0);\n const minutes = v.duration_ms ? Math.round(v.duration_ms / 60_000) : 0;\n const selectedTraits = metadata?.traits;\n const traits = selectedTraits === false ? [] : Array.isArray(selectedTraits) ? v.traits.filter((trait) => selectedTraits.includes(trait)) : v.traits;\n const meta = [\n ...(metadata?.languages === false ? [] : v.languages.map(languageName)),\n metadata?.pages !== false && v.pages ? m.plural(\"contents.pages\", v.pages) : null,\n metadata?.duration !== false && minutes ? m.plural(\"contents.minutes\", minutes) : null,\n ].filter(Boolean);\n const title = card.title || card.code;\n const entityClassName = cn(\"flex flex-wrap\", entityStyle === \"text\" ? \"gap-x-2 gap-y-1\" : \"gap-1\");\n const entityItems = (kind: string, entities: EntityChip[]) => entities.map((entity) => <li key={`${kind}:${entity.id}`} data-kind={kind}><Chip url={entity.path} name={entity.name || entity.slug} variant={entityStyle} /></li>);\n const entityList = entityGroups.length > 0 ? entityLayout === \"rows\" ? (\n <div className=\"grid gap-1\" data-ckui=\"card-groups\" data-layout=\"rows\">\n {entityGroups.map(({ kind, entities }) => <ul key={kind} className={entityClassName} aria-label={labelOf(labels, kind)} data-ckui=\"card-group-row\" data-kind={kind}>{entityItems(kind, entities)}</ul>)}\n </div>\n ) : (\n <ul className={entityClassName} data-ckui=\"card-groups\" data-layout=\"inline\">\n {entityGroups.flatMap(({ kind, entities }) => entityItems(kind, entities))}\n </ul>\n ) : null;\n const titleLink = <a href={url} onClick={onClick} className={cn(\"text-lg leading-snug font-medium break-words focus-visible:underline sm:text-xl\", titlePosition === \"cover\" ? \"absolute inset-x-0 bottom-0 z-10 bg-linear-to-t from-black/90 via-black/60 to-transparent px-3 pt-10 pb-3 text-white hover:underline\" : \"hover:underline\")} data-ckui=\"card-title\">\n <span className=\"line-clamp-2\">{title}</span>\n {lock && <span className=\"sr-only\"> ({lock})</span>}\n </a>;\n const [article, setArticle] = useState<HTMLElement | null>(null);\n return (\n <article ref={setArticle} className={cn(\"group/card grid content-start gap-2\", className)} data-ckui=\"content-card\" data-id={card.id} data-locked={lock ? \"\" : undefined} lang={card.language || undefined}>\n <div className=\"relative overflow-hidden rounded-lg\" data-ckui=\"card-cover\">\n <a href={url} onClick={onClick} className=\"relative block overflow-hidden rounded-lg bg-muted\" tabIndex={-1} aria-hidden>\n <div data-ckui=\"card-cover-frame\" className={coverAspect === undefined ? shape === \"landscape\" ? \"aspect-video\" : \"aspect-[3/4]\" : undefined} style={coverAspect === undefined ? undefined : { aspectRatio: coverAspect }}>\n {loadMedia ? <RenditionImg\n outputs={card.cover}\n alt=\"\"\n fit=\"cover\"\n skeleton\n loading=\"lazy\"\n className=\"size-full transition-transform group-hover/card:scale-[1.02]\"\n fallback={<span className=\"grid size-full place-items-center text-2xl font-semibold text-muted-foreground\">{title.charAt(0).toUpperCase()}</span>}\n /> : <span className=\"block size-full bg-muted\" aria-hidden />}\n </div>\n {loadMedia && v.hls && <HoverPreview item={{ kind: card.kind, id: card.id, version: v.id }} dir={v.hls} duration={v.duration_ms ? v.duration_ms / 1000 : undefined} target={article} />}\n {lock && (\n <span className=\"absolute top-2 left-2 inline-flex items-center gap-1 rounded-md bg-background/85 px-1.5 py-0.5 text-xs font-medium text-foreground shadow-sm\" data-ckui=\"card-lock\">\n <HugeiconsIcon icon={LockIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n {lock}\n </span>\n )}\n {progress > 0 && (\n <span className={cn(\"absolute inset-x-0 bottom-0 h-1 bg-background/60\", titlePosition === \"cover\" && \"z-20\")} data-ckui=\"card-progress\">\n <span className=\"block h-full bg-primary\" style={{ width: `${Math.round(progress * 100)}%` }} />\n </span>\n )}\n </a>\n {titlePosition === \"cover\" && titleLink}\n {loadMedia && <Decorations card={card} decorate={decorate} aboveTitle={titlePosition === \"cover\"} />}\n </div>\n <div className=\"grid gap-1\">\n {titlePosition === \"below\" && titleLink}\n {entityStyle === \"text\" && entityList}\n {owner && card.owner && <OwnerChip group={card.owner} href={ownerHref} loadMedia={loadMedia} />}\n {traits.length > 0 && <div className=\"flex flex-wrap gap-1\" data-ckui=\"card-traits\">\n {traits.map((trait) => <Badge key={trait} variant=\"outline\" className=\"h-auto rounded-md text-sm text-muted-foreground\" data-ckui=\"card-trait\" data-trait={trait}>{labelOf(labels, trait)}</Badge>)}\n </div>}\n {meta.length > 0 && <p className=\"text-xs text-muted-foreground\">{meta.join(\" · \")}</p>}\n {card.viewer?.completed && (\n <Badge variant=\"secondary\" className=\"justify-self-start\">\n {t(\"contents.completed\")}\n </Badge>\n )}\n {!card.access.allowed && card.access.free_after && <p className=\"text-xs text-muted-foreground\">{t(\"contents.freeFrom\", { date: formatDate(m, card.access.free_after, false) })}</p>}\n {entityStyle === \"chips\" && entityList}\n {showCounts && (card.counts.favorites !== undefined || card.counts.likes !== undefined) && (\n <p className=\"flex items-center gap-3 text-xs text-muted-foreground tabular-nums\" data-ckui=\"card-counts\">\n {card.counts.favorites !== undefined && (\n <span className=\"inline-flex items-center gap-1\" title={t(\"contents.favorites\")}>\n <HugeiconsIcon icon={FavouriteIcon} strokeWidth={2} className={cn(\"size-3.5\", card.viewer?.favorited && \"fill-current text-primary\")} aria-hidden />\n <span className=\"sr-only\">{t(\"contents.favorites\")}</span>\n {compact(card.counts.favorites, m.language)}\n </span>\n )}\n {card.counts.likes !== undefined && (\n <span className=\"inline-flex items-center gap-1\" title={t(\"contents.likes\")}>\n <HugeiconsIcon icon={ThumbsUpIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n <span className=\"sr-only\">{t(\"contents.likes\")}</span>\n {compact(card.counts.likes, m.language)}\n </span>\n )}\n </p>\n )}\n {children}\n </div>\n </article>\n );\n}\n\n/** The group that owns a content (a channel): its avatar and name, linking to its page. */\nexport function OwnerChip({ group, href, className, loadMedia = true }: { group: GroupLike; href?: (g: GroupLike) => string | undefined; className?: string; loadMedia?: boolean }) {\n return (\n <span className={cn(\"flex min-w-0 text-xs text-muted-foreground\", className)} data-ckui=\"card-owner\" data-id={group.id}>\n <GroupLink group={group} href={href} className=\"inline-flex min-w-0 items-center gap-1.5 hover:text-foreground\">\n {loadMedia ? <GroupAvatar group={group} className=\"size-5 shrink-0 text-[0.625rem]\" /> : <span className=\"size-5 shrink-0 rounded-full bg-muted\" aria-hidden />}\n <span className=\"truncate\">{group.name || group.slug}</span>\n </GroupLink>\n </span>\n );\n}\n\nfunction Chip({ url, name, variant = \"chips\" }: { url?: string; name: string; variant?: \"chips\" | \"text\" }) {\n const onClick = useLink(url);\n const cls = variant === \"text\" ? \"text-sm text-muted-foreground\" : \"inline-block rounded-md border border-border px-1.5 py-0.5 text-sm text-muted-foreground\";\n return url ? (\n <a href={url} onClick={onClick} className={cn(cls, \"hover:text-foreground\", variant === \"text\" ? \"hover:underline\" : \"hover:bg-muted\")}>\n {name}\n </a>\n ) : (\n <span className={cls}>{name}</span>\n );\n}\n\nexport interface PostCardProps extends Omit<ContentCardProps, \"shape\" | \"aspectRatio\" | \"titlePosition\" | \"chips\" | \"groupKinds\" | \"filterEntity\" | \"entityStyle\" | \"entityLayout\" | \"metadata\"> {\n /** Above the title: the post's channel or author; default its owner group's chip. */\n byline?: ReactNode;\n}\n\n/** A post in a feed: a wide cover, its title, when it was published and what unlocking it costs. */\nexport function PostCard({ card, href, onOpen, tiers, labels, byline, owner = true, ownerHref, loadMedia = true, showCounts = true, decorate, children, className }: PostCardProps) {\n const m = useMessages();\n const url = href ? href(card) : card.path;\n const open = useCallback(() => onOpen?.(card), [onOpen, card]);\n const onClick = useLink(url, open);\n const lock = lockLabel(m, card.access, tiers, labels);\n const title = card.title || card.code;\n return (\n <article className={cn(\"grid gap-3 rounded-lg border border-border p-3\", className)} data-ckui=\"post-card\" data-id={card.id} data-locked={lock ? \"\" : undefined} lang={card.language || undefined}>\n {byline ?? (owner && card.owner && <OwnerChip group={card.owner} href={ownerHref} loadMedia={loadMedia} className=\"text-sm\" />)}\n <a href={url} onClick={onClick} className=\"grid gap-2\">\n <span className=\"font-medium break-words hover:underline\" data-ckui=\"card-title\">\n {title}\n </span>\n {card.cover.length > 0 && (\n <span className=\"relative block aspect-video overflow-hidden rounded-md bg-muted\">\n {loadMedia && <RenditionImg outputs={card.cover} alt=\"\" fit=\"cover\" skeleton loading=\"lazy\" className={cn(\"size-full\", lock && \"blur-md\")} />}\n {loadMedia && <Decorations card={card} decorate={decorate} />}\n {lock && (\n <span className=\"absolute inset-0 grid place-items-center\">\n <span className=\"inline-flex items-center gap-1 rounded-md bg-background/85 px-2 py-1 text-xs font-medium shadow-sm\" data-ckui=\"card-lock\">\n <HugeiconsIcon icon={LockIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n {lock}\n </span>\n </span>\n )}\n </span>\n )}\n </a>\n <p className=\"flex flex-wrap items-center gap-3 text-xs text-muted-foreground\">\n {card.published_at && <RelativeTime at={card.published_at} />}\n {!card.cover.length && lock && <span data-ckui=\"card-lock\">{lock}</span>}\n {showCounts && card.counts.favorites !== undefined && (\n <span className=\"inline-flex items-center gap-1 tabular-nums\" data-ckui=\"card-counts\">\n <HugeiconsIcon icon={FavouriteIcon} strokeWidth={2} className={cn(\"size-3.5\", card.viewer?.favorited && \"fill-current text-primary\")} aria-hidden />\n <span className=\"sr-only\">{m.t(\"contents.favorites\")}</span>\n {compact(card.counts.favorites, m.language)}\n </span>\n )}\n </p>\n {children}\n </article>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwEA,SAAS,EAAY,EAAE,SAAM,aAAU,iBAA0F;CAC/H,IAAM,IAAS,IAAW,CAAI;CAE9B,OADI,KAAU,QAAQ,MAAW,KAAc,OAE7C,kBAAC,QAAD;EAAM,WAAW,EAAG,wCAAwC,KAAc,MAAM;EAAG,aAAU;EAC1F,UAAA;CACG,CAAA;AAEV;AAEA,IAAM,KAAW,GAAW,MAAsB,IAAI,KAAK,aAAa,GAAU;CAAE,UAAU;CAAW,uBAAuB;AAAE,CAAC,CAAC,CAAC,OAAO,CAAC;AAG7I,SAAgB,EAAU,GAAe,GAAe,IAA2B,CAAC,GAAG,GAAgC;CACrH,IAAI,EAAE,SAAS,OAAO;CACtB,IAAI,EAAE,SAAS,OAAO,EAAE,EAAE,wBAAwB;CAClD,IAAM,IAAQ,EAAE,SAAS;CACzB,IAAI,GAAO,OAAO,EAAM,OAAO,SAAS,WAAW,OAAO,EAAM,MAAM,MAAM,MAAM,IAAI,EAAE,EAAE,mBAAmB,IAAI,EAAW,GAAG,EAAM,KAAK;CAC1I,IAAM,IAAO,EAAE,MAAM,MAAM,MAAM,EAAM,SAAS,CAAC,CAAC;CAIlD,OAHI,IAAa,EAAQ,GAAQ,CAAI,IACjC,EAAE,MAAM,MAAM,MAAM,EAAE,SAAS,SAAS,CAAC,IAAU,EAAE,EAAE,kBAAkB,IACzE,EAAE,MAAM,SAAe,EAAE,EAAE,kBAAkB,IAC1C,EAAE,EAAE,iBAAiB;AAC9B;AAGA,SAAS,EAAQ,GAAyB,GAAqB;CAC7D,IAAM,IAAW,EAAW,CAAiB,CAAC,EAAE;CAChD,OAAO,GACJ,MAAqC;EACpC,IAAS,GACL,GAAC,KAAY,CAAC,KAAO,EAAE,oBAAoB,EAAE,WAAW,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,YAC3G,EAAE,eAAe,GACjB,EAAS,CAAG;CACd,GACA;EAAC;EAAU;EAAK;CAAM,CACxB;AACF;AAGA,SAAgB,EAAY,EAAE,SAAM,WAAQ,SAAM,WAAQ,WAAQ,YAAY,gBAAa,mBAAgB,SAAS,eAAY,IAAM,eAAY,YAAQ,GAAG,iBAAc,iBAAc,SAAS,mBAAe,UAAU,aAAU,iBAAa,IAAM,WAAO,WAAQ,YAAQ,IAAM,eAAW,cAAU,cAAU,iBAA+B;CACjV,IAAM,IAAI,EAAY,GAChB,EAAE,gBAAY,EAAW,EAAE,UAAO,CAAC,GACnC,IAAc,KAAe,EAAM,GAAQ,MAAM,MAAM,EAAE,SAAS,EAAK,QAAQ,EAAE,SAAS,OAAO,CAAC,EAAE,MAAM,GAC1G,EAAE,SAAM,GACR,KAAe,EAAgB,GAC/B,IAAM,IAAO,EAAK,CAAI,IAAI,EAAK,MAE/B,IAAU,EAAQ,GADX,QAAkB,IAAS,CAAI,GAAG,CAAC,GAAQ,CAAI,CAC/B,CAAI,GAC3B,IAAO,EAAU,GAAG,EAAK,QAAQ,IAAO,CAAM,GAC9C,IAAI,EAAK,SACT,IAAW,EAAK,UAAU,EAAK,OAAO,eAAe,IAAI,KAAK,IAAI,GAAG,EAAK,OAAO,WAAW,EAAK,OAAO,YAAY,IAAI,GAExH,KADQ,KAAc,OAAO,KAAK,EAAK,QAAQ,EAAA,CAC1B,KAAK,OAAU;EAAE;EAAM,WAAW,EAAK,SAAS,MAAS,CAAC,EAAA,CAAG,QAAQ,MAAW,CAAC,KAAgB,EAAa,GAAQ,CAAI,CAAC,CAAC,CAAC,MAAM,GAAG,EAAK;CAAE,EAAE,CAAC,CAAC,QAAQ,EAAE,kBAAe,EAAS,SAAS,CAAC,GAClN,IAAU,EAAE,cAAc,KAAK,MAAM,EAAE,cAAc,GAAM,IAAI,GAC/D,IAAiB,GAAU,QAC3B,IAAS,MAAmB,KAAQ,CAAC,IAAI,MAAM,QAAQ,CAAc,IAAI,EAAE,OAAO,QAAQ,MAAU,EAAe,SAAS,CAAK,CAAC,IAAI,EAAE,QACxI,IAAO;EACX,GAAI,GAAU,cAAc,KAAQ,CAAC,IAAI,EAAE,UAAU,IAAI,EAAY;EACrE,GAAU,UAAU,MAAS,EAAE,QAAQ,EAAE,OAAO,kBAAkB,EAAE,KAAK,IAAI;EAC7E,GAAU,aAAa,MAAS,IAAU,EAAE,OAAO,oBAAoB,CAAO,IAAI;CACpF,CAAC,CAAC,OAAO,OAAO,GACV,IAAQ,EAAK,SAAS,EAAK,MAC3B,IAAkB,EAAG,kBAAkB,MAAgB,SAAS,oBAAoB,OAAO,GAC3F,KAAe,GAAc,MAA2B,EAAS,KAAK,MAAW,kBAAC,MAAD;EAAiC,aAAW;EAAM,UAAA,kBAAC,GAAD;GAAM,KAAK,EAAO;GAAM,MAAM,EAAO,QAAQ,EAAO;GAAM,SAAS;EAAc,CAAA;CAAK,GAA/H,GAAG,EAAK,GAAG,EAAO,IAA6G,CAAC,GAC1N,IAAa,EAAa,SAAS,IAAI,OAAiB,SAC5D,kBAAC,OAAD;EAAK,WAAU;EAAa,aAAU;EAAc,eAAY;EAC7D,UAAA,EAAa,KAAK,EAAE,SAAM,kBAAe,kBAAC,MAAD;GAAe,WAAW;GAAiB,cAAY,EAAQ,GAAQ,CAAI;GAAG,aAAU;GAAiB,aAAW;GAAO,UAAA,EAAY,GAAM,CAAQ;EAAM,GAAlJ,CAAkJ,CAAC;CACnM,CAAA,IAEL,kBAAC,MAAD;EAAI,WAAW;EAAiB,aAAU;EAAc,eAAY;EACjE,UAAA,EAAa,SAAS,EAAE,SAAM,kBAAe,EAAY,GAAM,CAAQ,CAAC;CACvE,CAAA,IACF,MACE,KAAY,kBAAC,KAAD;EAAG,MAAM;EAAc;EAAS,WAAW,EAAG,mFAAmF,MAAkB,UAAU,yIAAyI,iBAAiB;EAAG,aAAU;EAApU,UAAA,CAChB,kBAAC,QAAD;GAAM,WAAU;GAAgB,UAAA;EAAY,CAAA,GAC3C,KAAQ,kBAAC,QAAD;GAAM,WAAU;GAAhB,UAAA;IAA0B;IAAG;IAAK;GAAO;EACjD,CAAA,CAAA;KACG,CAAC,IAAS,MAAc,EAA6B,IAAI;CAC/D,OACE,kBAAC,WAAD;EAAS,KAAK;EAAY,WAAW,EAAG,uCAAuC,EAAS;EAAG,aAAU;EAAe,WAAS,EAAK;EAAI,eAAa,IAAO,KAAK,KAAA;EAAW,MAAM,EAAK,YAAY,KAAA;EAAjM,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;GAAsC,aAAU;GAA/D,UAAA;IACE,kBAAC,KAAD;KAAG,MAAM;KAAc;KAAS,WAAU;KAAqD,UAAU;KAAI,eAAA;KAA7G,UAAA;MACE,kBAAC,OAAD;OAAK,aAAU;OAAmB,WAAW,MAAgB,KAAA,IAAY,MAAU,cAAc,iBAAiB,iBAAiB,KAAA;OAAW,OAAO,MAAgB,KAAA,IAAY,KAAA,IAAY,EAAE,aAAa,EAAY;OACrN,UAAA,IAAY,kBAAC,GAAD;QACX,SAAS,EAAK;QACd,KAAI;QACJ,KAAI;QACJ,UAAA;QACA,SAAQ;QACR,WAAU;QACV,UAAU,kBAAC,QAAD;SAAM,WAAU;SAAkF,UAAA,EAAM,OAAO,CAAC,CAAC,CAAC,YAAY;QAAQ,CAAA;OACjJ,CAAA,IAAI,kBAAC,QAAD;QAAM,WAAU;QAA2B,eAAA;OAAa,CAAA;MAC1D,CAAA;MACJ,KAAa,EAAE,OAAO,kBAAC,GAAD;OAAc,MAAM;QAAE,MAAM,EAAK;QAAM,IAAI,EAAK;QAAI,SAAS,EAAE;OAAG;OAAG,KAAK,EAAE;OAAK,UAAU,EAAE,cAAc,EAAE,cAAc,MAAO,KAAA;OAAW,QAAQ;MAAU,CAAA;MACrL,KACC,kBAAC,QAAD;OAAM,WAAU;OAA+I,aAAU;OAAzK,UAAA,CACE,kBAAC,GAAD;QAAe,MAAM;QAAU,aAAa;QAAG,WAAU;QAAW,eAAA;OAAa,CAAA,GAChF,CACG;;MAEP,IAAW,KACV,kBAAC,QAAD;OAAM,WAAW,EAAG,oDAAoD,MAAkB,WAAW,MAAM;OAAG,aAAU;OACtH,UAAA,kBAAC,QAAD;QAAM,WAAU;QAA0B,OAAO,EAAE,OAAO,GAAG,KAAK,MAAM,IAAW,GAAG,EAAE,GAAG;OAAI,CAAA;MAC3F,CAAA;KAEP;;IACF,MAAkB,WAAW;IAC7B,KAAa,kBAAC,GAAD;KAAmB;KAAgB;KAAU,YAAY,MAAkB;IAAU,CAAA;GAChG;EACL,CAAA,GAAA,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IACG,MAAkB,WAAW;IAC7B,MAAgB,UAAU;IAC1B,MAAS,EAAK,SAAS,kBAAC,GAAD;KAAW,OAAO,EAAK;KAAO,MAAM;KAAsB;IAAY,CAAA;IAC7F,EAAO,SAAS,KAAK,kBAAC,OAAD;KAAK,WAAU;KAAuB,aAAU;KACnE,UAAA,EAAO,KAAK,MAAU,kBAAC,GAAD;MAAmB,SAAQ;MAAU,WAAU;MAAkD,aAAU;MAAa,cAAY;MAAQ,UAAA,EAAQ,GAAQ,CAAK;KAAS,GAA9J,CAA8J,CAAC;IAC/L,CAAA;IACJ,EAAK,SAAS,KAAK,kBAAC,KAAD;KAAG,WAAU;KAAiC,UAAA,EAAK,KAAK,KAAK;IAAK,CAAA;IACrF,EAAK,QAAQ,aACZ,kBAAC,GAAD;KAAO,SAAQ;KAAY,WAAU;KAClC,UAAA,EAAE,oBAAoB;IAClB,CAAA;IAER,CAAC,EAAK,OAAO,WAAW,EAAK,OAAO,cAAc,kBAAC,KAAD;KAAG,WAAU;KAAiC,UAAA,EAAE,qBAAqB,EAAE,MAAM,EAAW,GAAG,EAAK,OAAO,YAAY,EAAK,EAAE,CAAC;IAAK,CAAA;IAClL,MAAgB,WAAW;IAC3B,OAAe,EAAK,OAAO,cAAc,KAAA,KAAa,EAAK,OAAO,UAAU,KAAA,MAC3E,kBAAC,KAAD;KAAG,WAAU;KAAqE,aAAU;KAA5F,UAAA,CACG,EAAK,OAAO,cAAc,KAAA,KACzB,kBAAC,QAAD;MAAM,WAAU;MAAiC,OAAO,EAAE,oBAAoB;MAA9E,UAAA;OACE,kBAAC,GAAD;QAAe,MAAM;QAAe,aAAa;QAAG,WAAW,EAAG,YAAY,EAAK,QAAQ,aAAa,2BAA2B;QAAG,eAAA;OAAa,CAAA;OACnJ,kBAAC,QAAD;QAAM,WAAU;QAAW,UAAA,EAAE,oBAAoB;OAAQ,CAAA;OACxD,EAAQ,EAAK,OAAO,WAAW,EAAE,QAAQ;MACtC;KAEP,CAAA,GAAA,EAAK,OAAO,UAAU,KAAA,KACrB,kBAAC,QAAD;MAAM,WAAU;MAAiC,OAAO,EAAE,gBAAgB;MAA1E,UAAA;OACE,kBAAC,GAAD;QAAe,MAAM;QAAc,aAAa;QAAG,WAAU;QAAW,eAAA;OAAa,CAAA;OACrF,kBAAC,QAAD;QAAM,WAAU;QAAW,UAAA,EAAE,gBAAgB;OAAQ,CAAA;OACpD,EAAQ,EAAK,OAAO,OAAO,EAAE,QAAQ;MAClC;KAEP,CAAA,CAAA;;IAEJ;GACE;EACE,CAAA,CAAA;;AAEb;AAGA,SAAgB,EAAU,EAAE,UAAO,SAAM,cAAW,eAAY,MAAoH;CAClL,OACE,kBAAC,QAAD;EAAM,WAAW,EAAG,8CAA8C,CAAS;EAAG,aAAU;EAAa,WAAS,EAAM;EAClH,UAAA,kBAAC,GAAD;GAAkB;GAAa;GAAM,WAAU;GAA/C,UAAA,CACG,IAAY,kBAAC,GAAD;IAAoB;IAAO,WAAU;GAAmC,CAAA,IAAI,kBAAC,QAAD;IAAM,WAAU;IAAwC,eAAA;GAAa,CAAA,GAC9J,kBAAC,QAAD;IAAM,WAAU;IAAY,UAAA,EAAM,QAAQ,EAAM;GAAW,CAAA,CAClD;;CACP,CAAA;AAEV;AAEA,SAAS,EAAK,EAAE,QAAK,SAAM,aAAU,WAAuE;CAC1G,IAAM,IAAU,EAAQ,CAAG,GACrB,IAAM,MAAY,SAAS,kCAAkC;CACnE,OAAO,IACL,kBAAC,KAAD;EAAG,MAAM;EAAc;EAAS,WAAW,EAAG,GAAK,yBAAyB,MAAY,SAAS,oBAAoB,gBAAgB;EAClI,UAAA;CACA,CAAA,IAEH,kBAAC,QAAD;EAAM,WAAW;EAAM,UAAA;CAAW,CAAA;AAEtC;AAQA,SAAgB,EAAS,EAAE,SAAM,SAAM,WAAQ,UAAO,WAAQ,WAAQ,WAAQ,IAAM,cAAW,eAAY,IAAM,gBAAa,IAAM,aAAU,aAAU,gBAA4B;CAClL,IAAM,IAAI,EAAY,GAChB,IAAM,IAAO,EAAK,CAAI,IAAI,EAAK,MAE/B,IAAU,EAAQ,GADX,QAAkB,IAAS,CAAI,GAAG,CAAC,GAAQ,CAAI,CAC/B,CAAI,GAC3B,IAAO,EAAU,GAAG,EAAK,QAAQ,GAAO,CAAM,GAC9C,IAAQ,EAAK,SAAS,EAAK;CACjC,OACE,kBAAC,WAAD;EAAS,WAAW,EAAG,kDAAkD,CAAS;EAAG,aAAU;EAAY,WAAS,EAAK;EAAI,eAAa,IAAO,KAAK,KAAA;EAAW,MAAM,EAAK,YAAY,KAAA;EAAxL,UAAA;GACG,MAAW,KAAS,EAAK,SAAS,kBAAC,GAAD;IAAW,OAAO,EAAK;IAAO,MAAM;IAAsB;IAAW,WAAU;GAAW,CAAA;GAC7H,kBAAC,KAAD;IAAG,MAAM;IAAc;IAAS,WAAU;IAA1C,UAAA,CACE,kBAAC,QAAD;KAAM,WAAU;KAA0C,aAAU;KACjE,UAAA;IACG,CAAA,GACL,EAAK,MAAM,SAAS,KACnB,kBAAC,QAAD;KAAM,WAAU;KAAhB,UAAA;MACG,KAAa,kBAAC,GAAD;OAAc,SAAS,EAAK;OAAO,KAAI;OAAG,KAAI;OAAQ,UAAA;OAAS,SAAQ;OAAO,WAAW,EAAG,aAAa,KAAQ,SAAS;MAAI,CAAA;MAC3I,KAAa,kBAAC,GAAD;OAAmB;OAAgB;MAAW,CAAA;MAC3D,KACC,kBAAC,QAAD;OAAM,WAAU;OACd,UAAA,kBAAC,QAAD;QAAM,WAAU;QAAqG,aAAU;QAA/H,UAAA,CACE,kBAAC,GAAD;SAAe,MAAM;SAAU,aAAa;SAAG,WAAU;SAAW,eAAA;QAAa,CAAA,GAChF,CACG;;MACF,CAAA;KAEJ;IAEP,CAAA,CAAA;;GACH,kBAAC,KAAD;IAAG,WAAU;IAAb,UAAA;KACG,EAAK,gBAAgB,kBAAC,GAAD,EAAc,IAAI,EAAK,aAAe,CAAA;KAC3D,CAAC,EAAK,MAAM,UAAU,KAAQ,kBAAC,QAAD;MAAM,aAAU;MAAa,UAAA;KAAW,CAAA;KACtE,KAAc,EAAK,OAAO,cAAc,KAAA,KACvC,kBAAC,QAAD;MAAM,WAAU;MAA8C,aAAU;MAAxE,UAAA;OACE,kBAAC,GAAD;QAAe,MAAM;QAAe,aAAa;QAAG,WAAW,EAAG,YAAY,EAAK,QAAQ,aAAa,2BAA2B;QAAG,eAAA;OAAa,CAAA;OACnJ,kBAAC,QAAD;QAAM,WAAU;QAAW,UAAA,EAAE,EAAE,oBAAoB;OAAQ,CAAA;OAC1D,EAAQ,EAAK,OAAO,WAAW,EAAE,QAAQ;MACtC;;IAEP;;GACF;EACM;;AAEb"}
@@ -147,6 +147,7 @@ function C({ card: e, open: t, position: n, exposure: r, cardProps: a }) {
147
147
  ref: u,
148
148
  children: /* @__PURE__ */ h(l, {
149
149
  card: e,
150
+ client: a.client,
150
151
  href: a.href,
151
152
  shape: a.shape,
152
153
  aspectRatio: a.aspectRatio,
@@ -156,6 +157,7 @@ function C({ card: e, open: t, position: n, exposure: r, cardProps: a }) {
156
157
  chips: a.chips,
157
158
  filterEntity: a.filterEntity,
158
159
  entityStyle: a.entityStyle,
160
+ entityLayout: a.entityLayout,
159
161
  metadata: a.metadata,
160
162
  showCounts: a.showCounts,
161
163
  tiers: a.tiers,
@@ -1 +1 @@
1
- {"version":3,"file":"daily-releases.js","names":[],"sources":["../../../src/components/content/daily-releases.tsx"],"sourcesContent":["import { ArrowDown01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useCallback, useId, useMemo, useState } from \"react\";\nimport type { DailyReleasesQuery } from \"../../client/discovery.js\";\nimport type { Card, ReleaseDay } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useDailyReleases, useExposure } from \"../../react/discovery.js\";\nimport { useContentScope } from \"../../react/use-resource.js\";\nimport { useNearViewport } from \"../../react/viewport.js\";\nimport { ContentKitUiRoot } from \"../../scope.js\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Skeleton } from \"#ckui/ui/skeleton\";\nimport { contentError } from \"../social/parts.js\";\nimport { ContentCard } from \"./card.js\";\nimport { cardGridLayout, type CardListProps } from \"./grid.js\";\n\nexport interface DailyReleasesProps extends Omit<CardListProps, \"infinite\" | \"pageSize\" | \"renderCard\" | \"loadMedia\"> {\n /** One complete calendar window. Defaults to 10 days in the browser's time zone. */\n query?: DailyReleasesQuery;\n /** Cards initially revealed across the whole window, allocated newest first. Default 30. */\n initialVisible?: number;\n /** Additional cached cards revealed by a day's Show more button. Default 30. */\n revealStep?: number;\n}\n\nconst count = (value: number, fallback: number) => Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback;\n\n/**\n * One metadata request for a calendar window, with no requests when a day is\n * opened or expanded. Only revealed, visible cards mount images and previews.\n */\nexport function DailyReleases({ query, initialVisible = 30, revealStep = 30, ...p }: DailyReleasesProps) {\n const m = useMessages();\n const { scope } = useContentScope(p.client);\n const request = useMemo(() => ({ days: 10, time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone, ...query }), [query]);\n const releases = useDailyReleases(request, { client: p.client });\n const layout = cardGridLayout(p.grid);\n const days = useMemo(() => releases.data?.days.filter((day) => day.contents.length).sort((a, b) => b.date.localeCompare(a.date)) ?? [], [releases.data]);\n let remaining = count(initialVisible, 30);\n return (\n <ContentKitUiRoot appearance={p.appearance} className={cn(\"grid gap-6 text-sm\", p.className)} data-ckui=\"daily-releases\" aria-busy={releases.loading}>\n {releases.loading && !releases.data ? <div className={layout.className} style={layout.style}>{Array.from({ length: 6 }, (_, i) => <Skeleton key={i} className={p.aspectRatio === undefined ? p.shape === \"landscape\" ? \"aspect-video\" : \"aspect-[3/4]\" : undefined} style={p.aspectRatio === undefined ? undefined : { aspectRatio: p.aspectRatio }} />)}</div> : releases.error && !releases.data ? (\n <div role=\"alert\" className=\"grid justify-items-start gap-2\">\n <p className=\"text-destructive\">{contentError(m, releases.error)}</p>\n <Button size=\"sm\" variant=\"outline\" onClick={releases.reload}>{m.t(\"common.retry\")}</Button>\n </div>\n ) : !days.length ? <p role=\"status\" className=\"text-muted-foreground\">{p.empty ?? m.t(\"contents.empty\")}</p> : days.map((day) => {\n const initial = Math.min(remaining, day.contents.length);\n remaining -= initial;\n return <ReleaseDaySection key={JSON.stringify([scope, request, releases.data?.from_date, day.date])} day={day} initial={initial} step={Math.max(1, count(revealStep, 30))} cardProps={p} />;\n })}\n </ContentKitUiRoot>\n );\n}\n\nfunction ReleaseDaySection({ day, initial, step, cardProps: p }: { day: ReleaseDay; initial: number; step: number; cardProps: Omit<DailyReleasesProps, \"query\" | \"initialVisible\" | \"revealStep\"> }) {\n const m = useMessages();\n const id = useId();\n const [open, setOpen] = useState(initial > 0);\n const [visible, setVisible] = useState(initial);\n const cards = useMemo(() => day.contents.slice(0, visible), [day.contents, visible]);\n const exposed = useMemo(() => open ? cards : [], [open, cards]);\n const exposure = useExposure(exposed, { surface: p.surface ?? \"releases\", render: day.date, client: p.client });\n const layout = cardGridLayout(p.grid);\n // The server already assigned the calendar day in the requested time zone.\n const label = new Intl.DateTimeFormat(m.language, { dateStyle: \"full\", timeZone: \"UTC\" }).format(new Date(`${day.date}T12:00:00Z`));\n const toggle = () => {\n if (!open && !visible) setVisible(Math.min(step, day.contents.length));\n setOpen(!open);\n };\n return (\n <section data-ckui=\"release-day\" data-date={day.date}>\n <h2>\n <Button id={`${id}-heading`} type=\"button\" variant=\"ghost\" className=\"h-auto w-full justify-start gap-3 px-0 py-3 text-start text-lg font-semibold\" aria-expanded={open} aria-controls={`${id}-contents`} onClick={toggle}>\n <HugeiconsIcon icon={ArrowDown01Icon} strokeWidth={2} className={cn(\"size-5 shrink-0 transition-transform duration-200 motion-reduce:transition-none\", !open && \"-rotate-90\")} aria-hidden />\n <time dateTime={day.date}>{label}</time>\n <span className=\"ml-auto shrink-0 text-sm font-normal text-muted-foreground\">{m.plural(\"contents.items\", day.contents.length)}</span>\n </Button>\n </h2>\n <div id={`${id}-contents`} role=\"region\" aria-labelledby={`${id}-heading`} aria-hidden={!open} inert={!open} className={cn(\"grid transition-[grid-template-rows,opacity] duration-250 ease-out motion-reduce:transition-none\", open ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\")}>\n <div className=\"min-h-0 overflow-hidden\">\n <ol className={cn(layout.className, \"pt-2\")} style={layout.style}>\n {cards.map((card, i) => <ReleaseCard key={card.id} card={card} open={open} position={i + 1} exposure={exposure} cardProps={p} />)}\n </ol>\n {visible < day.contents.length && <div className=\"flex justify-center pt-5 pb-1\"><Button type=\"button\" variant=\"outline\" onClick={() => setVisible((n) => Math.min(n + step, day.contents.length))}>{m.t(\"contents.showMore\")}</Button></div>}\n </div>\n </div>\n </section>\n );\n}\n\nfunction ReleaseCard({ card, open, position, exposure, cardProps: p }: { card: Card; open: boolean; position: number; exposure: ReturnType<typeof useExposure>; cardProps: Omit<DailyReleasesProps, \"query\" | \"initialVisible\" | \"revealStep\"> }) {\n const [viewport, visible] = useNearViewport({ margin: \"0px\" });\n const tracked = useMemo(() => exposure.track(card.id, position), [exposure, card.id, position]);\n const ref = useCallback((element: HTMLLIElement | null) => { viewport(element); tracked(element); }, [viewport, tracked]);\n return <li ref={ref}>\n <ContentCard card={card} href={p.href} shape={p.shape} aspectRatio={p.aspectRatio} titlePosition={p.titlePosition} loadMedia={open && visible} groupKinds={p.groupKinds} chips={p.chips} filterEntity={p.filterEntity} entityStyle={p.entityStyle} metadata={p.metadata} showCounts={p.showCounts} tiers={p.tiers} labels={p.labels} owner={p.owner} ownerHref={p.ownerHref} decorate={p.decorate} onOpen={() => exposure.click(card.id, position)} />\n </li>;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"daily-releases.js","names":[],"sources":["../../../src/components/content/daily-releases.tsx"],"sourcesContent":["import { ArrowDown01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useCallback, useId, useMemo, useState } from \"react\";\nimport type { DailyReleasesQuery } from \"../../client/discovery.js\";\nimport type { Card, ReleaseDay } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useDailyReleases, useExposure } from \"../../react/discovery.js\";\nimport { useContentScope } from \"../../react/use-resource.js\";\nimport { useNearViewport } from \"../../react/viewport.js\";\nimport { ContentKitUiRoot } from \"../../scope.js\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Skeleton } from \"#ckui/ui/skeleton\";\nimport { contentError } from \"../social/parts.js\";\nimport { ContentCard } from \"./card.js\";\nimport { cardGridLayout, type CardListProps } from \"./grid.js\";\n\nexport interface DailyReleasesProps extends Omit<CardListProps, \"infinite\" | \"pageSize\" | \"renderCard\" | \"loadMedia\"> {\n /** One complete calendar window. Defaults to 10 days in the browser's time zone. */\n query?: DailyReleasesQuery;\n /** Cards initially revealed across the whole window, allocated newest first. Default 30. */\n initialVisible?: number;\n /** Additional cached cards revealed by a day's Show more button. Default 30. */\n revealStep?: number;\n}\n\nconst count = (value: number, fallback: number) => Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback;\n\n/**\n * One metadata request for a calendar window, with no requests when a day is\n * opened or expanded. Only revealed, visible cards mount images and previews.\n */\nexport function DailyReleases({ query, initialVisible = 30, revealStep = 30, ...p }: DailyReleasesProps) {\n const m = useMessages();\n const { scope } = useContentScope(p.client);\n const request = useMemo(() => ({ days: 10, time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone, ...query }), [query]);\n const releases = useDailyReleases(request, { client: p.client });\n const layout = cardGridLayout(p.grid);\n const days = useMemo(() => releases.data?.days.filter((day) => day.contents.length).sort((a, b) => b.date.localeCompare(a.date)) ?? [], [releases.data]);\n let remaining = count(initialVisible, 30);\n return (\n <ContentKitUiRoot appearance={p.appearance} className={cn(\"grid gap-6 text-sm\", p.className)} data-ckui=\"daily-releases\" aria-busy={releases.loading}>\n {releases.loading && !releases.data ? <div className={layout.className} style={layout.style}>{Array.from({ length: 6 }, (_, i) => <Skeleton key={i} className={p.aspectRatio === undefined ? p.shape === \"landscape\" ? \"aspect-video\" : \"aspect-[3/4]\" : undefined} style={p.aspectRatio === undefined ? undefined : { aspectRatio: p.aspectRatio }} />)}</div> : releases.error && !releases.data ? (\n <div role=\"alert\" className=\"grid justify-items-start gap-2\">\n <p className=\"text-destructive\">{contentError(m, releases.error)}</p>\n <Button size=\"sm\" variant=\"outline\" onClick={releases.reload}>{m.t(\"common.retry\")}</Button>\n </div>\n ) : !days.length ? <p role=\"status\" className=\"text-muted-foreground\">{p.empty ?? m.t(\"contents.empty\")}</p> : days.map((day) => {\n const initial = Math.min(remaining, day.contents.length);\n remaining -= initial;\n return <ReleaseDaySection key={JSON.stringify([scope, request, releases.data?.from_date, day.date])} day={day} initial={initial} step={Math.max(1, count(revealStep, 30))} cardProps={p} />;\n })}\n </ContentKitUiRoot>\n );\n}\n\nfunction ReleaseDaySection({ day, initial, step, cardProps: p }: { day: ReleaseDay; initial: number; step: number; cardProps: Omit<DailyReleasesProps, \"query\" | \"initialVisible\" | \"revealStep\"> }) {\n const m = useMessages();\n const id = useId();\n const [open, setOpen] = useState(initial > 0);\n const [visible, setVisible] = useState(initial);\n const cards = useMemo(() => day.contents.slice(0, visible), [day.contents, visible]);\n const exposed = useMemo(() => open ? cards : [], [open, cards]);\n const exposure = useExposure(exposed, { surface: p.surface ?? \"releases\", render: day.date, client: p.client });\n const layout = cardGridLayout(p.grid);\n // The server already assigned the calendar day in the requested time zone.\n const label = new Intl.DateTimeFormat(m.language, { dateStyle: \"full\", timeZone: \"UTC\" }).format(new Date(`${day.date}T12:00:00Z`));\n const toggle = () => {\n if (!open && !visible) setVisible(Math.min(step, day.contents.length));\n setOpen(!open);\n };\n return (\n <section data-ckui=\"release-day\" data-date={day.date}>\n <h2>\n <Button id={`${id}-heading`} type=\"button\" variant=\"ghost\" className=\"h-auto w-full justify-start gap-3 px-0 py-3 text-start text-lg font-semibold\" aria-expanded={open} aria-controls={`${id}-contents`} onClick={toggle}>\n <HugeiconsIcon icon={ArrowDown01Icon} strokeWidth={2} className={cn(\"size-5 shrink-0 transition-transform duration-200 motion-reduce:transition-none\", !open && \"-rotate-90\")} aria-hidden />\n <time dateTime={day.date}>{label}</time>\n <span className=\"ml-auto shrink-0 text-sm font-normal text-muted-foreground\">{m.plural(\"contents.items\", day.contents.length)}</span>\n </Button>\n </h2>\n <div id={`${id}-contents`} role=\"region\" aria-labelledby={`${id}-heading`} aria-hidden={!open} inert={!open} className={cn(\"grid transition-[grid-template-rows,opacity] duration-250 ease-out motion-reduce:transition-none\", open ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\")}>\n <div className=\"min-h-0 overflow-hidden\">\n <ol className={cn(layout.className, \"pt-2\")} style={layout.style}>\n {cards.map((card, i) => <ReleaseCard key={card.id} card={card} open={open} position={i + 1} exposure={exposure} cardProps={p} />)}\n </ol>\n {visible < day.contents.length && <div className=\"flex justify-center pt-5 pb-1\"><Button type=\"button\" variant=\"outline\" onClick={() => setVisible((n) => Math.min(n + step, day.contents.length))}>{m.t(\"contents.showMore\")}</Button></div>}\n </div>\n </div>\n </section>\n );\n}\n\nfunction ReleaseCard({ card, open, position, exposure, cardProps: p }: { card: Card; open: boolean; position: number; exposure: ReturnType<typeof useExposure>; cardProps: Omit<DailyReleasesProps, \"query\" | \"initialVisible\" | \"revealStep\"> }) {\n const [viewport, visible] = useNearViewport({ margin: \"0px\" });\n const tracked = useMemo(() => exposure.track(card.id, position), [exposure, card.id, position]);\n const ref = useCallback((element: HTMLLIElement | null) => { viewport(element); tracked(element); }, [viewport, tracked]);\n return <li ref={ref}>\n <ContentCard card={card} client={p.client} href={p.href} shape={p.shape} aspectRatio={p.aspectRatio} titlePosition={p.titlePosition} loadMedia={open && visible} groupKinds={p.groupKinds} chips={p.chips} filterEntity={p.filterEntity} entityStyle={p.entityStyle} entityLayout={p.entityLayout} metadata={p.metadata} showCounts={p.showCounts} tiers={p.tiers} labels={p.labels} owner={p.owner} ownerHref={p.ownerHref} decorate={p.decorate} onOpen={() => exposure.click(card.id, position)} />\n </li>;\n}\n"],"mappings":"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"}
@@ -18,12 +18,14 @@ export interface CardListProps extends Omit<ContentCardProps, "card" | "onOpen"
18
18
  client?: ContentKitClient;
19
19
  className?: string;
20
20
  appearance?: ContentKitUiAppearance;
21
- /** Opt into a fluid grid with no desktop column ceiling. */
21
+ /** Opt into a fluid grid, optionally with a desktop column ceiling. */
22
22
  grid?: CardGridLayout;
23
23
  }
24
24
  export interface CardGridLayout {
25
25
  /** Minimum desktop card width in CSS pixels. Cards grow to fill available space. */
26
26
  minCardWidth: number;
27
+ /** Maximum desktop columns. Omit to use all available width without a ceiling. */
28
+ maxColumns?: number;
27
29
  /** Equal-width columns below the desktop breakpoint. Default 2. */
28
30
  mobileColumns?: number;
29
31
  }
@@ -15,11 +15,11 @@ import { cn as w } from "cn";
15
15
  var T = "grid grid-cols-[repeat(auto-fill,minmax(min(100%,10rem),1fr))] gap-x-4 gap-y-6";
16
16
  function E(e) {
17
17
  if (!e) return { className: T };
18
- let t = Number.isFinite(e.minCardWidth) ? Math.max(1, e.minCardWidth) : 160, n = e.mobileColumns !== void 0 && Number.isFinite(e.mobileColumns) ? Math.max(1, Math.floor(e.mobileColumns)) : 2;
18
+ let t = Number.isFinite(e.minCardWidth) ? Math.max(1, e.minCardWidth) : 160, n = e.mobileColumns !== void 0 && Number.isFinite(e.mobileColumns) ? Math.max(1, Math.floor(e.mobileColumns)) : 2, r = e.maxColumns !== void 0 && Number.isFinite(e.maxColumns) ? Math.max(1, Math.floor(e.maxColumns)) : void 0;
19
19
  return {
20
20
  className: "grid grid-cols-[repeat(var(--ckui-card-mobile-columns),minmax(0,1fr))] gap-x-4 gap-y-6 md:grid-cols-[repeat(auto-fill,minmax(min(100%,var(--ckui-card-min-width)),1fr))]",
21
21
  style: {
22
- "--ckui-card-min-width": `${t}px`,
22
+ "--ckui-card-min-width": r === void 0 ? `${t}px` : `max(${t}px, calc((100% - ${r - 1}rem) / ${r}))`,
23
23
  "--ckui-card-mobile-columns": n
24
24
  }
25
25
  };
@@ -74,6 +74,7 @@ function D({ list: n, render: c, ...l }) {
74
74
  ref: m.track(e.id, t + 1),
75
75
  children: l.renderCard ? l.renderCard(e, t + 1) : /* @__PURE__ */ S(g, {
76
76
  card: e,
77
+ client: l.client,
77
78
  href: l.href,
78
79
  shape: l.shape,
79
80
  aspectRatio: l.aspectRatio,
@@ -83,6 +84,7 @@ function D({ list: n, render: c, ...l }) {
83
84
  chips: l.chips,
84
85
  filterEntity: l.filterEntity,
85
86
  entityStyle: l.entityStyle,
87
+ entityLayout: l.entityLayout,
86
88
  metadata: l.metadata,
87
89
  showCounts: l.showCounts,
88
90
  tiers: T,
@@ -206,6 +208,7 @@ function M({ query: t = {}, ...r }) {
206
208
  render: JSON.stringify(s),
207
209
  renderCard: r.renderCard ?? ((e) => /* @__PURE__ */ S(g, {
208
210
  card: e,
211
+ client: r.client,
209
212
  href: r.href,
210
213
  shape: r.shape,
211
214
  aspectRatio: r.aspectRatio,
@@ -215,6 +218,7 @@ function M({ query: t = {}, ...r }) {
215
218
  chips: r.chips,
216
219
  filterEntity: r.filterEntity,
217
220
  entityStyle: r.entityStyle,
221
+ entityLayout: r.entityLayout,
218
222
  metadata: r.metadata,
219
223
  showCounts: r.showCounts,
220
224
  tiers: r.tiers,
@@ -292,6 +296,7 @@ function N({ title: e, similar: n, recommendations: i, history: a, group: s, lis
292
296
  ref: e ? N.track(e.id, t + 1) : void 0,
293
297
  children: e ? _.renderCard ? _.renderCard(e, t + 1) : /* @__PURE__ */ S(g, {
294
298
  card: e,
299
+ client: _.client,
295
300
  href: _.href,
296
301
  shape: _.shape,
297
302
  aspectRatio: _.aspectRatio,
@@ -301,6 +306,7 @@ function N({ title: e, similar: n, recommendations: i, history: a, group: s, lis
301
306
  chips: _.chips,
302
307
  filterEntity: _.filterEntity,
303
308
  entityStyle: _.entityStyle,
309
+ entityLayout: _.entityLayout,
304
310
  metadata: _.metadata,
305
311
  showCounts: _.showCounts,
306
312
  tiers: _.tiers ?? j?.tiers,
@@ -1 +1 @@
1
- {"version":3,"file":"grid.js","names":[],"sources":["../../../src/components/content/grid.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useContext, useEffect, useMemo, useState, type CSSProperties, type ReactNode } from \"react\";\nimport type { ContentKitUiAppearance } from \"../../appearance.js\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport type { ContentQuery, HistoryQuery, RailQuery, SearchQuery } from \"../../client/discovery.js\";\nimport type { Card } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { ContentKitContext } from \"../../react/context.js\";\nimport { useContents, useExposure, useHistory, useLockedCount, useRecommendations, useSearch, useSimilar, type UseRail } from \"../../react/discovery.js\";\nimport { useKinds } from \"../../react/groups.js\";\nimport type { UseList } from \"../../react/use-resource.js\";\nimport { useNearViewport } from \"../../react/viewport.js\";\nimport { ContentKitUiRoot } from \"../../scope.js\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Skeleton } from \"#ckui/ui/skeleton\";\nimport { contentError, useAction } from \"../social/parts.js\";\nimport { ContentCard, type ContentCardProps } from \"./card.js\";\n\nexport interface CardListProps extends Omit<ContentCardProps, \"card\" | \"onOpen\" | \"children\" | \"className\"> {\n /** Where the list is, for exposures and click attribution: \"home\", \"search\", \"similar\". */\n surface?: string;\n /** Loads the next page as the end nears, instead of a button. */\n infinite?: boolean;\n /** A card's entry instead of ContentCard. */\n renderCard?: (card: Card, position: number) => ReactNode;\n /** Shown when nothing matches. */\n empty?: ReactNode;\n pageSize?: number;\n client?: ContentKitClient;\n className?: string;\n appearance?: ContentKitUiAppearance;\n /** Opt into a fluid grid with no desktop column ceiling. */\n grid?: CardGridLayout;\n}\n\nconst GRID = \"grid grid-cols-[repeat(auto-fill,minmax(min(100%,10rem),1fr))] gap-x-4 gap-y-6\";\n\nexport interface CardGridLayout {\n /** Minimum desktop card width in CSS pixels. Cards grow to fill available space. */\n minCardWidth: number;\n /** Equal-width columns below the desktop breakpoint. Default 2. */\n mobileColumns?: number;\n}\n\n/** Shared by complete lists, release-day sections and grid-style previews. */\nexport function cardGridLayout(layout?: CardGridLayout): { className: string; style?: CSSProperties } {\n if (!layout) return { className: GRID };\n const width = Number.isFinite(layout.minCardWidth) ? Math.max(1, layout.minCardWidth) : 160;\n const columns = layout.mobileColumns !== undefined && Number.isFinite(layout.mobileColumns) ? Math.max(1, Math.floor(layout.mobileColumns)) : 2;\n return {\n className: \"grid grid-cols-[repeat(var(--ckui-card-mobile-columns),minmax(0,1fr))] gap-x-4 gap-y-6 md:grid-cols-[repeat(auto-fill,minmax(min(100%,var(--ckui-card-min-width)),1fr))]\",\n style: { \"--ckui-card-min-width\": `${width}px`, \"--ckui-card-mobile-columns\": columns } as CSSProperties,\n };\n}\n\n/** A list's cards in a responsive grid, paged, with exposures and click signals. */\nexport function CardGrid({ list, render, ...p }: CardListProps & { list: UseList<Card>; render: string }) {\n const m = useMessages();\n const { t } = m;\n const { kinds } = useKinds({ client: p.client });\n const exposure = useExposure(list.items, { surface: p.surface ?? \"list\", render, client: p.client });\n const [sentinel, near] = useNearViewport({ margin: \"600px 0px\", leaveMargin: \"1200px 0px\" });\n const { hasMore, loadingMore, loadMore } = list;\n useEffect(() => {\n if (p.infinite && near && hasMore && !loadingMore) void loadMore();\n }, [p.infinite, near, hasMore, loadingMore, loadMore]);\n const tiers = p.tiers ?? kinds?.tiers;\n const layout = cardGridLayout(p.grid);\n return (\n <ContentKitUiRoot appearance={p.appearance} className={cn(\"grid gap-4 text-sm\", p.className)} data-ckui=\"content-grid\" aria-busy={list.loading}>\n {list.loading && !list.items.length ? (\n <div className={layout.className} style={layout.style}>\n {Array.from({ length: 8 }, (_, i) => (\n <Skeleton key={i} className={p.aspectRatio === undefined ? p.shape === \"landscape\" ? \"aspect-video w-full\" : \"aspect-[3/4] w-full\" : \"w-full\"} style={p.aspectRatio === undefined ? undefined : { aspectRatio: p.aspectRatio }} />\n ))}\n </div>\n ) : list.error && !list.items.length ? (\n <div role=\"alert\" className=\"grid justify-items-start gap-2\">\n <p className=\"text-destructive\">{contentError(m, list.error)}</p>\n <Button size=\"sm\" variant=\"outline\" onClick={list.reload}>\n {t(\"common.retry\")}\n </Button>\n </div>\n ) : !list.items.length ? (\n <div className=\"text-muted-foreground\" role=\"status\">\n {p.empty ?? t(\"contents.empty\")}\n </div>\n ) : (\n <ol className={layout.className} style={layout.style}>\n {list.items.map((card, i) => (\n <li key={card.id} ref={exposure.track(card.id, i + 1)}>\n {p.renderCard ? (\n p.renderCard(card, i + 1)\n ) : (\n <ContentCard\n card={card}\n href={p.href}\n shape={p.shape}\n aspectRatio={p.aspectRatio}\n titlePosition={p.titlePosition}\n loadMedia={p.loadMedia}\n groupKinds={p.groupKinds}\n chips={p.chips}\n filterEntity={p.filterEntity}\n entityStyle={p.entityStyle}\n metadata={p.metadata}\n showCounts={p.showCounts}\n tiers={tiers}\n labels={p.labels}\n owner={p.owner}\n ownerHref={p.ownerHref}\n decorate={p.decorate}\n onOpen={() => exposure.click(card.id, i + 1)}\n />\n )}\n </li>\n ))}\n </ol>\n )}\n {list.hasMore && (\n <div ref={sentinel} className=\"grid justify-items-center\">\n <Button variant=\"outline\" size=\"sm\" disabled={list.loadingMore} onClick={() => void list.loadMore()}>\n {list.loadingMore ? t(\"contents.loading\") : t(\"contents.loadMore\")}\n </Button>\n </div>\n )}\n </ContentKitUiRoot>\n );\n}\n\n/**\n * What a list does with cards the reader doesn't open: shows them marked\n * (show), leaves them out (hide), or lists them apart, counted, behind a\n * toggle that loads them when opened (collapse).\n */\nexport type LockedCards = \"show\" | \"hide\" | \"collapse\";\n\nconst LOCKED_LEVELS = [\"tier\", \"group\", \"ppv\", \"group_ppv\"] as const;\n\n/** Contents as cards (GET /v1/contents): any sort and filter, paged. */\nexport function ContentGrid({ query, locked = \"show\", ...p }: CardListProps & { query: ContentQuery; locked?: LockedCards }) {\n const q = useMemo<ContentQuery>(\n () => ({ ...query, limit: query.limit ?? p.pageSize, ...(locked === \"show\" ? {} : { mode: \"hide\" }) }),\n [query, p.pageSize, locked],\n );\n const list = useContents(q, { client: p.client });\n return (\n <>\n {locked === \"collapse\" && <LockedSection {...p} query={query} />}\n <CardGrid {...p} list={list} render={JSON.stringify(q)} />\n </>\n );\n}\n\n/** A list's locked cards, counted, loaded and shown on opening. */\nfunction LockedSection({ query, ...p }: CardListProps & { query: ContentQuery }) {\n const m = useMessages();\n const [open, setOpen] = useState(false);\n const { count } = useLockedCount(query, { client: p.client });\n const q = useMemo<ContentQuery | null>(\n () => (open ? { ...query, mode: \"lock\", access: query.access ?? LOCKED_LEVELS, limit: query.limit ?? p.pageSize } : null),\n [open, query, p.pageSize],\n );\n const list = useContents(q, { client: p.client });\n const items = useMemo(() => list.items.filter((c) => !c.access.allowed), [list.items]);\n if (!count) return null;\n return (\n <ContentKitUiRoot appearance={p.appearance} className=\"grid gap-3 text-sm\" data-ckui=\"locked-cards\">\n <Button variant=\"outline\" size=\"sm\" className=\"justify-self-start\" aria-expanded={open} onClick={() => setOpen(!open)}>\n {open ? m.t(\"contents.lockedHide\") : m.plural(\"contents.lockedShow\", count)}\n </Button>\n {open && <CardGrid {...p} list={{ ...list, items }} render={JSON.stringify(q)} />}\n </ContentKitUiRoot>\n );\n}\n\n/** Search results as cards; nothing until the query has text. */\nexport function SearchResults({ query, ...p }: CardListProps & { query: SearchQuery }) {\n const { t } = useMessages();\n const q = useMemo(() => ({ ...query, limit: query.limit ?? p.pageSize }), [query, p.pageSize]);\n const list = useSearch(q, { client: p.client });\n if (!query.q.trim()) return null;\n return <CardGrid surface=\"search\" empty={t(\"contents.noResults\", { q: query.q.trim() })} {...p} list={list} render={JSON.stringify(q)} />;\n}\n\n/** The signed-in reader's history as cards, each removable, with a clear-all. */\nexport function ContentHistory({ query = {}, ...p }: CardListProps & { query?: HistoryQuery }) {\n const { t } = useMessages();\n const ctx = useContext(ContentKitContext);\n const q = useMemo(() => ({ ...query, limit: query.limit ?? p.pageSize }), [query, p.pageSize]);\n const list = useHistory(q, { client: p.client });\n const forget = useAction(\"history.forget\");\n if (ctx?.viewer === null) return <p className=\"text-muted-foreground\">{t(\"contents.historySignedOut\")}</p>;\n return (\n <div className=\"grid gap-3\" data-ckui=\"content-history\">\n {list.items.length > 0 && (\n <Button size=\"sm\" variant=\"outline\" className=\"justify-self-end\" disabled={forget.pending} onClick={() => void forget.run(() => list.forget(query.kind ? { kind: query.kind } : {}))}>\n {t(\"contents.clearHistory\")}\n </Button>\n )}\n <CardGrid\n surface=\"history\"\n empty={t(\"contents.historyEmpty\")}\n {...p}\n list={list}\n render={JSON.stringify(q)}\n renderCard={\n p.renderCard ??\n ((card) => (\n <ContentCard\n card={card}\n href={p.href}\n shape={p.shape}\n aspectRatio={p.aspectRatio}\n titlePosition={p.titlePosition}\n loadMedia={p.loadMedia}\n groupKinds={p.groupKinds}\n chips={p.chips}\n filterEntity={p.filterEntity}\n entityStyle={p.entityStyle}\n metadata={p.metadata}\n showCounts={p.showCounts}\n tiers={p.tiers}\n labels={p.labels}\n owner={p.owner}\n ownerHref={p.ownerHref}\n decorate={p.decorate}\n >\n <Button size=\"xs\" variant=\"ghost\" className=\"justify-self-start\" disabled={forget.pending} onClick={() => void forget.run(() => list.forget({ content_id: card.id }))}>\n {t(\"contents.removeFromHistory\")}\n </Button>\n </ContentCard>\n ))\n }\n />\n </div>\n );\n}\n\nexport interface ContentRailProps extends Omit<CardListProps, \"infinite\" | \"empty\"> {\n /** The rail's heading. */\n title: ReactNode;\n /** Contents like this one. */\n similar?: string;\n /** For the signed-in reader. */\n recommendations?: boolean;\n /** The reader's history: { status: \"in_progress\" } is a continue-watching rail. */\n history?: HistoryQuery;\n /** An ordered group's members in its order: a featured rail (its windows apply). */\n group?: string;\n /** Any list: a sort and filters. */\n list?: ContentQuery;\n /** Filters and reader options of a similar or recommended rail. */\n query?: RailQuery;\n /** A link beside the heading, e.g. to the whole list. */\n more?: ReactNode;\n}\n\n/**\n * A horizontal row of contents: similar, recommended, the reader's history,\n * an ordered group's members or any list. It hides itself when it has none or\n * the site runs without what it needs.\n */\nexport function ContentRail({ title, similar, recommendations, history, group, list: listQuery, query = {}, more, ...p }: ContentRailProps) {\n const limit = query.limit ?? p.pageSize ?? 12;\n const q = useMemo(() => ({ ...query, limit }), [query, limit]);\n const listing = useMemo(\n () => (listQuery ? { ...listQuery, limit: listQuery.limit ?? limit } : group ? { groups: [group], sort: \"position\" as const, limit } : null),\n [listQuery, group, limit],\n );\n const like = useSimilar(recommendations || history || listing ? null : similar, q, { client: p.client });\n const mine = useRecommendations(recommendations ? q : null, { client: p.client });\n const seen = useHistory(history ? { ...history, limit: history.limit ?? limit } : null, { client: p.client });\n const listed = useContents(listing, { client: p.client });\n const rail: UseRail = recommendations\n ? mine\n : history || listing\n ? (() => {\n const l = history ? seen : listed;\n return { items: l.items, loading: l.loading, error: l.error, unavailable: l.error?.code === \"not_configured\", reload: l.reload };\n })()\n : like;\n const { kinds } = useKinds({ client: p.client });\n const surface = p.surface ?? (recommendations ? \"recommendations\" : history ? \"history\" : group ? \"featured\" : listing ? \"list\" : \"similar\");\n const exposure = useExposure(rail.items, { surface, render: JSON.stringify([similar, recommendations, history, group, listing, q]), client: p.client });\n const source = similar || recommendations || history || group || listing;\n const layout = p.grid ? cardGridLayout(p.grid) : undefined;\n if (!source || rail.unavailable || (!rail.loading && !rail.items.length)) return null;\n return (\n <ContentKitUiRoot appearance={p.appearance} className={cn(\"grid gap-3 text-sm\", p.className)} data-ckui=\"content-rail\" data-surface={surface}>\n <div className=\"flex items-baseline justify-between gap-2\">\n <h2 className=\"text-base font-semibold\">{title}</h2>\n {more}\n </div>\n <ol className={layout?.className ?? \"flex snap-x gap-4 overflow-x-auto pb-2\"} style={layout?.style}>\n {(rail.loading && !rail.items.length ? Array.from({ length: 6 }, () => null) : rail.items).map((card, i) => (\n <li key={card?.id ?? i} className={layout ? undefined : \"w-36 shrink-0 snap-start sm:w-44\"} ref={card ? exposure.track(card.id, i + 1) : undefined}>\n {!card ? (\n <Skeleton className={p.aspectRatio === undefined ? p.shape === \"landscape\" ? \"aspect-video w-full\" : \"aspect-[3/4] w-full\" : \"w-full\"} style={p.aspectRatio === undefined ? undefined : { aspectRatio: p.aspectRatio }} />\n ) : p.renderCard ? (\n p.renderCard(card, i + 1)\n ) : (\n <ContentCard\n card={card}\n href={p.href}\n shape={p.shape}\n aspectRatio={p.aspectRatio}\n titlePosition={p.titlePosition}\n loadMedia={p.loadMedia}\n groupKinds={p.groupKinds}\n chips={p.chips}\n filterEntity={p.filterEntity}\n entityStyle={p.entityStyle}\n metadata={p.metadata}\n showCounts={p.showCounts}\n tiers={p.tiers ?? kinds?.tiers}\n labels={p.labels}\n owner={p.owner}\n ownerHref={p.ownerHref}\n decorate={p.decorate}\n onOpen={() => exposure.click(card.id, i + 1)}\n />\n )}\n </li>\n ))}\n </ol>\n </ContentKitUiRoot>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"grid.js","names":[],"sources":["../../../src/components/content/grid.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useContext, useEffect, useMemo, useState, type CSSProperties, type ReactNode } from \"react\";\nimport type { ContentKitUiAppearance } from \"../../appearance.js\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport type { ContentQuery, HistoryQuery, RailQuery, SearchQuery } from \"../../client/discovery.js\";\nimport type { Card } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { ContentKitContext } from \"../../react/context.js\";\nimport { useContents, useExposure, useHistory, useLockedCount, useRecommendations, useSearch, useSimilar, type UseRail } from \"../../react/discovery.js\";\nimport { useKinds } from \"../../react/groups.js\";\nimport type { UseList } from \"../../react/use-resource.js\";\nimport { useNearViewport } from \"../../react/viewport.js\";\nimport { ContentKitUiRoot } from \"../../scope.js\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Skeleton } from \"#ckui/ui/skeleton\";\nimport { contentError, useAction } from \"../social/parts.js\";\nimport { ContentCard, type ContentCardProps } from \"./card.js\";\n\nexport interface CardListProps extends Omit<ContentCardProps, \"card\" | \"onOpen\" | \"children\" | \"className\"> {\n /** Where the list is, for exposures and click attribution: \"home\", \"search\", \"similar\". */\n surface?: string;\n /** Loads the next page as the end nears, instead of a button. */\n infinite?: boolean;\n /** A card's entry instead of ContentCard. */\n renderCard?: (card: Card, position: number) => ReactNode;\n /** Shown when nothing matches. */\n empty?: ReactNode;\n pageSize?: number;\n client?: ContentKitClient;\n className?: string;\n appearance?: ContentKitUiAppearance;\n /** Opt into a fluid grid, optionally with a desktop column ceiling. */\n grid?: CardGridLayout;\n}\n\nconst GRID = \"grid grid-cols-[repeat(auto-fill,minmax(min(100%,10rem),1fr))] gap-x-4 gap-y-6\";\n\nexport interface CardGridLayout {\n /** Minimum desktop card width in CSS pixels. Cards grow to fill available space. */\n minCardWidth: number;\n /** Maximum desktop columns. Omit to use all available width without a ceiling. */\n maxColumns?: number;\n /** Equal-width columns below the desktop breakpoint. Default 2. */\n mobileColumns?: number;\n}\n\n/** Shared by complete lists, release-day sections and grid-style previews. */\nexport function cardGridLayout(layout?: CardGridLayout): { className: string; style?: CSSProperties } {\n if (!layout) return { className: GRID };\n const width = Number.isFinite(layout.minCardWidth) ? Math.max(1, layout.minCardWidth) : 160;\n const columns = layout.mobileColumns !== undefined && Number.isFinite(layout.mobileColumns) ? Math.max(1, Math.floor(layout.mobileColumns)) : 2;\n const maximum = layout.maxColumns !== undefined && Number.isFinite(layout.maxColumns) ? Math.max(1, Math.floor(layout.maxColumns)) : undefined;\n const trackWidth = maximum === undefined ? `${width}px` : `max(${width}px, calc((100% - ${maximum - 1}rem) / ${maximum}))`;\n return {\n className: \"grid grid-cols-[repeat(var(--ckui-card-mobile-columns),minmax(0,1fr))] gap-x-4 gap-y-6 md:grid-cols-[repeat(auto-fill,minmax(min(100%,var(--ckui-card-min-width)),1fr))]\",\n style: { \"--ckui-card-min-width\": trackWidth, \"--ckui-card-mobile-columns\": columns } as CSSProperties,\n };\n}\n\n/** A list's cards in a responsive grid, paged, with exposures and click signals. */\nexport function CardGrid({ list, render, ...p }: CardListProps & { list: UseList<Card>; render: string }) {\n const m = useMessages();\n const { t } = m;\n const { kinds } = useKinds({ client: p.client });\n const exposure = useExposure(list.items, { surface: p.surface ?? \"list\", render, client: p.client });\n const [sentinel, near] = useNearViewport({ margin: \"600px 0px\", leaveMargin: \"1200px 0px\" });\n const { hasMore, loadingMore, loadMore } = list;\n useEffect(() => {\n if (p.infinite && near && hasMore && !loadingMore) void loadMore();\n }, [p.infinite, near, hasMore, loadingMore, loadMore]);\n const tiers = p.tiers ?? kinds?.tiers;\n const layout = cardGridLayout(p.grid);\n return (\n <ContentKitUiRoot appearance={p.appearance} className={cn(\"grid gap-4 text-sm\", p.className)} data-ckui=\"content-grid\" aria-busy={list.loading}>\n {list.loading && !list.items.length ? (\n <div className={layout.className} style={layout.style}>\n {Array.from({ length: 8 }, (_, i) => (\n <Skeleton key={i} className={p.aspectRatio === undefined ? p.shape === \"landscape\" ? \"aspect-video w-full\" : \"aspect-[3/4] w-full\" : \"w-full\"} style={p.aspectRatio === undefined ? undefined : { aspectRatio: p.aspectRatio }} />\n ))}\n </div>\n ) : list.error && !list.items.length ? (\n <div role=\"alert\" className=\"grid justify-items-start gap-2\">\n <p className=\"text-destructive\">{contentError(m, list.error)}</p>\n <Button size=\"sm\" variant=\"outline\" onClick={list.reload}>\n {t(\"common.retry\")}\n </Button>\n </div>\n ) : !list.items.length ? (\n <div className=\"text-muted-foreground\" role=\"status\">\n {p.empty ?? t(\"contents.empty\")}\n </div>\n ) : (\n <ol className={layout.className} style={layout.style}>\n {list.items.map((card, i) => (\n <li key={card.id} ref={exposure.track(card.id, i + 1)}>\n {p.renderCard ? (\n p.renderCard(card, i + 1)\n ) : (\n <ContentCard\n card={card}\n client={p.client}\n href={p.href}\n shape={p.shape}\n aspectRatio={p.aspectRatio}\n titlePosition={p.titlePosition}\n loadMedia={p.loadMedia}\n groupKinds={p.groupKinds}\n chips={p.chips}\n filterEntity={p.filterEntity}\n entityStyle={p.entityStyle}\n entityLayout={p.entityLayout}\n metadata={p.metadata}\n showCounts={p.showCounts}\n tiers={tiers}\n labels={p.labels}\n owner={p.owner}\n ownerHref={p.ownerHref}\n decorate={p.decorate}\n onOpen={() => exposure.click(card.id, i + 1)}\n />\n )}\n </li>\n ))}\n </ol>\n )}\n {list.hasMore && (\n <div ref={sentinel} className=\"grid justify-items-center\">\n <Button variant=\"outline\" size=\"sm\" disabled={list.loadingMore} onClick={() => void list.loadMore()}>\n {list.loadingMore ? t(\"contents.loading\") : t(\"contents.loadMore\")}\n </Button>\n </div>\n )}\n </ContentKitUiRoot>\n );\n}\n\n/**\n * What a list does with cards the reader doesn't open: shows them marked\n * (show), leaves them out (hide), or lists them apart, counted, behind a\n * toggle that loads them when opened (collapse).\n */\nexport type LockedCards = \"show\" | \"hide\" | \"collapse\";\n\nconst LOCKED_LEVELS = [\"tier\", \"group\", \"ppv\", \"group_ppv\"] as const;\n\n/** Contents as cards (GET /v1/contents): any sort and filter, paged. */\nexport function ContentGrid({ query, locked = \"show\", ...p }: CardListProps & { query: ContentQuery; locked?: LockedCards }) {\n const q = useMemo<ContentQuery>(\n () => ({ ...query, limit: query.limit ?? p.pageSize, ...(locked === \"show\" ? {} : { mode: \"hide\" }) }),\n [query, p.pageSize, locked],\n );\n const list = useContents(q, { client: p.client });\n return (\n <>\n {locked === \"collapse\" && <LockedSection {...p} query={query} />}\n <CardGrid {...p} list={list} render={JSON.stringify(q)} />\n </>\n );\n}\n\n/** A list's locked cards, counted, loaded and shown on opening. */\nfunction LockedSection({ query, ...p }: CardListProps & { query: ContentQuery }) {\n const m = useMessages();\n const [open, setOpen] = useState(false);\n const { count } = useLockedCount(query, { client: p.client });\n const q = useMemo<ContentQuery | null>(\n () => (open ? { ...query, mode: \"lock\", access: query.access ?? LOCKED_LEVELS, limit: query.limit ?? p.pageSize } : null),\n [open, query, p.pageSize],\n );\n const list = useContents(q, { client: p.client });\n const items = useMemo(() => list.items.filter((c) => !c.access.allowed), [list.items]);\n if (!count) return null;\n return (\n <ContentKitUiRoot appearance={p.appearance} className=\"grid gap-3 text-sm\" data-ckui=\"locked-cards\">\n <Button variant=\"outline\" size=\"sm\" className=\"justify-self-start\" aria-expanded={open} onClick={() => setOpen(!open)}>\n {open ? m.t(\"contents.lockedHide\") : m.plural(\"contents.lockedShow\", count)}\n </Button>\n {open && <CardGrid {...p} list={{ ...list, items }} render={JSON.stringify(q)} />}\n </ContentKitUiRoot>\n );\n}\n\n/** Search results as cards; nothing until the query has text. */\nexport function SearchResults({ query, ...p }: CardListProps & { query: SearchQuery }) {\n const { t } = useMessages();\n const q = useMemo(() => ({ ...query, limit: query.limit ?? p.pageSize }), [query, p.pageSize]);\n const list = useSearch(q, { client: p.client });\n if (!query.q.trim()) return null;\n return <CardGrid surface=\"search\" empty={t(\"contents.noResults\", { q: query.q.trim() })} {...p} list={list} render={JSON.stringify(q)} />;\n}\n\n/** The signed-in reader's history as cards, each removable, with a clear-all. */\nexport function ContentHistory({ query = {}, ...p }: CardListProps & { query?: HistoryQuery }) {\n const { t } = useMessages();\n const ctx = useContext(ContentKitContext);\n const q = useMemo(() => ({ ...query, limit: query.limit ?? p.pageSize }), [query, p.pageSize]);\n const list = useHistory(q, { client: p.client });\n const forget = useAction(\"history.forget\");\n if (ctx?.viewer === null) return <p className=\"text-muted-foreground\">{t(\"contents.historySignedOut\")}</p>;\n return (\n <div className=\"grid gap-3\" data-ckui=\"content-history\">\n {list.items.length > 0 && (\n <Button size=\"sm\" variant=\"outline\" className=\"justify-self-end\" disabled={forget.pending} onClick={() => void forget.run(() => list.forget(query.kind ? { kind: query.kind } : {}))}>\n {t(\"contents.clearHistory\")}\n </Button>\n )}\n <CardGrid\n surface=\"history\"\n empty={t(\"contents.historyEmpty\")}\n {...p}\n list={list}\n render={JSON.stringify(q)}\n renderCard={\n p.renderCard ??\n ((card) => (\n <ContentCard\n card={card}\n client={p.client}\n href={p.href}\n shape={p.shape}\n aspectRatio={p.aspectRatio}\n titlePosition={p.titlePosition}\n loadMedia={p.loadMedia}\n groupKinds={p.groupKinds}\n chips={p.chips}\n filterEntity={p.filterEntity}\n entityStyle={p.entityStyle}\n entityLayout={p.entityLayout}\n metadata={p.metadata}\n showCounts={p.showCounts}\n tiers={p.tiers}\n labels={p.labels}\n owner={p.owner}\n ownerHref={p.ownerHref}\n decorate={p.decorate}\n >\n <Button size=\"xs\" variant=\"ghost\" className=\"justify-self-start\" disabled={forget.pending} onClick={() => void forget.run(() => list.forget({ content_id: card.id }))}>\n {t(\"contents.removeFromHistory\")}\n </Button>\n </ContentCard>\n ))\n }\n />\n </div>\n );\n}\n\nexport interface ContentRailProps extends Omit<CardListProps, \"infinite\" | \"empty\"> {\n /** The rail's heading. */\n title: ReactNode;\n /** Contents like this one. */\n similar?: string;\n /** For the signed-in reader. */\n recommendations?: boolean;\n /** The reader's history: { status: \"in_progress\" } is a continue-watching rail. */\n history?: HistoryQuery;\n /** An ordered group's members in its order: a featured rail (its windows apply). */\n group?: string;\n /** Any list: a sort and filters. */\n list?: ContentQuery;\n /** Filters and reader options of a similar or recommended rail. */\n query?: RailQuery;\n /** A link beside the heading, e.g. to the whole list. */\n more?: ReactNode;\n}\n\n/**\n * A horizontal row of contents: similar, recommended, the reader's history,\n * an ordered group's members or any list. It hides itself when it has none or\n * the site runs without what it needs.\n */\nexport function ContentRail({ title, similar, recommendations, history, group, list: listQuery, query = {}, more, ...p }: ContentRailProps) {\n const limit = query.limit ?? p.pageSize ?? 12;\n const q = useMemo(() => ({ ...query, limit }), [query, limit]);\n const listing = useMemo(\n () => (listQuery ? { ...listQuery, limit: listQuery.limit ?? limit } : group ? { groups: [group], sort: \"position\" as const, limit } : null),\n [listQuery, group, limit],\n );\n const like = useSimilar(recommendations || history || listing ? null : similar, q, { client: p.client });\n const mine = useRecommendations(recommendations ? q : null, { client: p.client });\n const seen = useHistory(history ? { ...history, limit: history.limit ?? limit } : null, { client: p.client });\n const listed = useContents(listing, { client: p.client });\n const rail: UseRail = recommendations\n ? mine\n : history || listing\n ? (() => {\n const l = history ? seen : listed;\n return { items: l.items, loading: l.loading, error: l.error, unavailable: l.error?.code === \"not_configured\", reload: l.reload };\n })()\n : like;\n const { kinds } = useKinds({ client: p.client });\n const surface = p.surface ?? (recommendations ? \"recommendations\" : history ? \"history\" : group ? \"featured\" : listing ? \"list\" : \"similar\");\n const exposure = useExposure(rail.items, { surface, render: JSON.stringify([similar, recommendations, history, group, listing, q]), client: p.client });\n const source = similar || recommendations || history || group || listing;\n const layout = p.grid ? cardGridLayout(p.grid) : undefined;\n if (!source || rail.unavailable || (!rail.loading && !rail.items.length)) return null;\n return (\n <ContentKitUiRoot appearance={p.appearance} className={cn(\"grid gap-3 text-sm\", p.className)} data-ckui=\"content-rail\" data-surface={surface}>\n <div className=\"flex items-baseline justify-between gap-2\">\n <h2 className=\"text-base font-semibold\">{title}</h2>\n {more}\n </div>\n <ol className={layout?.className ?? \"flex snap-x gap-4 overflow-x-auto pb-2\"} style={layout?.style}>\n {(rail.loading && !rail.items.length ? Array.from({ length: 6 }, () => null) : rail.items).map((card, i) => (\n <li key={card?.id ?? i} className={layout ? undefined : \"w-36 shrink-0 snap-start sm:w-44\"} ref={card ? exposure.track(card.id, i + 1) : undefined}>\n {!card ? (\n <Skeleton className={p.aspectRatio === undefined ? p.shape === \"landscape\" ? \"aspect-video w-full\" : \"aspect-[3/4] w-full\" : \"w-full\"} style={p.aspectRatio === undefined ? undefined : { aspectRatio: p.aspectRatio }} />\n ) : p.renderCard ? (\n p.renderCard(card, i + 1)\n ) : (\n <ContentCard\n card={card}\n client={p.client}\n href={p.href}\n shape={p.shape}\n aspectRatio={p.aspectRatio}\n titlePosition={p.titlePosition}\n loadMedia={p.loadMedia}\n groupKinds={p.groupKinds}\n chips={p.chips}\n filterEntity={p.filterEntity}\n entityStyle={p.entityStyle}\n entityLayout={p.entityLayout}\n metadata={p.metadata}\n showCounts={p.showCounts}\n tiers={p.tiers ?? kinds?.tiers}\n labels={p.labels}\n owner={p.owner}\n ownerHref={p.ownerHref}\n decorate={p.decorate}\n onOpen={() => exposure.click(card.id, i + 1)}\n />\n )}\n </li>\n ))}\n </ol>\n </ContentKitUiRoot>\n );\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import { ComponentProps, ReactNode } from 'react';
2
2
  import { DensityRange, Rendition } from '../client/rendition.js';
3
3
  export declare const DensityContext: import('react').Context<DensityRange>;
4
4
  export interface UseRenditionOptions {
5
- /** Density range; default the provider's (2–3×). */
5
+ /** Density range; default the provider's (1–3×). */
6
6
  density?: DensityRange;
7
7
  }
8
8
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"rendition-img.js","names":[],"sources":["../../src/components/rendition-img.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentProps, type CSSProperties, type ReactNode } from \"react\";\nimport { DEFAULT_DENSITY, densityFor, pickRendition, sortRenditions, type DensityRange, type Rendition } from \"../client/rendition.js\";\nimport { useReducedMotion } from \"../react/inline-preview.js\";\n\nexport const DensityContext = createContext<DensityRange>(DEFAULT_DENSITY);\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nexport interface UseRenditionOptions {\n /** Density range; default the provider's (2–3×). */\n density?: DensityRange;\n}\n\n/**\n * Picks a rendition for the element `ref` (a callback ref) is attached to: the narrowest at\n * least its rendered width × density. It re-picks on resize (fullscreen,\n * window changes), never steps down, and keeps the shown rendition until a\n * wider one has loaded, so nothing flickers.\n */\nexport function useRendition<T extends Rendition, E extends HTMLElement = HTMLImageElement>(outputs: readonly T[] | null | undefined, o: UseRenditionOptions = {}) {\n const provided = useContext(DensityContext);\n const range = o.density ?? provided;\n const [el, ref] = useState<E | null>(null);\n // -1 until measured; a hidden element measures 0 and gets the narrowest.\n const [width, setWidth] = useState(-1);\n useIsoLayoutEffect(() => {\n if (!el) return;\n const measure = () => setWidth((w) => Math.max(w, el.getBoundingClientRect().width));\n measure();\n if (typeof ResizeObserver !== \"function\") return;\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n return () => ro.disconnect();\n }, [el]);\n const sorted = useMemo(() => sortRenditions(outputs), [outputs]);\n const want = width >= 0 ? pickRendition(sorted, width, densityFor(range)) : undefined;\n const [shown, setShown] = useState<T | undefined>(undefined);\n const loaded = useRef(false);\n const current = shown && sorted.some((r) => r.url === shown.url) ? shown : undefined;\n useEffect(() => {\n if (!want || want.url === current?.url) return;\n // First pick, a new set, or before anything loaded: show it now.\n if (!current || !loaded.current) {\n loaded.current = false;\n setShown(want);\n return;\n }\n if (want.w <= current.w) return;\n let live = true;\n const img = new Image();\n img.onload = () => live && setShown(want);\n img.src = want.url;\n return () => {\n live = false;\n };\n }, [want, current]);\n const onLoad = useCallback(() => {\n loaded.current = true;\n }, []);\n return { ref, rendition: current ?? want, onLoad };\n}\n\nexport interface RenditionImgProps extends Omit<ComponentProps<\"img\">, \"src\" | \"srcSet\" | \"sizes\" | \"width\" | \"height\"> {\n outputs: readonly Rendition[] | null | undefined;\n density?: DensityRange;\n /** Rendered instead when there are no renditions or the image fails to load (a 404). */\n fallback?: ReactNode;\n /** A pulsing muted fill until the image has loaded. */\n skeleton?: boolean;\n /** object-fit of the image in its box. */\n fit?: \"cover\" | \"contain\";\n}\n\nconst SKELETON: CSSProperties = { backgroundColor: \"var(--ckui-muted, rgb(127 127 127 / 0.15))\" };\nconst PULSE = \"ckui-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite\";\n\n/**\n * An `<img>` showing the rendition its rendered size × density needs (see\n * useRendition), with a fallback for none or a failed load and an optional\n * loading skeleton.\n */\nexport function RenditionImg({ outputs, density, onLoad, onError, alt = \"\", fallback, skeleton, fit, style, ...img }: RenditionImgProps) {\n const { ref, rendition, onLoad: loaded } = useRendition(outputs, { density });\n const first = sortRenditions(outputs)[0];\n const r = rendition ?? first;\n const reduced = useReducedMotion();\n const [failed, setFailed] = useState<string | null>(null);\n const [done, setDone] = useState<string | null>(null);\n const [el, setEl] = useState<HTMLImageElement | null>(null);\n const src = rendition ? r?.url : undefined;\n // A cached image is complete before its load event; settle it before paint.\n useIsoLayoutEffect(() => {\n if (el && src && el.complete && el.naturalWidth > 0) setDone(src);\n }, [el, src]);\n const attach = useCallback(\n (node: HTMLImageElement | null) => {\n ref(node);\n setEl(node);\n },\n [ref],\n );\n if (!r || (first && failed === first.url)) return <>{fallback ?? null}</>;\n const loading = skeleton && done !== src;\n return (\n <img\n {...img}\n ref={attach}\n alt={alt}\n src={src}\n width={r.w}\n height={r.h || undefined}\n decoding=\"async\"\n data-rendition={rendition ? r.w : undefined}\n data-loading={loading ? \"\" : undefined}\n style={{ ...(fit ? { objectFit: fit } : {}), ...(loading ? { ...SKELETON, animation: reduced ? undefined : PULSE } : {}), ...style }}\n onLoad={(e) => {\n loaded();\n if (src) setDone(src);\n onLoad?.(e);\n }}\n onError={(e) => {\n if (first) setFailed(first.url);\n onError?.(e);\n }}\n />\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rendition-img.js","names":[],"sources":["../../src/components/rendition-img.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentProps, type CSSProperties, type ReactNode } from \"react\";\nimport { DEFAULT_DENSITY, densityFor, pickRendition, sortRenditions, type DensityRange, type Rendition } from \"../client/rendition.js\";\nimport { useReducedMotion } from \"../react/inline-preview.js\";\n\nexport const DensityContext = createContext<DensityRange>(DEFAULT_DENSITY);\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nexport interface UseRenditionOptions {\n /** Density range; default the provider's (1–3×). */\n density?: DensityRange;\n}\n\n/**\n * Picks a rendition for the element `ref` (a callback ref) is attached to: the narrowest at\n * least its rendered width × density. It re-picks on resize (fullscreen,\n * window changes), never steps down, and keeps the shown rendition until a\n * wider one has loaded, so nothing flickers.\n */\nexport function useRendition<T extends Rendition, E extends HTMLElement = HTMLImageElement>(outputs: readonly T[] | null | undefined, o: UseRenditionOptions = {}) {\n const provided = useContext(DensityContext);\n const range = o.density ?? provided;\n const [el, ref] = useState<E | null>(null);\n // -1 until measured; a hidden element measures 0 and gets the narrowest.\n const [width, setWidth] = useState(-1);\n useIsoLayoutEffect(() => {\n if (!el) return;\n const measure = () => setWidth((w) => Math.max(w, el.getBoundingClientRect().width));\n measure();\n if (typeof ResizeObserver !== \"function\") return;\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n return () => ro.disconnect();\n }, [el]);\n const sorted = useMemo(() => sortRenditions(outputs), [outputs]);\n const want = width >= 0 ? pickRendition(sorted, width, densityFor(range)) : undefined;\n const [shown, setShown] = useState<T | undefined>(undefined);\n const loaded = useRef(false);\n const current = shown && sorted.some((r) => r.url === shown.url) ? shown : undefined;\n useEffect(() => {\n if (!want || want.url === current?.url) return;\n // First pick, a new set, or before anything loaded: show it now.\n if (!current || !loaded.current) {\n loaded.current = false;\n setShown(want);\n return;\n }\n if (want.w <= current.w) return;\n let live = true;\n const img = new Image();\n img.onload = () => live && setShown(want);\n img.src = want.url;\n return () => {\n live = false;\n };\n }, [want, current]);\n const onLoad = useCallback(() => {\n loaded.current = true;\n }, []);\n return { ref, rendition: current ?? want, onLoad };\n}\n\nexport interface RenditionImgProps extends Omit<ComponentProps<\"img\">, \"src\" | \"srcSet\" | \"sizes\" | \"width\" | \"height\"> {\n outputs: readonly Rendition[] | null | undefined;\n density?: DensityRange;\n /** Rendered instead when there are no renditions or the image fails to load (a 404). */\n fallback?: ReactNode;\n /** A pulsing muted fill until the image has loaded. */\n skeleton?: boolean;\n /** object-fit of the image in its box. */\n fit?: \"cover\" | \"contain\";\n}\n\nconst SKELETON: CSSProperties = { backgroundColor: \"var(--ckui-muted, rgb(127 127 127 / 0.15))\" };\nconst PULSE = \"ckui-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite\";\n\n/**\n * An `<img>` showing the rendition its rendered size × density needs (see\n * useRendition), with a fallback for none or a failed load and an optional\n * loading skeleton.\n */\nexport function RenditionImg({ outputs, density, onLoad, onError, alt = \"\", fallback, skeleton, fit, style, ...img }: RenditionImgProps) {\n const { ref, rendition, onLoad: loaded } = useRendition(outputs, { density });\n const first = sortRenditions(outputs)[0];\n const r = rendition ?? first;\n const reduced = useReducedMotion();\n const [failed, setFailed] = useState<string | null>(null);\n const [done, setDone] = useState<string | null>(null);\n const [el, setEl] = useState<HTMLImageElement | null>(null);\n const src = rendition ? r?.url : undefined;\n // A cached image is complete before its load event; settle it before paint.\n useIsoLayoutEffect(() => {\n if (el && src && el.complete && el.naturalWidth > 0) setDone(src);\n }, [el, src]);\n const attach = useCallback(\n (node: HTMLImageElement | null) => {\n ref(node);\n setEl(node);\n },\n [ref],\n );\n if (!r || (first && failed === first.url)) return <>{fallback ?? null}</>;\n const loading = skeleton && done !== src;\n return (\n <img\n {...img}\n ref={attach}\n alt={alt}\n src={src}\n width={r.w}\n height={r.h || undefined}\n decoding=\"async\"\n data-rendition={rendition ? r.w : undefined}\n data-loading={loading ? \"\" : undefined}\n style={{ ...(fit ? { objectFit: fit } : {}), ...(loading ? { ...SKELETON, animation: reduced ? undefined : PULSE } : {}), ...style }}\n onLoad={(e) => {\n loaded();\n if (src) setDone(src);\n onLoad?.(e);\n }}\n onError={(e) => {\n if (first) setFailed(first.url);\n onError?.(e);\n }}\n />\n );\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ import { DensityRange } from '../client/rendition.js';
5
5
  export interface SlotImageProps extends Omit<ComponentProps<"img">, "src" | "srcSet" | "sizes" | "width" | "height"> {
6
6
  /** The item's published renditions; without any, the fallback shows. */
7
7
  image?: PublicPreset | null;
8
- /** Density range for picking the width; default the provider's (2–3×). */
8
+ /** Density range for picking the width; default the provider's (1–3×). */
9
9
  density?: DensityRange;
10
10
  round?: boolean;
11
11
  /** Width / height of the box; default the preset's aspect. */
@@ -1 +1 @@
1
- {"version":3,"file":"slot-image.js","names":[],"sources":["../../src/components/slot-image.tsx"],"sourcesContent":["import { ratio, type AspectRatio } from \"../client/aspect.js\";\nimport { Image01Icon, UserIcon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useState, type ComponentProps, type ReactNode } from \"react\";\nimport { publicRenditions, type PublicPreset } from \"../client/public.js\";\nimport type { DensityRange } from \"../client/rendition.js\";\nimport { RenditionImg } from \"./rendition-img.js\";\nimport { ContentKitUiRoot } from \"../scope.js\";\n\nexport interface SlotImageProps extends Omit<ComponentProps<\"img\">, \"src\" | \"srcSet\" | \"sizes\" | \"width\" | \"height\"> {\n /** The item's published renditions; without any, the fallback shows. */\n image?: PublicPreset | null;\n /** Density range for picking the width; default the provider's (2–3×). */\n density?: DensityRange;\n round?: boolean;\n /** Width / height of the box; default the preset's aspect. */\n aspect?: AspectRatio;\n /** Shown without an image (or when it fails to load); default a muted box with an icon. */\n fallback?: ReactNode;\n /** A pulsing fill until the image has loaded. */\n skeleton?: boolean;\n /** How the image fills the box. Default \"cover\". */\n fit?: \"cover\" | \"contain\";\n /** Accessible name of the empty state. */\n emptyLabel?: string;\n}\n\n/** A public image preset's file, in a box at its aspect, at the width its rendered width × density needs. */\nexport function SlotImage({ image, density, round, aspect, fallback, skeleton, fit = \"cover\", emptyLabel, className, alt = \"\", style, onError, ...img }: SlotImageProps) {\n const outputs = publicRenditions(image);\n const [failed, setFailed] = useState<string | null>(null);\n const key = outputs[0]?.url ?? \"\";\n const has = outputs.length > 0 && failed !== key;\n const a = ratio(aspect ?? image?.aspect) ?? 1;\n return (\n <ContentKitUiRoot\n className={cn(\"relative overflow-hidden bg-muted\", round ? \"rounded-full\" : \"rounded-lg\", className)}\n style={{ aspectRatio: String(a), ...style }}\n data-ckui=\"slot-image\"\n data-empty={has ? undefined : \"\"}\n >\n {has ? (\n <RenditionImg\n {...img}\n key={key}\n outputs={outputs}\n density={density}\n alt={alt}\n skeleton={skeleton}\n fit={fit}\n className=\"absolute inset-0 size-full\"\n onError={(e) => {\n setFailed(key);\n onError?.(e);\n }}\n />\n ) : (\n (fallback ?? (\n <div role={emptyLabel ? \"img\" : undefined} aria-label={emptyLabel} className=\"absolute inset-0 flex items-center justify-center text-muted-foreground/70\">\n <HugeiconsIcon icon={round ? UserIcon : Image01Icon} strokeWidth={1.5} className=\"size-1/3 max-h-10 max-w-10\" />\n </div>\n ))\n )}\n </ContentKitUiRoot>\n );\n}\n"],"mappings":";;;;;;;;;;AA6BA,SAAgB,EAAU,EAAE,UAAO,YAAS,UAAO,WAAQ,aAAU,aAAU,SAAM,SAAS,eAAY,cAAW,SAAM,IAAI,UAAO,YAAS,GAAG,KAAuB;CACvK,IAAM,IAAU,EAAiB,CAAK,GAChC,CAAC,GAAQ,KAAa,EAAwB,IAAI,GAClD,IAAM,EAAQ,EAAE,EAAE,OAAO,IACzB,IAAM,EAAQ,SAAS,KAAK,MAAW,GACvC,IAAI,EAAM,KAAU,GAAO,MAAM,KAAK;CAC5C,OACE,kBAAC,GAAD;EACE,WAAW,EAAG,qCAAqC,IAAQ,iBAAiB,cAAc,CAAS;EACnG,OAAO;GAAE,aAAa,OAAO,CAAC;GAAG,GAAG;EAAM;EAC1C,aAAU;EACV,cAAY,IAAM,KAAA,IAAY;EAE7B,UAAA,IACC,kBAAC,GAAD;GACE,GAAI;GACC;GACI;GACA;GACJ;GACK;GACL;GACL,WAAU;GACV,UAAU,MAAM;IAEd,AADA,EAAU,CAAG,GACb,IAAU,CAAC;GACb;EACD,CAAA,IAEA,KACC,kBAAC,OAAD;GAAK,MAAM,IAAa,QAAQ,KAAA;GAAW,cAAY;GAAY,WAAU;GAC3E,UAAA,kBAAC,GAAD;IAAe,MAAM,IAAQ,IAAW;IAAa,aAAa;IAAK,WAAU;GAA8B,CAAA;EAC5G,CAAA;CAGO,CAAA;AAEtB"}
1
+ {"version":3,"file":"slot-image.js","names":[],"sources":["../../src/components/slot-image.tsx"],"sourcesContent":["import { ratio, type AspectRatio } from \"../client/aspect.js\";\nimport { Image01Icon, UserIcon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useState, type ComponentProps, type ReactNode } from \"react\";\nimport { publicRenditions, type PublicPreset } from \"../client/public.js\";\nimport type { DensityRange } from \"../client/rendition.js\";\nimport { RenditionImg } from \"./rendition-img.js\";\nimport { ContentKitUiRoot } from \"../scope.js\";\n\nexport interface SlotImageProps extends Omit<ComponentProps<\"img\">, \"src\" | \"srcSet\" | \"sizes\" | \"width\" | \"height\"> {\n /** The item's published renditions; without any, the fallback shows. */\n image?: PublicPreset | null;\n /** Density range for picking the width; default the provider's (1–3×). */\n density?: DensityRange;\n round?: boolean;\n /** Width / height of the box; default the preset's aspect. */\n aspect?: AspectRatio;\n /** Shown without an image (or when it fails to load); default a muted box with an icon. */\n fallback?: ReactNode;\n /** A pulsing fill until the image has loaded. */\n skeleton?: boolean;\n /** How the image fills the box. Default \"cover\". */\n fit?: \"cover\" | \"contain\";\n /** Accessible name of the empty state. */\n emptyLabel?: string;\n}\n\n/** A public image preset's file, in a box at its aspect, at the width its rendered width × density needs. */\nexport function SlotImage({ image, density, round, aspect, fallback, skeleton, fit = \"cover\", emptyLabel, className, alt = \"\", style, onError, ...img }: SlotImageProps) {\n const outputs = publicRenditions(image);\n const [failed, setFailed] = useState<string | null>(null);\n const key = outputs[0]?.url ?? \"\";\n const has = outputs.length > 0 && failed !== key;\n const a = ratio(aspect ?? image?.aspect) ?? 1;\n return (\n <ContentKitUiRoot\n className={cn(\"relative overflow-hidden bg-muted\", round ? \"rounded-full\" : \"rounded-lg\", className)}\n style={{ aspectRatio: String(a), ...style }}\n data-ckui=\"slot-image\"\n data-empty={has ? undefined : \"\"}\n >\n {has ? (\n <RenditionImg\n {...img}\n key={key}\n outputs={outputs}\n density={density}\n alt={alt}\n skeleton={skeleton}\n fit={fit}\n className=\"absolute inset-0 size-full\"\n onError={(e) => {\n setFailed(key);\n onError?.(e);\n }}\n />\n ) : (\n (fallback ?? (\n <div role={emptyLabel ? \"img\" : undefined} aria-label={emptyLabel} className=\"absolute inset-0 flex items-center justify-center text-muted-foreground/70\">\n <HugeiconsIcon icon={round ? UserIcon : Image01Icon} strokeWidth={1.5} className=\"size-1/3 max-h-10 max-w-10\" />\n </div>\n ))\n )}\n </ContentKitUiRoot>\n );\n}\n"],"mappings":";;;;;;;;;;AA6BA,SAAgB,EAAU,EAAE,UAAO,YAAS,UAAO,WAAQ,aAAU,aAAU,SAAM,SAAS,eAAY,cAAW,SAAM,IAAI,UAAO,YAAS,GAAG,KAAuB;CACvK,IAAM,IAAU,EAAiB,CAAK,GAChC,CAAC,GAAQ,KAAa,EAAwB,IAAI,GAClD,IAAM,EAAQ,EAAE,EAAE,OAAO,IACzB,IAAM,EAAQ,SAAS,KAAK,MAAW,GACvC,IAAI,EAAM,KAAU,GAAO,MAAM,KAAK;CAC5C,OACE,kBAAC,GAAD;EACE,WAAW,EAAG,qCAAqC,IAAQ,iBAAiB,cAAc,CAAS;EACnG,OAAO;GAAE,aAAa,OAAO,CAAC;GAAG,GAAG;EAAM;EAC1C,aAAU;EACV,cAAY,IAAM,KAAA,IAAY;EAE7B,UAAA,IACC,kBAAC,GAAD;GACE,GAAI;GACC;GACI;GACA;GACJ;GACK;GACL;GACL,WAAU;GACV,UAAU,MAAM;IAEd,AADA,EAAU,CAAG,GACb,IAAU,CAAC;GACb;EACD,CAAA,IAEA,KACC,kBAAC,OAAD;GAAK,MAAM,IAAa,QAAQ,KAAA;GAAW,cAAY;GAAY,WAAU;GAC3E,UAAA,kBAAC,GAAD;IAAe,MAAM,IAAQ,IAAW;IAAa,aAAa;IAAK,WAAU;GAA8B,CAAA;EAC5G,CAAA;CAGO,CAAA;AAEtB"}
@@ -7,7 +7,7 @@ export interface VideoPosterProps extends Omit<ComponentProps<"div">, "children"
7
7
  poster?: PublicPreset | string | null;
8
8
  /** The box's "W:H"; default the preset's aspect, else "16:9". */
9
9
  aspect?: AspectRatio;
10
- /** Density range for picking the poster's width; default the provider's (2–3×). */
10
+ /** Density range for picking the poster's width; default the provider's (1–3×). */
11
11
  density?: DensityRange;
12
12
  alt?: string;
13
13
  /** Shown without a poster (or when it fails to load); default a muted box with a video icon. */
@@ -1 +1 @@
1
- {"version":3,"file":"video-poster.js","names":[],"sources":["../../src/components/video-poster.tsx"],"sourcesContent":["import { ratio, type AspectRatio } from \"../client/aspect.js\";\nimport { Video01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { publicRenditions, type PublicPreset } from \"../client/public.js\";\nimport type { DensityRange } from \"../client/rendition.js\";\nimport { RenditionImg } from \"./rendition-img.js\";\nimport { ContentKitUiRoot } from \"../scope.js\";\n\nexport interface VideoPosterProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n /** The poster's public preset (its aspect is the video's), or an image URL. */\n poster?: PublicPreset | string | null;\n /** The box's \"W:H\"; default the preset's aspect, else \"16:9\". */\n aspect?: AspectRatio;\n /** Density range for picking the poster's width; default the provider's (2–3×). */\n density?: DensityRange;\n alt?: string;\n /** Shown without a poster (or when it fails to load); default a muted box with a video icon. */\n fallback?: ReactNode;\n /** Overlays (duration, badges). */\n children?: ReactNode;\n}\n\n/**\n * A poster at its aspect, uncropped, spanning its container's width, at the\n * width its rendered width × density needs. It never plays: use it for\n * videos the viewer cannot play (a locked post); playable ones\n * preview inline in VideoPlayer and MediaGallery.\n */\nexport function VideoPoster({ poster, aspect, density, alt = \"\", fallback, className, style, children, ...div }: VideoPosterProps) {\n const preset = typeof poster === \"string\" ? null : poster;\n const outputs = publicRenditions(preset);\n const shape = aspect ?? (preset?.aspect || \"16:9\");\n const img = \"absolute inset-0 size-full object-contain\";\n const empty = fallback ?? (\n <div className=\"absolute inset-0 flex items-center justify-center text-muted-foreground/70\">\n <HugeiconsIcon icon={Video01Icon} strokeWidth={1.5} className=\"size-10\" />\n </div>\n );\n return (\n <ContentKitUiRoot\n {...div}\n className={cn(\"relative w-full overflow-hidden rounded-lg bg-muted\", className)}\n style={{ aspectRatio: String(ratio(shape) ?? 16 / 9), ...style }}\n data-ckui=\"video-poster\"\n >\n {typeof poster === \"string\" ? (\n <img src={poster} alt={alt} loading=\"lazy\" decoding=\"async\" className={img} />\n ) : outputs.length ? (\n <RenditionImg outputs={outputs} density={density} alt={alt} loading=\"lazy\" className={img} fallback={empty} />\n ) : (\n empty\n )}\n {children}\n </ContentKitUiRoot>\n );\n}\n"],"mappings":";;;;;;;;;AA8BA,SAAgB,EAAY,EAAE,WAAQ,WAAQ,YAAS,SAAM,IAAI,aAAU,cAAW,UAAO,aAAU,GAAG,KAAyB;CACjI,IAAM,IAAS,OAAO,KAAW,WAAW,OAAO,GAC7C,IAAU,EAAiB,CAAM,GACjC,IAAQ,MAAW,GAAQ,UAAU,SACrC,IAAM,6CACN,IAAQ,KACZ,kBAAC,OAAD;EAAK,WAAU;EACb,UAAA,kBAAC,GAAD;GAAe,MAAM;GAAa,aAAa;GAAK,WAAU;EAAW,CAAA;CACtE,CAAA;CAEP,OACE,kBAAC,GAAD;EACE,GAAI;EACJ,WAAW,EAAG,uDAAuD,CAAS;EAC9E,OAAO;GAAE,aAAa,OAAO,EAAM,CAAK,KAAK,KAAK,CAAC;GAAG,GAAG;EAAM;EAC/D,aAAU;EAJZ,UAAA,CAMG,OAAO,KAAW,WACjB,kBAAC,OAAD;GAAK,KAAK;GAAa;GAAK,SAAQ;GAAO,UAAS;GAAQ,WAAW;EAAM,CAAA,IAC3E,EAAQ,SACV,kBAAC,GAAD;GAAuB;GAAkB;GAAc;GAAK,SAAQ;GAAO,WAAW;GAAK,UAAU;EAAQ,CAAA,IAE7G,GAED,CACe;;AAEtB"}
1
+ {"version":3,"file":"video-poster.js","names":[],"sources":["../../src/components/video-poster.tsx"],"sourcesContent":["import { ratio, type AspectRatio } from \"../client/aspect.js\";\nimport { Video01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { publicRenditions, type PublicPreset } from \"../client/public.js\";\nimport type { DensityRange } from \"../client/rendition.js\";\nimport { RenditionImg } from \"./rendition-img.js\";\nimport { ContentKitUiRoot } from \"../scope.js\";\n\nexport interface VideoPosterProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n /** The poster's public preset (its aspect is the video's), or an image URL. */\n poster?: PublicPreset | string | null;\n /** The box's \"W:H\"; default the preset's aspect, else \"16:9\". */\n aspect?: AspectRatio;\n /** Density range for picking the poster's width; default the provider's (1–3×). */\n density?: DensityRange;\n alt?: string;\n /** Shown without a poster (or when it fails to load); default a muted box with a video icon. */\n fallback?: ReactNode;\n /** Overlays (duration, badges). */\n children?: ReactNode;\n}\n\n/**\n * A poster at its aspect, uncropped, spanning its container's width, at the\n * width its rendered width × density needs. It never plays: use it for\n * videos the viewer cannot play (a locked post); playable ones\n * preview inline in VideoPlayer and MediaGallery.\n */\nexport function VideoPoster({ poster, aspect, density, alt = \"\", fallback, className, style, children, ...div }: VideoPosterProps) {\n const preset = typeof poster === \"string\" ? null : poster;\n const outputs = publicRenditions(preset);\n const shape = aspect ?? (preset?.aspect || \"16:9\");\n const img = \"absolute inset-0 size-full object-contain\";\n const empty = fallback ?? (\n <div className=\"absolute inset-0 flex items-center justify-center text-muted-foreground/70\">\n <HugeiconsIcon icon={Video01Icon} strokeWidth={1.5} className=\"size-10\" />\n </div>\n );\n return (\n <ContentKitUiRoot\n {...div}\n className={cn(\"relative w-full overflow-hidden rounded-lg bg-muted\", className)}\n style={{ aspectRatio: String(ratio(shape) ?? 16 / 9), ...style }}\n data-ckui=\"video-poster\"\n >\n {typeof poster === \"string\" ? (\n <img src={poster} alt={alt} loading=\"lazy\" decoding=\"async\" className={img} />\n ) : outputs.length ? (\n <RenditionImg outputs={outputs} density={density} alt={alt} loading=\"lazy\" className={img} fallback={empty} />\n ) : (\n empty\n )}\n {children}\n </ContentKitUiRoot>\n );\n}\n"],"mappings":";;;;;;;;;AA8BA,SAAgB,EAAY,EAAE,WAAQ,WAAQ,YAAS,SAAM,IAAI,aAAU,cAAW,UAAO,aAAU,GAAG,KAAyB;CACjI,IAAM,IAAS,OAAO,KAAW,WAAW,OAAO,GAC7C,IAAU,EAAiB,CAAM,GACjC,IAAQ,MAAW,GAAQ,UAAU,SACrC,IAAM,6CACN,IAAQ,KACZ,kBAAC,OAAD;EAAK,WAAU;EACb,UAAA,kBAAC,GAAD;GAAe,MAAM;GAAa,aAAa;GAAK,WAAU;EAAW,CAAA;CACtE,CAAA;CAEP,OACE,kBAAC,GAAD;EACE,GAAI;EACJ,WAAW,EAAG,uDAAuD,CAAS;EAC9E,OAAO;GAAE,aAAa,OAAO,EAAM,CAAK,KAAK,KAAK,CAAC;GAAG,GAAG;EAAM;EAC/D,aAAU;EAJZ,UAAA,CAMG,OAAO,KAAW,WACjB,kBAAC,OAAD;GAAK,KAAK;GAAa;GAAK,SAAQ;GAAO,UAAS;GAAQ,WAAW;EAAM,CAAA,IAC3E,EAAQ,SACV,kBAAC,GAAD;GAAuB;GAAkB;GAAc;GAAK,SAAQ;GAAO,WAAW;GAAK,UAAU;EAAQ,CAAA,IAE7G,GAED,CACe;;AAEtB"}
@@ -12,7 +12,7 @@ export interface ContentKitUiProviderProps {
12
12
  messages?: ContentKitUiMessageBundle | readonly ContentKitUiMessageBundle[];
13
13
  /** Host translation hook, consulted before the bundles. */
14
14
  t?: ContentKitUiTranslate;
15
- /** Device-pixel density range images and covers are picked for. Default [2, 3]. */
15
+ /** Device-pixel density range images and covers are picked for. Default [1, 3]. */
16
16
  density?: DensityRange;
17
17
  /** Playable videos preview muted inline (hover, or in view on touch). Default true. */
18
18
  inlinePreview?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","names":[],"sources":["../src/provider.tsx"],"sourcesContent":["import { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport type { ContentKitUiAppearance } from \"./appearance.js\";\nimport { DEFAULT_DENSITY, type DensityRange } from \"./client/rendition.js\";\nimport { DensityContext } from \"./components/rendition-img.js\";\nimport { MessagesContext } from \"./i18n/context.js\";\nimport { createTranslator, resolveMessages, type ContentKitUiMessageBundle, type ContentKitUiTranslate } from \"./i18n/messages.js\";\nimport { InlinePreviewContext } from \"./react/inline-preview.js\";\nimport { AppearanceContext } from \"./scope.js\";\n\n/** Built-in bundles besides English, loaded on demand. */\nconst BUNDLES: Record<string, () => Promise<ContentKitUiMessageBundle>> = {\n de: () => import(\"./locales/de.js\").then((m) => m.de),\n es: () => import(\"./locales/es.js\").then((m) => m.es),\n ja: () => import(\"./locales/ja.js\").then((m) => m.ja),\n ko: () => import(\"./locales/ko.js\").then((m) => m.ko),\n zh: () => import(\"./locales/zh.js\").then((m) => m.zh),\n};\n\n/** The built-in bundle's language for a BCP 47 tag (\"ja-JP\" → \"ja\"); undefined for English or one we lack. */\nexport function bundleLanguage(language: string | null | undefined): string | undefined {\n const primary = language?.toLowerCase().split(/[-_]/)[0];\n return primary && primary in BUNDLES ? primary : undefined;\n}\n\nexport interface ContentKitUiProviderProps {\n appearance?: ContentKitUiAppearance;\n /** BCP 47 tag; its built-in bundle loads under `messages`. English until it arrives. */\n language?: string;\n /** Bundle(s) layered over English and the language's bundle; later entries win. */\n messages?: ContentKitUiMessageBundle | readonly ContentKitUiMessageBundle[];\n /** Host translation hook, consulted before the bundles. */\n t?: ContentKitUiTranslate;\n /** Device-pixel density range images and covers are picked for. Default [2, 3]. */\n density?: DensityRange;\n /** Playable videos preview muted inline (hover, or in view on touch). Default true. */\n inlinePreview?: boolean;\n children?: ReactNode;\n}\n\n/** The translator for a language's bundle (loaded on demand) under the given bundles and host hook. */\nexport function useTranslator(language?: string, messages?: ContentKitUiMessageBundle | readonly ContentKitUiMessageBundle[], t?: ContentKitUiTranslate) {\n const lang = bundleLanguage(language);\n const [loaded, setLoaded] = useState<{ lang: string; bundle: ContentKitUiMessageBundle } | null>(null);\n useEffect(() => {\n if (!lang) return;\n let live = true;\n BUNDLES[lang]!().then(\n (bundle) => live && setLoaded({ lang, bundle }),\n () => {},\n );\n return () => {\n live = false;\n };\n }, [lang]);\n const bundle = lang && loaded?.lang === lang ? loaded.bundle : undefined;\n return useMemo(() => {\n const own = messages ? (Array.isArray(messages) ? messages : [messages]) : [];\n return createTranslator(resolveMessages(bundle ? [bundle, ...own] : own), t, language);\n }, [bundle, messages, t, language]);\n}\n\n/** Look and language for every component below. Renders no DOM; components create their own `.ckui` roots. */\nexport function ContentKitUiProvider({ appearance, language, messages, t, density = DEFAULT_DENSITY, inlinePreview = true, children }: ContentKitUiProviderProps) {\n const translator = useTranslator(language, messages, t);\n return (\n <AppearanceContext.Provider value={appearance}>\n <MessagesContext.Provider value={translator}>\n <DensityContext.Provider value={density}>\n <InlinePreviewContext.Provider value={inlinePreview}>{children}</InlinePreviewContext.Provider>\n </DensityContext.Provider>\n </MessagesContext.Provider>\n </AppearanceContext.Provider>\n );\n}\n"],"mappings":";;;;;;;;;AAUA,IAAM,IAAoE;CACxE,UAAU,OAAO,kBAAkB,CAAC,MAAM,MAAM,EAAE,EAAE;CACpD,UAAU,OAAO,kBAAkB,CAAC,MAAM,MAAM,EAAE,EAAE;CACpD,UAAU,OAAO,kBAAkB,CAAC,MAAM,MAAM,EAAE,EAAE;CACpD,UAAU,OAAO,kBAAkB,CAAC,MAAM,MAAM,EAAE,EAAE;CACpD,UAAU,OAAO,kBAAkB,CAAC,MAAM,MAAM,EAAE,EAAE;AACtD;AAGA,SAAgB,EAAe,GAAyD;CACtF,IAAM,IAAU,GAAU,YAAY,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC;CACtD,OAAO,KAAW,KAAW,IAAU,IAAU,KAAA;AACnD;AAkBA,SAAgB,EAAc,GAAmB,GAA6E,GAA2B;CACvJ,IAAM,IAAO,EAAe,CAAQ,GAC9B,CAAC,GAAQ,KAAa,EAAqE,IAAI;CACrG,QAAgB;EACd,IAAI,CAAC,GAAM;EACX,IAAI,IAAO;EAKX,OAJA,EAAQ,EAAK,CAAE,CAAC,CAAC,MACd,MAAW,KAAQ,EAAU;GAAE;GAAM;EAAO,CAAC,SACxC,CAAC,CACT,SACa;GACX,IAAO;EACT;CACF,GAAG,CAAC,CAAI,CAAC;CACT,IAAM,IAAS,KAAQ,GAAQ,SAAS,IAAO,EAAO,SAAS,KAAA;CAC/D,OAAO,QAAc;EACnB,IAAM,IAAM,IAAY,MAAM,QAAQ,CAAQ,IAAI,IAAW,CAAC,CAAQ,IAAK,CAAC;EAC5E,OAAO,EAAiB,EAAgB,IAAS,CAAC,GAAQ,GAAG,CAAG,IAAI,CAAG,GAAG,GAAG,CAAQ;CACvF,GAAG;EAAC;EAAQ;EAAU;EAAG;CAAQ,CAAC;AACpC;AAGA,SAAgB,EAAqB,EAAE,eAAY,aAAU,aAAU,MAAG,aAAU,GAAiB,mBAAgB,IAAM,eAAuC;CAChK,IAAM,IAAa,EAAc,GAAU,GAAU,CAAC;CACtD,OACE,kBAAC,EAAkB,UAAnB;EAA4B,OAAO;EACjC,UAAA,kBAAC,EAAgB,UAAjB;GAA0B,OAAO;GAC/B,UAAA,kBAAC,EAAe,UAAhB;IAAyB,OAAO;IAC9B,UAAA,kBAAC,EAAqB,UAAtB;KAA+B,OAAO;KAAgB;IAAwC,CAAA;GACvE,CAAA;EACD,CAAA;CACA,CAAA;AAEhC"}
1
+ {"version":3,"file":"provider.js","names":[],"sources":["../src/provider.tsx"],"sourcesContent":["import { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport type { ContentKitUiAppearance } from \"./appearance.js\";\nimport { DEFAULT_DENSITY, type DensityRange } from \"./client/rendition.js\";\nimport { DensityContext } from \"./components/rendition-img.js\";\nimport { MessagesContext } from \"./i18n/context.js\";\nimport { createTranslator, resolveMessages, type ContentKitUiMessageBundle, type ContentKitUiTranslate } from \"./i18n/messages.js\";\nimport { InlinePreviewContext } from \"./react/inline-preview.js\";\nimport { AppearanceContext } from \"./scope.js\";\n\n/** Built-in bundles besides English, loaded on demand. */\nconst BUNDLES: Record<string, () => Promise<ContentKitUiMessageBundle>> = {\n de: () => import(\"./locales/de.js\").then((m) => m.de),\n es: () => import(\"./locales/es.js\").then((m) => m.es),\n ja: () => import(\"./locales/ja.js\").then((m) => m.ja),\n ko: () => import(\"./locales/ko.js\").then((m) => m.ko),\n zh: () => import(\"./locales/zh.js\").then((m) => m.zh),\n};\n\n/** The built-in bundle's language for a BCP 47 tag (\"ja-JP\" → \"ja\"); undefined for English or one we lack. */\nexport function bundleLanguage(language: string | null | undefined): string | undefined {\n const primary = language?.toLowerCase().split(/[-_]/)[0];\n return primary && primary in BUNDLES ? primary : undefined;\n}\n\nexport interface ContentKitUiProviderProps {\n appearance?: ContentKitUiAppearance;\n /** BCP 47 tag; its built-in bundle loads under `messages`. English until it arrives. */\n language?: string;\n /** Bundle(s) layered over English and the language's bundle; later entries win. */\n messages?: ContentKitUiMessageBundle | readonly ContentKitUiMessageBundle[];\n /** Host translation hook, consulted before the bundles. */\n t?: ContentKitUiTranslate;\n /** Device-pixel density range images and covers are picked for. Default [1, 3]. */\n density?: DensityRange;\n /** Playable videos preview muted inline (hover, or in view on touch). Default true. */\n inlinePreview?: boolean;\n children?: ReactNode;\n}\n\n/** The translator for a language's bundle (loaded on demand) under the given bundles and host hook. */\nexport function useTranslator(language?: string, messages?: ContentKitUiMessageBundle | readonly ContentKitUiMessageBundle[], t?: ContentKitUiTranslate) {\n const lang = bundleLanguage(language);\n const [loaded, setLoaded] = useState<{ lang: string; bundle: ContentKitUiMessageBundle } | null>(null);\n useEffect(() => {\n if (!lang) return;\n let live = true;\n BUNDLES[lang]!().then(\n (bundle) => live && setLoaded({ lang, bundle }),\n () => {},\n );\n return () => {\n live = false;\n };\n }, [lang]);\n const bundle = lang && loaded?.lang === lang ? loaded.bundle : undefined;\n return useMemo(() => {\n const own = messages ? (Array.isArray(messages) ? messages : [messages]) : [];\n return createTranslator(resolveMessages(bundle ? [bundle, ...own] : own), t, language);\n }, [bundle, messages, t, language]);\n}\n\n/** Look and language for every component below. Renders no DOM; components create their own `.ckui` roots. */\nexport function ContentKitUiProvider({ appearance, language, messages, t, density = DEFAULT_DENSITY, inlinePreview = true, children }: ContentKitUiProviderProps) {\n const translator = useTranslator(language, messages, t);\n return (\n <AppearanceContext.Provider value={appearance}>\n <MessagesContext.Provider value={translator}>\n <DensityContext.Provider value={density}>\n <InlinePreviewContext.Provider value={inlinePreview}>{children}</InlinePreviewContext.Provider>\n </DensityContext.Provider>\n </MessagesContext.Provider>\n </AppearanceContext.Provider>\n );\n}\n"],"mappings":";;;;;;;;;AAUA,IAAM,IAAoE;CACxE,UAAU,OAAO,kBAAkB,CAAC,MAAM,MAAM,EAAE,EAAE;CACpD,UAAU,OAAO,kBAAkB,CAAC,MAAM,MAAM,EAAE,EAAE;CACpD,UAAU,OAAO,kBAAkB,CAAC,MAAM,MAAM,EAAE,EAAE;CACpD,UAAU,OAAO,kBAAkB,CAAC,MAAM,MAAM,EAAE,EAAE;CACpD,UAAU,OAAO,kBAAkB,CAAC,MAAM,MAAM,EAAE,EAAE;AACtD;AAGA,SAAgB,EAAe,GAAyD;CACtF,IAAM,IAAU,GAAU,YAAY,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC;CACtD,OAAO,KAAW,KAAW,IAAU,IAAU,KAAA;AACnD;AAkBA,SAAgB,EAAc,GAAmB,GAA6E,GAA2B;CACvJ,IAAM,IAAO,EAAe,CAAQ,GAC9B,CAAC,GAAQ,KAAa,EAAqE,IAAI;CACrG,QAAgB;EACd,IAAI,CAAC,GAAM;EACX,IAAI,IAAO;EAKX,OAJA,EAAQ,EAAK,CAAE,CAAC,CAAC,MACd,MAAW,KAAQ,EAAU;GAAE;GAAM;EAAO,CAAC,SACxC,CAAC,CACT,SACa;GACX,IAAO;EACT;CACF,GAAG,CAAC,CAAI,CAAC;CACT,IAAM,IAAS,KAAQ,GAAQ,SAAS,IAAO,EAAO,SAAS,KAAA;CAC/D,OAAO,QAAc;EACnB,IAAM,IAAM,IAAY,MAAM,QAAQ,CAAQ,IAAI,IAAW,CAAC,CAAQ,IAAK,CAAC;EAC5E,OAAO,EAAiB,EAAgB,IAAS,CAAC,GAAQ,GAAG,CAAG,IAAI,CAAG,GAAG,GAAG,CAAQ;CACvF,GAAG;EAAC;EAAQ;EAAU;EAAG;CAAQ,CAAC;AACpC;AAGA,SAAgB,EAAqB,EAAE,eAAY,aAAU,aAAU,MAAG,aAAU,GAAiB,mBAAgB,IAAM,eAAuC;CAChK,IAAM,IAAa,EAAc,GAAU,GAAU,CAAC;CACtD,OACE,kBAAC,EAAkB,UAAnB;EAA4B,OAAO;EACjC,UAAA,kBAAC,EAAgB,UAAjB;GAA0B,OAAO;GAC/B,UAAA,kBAAC,EAAe,UAAhB;IAAyB,OAAO;IAC9B,UAAA,kBAAC,EAAqB,UAAtB;KAA+B,OAAO;KAAgB;IAAwC,CAAA;GACvE,CAAA;EACD,CAAA;CACA,CAAA;AAEhC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openrails/contentkit-ui",
3
- "version": "0.98.1",
3
+ "version": "0.98.3",
4
4
  "description": "ContentKit in the browser: client, React hooks and styled components for media, uploads and content URLs.",
5
5
  "type": "module",
6
6
  "license": "MIT",