@openrails/contentkit-ui 0.85.1 → 0.87.0-rc.1

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.
Files changed (211) hide show
  1. package/README.md +28 -7
  2. package/dist/client/beacons.d.ts +13 -4
  3. package/dist/client/beacons.js +14 -7
  4. package/dist/client/beacons.js.map +1 -1
  5. package/dist/client/client.d.ts +3 -0
  6. package/dist/client/client.js +29 -27
  7. package/dist/client/client.js.map +1 -1
  8. package/dist/client/codes.d.ts +6 -1
  9. package/dist/client/codes.js +4 -1
  10. package/dist/client/codes.js.map +1 -1
  11. package/dist/client/content/types.d.ts +4 -2
  12. package/dist/client/content/types.js.map +1 -1
  13. package/dist/client/discovery.js +3 -0
  14. package/dist/client/discovery.js.map +1 -1
  15. package/dist/client/errors.d.ts +6 -0
  16. package/dist/client/errors.js +6 -2
  17. package/dist/client/errors.js.map +1 -1
  18. package/dist/client/gallery.d.ts +3 -5
  19. package/dist/client/gallery.js +16 -20
  20. package/dist/client/gallery.js.map +1 -1
  21. package/dist/client/generated/error-codes.d.ts +1 -1
  22. package/dist/client/generated/error-codes.js +9 -1
  23. package/dist/client/generated/error-codes.js.map +1 -1
  24. package/dist/client/generated/routes.d.ts +29 -3
  25. package/dist/client/generated/routes.js +44 -3
  26. package/dist/client/generated/routes.js.map +1 -1
  27. package/dist/client/generated/wire.d.ts +60 -10
  28. package/dist/client/generated/wire.js.map +1 -1
  29. package/dist/client/index.d.ts +2 -1
  30. package/dist/client/media/queue.d.ts +4 -2
  31. package/dist/client/media/queue.js +18 -12
  32. package/dist/client/media/queue.js.map +1 -1
  33. package/dist/client/media/rules.d.ts +7 -1
  34. package/dist/client/media/rules.js +27 -21
  35. package/dist/client/media/rules.js.map +1 -1
  36. package/dist/client/media/windows.js +1 -1
  37. package/dist/client/media/windows.js.map +1 -1
  38. package/dist/client/stats.d.ts +32 -0
  39. package/dist/client/stats.js +35 -0
  40. package/dist/client/stats.js.map +1 -0
  41. package/dist/client/versions.d.ts +6 -4
  42. package/dist/client/versions.js +24 -26
  43. package/dist/client/versions.js.map +1 -1
  44. package/dist/client.js +30 -29
  45. package/dist/components/admin/content-manager.js +206 -206
  46. package/dist/components/admin/content-manager.js.map +1 -1
  47. package/dist/components/admin/group-manager.js +71 -70
  48. package/dist/components/admin/group-manager.js.map +1 -1
  49. package/dist/components/admin/media-item-panel.d.ts +24 -7
  50. package/dist/components/admin/media-item-panel.js +209 -197
  51. package/dist/components/admin/media-item-panel.js.map +1 -1
  52. package/dist/components/admin/poll-manager.js +25 -25
  53. package/dist/components/admin/stats-dashboard.d.ts +22 -0
  54. package/dist/components/admin/stats-dashboard.js +257 -0
  55. package/dist/components/admin/stats-dashboard.js.map +1 -0
  56. package/dist/components/content/card.d.ts +5 -3
  57. package/dist/components/content/card.js +155 -133
  58. package/dist/components/content/card.js.map +1 -1
  59. package/dist/components/content/download-button.d.ts +26 -0
  60. package/dist/components/content/download-button.js +51 -0
  61. package/dist/components/content/download-button.js.map +1 -0
  62. package/dist/components/content/editors.js +455 -427
  63. package/dist/components/content/editors.js.map +1 -1
  64. package/dist/components/content/grid.d.ts +8 -1
  65. package/dist/components/content/grid.js +153 -104
  66. package/dist/components/content/grid.js.map +1 -1
  67. package/dist/components/content/index.d.ts +4 -2
  68. package/dist/components/content/listing.d.ts +3 -1
  69. package/dist/components/content/listing.js +34 -34
  70. package/dist/components/content/listing.js.map +1 -1
  71. package/dist/components/content/paywall.d.ts +17 -5
  72. package/dist/components/content/paywall.js +115 -100
  73. package/dist/components/content/paywall.js.map +1 -1
  74. package/dist/components/content/reader-stats.d.ts +12 -0
  75. package/dist/components/content/reader-stats.js +47 -0
  76. package/dist/components/content/reader-stats.js.map +1 -0
  77. package/dist/components/groups/banner.js +33 -33
  78. package/dist/components/groups/directory.js +11 -11
  79. package/dist/components/groups/editor.js +293 -293
  80. package/dist/components/groups/editor.js.map +1 -1
  81. package/dist/components/groups/header.js +27 -27
  82. package/dist/components/groups/header.js.map +1 -1
  83. package/dist/components/groups/memberships.js +180 -180
  84. package/dist/components/groups/parts.d.ts +0 -2
  85. package/dist/components/groups/parts.js +9 -27
  86. package/dist/components/groups/parts.js.map +1 -1
  87. package/dist/components/media-folder-editor.js +239 -189
  88. package/dist/components/media-folder-editor.js.map +1 -1
  89. package/dist/components/media-gallery.d.ts +1 -4
  90. package/dist/components/media-gallery.js +19 -34
  91. package/dist/components/media-gallery.js.map +1 -1
  92. package/dist/components/notifications.js +17 -17
  93. package/dist/components/slot-upload.d.ts +2 -2
  94. package/dist/components/slot-upload.js +84 -79
  95. package/dist/components/slot-upload.js.map +1 -1
  96. package/dist/components/social/bans.js +34 -34
  97. package/dist/components/social/comments.js +1 -1
  98. package/dist/components/social/comments.js.map +1 -1
  99. package/dist/components/social/engagement.d.ts +2 -1
  100. package/dist/components/social/engagement.js +5 -3
  101. package/dist/components/social/engagement.js.map +1 -1
  102. package/dist/components/social/moderation.js +52 -48
  103. package/dist/components/social/moderation.js.map +1 -1
  104. package/dist/components/video-player.d.ts +2 -0
  105. package/dist/components/video-player.js +318 -314
  106. package/dist/components/video-player.js.map +1 -1
  107. package/dist/i18n/context.d.ts +2 -0
  108. package/dist/i18n/context.js +23 -5
  109. package/dist/i18n/context.js.map +1 -1
  110. package/dist/index.d.ts +2 -1
  111. package/dist/index.js +13 -10
  112. package/dist/locales/admin/de.js +27 -0
  113. package/dist/locales/admin/de.js.map +1 -1
  114. package/dist/locales/admin/en.d.ts +27 -0
  115. package/dist/locales/admin/en.js +27 -0
  116. package/dist/locales/admin/en.js.map +1 -1
  117. package/dist/locales/admin/es.js +27 -0
  118. package/dist/locales/admin/es.js.map +1 -1
  119. package/dist/locales/admin/ja.js +27 -0
  120. package/dist/locales/admin/ja.js.map +1 -1
  121. package/dist/locales/admin/ko.js +27 -0
  122. package/dist/locales/admin/ko.js.map +1 -1
  123. package/dist/locales/admin/zh.js +27 -0
  124. package/dist/locales/admin/zh.js.map +1 -1
  125. package/dist/locales/content/de.js +6 -0
  126. package/dist/locales/content/de.js.map +1 -1
  127. package/dist/locales/content/en.d.ts +6 -0
  128. package/dist/locales/content/en.js +6 -0
  129. package/dist/locales/content/en.js.map +1 -1
  130. package/dist/locales/content/es.js +6 -0
  131. package/dist/locales/content/es.js.map +1 -1
  132. package/dist/locales/content/ja.js +6 -0
  133. package/dist/locales/content/ja.js.map +1 -1
  134. package/dist/locales/content/ko.js +6 -0
  135. package/dist/locales/content/ko.js.map +1 -1
  136. package/dist/locales/content/zh.js +6 -0
  137. package/dist/locales/content/zh.js.map +1 -1
  138. package/dist/locales/de.js +14 -2
  139. package/dist/locales/de.js.map +1 -1
  140. package/dist/locales/discovery/de.js +3 -0
  141. package/dist/locales/discovery/de.js.map +1 -1
  142. package/dist/locales/discovery/en.d.ts +3 -0
  143. package/dist/locales/discovery/en.js +3 -0
  144. package/dist/locales/discovery/en.js.map +1 -1
  145. package/dist/locales/discovery/es.js +3 -0
  146. package/dist/locales/discovery/es.js.map +1 -1
  147. package/dist/locales/discovery/ja.js +3 -0
  148. package/dist/locales/discovery/ja.js.map +1 -1
  149. package/dist/locales/discovery/ko.js +3 -0
  150. package/dist/locales/discovery/ko.js.map +1 -1
  151. package/dist/locales/discovery/zh.js +3 -0
  152. package/dist/locales/discovery/zh.js.map +1 -1
  153. package/dist/locales/en.d.ts +50 -2
  154. package/dist/locales/en.js +14 -2
  155. package/dist/locales/en.js.map +1 -1
  156. package/dist/locales/es.js +14 -2
  157. package/dist/locales/es.js.map +1 -1
  158. package/dist/locales/ja.js +14 -2
  159. package/dist/locales/ja.js.map +1 -1
  160. package/dist/locales/ko.js +14 -2
  161. package/dist/locales/ko.js.map +1 -1
  162. package/dist/locales/zh.js +14 -2
  163. package/dist/locales/zh.js.map +1 -1
  164. package/dist/react/admin.js +22 -23
  165. package/dist/react/admin.js.map +1 -1
  166. package/dist/react/canonical.d.ts +4 -3
  167. package/dist/react/canonical.js +34 -33
  168. package/dist/react/canonical.js.map +1 -1
  169. package/dist/react/comments.d.ts +1 -1
  170. package/dist/react/comments.js +3 -2
  171. package/dist/react/comments.js.map +1 -1
  172. package/dist/react/contents.js +144 -144
  173. package/dist/react/contents.js.map +1 -1
  174. package/dist/react/context.d.ts +1 -1
  175. package/dist/react/context.js.map +1 -1
  176. package/dist/react/detail.d.ts +12 -6
  177. package/dist/react/detail.js +31 -24
  178. package/dist/react/detail.js.map +1 -1
  179. package/dist/react/discovery.d.ts +11 -0
  180. package/dist/react/discovery.js +49 -30
  181. package/dist/react/discovery.js.map +1 -1
  182. package/dist/react/engagement.d.ts +6 -2
  183. package/dist/react/engagement.js +2 -6
  184. package/dist/react/engagement.js.map +1 -1
  185. package/dist/react/folder.d.ts +7 -2
  186. package/dist/react/folder.js +165 -147
  187. package/dist/react/folder.js.map +1 -1
  188. package/dist/react/groups.d.ts +0 -1
  189. package/dist/react/groups.js +13 -12
  190. package/dist/react/groups.js.map +1 -1
  191. package/dist/react/index.d.ts +6 -4
  192. package/dist/react/location.d.ts +11 -0
  193. package/dist/react/location.js +18 -0
  194. package/dist/react/location.js.map +1 -0
  195. package/dist/react/public.d.ts +8 -0
  196. package/dist/react/public.js +32 -27
  197. package/dist/react/public.js.map +1 -1
  198. package/dist/react/stats.d.ts +24 -0
  199. package/dist/react/stats.js +28 -0
  200. package/dist/react/stats.js.map +1 -0
  201. package/dist/react/use-resource.js +8 -7
  202. package/dist/react/use-resource.js.map +1 -1
  203. package/dist/react/versions.d.ts +25 -5
  204. package/dist/react/versions.js +61 -40
  205. package/dist/react/versions.js.map +1 -1
  206. package/dist/react.js +26 -24
  207. package/dist/styles.css +1 -1
  208. package/dist/urls/index.d.ts +35 -10
  209. package/dist/urls.js +100 -70
  210. package/dist/urls.js.map +1 -1
  211. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"media-item-panel.js","names":[],"sources":["../../../src/components/admin/media-item-panel.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { toContentKitError, type ContentKitError } from \"../../client/errors.js\";\nimport type { FileInfo, RefBody } from \"../../client/generated/wire.js\";\nimport type { Progress } from \"../../client/media/client.js\";\nimport { samePath } from \"../../client/media/client.js\";\nimport { isProcessing } from \"../../client/media/windows.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useFlowClient } from \"../../react/admin.js\";\nimport { useErrorReporter, useGuarded } from \"../../react/context.js\";\nimport { usePublicImage } from \"../../react/public.js\";\nimport { useMediaRead } from \"../../react/read.js\";\nimport { Alert, AlertDescription } from \"#ckui/ui/alert\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Input } from \"#ckui/ui/input\";\nimport { Label } from \"#ckui/ui/label\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"#ckui/ui/select\";\nimport { Switch } from \"#ckui/ui/switch\";\nimport { EncodeProgress } from \"../encode-progress.js\";\nimport { SlotImage } from \"../slot-image.js\";\nimport { VideoPosterPicker } from \"../video-poster-picker.js\";\nimport { ErrorLine, useAction } from \"../social/parts.js\";\nimport { AdminRoot, LoadFailed, NoAccess, Rows, useConfirm, type AdminFlowProps } from \"./shell.js\";\n\ntype Choice = string | { value: string; label: string };\n\nexport interface MediaItemPanelProps extends AdminFlowProps {\n /** The video item. */\n item: RefBody;\n /** The video's upload path. Default \"source\". */\n source?: string;\n /** The subtitle uploads' path pattern; null hides them. Default \"subs/{name}\". */\n subtitles?: string | null;\n /** The poster's upload path (its frames come from the video); null hides it. Default \"poster\". */\n poster?: string | null;\n /** The poster's public preset. Default \"poster\". */\n posterPreset?: string;\n /** Subtitle languages offered: codes, or codes with labels. Without them the language is typed. */\n languages?: readonly Choice[];\n}\n\ntype ItemState = \"ready\" | \"processing\" | \"failed\" | \"empty\";\nconst choices = (list?: readonly Choice[]) => (list ?? []).map((c) => (typeof c === \"string\" ? { value: c, label: c } : c));\nconst under = (pattern: string) => pattern.slice(0, pattern.indexOf(\"{name}\"));\n\n/**\n * One video item for its editors: its encode state and progress, the source\n * (replace it), subtitle sidecars (language, label, forced), the poster from\n * a frame or an image, and regenerate, which retries failed uploads.\n */\nexport function MediaItemPanel({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: MediaItemPanelProps) {\n const client = useFlowClient({ client: own, baseUrl, auth });\n return (\n <AdminRoot flow=\"media-item-panel\" appearance={appearance} className={className} language={language} messages={messages} guard={guard}>\n <Panel {...p} client={client} />\n </AdminRoot>\n );\n}\n\ntype Inner = Omit<MediaItemPanelProps, \"client\" | \"baseUrl\" | \"auth\" | \"guard\" | \"appearance\" | \"className\" | \"language\" | \"messages\"> & { client: ContentKitClient };\n\nfunction Panel({ item, client, source = \"source\", subtitles = \"subs/{name}\", poster = \"poster\", posterPreset = \"poster\", languages, onError }: Inner) {\n const m = useMessages();\n const { t } = m;\n const r = useMediaRead(item, { editor: true, client });\n const regen = useAction(\"media.save\", onError);\n if (!r.read) {\n if (r.error?.code === \"not_found\") return <NoAccess>{t(\"mediaPanel.noAccess\")}</NoAccess>;\n return r.error ? <LoadFailed error={r.error} message={t(\"mediaPanel.loadFailed\")} retry={r.reload} /> : <Rows count={2} />;\n }\n if (!r.read.uploads) return <NoAccess>{t(\"mediaPanel.noAccess\")}</NoAccess>;\n const uploads = r.read.files.filter((f) => f.upload && !f.unattached);\n const video = uploads.find((f) => samePath(f.path, source));\n const failed = uploads.filter((f) => f.failed);\n const state: ItemState = !video ? \"empty\" : failed.length ? \"failed\" : uploads.some(isProcessing) ? \"processing\" : \"ready\";\n const subs = subtitles === null ? [] : uploads.filter((f) => f.path.startsWith(under(subtitles)));\n return (\n <>\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"text-base font-semibold\">{t(\"mediaPanel.title\")}</h2>\n <Badge variant={state === \"failed\" ? \"destructive\" : state === \"ready\" ? \"outline\" : \"secondary\"} data-ckui=\"media-state\" data-state={state}>\n {t(`mediaPanel.${state}`)}\n </Badge>\n <Button size=\"sm\" variant=\"outline\" className=\"ms-auto\" disabled={regen.pending || !uploads.length} onClick={() => void regen.run(() => client.media.commit(item, [{ op: \"regenerate\" }]))}>\n {t(\"mediaPanel.regenerate\")}\n </Button>\n </div>\n <ErrorLine error={regen.error} />\n {failed.map((f) => (\n <Alert key={f.path} variant=\"destructive\" data-ckui=\"media-failure\">\n <AlertDescription>\n {t(\"mediaPanel.failedFile\", { name: f.path.split(\"/\").pop()!, reason: m.error({ code: f.failed!.code, message: f.failed!.message, status: 422 }) })}\n </AlertDescription>\n </Alert>\n ))}\n <SourceSection item={item} path={source} video={video} client={client} onError={onError} />\n {poster !== null && video && <PosterSection item={item} path={poster} video={source} preset={posterPreset} client={client} onError={onError} />}\n {subtitles !== null && <Subtitles item={item} pattern={subtitles} files={subs} languages={languages} client={client} onError={onError} />}\n </>\n );\n}\n\nfunction bytes(n?: number): string {\n if (!n) return \"\";\n const mb = n / (1 << 20);\n return mb >= 1024 ? `${(mb / 1024).toFixed(1)} GB` : `${mb.toFixed(mb < 10 ? 1 : 0)} MB`;\n}\n\nfunction duration(s?: number): string {\n if (!s) return \"\";\n const m = Math.floor(s / 60);\n const sec = Math.round(s - m * 60);\n return `${m}:${String(sec).padStart(2, \"0\")}`;\n}\n\nfunction SourceSection({ item, path, video, client, onError }: { item: RefBody; path: string; video?: FileInfo; client: ContentKitClient; onError?: Inner[\"onError\"] }) {\n const { t } = useMessages();\n const file = useRef<HTMLInputElement>(null);\n const [progress, setProgress] = useState<Progress | null>(null);\n const report = useErrorReporter(onError);\n const guarded = useGuarded();\n const [error, setError] = useState<ContentKitError | null>(null);\n const replace = async (f: File) => {\n setError(null);\n try {\n await guarded(() => client.media.put(f, { ref: item, path, wait: false, onProgress: setProgress }));\n } catch (e) {\n const err = toContentKitError(e);\n setError(err);\n report(err, \"media.save\");\n } finally {\n setProgress(null);\n }\n };\n const facts = video ? [video.w && video.h ? `${video.w}×${video.h}` : \"\", duration(video.dur), bytes(video.size)].filter(Boolean).join(\" · \") : \"\";\n return (\n <section className=\"grid gap-2\" data-ckui=\"media-source\">\n <h3 className=\"font-medium\">{t(\"mediaPanel.source\")}</h3>\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"min-w-0 flex-1 text-muted-foreground\">{video ? facts || video.path : t(\"mediaPanel.noSource\")}</span>\n <Button size=\"sm\" variant=\"outline\" disabled={!!progress} onClick={() => file.current?.click()}>\n {t(video ? \"mediaPanel.replace\" : \"mediaPanel.upload\")}\n </Button>\n <input\n ref={file}\n type=\"file\"\n accept=\"video/*\"\n hidden\n aria-label={t(\"mediaPanel.source\")}\n onChange={(e) => {\n const f = e.target.files?.[0];\n e.target.value = \"\";\n if (f) void replace(f);\n }}\n />\n </div>\n {progress && (\n <div className=\"grid gap-1\" role=\"status\">\n <div className=\"h-1.5 overflow-hidden rounded-full bg-muted\">\n <div className=\"h-full bg-primary transition-[width]\" style={{ width: `${progress.total ? Math.round((progress.loaded / progress.total) * 100) : 0}%` }} />\n </div>\n <span className=\"text-xs text-muted-foreground\">{t(`mediaPanel.phase_${progress.phase}`)}</span>\n </div>\n )}\n {video && isProcessing(video) && !progress && <EncodeProgress progress={video.progress} />}\n <ErrorLine error={error} />\n </section>\n );\n}\n\nfunction PosterSection({ item, path, video, preset, client, onError }: { item: RefBody; path: string; video: string; preset: string; client: ContentKitClient; onError?: Inner[\"onError\"] }) {\n const { t } = useMessages();\n const [open, setOpen] = useState(false);\n const { image, reload } = usePublicImage(item, preset, { client });\n return (\n <section className=\"grid gap-2\" data-ckui=\"media-poster\">\n <h3 className=\"font-medium\">{t(\"mediaPanel.poster\")}</h3>\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"w-40 overflow-hidden rounded-md border border-border\">\n <SlotImage image={image} fallback={<div className=\"aspect-video bg-muted\" />} />\n </div>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setOpen(true)}>\n {t(\"mediaPanel.choosePoster\")}\n </Button>\n </div>\n <VideoPosterPicker open={open} onOpenChange={setOpen} item={item} video={video} path={path} image={image} client={client} onError={onError} onChange={() => reload()} />\n </section>\n );\n}\n\nfunction Subtitles({ item, pattern, files, languages: offered, client, onError }: { item: RefBody; pattern: string; files: FileInfo[]; languages?: readonly Choice[]; client: ContentKitClient; onError?: Inner[\"onError\"] }) {\n const { t } = useMessages();\n const ids = useId();\n const languages = choices(offered);\n const [lang, setLang] = useState(languages[0]?.value ?? \"\");\n const [label, setLabel] = useState(\"\");\n const [forced, setForced] = useState(false);\n const [file, setFile] = useState<File | null>(null);\n const input = useRef<HTMLInputElement>(null);\n const add = useAction(\"media.save\", onError);\n const remove = useAction(\"media.save\", onError);\n const { confirm, dialog } = useConfirm();\n const save = async () => {\n if (!file || !lang.trim()) return;\n const code = lang.trim();\n const name = `${code}${forced ? \"-forced\" : \"\"}`;\n const done = await add.run(() =>\n client.media.put(file, {\n ref: item,\n path: pattern.replace(\"{name}\", name),\n wait: false,\n meta: { lang: code, label: label.trim() || (languages.find((l) => l.value === code)?.label ?? code), forced },\n }),\n );\n if (done) {\n setFile(null);\n if (input.current) input.current.value = \"\";\n setLabel(\"\");\n setForced(false);\n }\n };\n const drop = async (f: FileInfo) => {\n if (await confirm({ title: t(\"mediaPanel.removeSubtitleTitle\", { name: String(f.meta?.label ?? f.path) }), confirm: t(\"common.remove\") })) {\n await remove.run(() => client.media.commit(item, [{ op: \"remove\", path: f.path }]));\n }\n };\n return (\n <section className=\"grid gap-2\" data-ckui=\"media-subtitles\">\n <h3 className=\"font-medium\">{t(\"mediaPanel.subtitles\")}</h3>\n {files.length ? (\n <ul className=\"grid divide-y divide-border rounded-lg border border-border\">\n {files.map((f) => (\n <li key={f.path} className=\"flex flex-wrap items-center gap-2 p-2.5\" data-ckui=\"subtitle\">\n <Badge variant=\"outline\">{String(f.meta?.lang ?? \"?\")}</Badge>\n <span className=\"min-w-0 flex-1 break-words\">{String(f.meta?.label ?? f.path.split(\"/\").pop())}</span>\n {f.meta?.forced === true && <Badge variant=\"secondary\">{t(\"mediaPanel.forced\")}</Badge>}\n {f.failed && <Badge variant=\"destructive\">{t(\"mediaPanel.failed\")}</Badge>}\n <Button size=\"xs\" variant=\"ghost\" disabled={remove.pending} onClick={() => void drop(f)}>\n {t(\"common.remove\")}\n </Button>\n </li>\n ))}\n </ul>\n ) : (\n <p className=\"text-muted-foreground\">{t(\"mediaPanel.noSubtitles\")}</p>\n )}\n <form\n className=\"grid gap-2 rounded-lg border border-dashed border-border p-3 sm:grid-cols-2\"\n onSubmit={(e) => {\n e.preventDefault();\n void save();\n }}\n >\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-file`}>{t(\"mediaPanel.subtitleFile\")}</Label>\n <Input id={`${ids}-file`} ref={input} type=\"file\" accept=\".vtt,.srt,text/vtt,application/x-subrip\" onChange={(e) => setFile(e.target.files?.[0] ?? null)} />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-lang`}>{t(\"mediaPanel.language\")}</Label>\n {languages.length ? (\n <Select value={lang} onValueChange={(v) => setLang(String(v))}>\n <SelectTrigger id={`${ids}-lang`} className=\"w-full\">\n <SelectValue>{(v: string) => languages.find((l) => l.value === v)?.label ?? v}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {languages.map((l) => (\n <SelectItem key={l.value} value={l.value}>\n {l.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : (\n <Input id={`${ids}-lang`} value={lang} placeholder=\"en\" onChange={(e) => setLang(e.target.value)} />\n )}\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-label`}>{t(\"mediaPanel.label\")}</Label>\n <Input id={`${ids}-label`} value={label} placeholder={t(\"mediaPanel.labelPlaceholder\")} onChange={(e) => setLabel(e.target.value)} />\n </div>\n <div className=\"flex items-center gap-2 self-end\">\n <Switch id={`${ids}-forced`} checked={forced} onCheckedChange={setForced} />\n <Label htmlFor={`${ids}-forced`}>{t(\"mediaPanel.forcedLabel\")}</Label>\n </div>\n <ErrorLine error={add.error} className=\"sm:col-span-2\" />\n <Button type=\"submit\" size=\"sm\" className=\"justify-self-start\" disabled={add.pending || !file || !lang.trim()}>\n {add.pending ? t(\"common.saving\") : t(\"mediaPanel.addSubtitle\")}\n </Button>\n </form>\n <ErrorLine error={remove.error} />\n {dialog}\n </section>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"media-item-panel.js","names":[],"sources":["../../../src/components/admin/media-item-panel.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { toContentKitError, type ContentKitError } from \"../../client/errors.js\";\nimport type { FileInfo, RefBody, UploadRule } from \"../../client/generated/wire.js\";\nimport { isSubtitleType, isVideoType } from \"../../client/gallery.js\";\nimport { acceptOf, isPattern } from \"../../client/media/rules.js\";\nimport type { Progress } from \"../../client/media/client.js\";\nimport { samePath } from \"../../client/media/client.js\";\nimport { isProcessing } from \"../../client/media/windows.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useFlowClient } from \"../../react/admin.js\";\nimport { useContentKitClient, useErrorReporter, useGuarded, type ContentKitErrorHandler } from \"../../react/context.js\";\nimport { usePublicImage } from \"../../react/public.js\";\nimport { useMediaRead } from \"../../react/read.js\";\nimport { Alert, AlertDescription } from \"#ckui/ui/alert\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Input } from \"#ckui/ui/input\";\nimport { Label } from \"#ckui/ui/label\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"#ckui/ui/select\";\nimport { Switch } from \"#ckui/ui/switch\";\nimport { EncodeProgress } from \"../encode-progress.js\";\nimport { SlotImage } from \"../slot-image.js\";\nimport { VideoPosterPicker } from \"../video-poster-picker.js\";\nimport { ErrorLine, useAction } from \"../social/parts.js\";\nimport { AdminRoot, LoadFailed, NoAccess, Rows, useConfirm, type AdminFlowProps } from \"./shell.js\";\n\ntype Choice = string | { value: string; label: string };\n\ninterface VideoPaths {\n /** The video's upload path. Default the kind's literal video upload. */\n source?: string;\n /** The subtitle uploads' path pattern; null hides them. Default the kind's subtitle pattern. */\n subtitles?: string | null;\n /** The poster's upload path (its frames come from the video); null hides it. Default the kind's upload framed from the video. */\n poster?: string | null;\n /** The poster's public preset. Default \"poster\". */\n posterPreset?: string;\n /** Subtitle languages offered: codes, or codes with labels. Without them the language is typed. */\n languages?: readonly Choice[];\n}\n\nexport interface MediaItemPanelProps extends AdminFlowProps, VideoPaths {\n /** The video item. */\n item: RefBody;\n}\n\n/** A video item's upload paths from its kind's rules: the literal video upload, the image upload framed from it, a subtitle pattern. */\nexport function videoPaths(rules: readonly UploadRule[]): { source?: string; poster?: string; subtitles?: string } {\n const only = (r: UploadRule, is: (t: string) => boolean) => r.types.length > 0 && r.types.every(is);\n const source = rules.find((r) => !isPattern(r.path) && only(r, isVideoType))?.path;\n return {\n source,\n poster: source ? rules.find((r) => !isPattern(r.path) && r.frames === source)?.path : undefined,\n subtitles: rules.find((r) => isPattern(r.path) && only(r, isSubtitleType))?.path,\n };\n}\n\ntype ItemState = \"ready\" | \"processing\" | \"failed\" | \"empty\";\nconst choices = (list?: readonly Choice[]) => (list ?? []).map((c) => (typeof c === \"string\" ? { value: c, label: c } : c));\nconst under = (pattern: string) => pattern.slice(0, pattern.indexOf(\"{name}\"));\n\n/**\n * One video item for its editors: its encode state and progress, the source\n * (replace it), subtitle sidecars (language, label, forced), the poster from\n * a frame or an image, and regenerate, which retries failed uploads.\n */\nexport function MediaItemPanel({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: MediaItemPanelProps) {\n const client = useFlowClient({ client: own, baseUrl, auth });\n return (\n <AdminRoot flow=\"media-item-panel\" appearance={appearance} className={className} language={language} messages={messages} guard={guard}>\n <VideoMediaEditor {...p} client={client} />\n </AdminRoot>\n );\n}\n\nexport interface VideoMediaEditorProps extends VideoPaths {\n item: RefBody;\n client?: ContentKitClient;\n onError?: ContentKitErrorHandler;\n}\n\n/** MediaItemPanel's body, for a host's own editor (the content manager's version media). */\nexport function VideoMediaEditor({ item, client: own, source: givenSource, subtitles: givenSubtitles, poster: givenPoster, posterPreset = \"poster\", languages, onError }: VideoMediaEditorProps) {\n const m = useMessages();\n const { t } = m;\n const client = useContentKitClient(own);\n const r = useMediaRead(item, { editor: true, client });\n const regen = useAction(\"media.save\", onError);\n if (!r.read) {\n if (r.error?.code === \"not_found\") return <NoAccess>{t(\"mediaPanel.noAccess\")}</NoAccess>;\n return r.error ? <LoadFailed error={r.error} message={t(\"mediaPanel.loadFailed\")} retry={r.reload} /> : <Rows count={2} />;\n }\n if (!r.read.uploads) return <NoAccess>{t(\"mediaPanel.noAccess\")}</NoAccess>;\n const rules = r.read.uploads;\n const derived = videoPaths(rules);\n const source = givenSource ?? derived.source;\n const poster = givenPoster === undefined ? derived.poster : givenPoster;\n const subtitles = givenSubtitles === undefined ? derived.subtitles : givenSubtitles;\n const accept = (path: string) => acceptOf(rules.filter((x) => x.path === path));\n const uploads = r.read.files.filter((f) => f.upload && !f.unattached);\n const video = source ? uploads.find((f) => samePath(f.path, source)) : undefined;\n const failed = uploads.filter((f) => f.failed);\n const state: ItemState = !video ? \"empty\" : failed.length ? \"failed\" : uploads.some(isProcessing) ? \"processing\" : \"ready\";\n const subs = subtitles ? uploads.filter((f) => f.path.startsWith(under(subtitles))) : [];\n return (\n <>\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"text-base font-semibold\">{t(\"mediaPanel.title\")}</h2>\n <Badge variant={state === \"failed\" ? \"destructive\" : state === \"ready\" ? \"outline\" : \"secondary\"} data-ckui=\"media-state\" data-state={state}>\n {t(`mediaPanel.${state}`)}\n </Badge>\n <Button size=\"sm\" variant=\"outline\" className=\"ms-auto\" disabled={regen.pending || !uploads.length} onClick={() => void regen.run(() => client.media.commit(item, [{ op: \"regenerate\" }]))}>\n {t(\"mediaPanel.regenerate\")}\n </Button>\n </div>\n <ErrorLine error={regen.error} />\n {failed.map((f) => (\n <Alert key={f.path} variant=\"destructive\" data-ckui=\"media-failure\">\n <AlertDescription>\n {t(\"mediaPanel.failedFile\", { name: f.path.split(\"/\").pop()!, reason: m.error({ code: f.failed!.code, message: f.failed!.message, status: 422 }) })}\n </AlertDescription>\n </Alert>\n ))}\n {source && <SourceSection item={item} path={source} accept={accept(source) ?? \"video/*\"} video={video} client={client} onError={onError} />}\n {source && poster && video && <PosterSection item={item} path={poster} video={source} preset={posterPreset} client={client} onError={onError} />}\n {subtitles && <Subtitles item={item} pattern={subtitles} accept={accept(subtitles)} files={subs} languages={languages} client={client} onError={onError} />}\n </>\n );\n}\n\ntype Inner = { client: ContentKitClient; onError?: ContentKitErrorHandler };\n\nfunction bytes(n?: number): string {\n if (!n) return \"\";\n const mb = n / (1 << 20);\n return mb >= 1024 ? `${(mb / 1024).toFixed(1)} GB` : `${mb.toFixed(mb < 10 ? 1 : 0)} MB`;\n}\n\nfunction duration(s?: number): string {\n if (!s) return \"\";\n const m = Math.floor(s / 60);\n const sec = Math.round(s - m * 60);\n return `${m}:${String(sec).padStart(2, \"0\")}`;\n}\n\nfunction SourceSection({ item, path, accept, video, client, onError }: { item: RefBody; path: string; accept: string; video?: FileInfo } & Inner) {\n const { t } = useMessages();\n const file = useRef<HTMLInputElement>(null);\n const [progress, setProgress] = useState<Progress | null>(null);\n const report = useErrorReporter(onError);\n const guarded = useGuarded();\n const [error, setError] = useState<ContentKitError | null>(null);\n const replace = async (f: File) => {\n setError(null);\n try {\n await guarded(() => client.media.put(f, { ref: item, path, wait: false, onProgress: setProgress }));\n } catch (e) {\n const err = toContentKitError(e);\n setError(err);\n report(err, \"media.save\");\n } finally {\n setProgress(null);\n }\n };\n const facts = video ? [video.w && video.h ? `${video.w}×${video.h}` : \"\", duration(video.dur), bytes(video.size)].filter(Boolean).join(\" · \") : \"\";\n return (\n <section className=\"grid gap-2\" data-ckui=\"media-source\">\n <h3 className=\"font-medium\">{t(\"mediaPanel.source\")}</h3>\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"min-w-0 flex-1 text-muted-foreground\">{video ? facts || video.path : t(\"mediaPanel.noSource\")}</span>\n <Button size=\"sm\" variant=\"outline\" disabled={!!progress} onClick={() => file.current?.click()}>\n {t(video ? \"mediaPanel.replace\" : \"mediaPanel.upload\")}\n </Button>\n <input\n ref={file}\n type=\"file\"\n accept={accept}\n hidden\n aria-label={t(\"mediaPanel.source\")}\n onChange={(e) => {\n const f = e.target.files?.[0];\n e.target.value = \"\";\n if (f) void replace(f);\n }}\n />\n </div>\n {progress && (\n <div className=\"grid gap-1\" role=\"status\">\n <div className=\"h-1.5 overflow-hidden rounded-full bg-muted\">\n <div className=\"h-full bg-primary transition-[width]\" style={{ width: `${progress.total ? Math.round((progress.loaded / progress.total) * 100) : 0}%` }} />\n </div>\n <span className=\"text-xs text-muted-foreground\">{t(`mediaPanel.phase_${progress.phase}`)}</span>\n </div>\n )}\n {video && isProcessing(video) && !progress && <EncodeProgress progress={video.progress} />}\n <ErrorLine error={error} />\n </section>\n );\n}\n\nfunction PosterSection({ item, path, video, preset, client, onError }: { item: RefBody; path: string; video: string; preset: string } & Inner) {\n const { t } = useMessages();\n const [open, setOpen] = useState(false);\n const { image, reload } = usePublicImage(item, preset, { client });\n return (\n <section className=\"grid gap-2\" data-ckui=\"media-poster\">\n <h3 className=\"font-medium\">{t(\"mediaPanel.poster\")}</h3>\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"w-40 overflow-hidden rounded-md border border-border\">\n <SlotImage image={image} fallback={<div className=\"aspect-video bg-muted\" />} />\n </div>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setOpen(true)}>\n {t(\"mediaPanel.choosePoster\")}\n </Button>\n </div>\n <VideoPosterPicker open={open} onOpenChange={setOpen} item={item} video={video} path={path} image={image} client={client} onError={onError} onChange={() => reload()} />\n </section>\n );\n}\n\nfunction Subtitles({ item, pattern, accept, files, languages: offered, client, onError }: { item: RefBody; pattern: string; accept?: string; files: FileInfo[]; languages?: readonly Choice[] } & Inner) {\n const { t } = useMessages();\n const ids = useId();\n const languages = choices(offered);\n const [lang, setLang] = useState(languages[0]?.value ?? \"\");\n const [label, setLabel] = useState(\"\");\n const [forced, setForced] = useState(false);\n const [file, setFile] = useState<File | null>(null);\n const input = useRef<HTMLInputElement>(null);\n const add = useAction(\"media.save\", onError);\n const remove = useAction(\"media.save\", onError);\n const { confirm, dialog } = useConfirm();\n const save = async () => {\n if (!file || !lang.trim()) return;\n const code = lang.trim();\n const name = `${code}${forced ? \"-forced\" : \"\"}`;\n const done = await add.run(() =>\n client.media.put(file, {\n ref: item,\n path: pattern.replace(\"{name}\", name),\n wait: false,\n meta: { lang: code, label: label.trim() || (languages.find((l) => l.value === code)?.label ?? code), forced },\n }),\n );\n if (done) {\n setFile(null);\n if (input.current) input.current.value = \"\";\n setLabel(\"\");\n setForced(false);\n }\n };\n const drop = async (f: FileInfo) => {\n if (await confirm({ title: t(\"mediaPanel.removeSubtitleTitle\", { name: String(f.meta?.label ?? f.path) }), confirm: t(\"common.remove\") })) {\n await remove.run(() => client.media.commit(item, [{ op: \"remove\", path: f.path }]));\n }\n };\n return (\n <section className=\"grid gap-2\" data-ckui=\"media-subtitles\">\n <h3 className=\"font-medium\">{t(\"mediaPanel.subtitles\")}</h3>\n {files.length ? (\n <ul className=\"grid divide-y divide-border rounded-lg border border-border\">\n {files.map((f) => (\n <li key={f.path} className=\"flex flex-wrap items-center gap-2 p-2.5\" data-ckui=\"subtitle\">\n <Badge variant=\"outline\">{String(f.meta?.lang ?? \"?\")}</Badge>\n <span className=\"min-w-0 flex-1 break-words\">{String(f.meta?.label ?? f.path.split(\"/\").pop())}</span>\n {f.meta?.forced === true && <Badge variant=\"secondary\">{t(\"mediaPanel.forced\")}</Badge>}\n {f.failed && <Badge variant=\"destructive\">{t(\"mediaPanel.failed\")}</Badge>}\n <Button size=\"xs\" variant=\"ghost\" disabled={remove.pending} onClick={() => void drop(f)}>\n {t(\"common.remove\")}\n </Button>\n </li>\n ))}\n </ul>\n ) : (\n <p className=\"text-muted-foreground\">{t(\"mediaPanel.noSubtitles\")}</p>\n )}\n <form\n className=\"grid gap-2 rounded-lg border border-dashed border-border p-3 sm:grid-cols-2\"\n onSubmit={(e) => {\n e.preventDefault();\n void save();\n }}\n >\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-file`}>{t(\"mediaPanel.subtitleFile\")}</Label>\n <Input id={`${ids}-file`} ref={input} type=\"file\" accept={accept ?? \".vtt,.srt,text/vtt,application/x-subrip\"} onChange={(e) => setFile(e.target.files?.[0] ?? null)} />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-lang`}>{t(\"mediaPanel.language\")}</Label>\n {languages.length ? (\n <Select value={lang} onValueChange={(v) => setLang(String(v))}>\n <SelectTrigger id={`${ids}-lang`} className=\"w-full\">\n <SelectValue>{(v: string) => languages.find((l) => l.value === v)?.label ?? v}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {languages.map((l) => (\n <SelectItem key={l.value} value={l.value}>\n {l.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : (\n <Input id={`${ids}-lang`} value={lang} placeholder=\"en\" onChange={(e) => setLang(e.target.value)} />\n )}\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-label`}>{t(\"mediaPanel.label\")}</Label>\n <Input id={`${ids}-label`} value={label} placeholder={t(\"mediaPanel.labelPlaceholder\")} onChange={(e) => setLabel(e.target.value)} />\n </div>\n <div className=\"flex items-center gap-2 self-end\">\n <Switch id={`${ids}-forced`} checked={forced} onCheckedChange={setForced} />\n <Label htmlFor={`${ids}-forced`}>{t(\"mediaPanel.forcedLabel\")}</Label>\n </div>\n <ErrorLine error={add.error} className=\"sm:col-span-2\" />\n <Button type=\"submit\" size=\"sm\" className=\"justify-self-start\" disabled={add.pending || !file || !lang.trim()}>\n {add.pending ? t(\"common.saving\") : t(\"mediaPanel.addSubtitle\")}\n </Button>\n </form>\n <ErrorLine error={remove.error} />\n {dialog}\n </section>\n );\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { useMessages as e } from "../../i18n/context.js";
2
- import { Button as t } from "../../ui/button.js";
3
- import { keyOf as n, useContentScope as r, usePaged as i } from "../../react/use-resource.js";
2
+ import { keyOf as t, useContentScope as n, usePaged as r } from "../../react/use-resource.js";
3
+ import { Button as i } from "../../ui/button.js";
4
4
  import { Badge as a } from "../../ui/badge.js";
5
5
  import { Input as o } from "../../ui/input.js";
6
6
  import { Label as s } from "../../ui/label.js";
@@ -46,33 +46,33 @@ function L({ client: e, baseUrl: t, auth: n, guard: r, appearance: i, className:
46
46
  })
47
47
  });
48
48
  }
49
- function R({ basePath: n, navigation: r, ...i }) {
50
- let { t: a } = e(), o = w(n, r), s = N({ client: i.client });
49
+ function R({ basePath: t, navigation: n, ...r }) {
50
+ let { t: a } = e(), o = w(t, n), s = N({ client: r.client });
51
51
  if (s.loading) return /* @__PURE__ */ j(b, {});
52
52
  if (!s.allowed.polls) return /* @__PURE__ */ j(v, { children: a("pollManager.noAccess") });
53
53
  if (!o.view) return /* @__PURE__ */ j(z, {
54
- ...i,
54
+ ...r,
55
55
  loc: o
56
56
  });
57
- let c = /* @__PURE__ */ j(t, {
57
+ let c = /* @__PURE__ */ j(i, {
58
58
  variant: "ghost",
59
59
  size: "sm",
60
60
  className: "justify-self-start",
61
61
  onClick: () => o.go(""),
62
62
  children: a("pollManager.back")
63
- }), l = I(i.languages);
63
+ }), l = I(r.languages);
64
64
  return /* @__PURE__ */ M(A, { children: [c, o.view === "new" ? /* @__PURE__ */ j(C, {
65
65
  languages: l.length ? l : void 0,
66
- freeText: i.freeText,
67
- maxOptions: i.maxOptions,
68
- client: i.client,
69
- onError: i.onError,
66
+ freeText: r.freeText,
67
+ maxOptions: r.maxOptions,
68
+ client: r.client,
69
+ onError: r.onError,
70
70
  onCreated: (e) => o.go(e.id, void 0, { replace: !0 })
71
71
  }) : /* @__PURE__ */ j(C, {
72
72
  poll: o.view,
73
- maxOptions: i.maxOptions,
74
- client: i.client,
75
- onError: i.onError,
73
+ maxOptions: r.maxOptions,
74
+ client: r.client,
75
+ onError: r.onError,
76
76
  onDeleted: () => o.go("", void 0, { replace: !0 })
77
77
  }, o.view)] });
78
78
  }
@@ -88,7 +88,7 @@ function z({ loc: g, client: v, languages: C, pageSize: w = 20, onError: T }) {
88
88
  ...e
89
89
  }), [q, J] = k(R);
90
90
  D(() => J(R), [R]);
91
- let { store: Y, scope: X } = r(v), Z = i(Y, n("polls-page", X, R, z, V, H, U, w), { type: "polls" }, W, w, (e, t) => v.polls.adminList({
91
+ let { store: Y, scope: X } = n(v), Z = r(Y, t("polls-page", X, R, z, V, H, U, w), { type: "polls" }, W, w, (e, t) => v.polls.adminList({
92
92
  ...e,
93
93
  q: R || void 0,
94
94
  language: z || void 0,
@@ -111,7 +111,7 @@ function z({ loc: g, client: v, languages: C, pageSize: w = 20, onError: T }) {
111
111
  children: [/* @__PURE__ */ j("h2", {
112
112
  className: "text-base font-semibold",
113
113
  children: A("pollManager.title")
114
- }), /* @__PURE__ */ j(t, {
114
+ }), /* @__PURE__ */ j(i, {
115
115
  size: "sm",
116
116
  onClick: () => g.go("new"),
117
117
  children: A("pollManager.new")
@@ -139,7 +139,7 @@ function z({ loc: g, client: v, languages: C, pageSize: w = 20, onError: T }) {
139
139
  placeholder: A("pollManager.search"),
140
140
  onChange: (e) => J(e.target.value)
141
141
  }),
142
- /* @__PURE__ */ j(t, {
142
+ /* @__PURE__ */ j(i, {
143
143
  type: "submit",
144
144
  size: "sm",
145
145
  variant: "outline",
@@ -214,7 +214,7 @@ function z({ loc: g, client: v, languages: C, pageSize: w = 20, onError: T }) {
214
214
  })
215
215
  })]
216
216
  }),
217
- (H || U || R || z || V) && /* @__PURE__ */ j(t, {
217
+ (H || U || R || z || V) && /* @__PURE__ */ j(i, {
218
218
  size: "sm",
219
219
  variant: "ghost",
220
220
  onClick: () => g.go(""),
@@ -231,11 +231,11 @@ function z({ loc: g, client: v, languages: C, pageSize: w = 20, onError: T }) {
231
231
  className: "grid divide-y divide-border rounded-lg border border-border",
232
232
  "aria-busy": Z.loading,
233
233
  children: Z.items.map((e) => {
234
- let n = P(e);
234
+ let t = P(e);
235
235
  return /* @__PURE__ */ M("li", {
236
236
  className: "flex flex-wrap items-start gap-3 p-3",
237
237
  "data-ckui": "poll-row",
238
- "data-state": n,
238
+ "data-state": t,
239
239
  children: [/* @__PURE__ */ M("div", {
240
240
  className: "grid min-w-0 flex-1 gap-1",
241
241
  children: [/* @__PURE__ */ j("button", {
@@ -247,24 +247,24 @@ function z({ loc: g, client: v, languages: C, pageSize: w = 20, onError: T }) {
247
247
  className: "flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground",
248
248
  children: [
249
249
  /* @__PURE__ */ j(a, {
250
- variant: n === "live" ? "default" : n === "inactive" ? "outline" : "secondary",
251
- children: A(`pollManager.${n}`)
250
+ variant: t === "live" ? "default" : t === "inactive" ? "outline" : "secondary",
251
+ children: A(`pollManager.${t}`)
252
252
  }),
253
253
  /* @__PURE__ */ j("span", { children: e.language }),
254
254
  e.kind === "free_text" && /* @__PURE__ */ j("span", { children: A("pollEditor.freeText") }),
255
- /* @__PURE__ */ j("span", { children: A(n === "scheduled" ? "pollManager.goesLive" : "pollManager.wentLive", { date: m(E, e.live_at) }) }),
255
+ /* @__PURE__ */ j("span", { children: A(t === "scheduled" ? "pollManager.goesLive" : "pollManager.wentLive", { date: m(E, e.live_at) }) }),
256
256
  e.closes_at && /* @__PURE__ */ j("span", { children: A("poll.closes", { date: m(E, e.closes_at) }) }),
257
257
  /* @__PURE__ */ j("span", { children: e.kind === "free_text" ? E.plural("poll.answers", e.answer_count ?? 0) : E.plural("poll.votes", e.total_votes) })
258
258
  ]
259
259
  })]
260
260
  }), /* @__PURE__ */ M("div", {
261
261
  className: "flex gap-2",
262
- children: [/* @__PURE__ */ j(t, {
262
+ children: [/* @__PURE__ */ j(i, {
263
263
  size: "sm",
264
264
  variant: "outline",
265
265
  onClick: () => g.go(e.id),
266
266
  children: A("comments.edit")
267
- }), /* @__PURE__ */ j(t, {
267
+ }), /* @__PURE__ */ j(i, {
268
268
  size: "sm",
269
269
  variant: "destructive",
270
270
  disabled: Q.pending,
@@ -0,0 +1,22 @@
1
+ import { RankedContent } from '../../client/generated/wire.js';
2
+ import { AdminFlowProps } from './shell.js';
3
+ type Choice = string | {
4
+ value: string;
5
+ label: string;
6
+ };
7
+ export interface StatsDashboardProps extends AdminFlowProps {
8
+ /** Content kinds offered: codes, or codes with labels; the first shows first. */
9
+ kinds: readonly Choice[];
10
+ /** A ranked item's link; default its path. */
11
+ contentHref?: (content: RankedContent) => string | undefined;
12
+ /** Rows in the ranking. Default 10. */
13
+ limit?: number;
14
+ }
15
+ /**
16
+ * Content statistics for staff (root:content:view-restricted): a kind's views
17
+ * per day (view sessions, distinct and signed-in viewers) and its content
18
+ * ranked by views or comments over a period. Views need the signal plane;
19
+ * without it the dashboard says so and still ranks by comments.
20
+ */
21
+ export declare function StatsDashboard({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: StatsDashboardProps): import('react').JSX.Element;
22
+ export {};
@@ -0,0 +1,257 @@
1
+ import { useMessages as e } from "../../i18n/context.js";
2
+ import { Select as t, SelectContent as n, SelectItem as r, SelectTrigger as i, SelectValue as a } from "../../ui/select.js";
3
+ import { AdminRoot as o, LoadFailed as s, NoAccess as c, Rows as l } from "./shell.js";
4
+ import { useFlowClient as u } from "../../react/admin.js";
5
+ import { useActivity as d, useTopContents as f } from "../../react/stats.js";
6
+ import { useState as p } from "react";
7
+ import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
8
+ //#region src/components/admin/stats-dashboard.tsx
9
+ var _ = (e) => e.map((e) => typeof e == "string" ? {
10
+ value: e,
11
+ label: e
12
+ } : e), v = [
13
+ 7,
14
+ 30,
15
+ 90
16
+ ], y = ["views", "comments"], b = [
17
+ "7d",
18
+ "30d",
19
+ "90d",
20
+ "365d",
21
+ "all"
22
+ ], x = (e) => e?.code === "forbidden" || e?.code === "unauthorized";
23
+ function S({ client: e, baseUrl: t, auth: n, guard: r, appearance: i, className: a, language: s, messages: c, ...l }) {
24
+ let d = u({
25
+ client: e,
26
+ baseUrl: t,
27
+ auth: n
28
+ });
29
+ return /* @__PURE__ */ h(o, {
30
+ flow: "stats-dashboard",
31
+ appearance: i,
32
+ className: a,
33
+ language: s,
34
+ messages: c,
35
+ guard: r,
36
+ children: /* @__PURE__ */ h(C, {
37
+ ...l,
38
+ client: d
39
+ })
40
+ });
41
+ }
42
+ function C({ kinds: t, contentHref: n, limit: r = 10, client: i }) {
43
+ let { t: a } = e(), o = _(t), [u, S] = p(o[0]?.value ?? ""), [C, T] = p(7), [E, D] = p("views"), [O, k] = p("30d"), A = d(u ? {
44
+ kind: u,
45
+ days: C
46
+ } : null, { client: i }), j = f(u ? {
47
+ kind: u,
48
+ metric: E,
49
+ period: O,
50
+ limit: r
51
+ } : null, { client: i });
52
+ if (x(A.error) || x(j.error)) return /* @__PURE__ */ h(c, { children: a("stats.noAccess") });
53
+ let M = A.data?.days ?? [], N = Math.max(1, ...M.map((e) => e.views)), P = M.reduce((e, t) => e + t.views, 0);
54
+ return /* @__PURE__ */ g(m, { children: [
55
+ /* @__PURE__ */ g("div", {
56
+ className: "flex flex-wrap items-center justify-between gap-2",
57
+ children: [/* @__PURE__ */ h("h2", {
58
+ className: "text-base font-semibold",
59
+ children: a("stats.title")
60
+ }), o.length > 1 && /* @__PURE__ */ h(w, {
61
+ label: a("stats.kind"),
62
+ value: u,
63
+ onChange: S,
64
+ options: o
65
+ })]
66
+ }),
67
+ /* @__PURE__ */ g("section", {
68
+ className: "grid gap-3",
69
+ "data-ckui": "stats-activity",
70
+ children: [/* @__PURE__ */ g("div", {
71
+ className: "flex flex-wrap items-center justify-between gap-2",
72
+ children: [/* @__PURE__ */ h("h3", {
73
+ className: "font-medium",
74
+ children: a("stats.activity")
75
+ }), /* @__PURE__ */ h(w, {
76
+ label: a("stats.days"),
77
+ value: String(C),
78
+ onChange: (e) => T(Number(e)),
79
+ options: v.map((e) => ({
80
+ value: String(e),
81
+ label: a("stats.lastDays", { count: e })
82
+ }))
83
+ })]
84
+ }), A.unavailable ? /* @__PURE__ */ h("p", {
85
+ className: "text-muted-foreground",
86
+ role: "status",
87
+ children: a("stats.unavailable")
88
+ }) : A.error ? /* @__PURE__ */ h(s, {
89
+ error: A.error,
90
+ message: a("stats.loadFailed"),
91
+ retry: A.reload
92
+ }) : A.loading && !A.data ? /* @__PURE__ */ h(l, { count: 2 }) : M.length ? /* @__PURE__ */ g("table", {
93
+ className: "w-full text-left tabular-nums",
94
+ "aria-busy": A.loading,
95
+ children: [
96
+ /* @__PURE__ */ h("thead", {
97
+ className: "text-xs text-muted-foreground",
98
+ children: /* @__PURE__ */ g("tr", { children: [
99
+ /* @__PURE__ */ h("th", {
100
+ className: "py-1 font-medium",
101
+ children: a("stats.date")
102
+ }),
103
+ /* @__PURE__ */ h("th", {
104
+ className: "py-1 font-medium",
105
+ children: a("stats.views")
106
+ }),
107
+ /* @__PURE__ */ h("th", {
108
+ className: "py-1 text-end font-medium",
109
+ children: a("stats.viewers")
110
+ }),
111
+ /* @__PURE__ */ h("th", {
112
+ className: "py-1 text-end font-medium",
113
+ children: a("stats.users")
114
+ })
115
+ ] })
116
+ }),
117
+ /* @__PURE__ */ h("tbody", { children: M.map((e) => /* @__PURE__ */ g("tr", {
118
+ className: "border-t border-border",
119
+ "data-ckui": "stats-day",
120
+ children: [
121
+ /* @__PURE__ */ h("td", {
122
+ className: "py-1.5 pe-3 whitespace-nowrap",
123
+ children: e.date
124
+ }),
125
+ /* @__PURE__ */ h("td", {
126
+ className: "w-1/2 py-1.5 pe-3",
127
+ children: /* @__PURE__ */ g("span", {
128
+ className: "flex items-center gap-2",
129
+ children: [/* @__PURE__ */ h("span", {
130
+ "aria-hidden": !0,
131
+ className: "h-2 rounded-full bg-primary",
132
+ style: { width: `${Math.max(2, e.views / N * 100)}%` }
133
+ }), e.views]
134
+ })
135
+ }),
136
+ /* @__PURE__ */ h("td", {
137
+ className: "py-1.5 text-end",
138
+ children: e.viewers
139
+ }),
140
+ /* @__PURE__ */ h("td", {
141
+ className: "py-1.5 text-end",
142
+ children: e.users
143
+ })
144
+ ]
145
+ }, e.date)) }),
146
+ /* @__PURE__ */ h("tfoot", { children: /* @__PURE__ */ g("tr", {
147
+ className: "border-t border-border font-medium",
148
+ children: [
149
+ /* @__PURE__ */ h("td", {
150
+ className: "py-1.5",
151
+ children: a("stats.total")
152
+ }),
153
+ /* @__PURE__ */ h("td", {
154
+ className: "py-1.5",
155
+ "data-ckui": "stats-total",
156
+ children: P
157
+ }),
158
+ /* @__PURE__ */ h("td", {}),
159
+ /* @__PURE__ */ h("td", {})
160
+ ]
161
+ }) })
162
+ ]
163
+ }) : /* @__PURE__ */ h("p", {
164
+ className: "text-muted-foreground",
165
+ children: a("stats.noActivity")
166
+ })]
167
+ }),
168
+ /* @__PURE__ */ g("section", {
169
+ className: "grid gap-3",
170
+ "data-ckui": "stats-top",
171
+ children: [/* @__PURE__ */ g("div", {
172
+ className: "flex flex-wrap items-center justify-between gap-2",
173
+ children: [/* @__PURE__ */ h("h3", {
174
+ className: "font-medium",
175
+ children: a("stats.top")
176
+ }), /* @__PURE__ */ g("div", {
177
+ className: "flex flex-wrap gap-2",
178
+ children: [/* @__PURE__ */ h(w, {
179
+ label: a("stats.metric"),
180
+ value: E,
181
+ onChange: (e) => D(e),
182
+ options: y.map((e) => ({
183
+ value: e,
184
+ label: a(`stats.metric_${e}`)
185
+ }))
186
+ }), /* @__PURE__ */ h(w, {
187
+ label: a("stats.period"),
188
+ value: O,
189
+ onChange: (e) => k(e),
190
+ options: b.map((e) => ({
191
+ value: e,
192
+ label: a(`stats.period_${e}`)
193
+ }))
194
+ })]
195
+ })]
196
+ }), j.unavailable ? /* @__PURE__ */ h("p", {
197
+ className: "text-muted-foreground",
198
+ role: "status",
199
+ children: a("stats.unavailable")
200
+ }) : j.error ? /* @__PURE__ */ h(s, {
201
+ error: j.error,
202
+ message: a("stats.loadFailed"),
203
+ retry: j.reload
204
+ }) : j.loading && !j.data ? /* @__PURE__ */ h(l, { count: 3 }) : j.data?.contents.length ? /* @__PURE__ */ h("ol", {
205
+ className: "grid divide-y divide-border rounded-lg border border-border",
206
+ "aria-busy": j.loading,
207
+ children: j.data.contents.map((e, t) => {
208
+ let r = n ? n(e) : e.path;
209
+ return /* @__PURE__ */ g("li", {
210
+ className: "flex items-center gap-3 p-2.5",
211
+ "data-ckui": "stats-ranked",
212
+ children: [
213
+ /* @__PURE__ */ h("span", {
214
+ className: "w-6 text-end text-muted-foreground tabular-nums",
215
+ children: t + 1
216
+ }),
217
+ r ? /* @__PURE__ */ h("a", {
218
+ className: "min-w-0 flex-1 break-words hover:underline",
219
+ href: r,
220
+ children: e.title
221
+ }) : /* @__PURE__ */ h("span", {
222
+ className: "min-w-0 flex-1 break-words",
223
+ children: e.title
224
+ }),
225
+ /* @__PURE__ */ h("span", {
226
+ className: "tabular-nums",
227
+ "data-ckui": "stats-value",
228
+ children: e.value
229
+ })
230
+ ]
231
+ }, e.id);
232
+ })
233
+ }) : /* @__PURE__ */ h("p", {
234
+ className: "text-muted-foreground",
235
+ children: a("stats.noTop")
236
+ })]
237
+ })
238
+ ] });
239
+ }
240
+ function w({ label: e, value: o, onChange: s, options: c }) {
241
+ return /* @__PURE__ */ g(t, {
242
+ value: o,
243
+ onValueChange: (e) => s(String(e)),
244
+ children: [/* @__PURE__ */ h(i, {
245
+ size: "sm",
246
+ "aria-label": e,
247
+ children: /* @__PURE__ */ h(a, { children: (e) => c.find((t) => t.value === e)?.label ?? e })
248
+ }), /* @__PURE__ */ h(n, { children: c.map((e) => /* @__PURE__ */ h(r, {
249
+ value: e.value,
250
+ children: e.label
251
+ }, e.value)) })]
252
+ });
253
+ }
254
+ //#endregion
255
+ export { S as StatsDashboard };
256
+
257
+ //# sourceMappingURL=stats-dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats-dashboard.js","names":[],"sources":["../../../src/components/admin/stats-dashboard.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport type { ContentKitError } from \"../../client/errors.js\";\nimport type { Period } from \"../../client/discovery.js\";\nimport type { RankedContent } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useFlowClient } from \"../../react/admin.js\";\nimport { useActivity, useTopContents } from \"../../react/stats.js\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"#ckui/ui/select\";\nimport { AdminRoot, LoadFailed, NoAccess, Rows, type AdminFlowProps } from \"./shell.js\";\n\ntype Choice = string | { value: string; label: string };\ntype Metric = \"views\" | \"comments\";\n\nexport interface StatsDashboardProps extends AdminFlowProps {\n /** Content kinds offered: codes, or codes with labels; the first shows first. */\n kinds: readonly Choice[];\n /** A ranked item's link; default its path. */\n contentHref?: (content: RankedContent) => string | undefined;\n /** Rows in the ranking. Default 10. */\n limit?: number;\n}\n\nconst choices = (list: readonly Choice[]) => list.map((c) => (typeof c === \"string\" ? { value: c, label: c } : c));\nconst DAYS = [7, 30, 90] as const;\nconst METRICS: readonly Metric[] = [\"views\", \"comments\"];\nconst PERIODS: readonly Period[] = [\"7d\", \"30d\", \"90d\", \"365d\", \"all\"];\nconst refused = (e?: ContentKitError) => e?.code === \"forbidden\" || e?.code === \"unauthorized\";\n\n/**\n * Content statistics for staff (root:content:view-restricted): a kind's views\n * per day (view sessions, distinct and signed-in viewers) and its content\n * ranked by views or comments over a period. Views need the signal plane;\n * without it the dashboard says so and still ranks by comments.\n */\nexport function StatsDashboard({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: StatsDashboardProps) {\n const client = useFlowClient({ client: own, baseUrl, auth });\n return (\n <AdminRoot flow=\"stats-dashboard\" appearance={appearance} className={className} language={language} messages={messages} guard={guard}>\n <Dashboard {...p} client={client} />\n </AdminRoot>\n );\n}\n\ntype Inner = Omit<StatsDashboardProps, \"client\" | \"baseUrl\" | \"auth\" | \"guard\" | \"appearance\" | \"className\" | \"language\" | \"messages\"> & { client: ContentKitClient };\n\nfunction Dashboard({ kinds, contentHref, limit = 10, client }: Inner) {\n const { t } = useMessages();\n const options = choices(kinds);\n const [kind, setKind] = useState(options[0]?.value ?? \"\");\n const [days, setDays] = useState<number>(7);\n const [metric, setMetric] = useState<Metric>(\"views\");\n const [period, setPeriod] = useState<Period>(\"30d\");\n const activity = useActivity(kind ? { kind, days } : null, { client });\n const top = useTopContents(kind ? { kind, metric, period, limit } : null, { client });\n if (refused(activity.error) || refused(top.error)) return <NoAccess>{t(\"stats.noAccess\")}</NoAccess>;\n const rows = activity.data?.days ?? [];\n const most = Math.max(1, ...rows.map((d) => d.views));\n const total = rows.reduce((n, d) => n + d.views, 0);\n return (\n <>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h2 className=\"text-base font-semibold\">{t(\"stats.title\")}</h2>\n {options.length > 1 && (\n <Pick label={t(\"stats.kind\")} value={kind} onChange={setKind} options={options} />\n )}\n </div>\n\n <section className=\"grid gap-3\" data-ckui=\"stats-activity\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h3 className=\"font-medium\">{t(\"stats.activity\")}</h3>\n <Pick label={t(\"stats.days\")} value={String(days)} onChange={(v) => setDays(Number(v))} options={DAYS.map((d) => ({ value: String(d), label: t(\"stats.lastDays\", { count: d }) }))} />\n </div>\n {activity.unavailable ? (\n <p className=\"text-muted-foreground\" role=\"status\">\n {t(\"stats.unavailable\")}\n </p>\n ) : activity.error ? (\n <LoadFailed error={activity.error} message={t(\"stats.loadFailed\")} retry={activity.reload} />\n ) : activity.loading && !activity.data ? (\n <Rows count={2} />\n ) : !rows.length ? (\n <p className=\"text-muted-foreground\">{t(\"stats.noActivity\")}</p>\n ) : (\n <table className=\"w-full text-left tabular-nums\" aria-busy={activity.loading}>\n <thead className=\"text-xs text-muted-foreground\">\n <tr>\n <th className=\"py-1 font-medium\">{t(\"stats.date\")}</th>\n <th className=\"py-1 font-medium\">{t(\"stats.views\")}</th>\n <th className=\"py-1 text-end font-medium\">{t(\"stats.viewers\")}</th>\n <th className=\"py-1 text-end font-medium\">{t(\"stats.users\")}</th>\n </tr>\n </thead>\n <tbody>\n {rows.map((d) => (\n <tr key={d.date} className=\"border-t border-border\" data-ckui=\"stats-day\">\n <td className=\"py-1.5 pe-3 whitespace-nowrap\">{d.date}</td>\n <td className=\"w-1/2 py-1.5 pe-3\">\n <span className=\"flex items-center gap-2\">\n <span aria-hidden className=\"h-2 rounded-full bg-primary\" style={{ width: `${Math.max(2, (d.views / most) * 100)}%` }} />\n {d.views}\n </span>\n </td>\n <td className=\"py-1.5 text-end\">{d.viewers}</td>\n <td className=\"py-1.5 text-end\">{d.users}</td>\n </tr>\n ))}\n </tbody>\n <tfoot>\n <tr className=\"border-t border-border font-medium\">\n <td className=\"py-1.5\">{t(\"stats.total\")}</td>\n <td className=\"py-1.5\" data-ckui=\"stats-total\">\n {total}\n </td>\n <td />\n <td />\n </tr>\n </tfoot>\n </table>\n )}\n </section>\n\n <section className=\"grid gap-3\" data-ckui=\"stats-top\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h3 className=\"font-medium\">{t(\"stats.top\")}</h3>\n <div className=\"flex flex-wrap gap-2\">\n <Pick label={t(\"stats.metric\")} value={metric} onChange={(v) => setMetric(v as Metric)} options={METRICS.map((x) => ({ value: x, label: t(`stats.metric_${x}`) }))} />\n <Pick label={t(\"stats.period\")} value={period} onChange={(v) => setPeriod(v as Period)} options={PERIODS.map((x) => ({ value: x, label: t(`stats.period_${x}`) }))} />\n </div>\n </div>\n {top.unavailable ? (\n <p className=\"text-muted-foreground\" role=\"status\">\n {t(\"stats.unavailable\")}\n </p>\n ) : top.error ? (\n <LoadFailed error={top.error} message={t(\"stats.loadFailed\")} retry={top.reload} />\n ) : top.loading && !top.data ? (\n <Rows count={3} />\n ) : !top.data?.contents.length ? (\n <p className=\"text-muted-foreground\">{t(\"stats.noTop\")}</p>\n ) : (\n <ol className=\"grid divide-y divide-border rounded-lg border border-border\" aria-busy={top.loading}>\n {top.data.contents.map((c, i) => {\n const href = contentHref ? contentHref(c) : c.path;\n return (\n <li key={c.id} className=\"flex items-center gap-3 p-2.5\" data-ckui=\"stats-ranked\">\n <span className=\"w-6 text-end text-muted-foreground tabular-nums\">{i + 1}</span>\n {href ? (\n <a className=\"min-w-0 flex-1 break-words hover:underline\" href={href}>\n {c.title}\n </a>\n ) : (\n <span className=\"min-w-0 flex-1 break-words\">{c.title}</span>\n )}\n <span className=\"tabular-nums\" data-ckui=\"stats-value\">\n {c.value}\n </span>\n </li>\n );\n })}\n </ol>\n )}\n </section>\n </>\n );\n}\n\nfunction Pick({ label, value, onChange, options }: { label: string; value: string; onChange: (v: string) => void; options: { value: string; label: string }[] }) {\n return (\n <Select value={value} onValueChange={(v) => onChange(String(v))}>\n <SelectTrigger size=\"sm\" aria-label={label}>\n <SelectValue>{(v: string) => options.find((o) => o.value === v)?.label ?? v}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {options.map((o) => (\n <SelectItem key={o.value} value={o.value}>\n {o.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n );\n}\n"],"mappings":"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"}
@@ -22,14 +22,16 @@ export interface ContentCardProps {
22
22
  owner?: boolean;
23
23
  /** The owner group's link; default its canonical path. */
24
24
  ownerHref?: (g: GroupLike) => string | undefined;
25
+ /** Over the cover: the host's badges for the card, placed in the cover's box; clicks reach the card. */
26
+ decorate?: (card: Card) => ReactNode;
25
27
  /** Below the meta line: the host's own. */
26
28
  children?: ReactNode;
27
29
  className?: string;
28
30
  }
29
31
  /** Why a card is locked, in words (its price when it is on sale); null when it opens. */
30
32
  export declare function lockLabel(m: Translator, a: CardAccess, tiers?: readonly string[], labels?: Labels): string | null;
31
- /** One content in a listing: cover, title, version, groups, counts, the reader's progress and why it is locked. */
32
- export declare function ContentCard({ card, href, onOpen, shape, groupKinds, chips, tiers, labels, owner, ownerHref, children, className }: ContentCardProps): import('react').JSX.Element;
33
+ /** One content in a listing: cover, title, version, groups, counts, the reader's progress and why it is locked; what its kind's locked view hides is absent. */
34
+ export declare function ContentCard({ card, href, onOpen, shape, groupKinds, chips, tiers, labels, owner, ownerHref, decorate, children, className }: ContentCardProps): import('react').JSX.Element;
33
35
  /** The group that owns a content (a channel): its avatar and name, linking to its page. */
34
36
  export declare function OwnerChip({ group, href, className }: {
35
37
  group: GroupLike;
@@ -41,4 +43,4 @@ export interface PostCardProps extends Omit<ContentCardProps, "shape" | "chips"
41
43
  byline?: ReactNode;
42
44
  }
43
45
  /** A post in a feed: a wide cover, its title, when it was published and what unlocking it costs. */
44
- export declare function PostCard({ card, href, onOpen, tiers, labels, byline, owner, ownerHref, children, className }: PostCardProps): import('react').JSX.Element;
46
+ export declare function PostCard({ card, href, onOpen, tiers, labels, byline, owner, ownerHref, decorate, children, className }: PostCardProps): import('react').JSX.Element;