@openrails/contentkit-ui 0.99.2 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/client/generated/wire.d.ts +1 -0
- package/dist/client/generated/wire.js.map +1 -1
- package/dist/components/admin/group-manager.js +124 -117
- package/dist/components/admin/group-manager.js.map +1 -1
- package/dist/components/admin/user-comment-standing.js +8 -6
- package/dist/components/admin/user-comment-standing.js.map +1 -1
- package/dist/components/content/card.js +2 -2
- package/dist/components/content/card.js.map +1 -1
- package/dist/components/content/editors.d.ts +2 -2
- package/dist/components/content/editors.js +447 -328
- package/dist/components/content/editors.js.map +1 -1
- package/dist/components/content/filters.js +36 -18
- package/dist/components/content/filters.js.map +1 -1
- package/dist/components/content/grid.js +1 -1
- package/dist/components/content/grid.js.map +1 -1
- package/dist/components/groups/directory.js +13 -7
- package/dist/components/groups/directory.js.map +1 -1
- package/dist/components/groups/memberships.js +13 -7
- package/dist/components/groups/memberships.js.map +1 -1
- package/dist/components/rendition-img.js +73 -55
- package/dist/components/rendition-img.js.map +1 -1
- package/dist/components/social/moderation.js +14 -5
- package/dist/components/social/moderation.js.map +1 -1
- package/dist/react/read.d.ts +3 -2
- package/dist/react/read.js +68 -61
- package/dist/react/read.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group-manager.js","names":[],"sources":["../../../src/components/admin/group-manager.tsx"],"sourcesContent":["import { useMemo, type ReactNode } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { LETTERS } from \"../../client/group-fields.js\";\nimport type { ContentModel, Membership } from \"../../client/generated/wire.js\";\nimport { useLanguageName, useMessages } from \"../../i18n/context.js\";\nimport { useAdminLocation, useFlowClient, useStaffAccess, type AdminLocation, type FlowClientOptions, type StaffAccess } from \"../../react/admin.js\";\nimport { useGroups, useKinds, type GroupSort } from \"../../react/groups.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"#ckui/ui/select\";\nimport { Tabs, TabsList, TabsTrigger } from \"#ckui/ui/tabs\";\nimport { GroupEditor } from \"../groups/editor.js\";\nimport { GroupMembers } from \"../groups/memberships.js\";\nimport { labelOf, type Labels } from \"../groups/parts.js\";\nimport { AdminRoot, LoadFailed, NoAccess, Rows, type AdminFlowProps, type AdminRouteProps } from \"./shell.js\";\n\nexport interface GroupManagerProps extends AdminFlowProps, AdminRouteProps {\n /** The group kinds managed, in tab order; default the content model's site-owned kinds. */\n groupKinds?: readonly string[];\n /** Display names of kinds, attributes, roles and values by identifier. */\n labels?: Labels;\n /** Counts of this content kind; default every kind. */\n contentKind?: string;\n /** One member, e.g. a link with its title; default its content id. */\n renderContent?: (member: Membership) => ReactNode;\n pageSize?: number;\n}\n\n/** Whether the caller may manage groups (root:groups:write). */\nexport function useGroupManagerAccess(o: FlowClientOptions = {}): StaffAccess<\"groups\"> {\n return useStaffAccess([\"groups\"] as const, o);\n}\n\nconst SORTS: readonly GroupSort[] = [\"name\", \"count\", \"newest\"];\n\n/**\n * Staff: groups per kind. A kind's groups by letter, order and language; a\n * new group; a group's editor (GroupEditor) and its members (GroupMembers).\n * Kinds are at basePath/{kind}, a group at basePath/{kind}/{id}, filters in the query.\n */\nexport function GroupManager({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: GroupManagerProps) {\n const client = useFlowClient({ client: own, baseUrl, auth });\n return (\n <AdminRoot flow=\"group-manager\" appearance={appearance} className={className} language={language} messages={messages} guard={guard}>\n <Manager {...p} client={client} />\n </AdminRoot>\n );\n}\n\ntype Inner = Omit<GroupManagerProps, \"client\" | \"baseUrl\" | \"auth\" | \"guard\" | \"appearance\" | \"className\" | \"language\" | \"messages\"> & { client: ContentKitClient };\ntype Ctx = Inner & { loc: AdminLocation; kind: string; kinds: ContentModel };\n\nfunction Manager({ basePath, navigation, ...p }: Inner) {\n const { t } = useMessages();\n const loc = useAdminLocation(basePath, navigation);\n const access = useGroupManagerAccess({ client: p.client });\n const model = useKinds({ client: p.client });\n const kinds = model.kinds;\n const list = useMemo(\n () =>\n p.groupKinds ??\n Object.entries(kinds?.entity_kinds ?? {})\n .filter(([, k]) => k.owner !== \"user\")\n .map(([name]) => name)\n .sort(),\n [p.groupKinds, kinds],\n );\n if (access.loading || model.loading) return <Rows />;\n if (!access.allowed.groups) return <NoAccess>{t(\"groupManager.noAccess\")}</NoAccess>;\n if (!kinds) return <LoadFailed error={model.error} message={t(\"groupManager.kindsFailed\")} retry={model.reload} />;\n if (!list.length) return <p className=\"text-muted-foreground\">{t(\"groupManager.noKinds\")}</p>;\n const [k, sub] = loc.view.split(\"/\");\n const kind = list.includes(k ?? \"\") ? k! : list[0]!;\n const ctx: Ctx = { ...p, loc, kind, kinds };\n return (\n <>\n {list.length > 1 && (\n <Tabs value={kind} onValueChange={(v) => loc.go(String(v))}>\n <TabsList className=\"flex-wrap group-data-horizontal/tabs:h-auto\">\n {list.map((x) => (\n <TabsTrigger key={x} value={x} className=\"h-7.5\">\n {labelOf(p.labels, x)}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n )}\n {!sub ? <List key={kind} {...ctx} /> : sub === \"new\" ? <Create key={kind} {...ctx} /> : <Edit key={sub} {...ctx} id={sub} />}\n </>\n );\n}\n\nfunction Back({ loc, kind }: { loc: AdminLocation; kind: string }) {\n const { t } = useMessages();\n return (\n <Button variant=\"ghost\" size=\"sm\" className=\"justify-self-start\" onClick={() => loc.go(kind)}>\n {t(\"groupManager.back\")}\n </Button>\n );\n}\n\nfunction List({ loc, kind, kinds, client, contentKind, labels, pageSize = 50 }: Ctx) {\n const m = useMessages();\n const { t } = m;\n const languageName = useLanguageName();\n const languages = kinds.languages;\n const letterParam = loc.query.get(\"letter\") ?? \"\";\n const letter = LETTERS.includes(letterParam) ? letterParam : undefined;\n const sortParam = loc.query.get(\"sort\") ?? \"\";\n const sort = (SORTS as readonly string[]).includes(sortParam) ? (sortParam as GroupSort) : \"name\";\n const languageParam = loc.query.get(\"language\") ?? \"\";\n const language = languages.includes(languageParam) ? languageParam : languages[0];\n const filters: { letter?: string; sort?: GroupSort; language?: string } = { letter, sort: sort === \"name\" ? undefined : sort, language: language === languages[0] ? undefined : language };\n const filter = (next: Partial<typeof filters>) => loc.go(kind, { ...filters, ...next });\n const groups = useGroups({ kind, letter, sort, language, contentKind, pageSize }, { client, management: true });\n const label = labelOf(labels, kind);\n return (\n <div className=\"grid gap-4\" data-ckui=\"group-list\" data-kind={kind}>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h2 className=\"text-base font-semibold\">{label}</h2>\n <Button size=\"sm\" onClick={() => loc.go(`${kind}/new`)}>\n {t(\"groupManager.new\", { kind: label })}\n </Button>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <Select value={sort} onValueChange={(v) => filter({ sort: v === \"name\" ? undefined : (String(v) as GroupSort) })}>\n <SelectTrigger size=\"sm\" aria-label={t(\"groupManager.sort\")}>\n <SelectValue>{(v: string) => t(`groupManager.sort_${v as GroupSort}`)}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {SORTS.map((s) => (\n <SelectItem key={s} value={s}>\n {t(`groupManager.sort_${s}`)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {languages.length > 1 && (\n <Select value={language} onValueChange={(v) => filter({ language: v === languages[0] ? undefined : String(v) })}>\n <SelectTrigger size=\"sm\" aria-label={t(\"groupManager.language\")}>\n <SelectValue>{(v: string) => languageName(v)}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {languages.map((l) => (\n <SelectItem key={l} value={l}>\n {languageName(l)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n <nav aria-label={t(\"groups.byLetter\")} className=\"flex flex-wrap gap-1\" data-ckui=\"letters\">\n {[undefined, ...LETTERS].map((l) => (\n <Button\n key={l ?? \"all\"}\n size=\"xs\"\n variant={letter === l ? \"default\" : \"ghost\"}\n aria-pressed={letter === l}\n aria-label={l === undefined ? undefined : l === \"#\" ? t(\"groups.otherLetters\") : l.toUpperCase()}\n className=\"min-w-7\"\n onClick={() => filter({ letter: l })}\n >\n {l === undefined ? t(\"groups.all\") : l.toUpperCase()}\n </Button>\n ))}\n </nav>\n {groups.loading && !groups.items.length ? (\n <Rows />\n ) : groups.error && !groups.items.length ? (\n <LoadFailed error={groups.error} message={t(\"groupManager.loadFailed\")} retry={groups.reload} />\n ) : !groups.items.length ? (\n <p className=\"text-muted-foreground\">{t(\"groups.empty\")}</p>\n ) : (\n <ul className=\"grid divide-y divide-border rounded-lg border border-border\" aria-busy={groups.loading}>\n {groups.items.map((g) => (\n <li key={g.id} className=\"flex flex-wrap items-center gap-3 p-3\" data-ckui=\"group-row\" data-slug={g.slug}>\n <div className=\"grid min-w-0 flex-1 gap-0.5\">\n <button type=\"button\" className=\"text-left font-medium break-words hover:underline\" onClick={() => loc.go(`${kind}/${g.id}`)}>\n {g.name || g.slug}\n </button>\n <span className=\"text-xs text-muted-foreground\">{g.slug}</span>\n {g.internal && <Badge variant=\"secondary\" className=\"justify-self-start\">{t(\"groupEditor.internal\")}</Badge>}\n </div>\n <span className=\"text-sm text-muted-foreground tabular-nums\" title={t(\"groupManager.count\")}>\n {new Intl.NumberFormat(m.language).format(g.count)}\n </span>\n </li>\n ))}\n </ul>\n )}\n {groups.hasMore && (\n <Button variant=\"outline\" size=\"sm\" className=\"justify-self-center\" disabled={groups.loadingMore} onClick={() => void groups.loadMore()}>\n {groups.loadingMore ? t(\"groups.loading\") : t(\"groups.loadMore\")}\n </Button>\n )}\n </div>\n );\n}\n\nfunction Create({ loc, kind, client, labels, onError }: Ctx) {\n return (\n <div className=\"grid gap-4\">\n <Back loc={loc} kind={kind} />\n <GroupEditor\n management\n id={null}\n kind={kind}\n labels={labels}\n client={client}\n onError={onError}\n onCreated={(g) => loc.go(`${kind}/${g.id}`, undefined, { replace: true })}\n />\n </div>\n );\n}\n\nfunction Edit({ loc, kind, client, labels, onError, renderContent, id }: Ctx & { id: string }) {\n return (\n <div className=\"grid gap-4\">\n <Back loc={loc} kind={kind} />\n <GroupEditor\n management\n id={id}\n kind={kind}\n labels={labels}\n client={client}\n onError={onError}\n onMerged={(r) => loc.go(`${kind}/${r.into}`, undefined, { replace: true })}\n >\n {(group) => (\n <GroupMembers\n group={group}\n labels={labels}\n client={client}\n onError={onError}\n renderContent={renderContent}\n onUseCover={(content) => client.entities.update(group.id, { cover: { content_id: content } })}\n />\n )}\n </GroupEditor>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"group-manager.js","names":[],"sources":["../../../src/components/admin/group-manager.tsx"],"sourcesContent":["import { useMemo, type ReactNode } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { LETTERS } from \"../../client/group-fields.js\";\nimport type { ContentModel, Membership } from \"../../client/generated/wire.js\";\nimport { useLanguageName, useMessages } from \"../../i18n/context.js\";\nimport { useAdminLocation, useFlowClient, useStaffAccess, type AdminLocation, type FlowClientOptions, type StaffAccess } from \"../../react/admin.js\";\nimport { useGroups, useKinds, type GroupSort } from \"../../react/groups.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"#ckui/ui/select\";\nimport { Tabs, TabsList, TabsTrigger } from \"#ckui/ui/tabs\";\nimport { GroupEditor } from \"../groups/editor.js\";\nimport { GroupMembers } from \"../groups/memberships.js\";\nimport { labelOf, type Labels } from \"../groups/parts.js\";\nimport { contentError } from \"../social/parts.js\";\nimport { AdminRoot, LoadFailed, NoAccess, Rows, type AdminFlowProps, type AdminRouteProps } from \"./shell.js\";\n\nexport interface GroupManagerProps extends AdminFlowProps, AdminRouteProps {\n /** The group kinds managed, in tab order; default the content model's site-owned kinds. */\n groupKinds?: readonly string[];\n /** Display names of kinds, attributes, roles and values by identifier. */\n labels?: Labels;\n /** Counts of this content kind; default every kind. */\n contentKind?: string;\n /** One member, e.g. a link with its title; default its content id. */\n renderContent?: (member: Membership) => ReactNode;\n pageSize?: number;\n}\n\n/** Whether the caller may manage groups (root:groups:write). */\nexport function useGroupManagerAccess(o: FlowClientOptions = {}): StaffAccess<\"groups\"> {\n return useStaffAccess([\"groups\"] as const, o);\n}\n\nconst SORTS: readonly GroupSort[] = [\"name\", \"count\", \"newest\"];\n\n/**\n * Staff: groups per kind. A kind's groups by letter, order and language; a\n * new group; a group's editor (GroupEditor) and its members (GroupMembers).\n * Kinds are at basePath/{kind}, a group at basePath/{kind}/{id}, filters in the query.\n */\nexport function GroupManager({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: GroupManagerProps) {\n const client = useFlowClient({ client: own, baseUrl, auth });\n return (\n <AdminRoot flow=\"group-manager\" appearance={appearance} className={className} language={language} messages={messages} guard={guard}>\n <Manager {...p} client={client} />\n </AdminRoot>\n );\n}\n\ntype Inner = Omit<GroupManagerProps, \"client\" | \"baseUrl\" | \"auth\" | \"guard\" | \"appearance\" | \"className\" | \"language\" | \"messages\"> & { client: ContentKitClient };\ntype Ctx = Inner & { loc: AdminLocation; kind: string; kinds: ContentModel };\n\nfunction Manager({ basePath, navigation, ...p }: Inner) {\n const { t } = useMessages();\n const loc = useAdminLocation(basePath, navigation);\n const access = useGroupManagerAccess({ client: p.client });\n const model = useKinds({ client: p.client });\n const kinds = model.kinds;\n const list = useMemo(\n () =>\n p.groupKinds ??\n Object.entries(kinds?.entity_kinds ?? {})\n .filter(([, k]) => k.owner !== \"user\")\n .map(([name]) => name)\n .sort(),\n [p.groupKinds, kinds],\n );\n if (access.loading || model.loading) return <Rows />;\n if (!access.allowed.groups) return <NoAccess>{t(\"groupManager.noAccess\")}</NoAccess>;\n if (!kinds) return <LoadFailed error={model.error} message={t(\"groupManager.kindsFailed\")} retry={model.reload} />;\n if (!list.length) return <p className=\"text-muted-foreground\">{t(\"groupManager.noKinds\")}</p>;\n const [k, sub] = loc.view.split(\"/\");\n const kind = list.includes(k ?? \"\") ? k! : list[0]!;\n const ctx: Ctx = { ...p, loc, kind, kinds };\n return (\n <>\n {list.length > 1 && (\n <Tabs value={kind} onValueChange={(v) => loc.go(String(v))}>\n <TabsList className=\"flex-wrap group-data-horizontal/tabs:h-auto\">\n {list.map((x) => (\n <TabsTrigger key={x} value={x} className=\"h-7.5\">\n {labelOf(p.labels, x)}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n )}\n {!sub ? <List key={kind} {...ctx} /> : sub === \"new\" ? <Create key={kind} {...ctx} /> : <Edit key={sub} {...ctx} id={sub} />}\n </>\n );\n}\n\nfunction Back({ loc, kind }: { loc: AdminLocation; kind: string }) {\n const { t } = useMessages();\n return (\n <Button variant=\"ghost\" size=\"sm\" className=\"justify-self-start\" onClick={() => loc.go(kind)}>\n {t(\"groupManager.back\")}\n </Button>\n );\n}\n\nfunction List({ loc, kind, kinds, client, contentKind, labels, pageSize = 50 }: Ctx) {\n const m = useMessages();\n const { t } = m;\n const languageName = useLanguageName();\n const languages = kinds.languages;\n const letterParam = loc.query.get(\"letter\") ?? \"\";\n const letter = LETTERS.includes(letterParam) ? letterParam : undefined;\n const sortParam = loc.query.get(\"sort\") ?? \"\";\n const sort = (SORTS as readonly string[]).includes(sortParam) ? (sortParam as GroupSort) : \"name\";\n const languageParam = loc.query.get(\"language\") ?? \"\";\n const language = languages.includes(languageParam) ? languageParam : languages[0];\n const filters: { letter?: string; sort?: GroupSort; language?: string } = { letter, sort: sort === \"name\" ? undefined : sort, language: language === languages[0] ? undefined : language };\n const filter = (next: Partial<typeof filters>) => loc.go(kind, { ...filters, ...next });\n const groups = useGroups({ kind, letter, sort, language, contentKind, pageSize }, { client, management: true });\n const label = labelOf(labels, kind);\n return (\n <div className=\"grid gap-4\" data-ckui=\"group-list\" data-kind={kind}>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h2 className=\"text-base font-semibold\">{label}</h2>\n <Button size=\"sm\" onClick={() => loc.go(`${kind}/new`)}>\n {t(\"groupManager.new\", { kind: label })}\n </Button>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <Select value={sort} onValueChange={(v) => filter({ sort: v === \"name\" ? undefined : (String(v) as GroupSort) })}>\n <SelectTrigger size=\"sm\" aria-label={t(\"groupManager.sort\")}>\n <SelectValue>{(v: string) => t(`groupManager.sort_${v as GroupSort}`)}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {SORTS.map((s) => (\n <SelectItem key={s} value={s}>\n {t(`groupManager.sort_${s}`)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {languages.length > 1 && (\n <Select value={language} onValueChange={(v) => filter({ language: v === languages[0] ? undefined : String(v) })}>\n <SelectTrigger size=\"sm\" aria-label={t(\"groupManager.language\")}>\n <SelectValue>{(v: string) => languageName(v)}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {languages.map((l) => (\n <SelectItem key={l} value={l}>\n {languageName(l)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n <nav aria-label={t(\"groups.byLetter\")} className=\"flex flex-wrap gap-1\" data-ckui=\"letters\">\n {[undefined, ...LETTERS].map((l) => (\n <Button\n key={l ?? \"all\"}\n size=\"xs\"\n variant={letter === l ? \"default\" : \"ghost\"}\n aria-pressed={letter === l}\n aria-label={l === undefined ? undefined : l === \"#\" ? t(\"groups.otherLetters\") : l.toUpperCase()}\n className=\"min-w-7\"\n onClick={() => filter({ letter: l })}\n >\n {l === undefined ? t(\"groups.all\") : l.toUpperCase()}\n </Button>\n ))}\n </nav>\n {groups.loading && !groups.items.length ? (\n <Rows />\n ) : groups.error && !groups.items.length ? (\n <LoadFailed error={groups.error} message={t(\"groupManager.loadFailed\")} retry={groups.reload} />\n ) : !groups.items.length ? (\n <p className=\"text-muted-foreground\">{t(\"groups.empty\")}</p>\n ) : (\n <ul className=\"grid divide-y divide-border rounded-lg border border-border\" aria-busy={groups.loading}>\n {groups.items.map((g) => (\n <li key={g.id} className=\"flex flex-wrap items-center gap-3 p-3\" data-ckui=\"group-row\" data-slug={g.slug}>\n <div className=\"grid min-w-0 flex-1 gap-0.5\">\n <button type=\"button\" className=\"text-left font-medium break-words hover:underline\" onClick={() => loc.go(`${kind}/${g.id}`)}>\n {g.name || g.slug}\n </button>\n <span className=\"text-xs text-muted-foreground\">{g.slug}</span>\n {g.internal && <Badge variant=\"secondary\" className=\"justify-self-start\">{t(\"groupEditor.internal\")}</Badge>}\n </div>\n <span className=\"text-sm text-muted-foreground tabular-nums\" title={t(\"groupManager.count\")}>\n {new Intl.NumberFormat(m.language).format(g.count)}\n </span>\n </li>\n ))}\n </ul>\n )}\n {groups.hasMore && (\n <div className=\"grid justify-items-center gap-2\">\n {groups.error && <p role=\"alert\" className=\"text-destructive\">{contentError(m, groups.error)}</p>}\n <Button variant=\"outline\" size=\"sm\" disabled={groups.loadingMore} onClick={() => void groups.loadMore()}>\n {groups.loadingMore ? t(\"groups.loading\") : groups.error ? t(\"common.retry\") : t(\"groups.loadMore\")}\n </Button>\n </div>\n )}\n </div>\n );\n}\n\nfunction Create({ loc, kind, client, labels, onError }: Ctx) {\n return (\n <div className=\"grid gap-4\">\n <Back loc={loc} kind={kind} />\n <GroupEditor\n management\n id={null}\n kind={kind}\n labels={labels}\n client={client}\n onError={onError}\n onCreated={(g) => loc.go(`${kind}/${g.id}`, undefined, { replace: true })}\n />\n </div>\n );\n}\n\nfunction Edit({ loc, kind, client, labels, onError, renderContent, id }: Ctx & { id: string }) {\n return (\n <div className=\"grid gap-4\">\n <Back loc={loc} kind={kind} />\n <GroupEditor\n management\n id={id}\n kind={kind}\n labels={labels}\n client={client}\n onError={onError}\n onMerged={(r) => loc.go(`${kind}/${r.into}`, undefined, { replace: true })}\n >\n {(group) => (\n <GroupMembers\n group={group}\n labels={labels}\n client={client}\n onError={onError}\n renderContent={renderContent}\n onUseCover={(content) => client.entities.update(group.id, { cover: { content_id: content } })}\n />\n )}\n </GroupEditor>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -213,12 +213,14 @@ function k({ user: r, client: a, itemHref: o, pageSize: l = 10, onError: d }) {
|
|
|
213
213
|
className: "text-muted-foreground",
|
|
214
214
|
children: p("standing.noComments")
|
|
215
215
|
}),
|
|
216
|
-
h.hasMore && /* @__PURE__ */
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
216
|
+
h.hasMore && /* @__PURE__ */ x("div", {
|
|
217
|
+
className: "grid justify-items-center gap-2",
|
|
218
|
+
children: [/* @__PURE__ */ b(s, { error: h.error }), /* @__PURE__ */ b(t, {
|
|
219
|
+
variant: "outline",
|
|
220
|
+
disabled: h.loadingMore,
|
|
221
|
+
onClick: () => void h.loadMore(),
|
|
222
|
+
children: h.error ? p("common.retry") : p("moderation.loadMore")
|
|
223
|
+
})]
|
|
222
224
|
})
|
|
223
225
|
]
|
|
224
226
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-comment-standing.js","names":[],"sources":["../../../src/components/admin/user-comment-standing.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useState } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport type { AdminComment } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useFlowClient, useStaffAccess, type FlowClientOptions, type StaffAccess } from \"../../react/admin.js\";\nimport { useAdminComments, useGlobalCommentBan } from \"../../react/comments.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Skeleton } from \"#ckui/ui/skeleton\";\nimport { CommentBanDialog } from \"../social/bans.js\";\nimport { ConfirmDialog, ErrorLine, formatDate, RelativeTime, useAction } from \"../social/parts.js\";\nimport { AdminRoot, LoadFailed, NoAccess, Rows, type AdminFlowProps } from \"./shell.js\";\n\nexport interface UserCommentStandingProps extends AdminFlowProps {\n /** The user's id. */\n user: string;\n /** Their name in headings and dialogs; else their username once known. */\n name?: string;\n /** A link to a commented item. */\n itemHref?: (item: { kind: string; id: string }) => string | undefined;\n /** Comments per page. Default 10. */\n pageSize?: number;\n}\n\ntype StandingArea = \"bans\" | \"comments\";\nconst AREAS: readonly StandingArea[] = [\"bans\", \"comments\"];\n\n/** Whether the caller may manage site-wide comment bans and moderate comments: what UserCommentStanding shows. */\nexport function useUserCommentStandingAccess(o: FlowClientOptions = {}): StaffAccess<StandingArea> {\n return useStaffAccess(AREAS, o);\n}\n\n/**\n * A user page's comment panel: the person's site-wide comment ban (ban,\n * replace, lift) and their recent comments (delete, restore), each as the\n * caller's permissions allow.\n */\nexport function UserCommentStanding({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: UserCommentStandingProps) {\n const client = useFlowClient({ client: own, baseUrl, auth });\n return (\n <AdminRoot flow=\"user-comment-standing\" appearance={appearance} className={className} language={language} messages={messages} guard={guard}>\n <Standing {...p} client={client} />\n </AdminRoot>\n );\n}\n\ntype Inner = Omit<UserCommentStandingProps, \"client\" | \"baseUrl\" | \"auth\" | \"guard\" | \"appearance\" | \"className\" | \"language\" | \"messages\"> & { client: ContentKitClient };\n\nfunction Standing(p: Inner) {\n const { t } = useMessages();\n const access = useUserCommentStandingAccess({ client: p.client });\n const title = <h3 className=\"text-base font-semibold\">{t(\"standing.title\")}</h3>;\n if (access.loading)\n return (\n <>\n {title}\n <Rows count={2} />\n </>\n );\n if (!access.any)\n return (\n <>\n {title}\n <NoAccess>{t(\"standing.noAccess\")}</NoAccess>\n </>\n );\n return (\n <>\n {title}\n {access.allowed.bans && <BanCard {...p} />}\n {access.allowed.comments && <Recent {...p} />}\n </>\n );\n}\n\nfunction BanCard({ user, name, client, onError }: Inner) {\n const m = useMessages();\n const { t } = m;\n const g = useGlobalCommentBan(user, { client });\n const [editing, setEditing] = useState(false);\n const [lifting, setLifting] = useState(false);\n const lift = useAction(\"ban.lift\", onError);\n const who = name ?? g.ban?.user?.username ?? user;\n const ban = g.ban;\n return (\n <section className=\"grid gap-2 rounded-lg border border-border p-3\" data-ckui=\"standing-ban\" aria-busy={g.loading}>\n <h4 className=\"font-medium\">{t(\"standing.ban\")}</h4>\n {g.loading && !ban ? (\n <Skeleton className=\"h-10 w-full\" />\n ) : g.error ? (\n <LoadFailed error={g.error} message={t(\"standing.banLoadFailed\")} retry={g.reload} />\n ) : !ban ? (\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <p className=\"text-muted-foreground\">{t(\"standing.notBanned\")}</p>\n <Button size=\"sm\" variant=\"destructive\" onClick={() => setEditing(true)}>\n {t(\"comments.ban\")}\n </Button>\n </div>\n ) : (\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"grid min-w-0 gap-0.5\">\n <div>\n <Badge variant={ban.expired ? \"outline\" : \"destructive\"}>{t(ban.expired ? \"bans.expired\" : \"bans.active\")}</Badge>\n </div>\n {ban.reason && <p className=\"break-words\">{t(\"bans.reason\", { reason: ban.reason })}</p>}\n <p className=\"text-muted-foreground\">\n {ban.until ? t(\"bans.until\", { date: formatDate(m, ban.until) }) : t(\"bans.untilLifted\")} · {t(\"bans.bannedAt\", { date: formatDate(m, ban.banned_at, false) })}\n </p>\n </div>\n <div className=\"flex gap-2\">\n <Button size=\"sm\" variant=\"outline\" onClick={() => setEditing(true)}>\n {t(\"bans.update\")}\n </Button>\n <Button size=\"sm\" variant=\"destructive\" onClick={() => setLifting(true)}>\n {t(\"bans.lift\")}\n </Button>\n </div>\n </div>\n )}\n <ErrorLine error={lift.error} />\n {editing && <CommentBanDialog open onOpenChange={setEditing} user={{ id: user, name: who }} scopes={[\"global\"]} ban={ban} client={client} onError={onError} />}\n <ConfirmDialog\n open={lifting}\n onOpenChange={setLifting}\n title={t(\"bans.liftTitle\", { name: who })}\n description={t(\"bans.liftBody\")}\n confirm={t(\"bans.lift\")}\n onConfirm={() => void lift.run(g.lift)}\n />\n </section>\n );\n}\n\nfunction status(c: AdminComment): \"deleted\" | \"held\" | \"rejected\" | \"published\" {\n if (c.deleted) return \"deleted\";\n return c.moderation === \"held\" ? \"held\" : c.moderation === \"rejected\" ? \"rejected\" : \"published\";\n}\n\nfunction Recent({ user, client, itemHref, pageSize = 10, onError }: Inner) {\n const { t } = useMessages();\n const list = useAdminComments({ author: user, pageSize, client });\n const del = useAction(\"comment.delete\", onError);\n const restore = useAction(\"comment.restore\", onError);\n return (\n <section className=\"grid gap-2\" data-ckui=\"standing-comments\">\n <h4 className=\"font-medium\">{t(\"standing.comments\")}</h4>\n <ErrorLine error={del.error ?? restore.error} />\n {list.loading && !list.items.length ? (\n <Rows count={2} />\n ) : list.error && !list.items.length ? (\n <LoadFailed error={list.error} message={t(\"moderation.loadFailed\")} retry={list.reload} />\n ) : !list.items.length ? (\n <p className=\"text-muted-foreground\">{t(\"standing.noComments\")}</p>\n ) : (\n <ul className=\"grid divide-y divide-border rounded-lg border border-border\">\n {list.items.map((c) => {\n const s = status(c);\n const href = itemHref?.({ kind: c.content_kind, id: c.content_id });\n return (\n <li key={c.id} className=\"grid gap-1.5 p-3\" data-ckui=\"admin-comment\" data-status={s}>\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground\">\n <RelativeTime at={c.created_at} />\n <Badge variant={s === \"published\" ? \"outline\" : s === \"held\" ? \"secondary\" : \"destructive\"}>{t(`moderation.${s}`)}</Badge>\n <span>\n {t(\"moderation.on\")}{\" \"}\n {href ? (\n <a className=\"hover:underline\" href={href}>\n {c.content_kind}/{c.content_id}\n </a>\n ) : (\n `${c.content_kind}/${c.content_id}`\n )}\n </span>\n </div>\n <p className={cn(\"line-clamp-3 break-words whitespace-pre-wrap\", c.deleted && \"text-muted-foreground line-through\")}>{c.body}</p>\n <div>\n {c.deleted ? (\n <Button size=\"sm\" variant=\"outline\" disabled={restore.pending} onClick={() => void restore.run(() => client.comments.restore(c.id))}>\n {t(\"moderation.restore\")}\n </Button>\n ) : (\n <Button size=\"sm\" variant=\"destructive\" disabled={del.pending} onClick={() => void del.run(() => client.comments.delete(c.id))}>\n {t(\"moderation.delete\")}\n </Button>\n )}\n </div>\n </li>\n );\n })}\n </ul>\n )}\n {list.hasMore && (\n <Button variant=\"outline\" className=\"justify-self-center\" disabled={list.loadingMore} onClick={() => void list.loadMore()}>\n {t(\"moderation.loadMore\")}\n </Button>\n )}\n </section>\n );\n}\n\n"],"mappings":";;;;;;;;;;;;;AA0BA,IAAM,IAAiC,CAAC,QAAQ,UAAU;AAG1D,SAAgB,EAA6B,IAAuB,CAAC,GAA8B;CACjG,OAAO,EAAe,GAAO,CAAC;AAChC;AAOA,SAAgB,EAAoB,EAAE,QAAQ,GAAK,YAAS,SAAM,UAAO,eAAY,cAAW,aAAU,aAAU,GAAG,KAA+B;CACpJ,IAAM,IAAS,EAAc;EAAE,QAAQ;EAAK;EAAS;CAAK,CAAC;CAC3D,OACE,kBAAC,GAAD;EAAW,MAAK;EAAoC;EAAuB;EAAqB;EAAoB;EAAiB;EACnI,UAAA,kBAAC,GAAD;GAAU,GAAI;GAAW;EAAS,CAAA;CACzB,CAAA;AAEf;AAIA,SAAS,EAAS,GAAU;CAC1B,IAAM,EAAE,SAAM,EAAY,GACpB,IAAS,EAA6B,EAAE,QAAQ,EAAE,OAAO,CAAC,GAC1D,IAAQ,kBAAC,MAAD;EAAI,WAAU;EAA2B,UAAA,EAAE,gBAAgB;CAAM,CAAA;CAe/E,OAdI,EAAO,UAEP,kBAAA,GAAA,EAAA,UAAA,CACG,GACD,kBAAC,GAAD,EAAM,OAAO,EAAI,CAAA,CACjB,EAAA,CAAA,IAED,EAAO,MAQV,kBAAA,GAAA,EAAA,UAAA;EACG;EACA,EAAO,QAAQ,QAAQ,kBAAC,GAAD,EAAS,GAAI,EAAI,CAAA;EACxC,EAAO,QAAQ,YAAY,kBAAC,GAAD,EAAQ,GAAI,EAAI,CAAA;CAC5C,EAAA,CAAA,IAVA,kBAAA,GAAA,EAAA,UAAA,CACG,GACD,kBAAC,GAAD,EAAA,UAAW,EAAE,mBAAmB,EAAY,CAAA,CAC5C,EAAA,CAAA;AASR;AAEA,SAAS,EAAQ,EAAE,SAAM,SAAM,WAAQ,cAAkB;CACvD,IAAM,IAAI,EAAY,GAChB,EAAE,SAAM,GACR,IAAI,EAAoB,GAAM,EAAE,UAAO,CAAC,GACxC,CAAC,GAAS,KAAc,EAAS,EAAK,GACtC,CAAC,GAAS,KAAc,EAAS,EAAK,GACtC,IAAO,EAAU,YAAY,CAAO,GACpC,IAAM,KAAQ,EAAE,KAAK,MAAM,YAAY,GACvC,IAAM,EAAE;CACd,OACE,kBAAC,WAAD;EAAS,WAAU;EAAiD,aAAU;EAAe,aAAW,EAAE;EAA1G,UAAA;GACE,kBAAC,MAAD;IAAI,WAAU;IAAe,UAAA,EAAE,cAAc;GAAM,CAAA;GAClD,EAAE,WAAW,CAAC,IACb,kBAAC,GAAD,EAAU,WAAU,cAAe,CAAA,IACjC,EAAE,QACJ,kBAAC,GAAD;IAAY,OAAO,EAAE;IAAO,SAAS,EAAE,wBAAwB;IAAG,OAAO,EAAE;GAAS,CAAA,IACjF,IAQH,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,kBAAC,OAAD,EAAA,UACE,kBAAC,GAAD;OAAO,SAAS,EAAI,UAAU,YAAY;OAAgB,UAAA,EAAE,EAAI,UAAU,iBAAiB,aAAa;MAAS,CAAA,EAC9G,CAAA;MACJ,EAAI,UAAU,kBAAC,KAAD;OAAG,WAAU;OAAe,UAAA,EAAE,eAAe,EAAE,QAAQ,EAAI,OAAO,CAAC;MAAK,CAAA;MACvF,kBAAC,KAAD;OAAG,WAAU;OAAb,UAAA;QACG,EAAI,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAW,GAAG,EAAI,KAAK,EAAE,CAAC,IAAI,EAAE,kBAAkB;QAAE;QAAI,EAAE,iBAAiB,EAAE,MAAM,EAAW,GAAG,EAAI,WAAW,EAAK,EAAE,CAAC;OAC5J;;KACA;IACL,CAAA,GAAA,kBAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,kBAAC,GAAD;MAAQ,MAAK;MAAK,SAAQ;MAAU,eAAe,EAAW,EAAI;MAC/D,UAAA,EAAE,aAAa;KACV,CAAA,GACR,kBAAC,GAAD;MAAQ,MAAK;MAAK,SAAQ;MAAc,eAAe,EAAW,EAAI;MACnE,UAAA,EAAE,WAAW;KACR,CAAA,CACL;IACF,CAAA,CAAA;QAzBL,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,kBAAC,KAAD;KAAG,WAAU;KAAyB,UAAA,EAAE,oBAAoB;IAAK,CAAA,GACjE,kBAAC,GAAD;KAAQ,MAAK;KAAK,SAAQ;KAAc,eAAe,EAAW,EAAI;KACnE,UAAA,EAAE,cAAc;IACX,CAAA,CACL;GAEL,CAAA;GAoBF,kBAAC,GAAD,EAAW,OAAO,EAAK,MAAQ,CAAA;GAC9B,KAAW,kBAAC,GAAD;IAAkB,MAAA;IAAK,cAAc;IAAY,MAAM;KAAE,IAAI;KAAM,MAAM;IAAI;IAAG,QAAQ,CAAC,QAAQ;IAAQ;IAAa;IAAiB;GAAU,CAAA;GAC7J,kBAAC,GAAD;IACE,MAAM;IACN,cAAc;IACd,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAI,CAAC;IACxC,aAAa,EAAE,eAAe;IAC9B,SAAS,EAAE,WAAW;IACtB,iBAAiB,KAAK,EAAK,IAAI,EAAE,IAAI;GACtC,CAAA;EACM;;AAEb;AAEA,SAAS,EAAO,GAAgE;CAE9E,OADI,EAAE,UAAgB,YACf,EAAE,eAAe,SAAS,SAAS,EAAE,eAAe,aAAa,aAAa;AACvF;AAEA,SAAS,EAAO,EAAE,SAAM,WAAQ,aAAU,cAAW,IAAI,cAAkB;CACzE,IAAM,EAAE,SAAM,EAAY,GACpB,IAAO,EAAiB;EAAE,QAAQ;EAAM;EAAU;CAAO,CAAC,GAC1D,IAAM,EAAU,kBAAkB,CAAO,GACzC,IAAU,EAAU,mBAAmB,CAAO;CACpD,OACE,kBAAC,WAAD;EAAS,WAAU;EAAa,aAAU;EAA1C,UAAA;GACE,kBAAC,MAAD;IAAI,WAAU;IAAe,UAAA,EAAE,mBAAmB;GAAM,CAAA;GACxD,kBAAC,GAAD,EAAW,OAAO,EAAI,SAAS,EAAQ,MAAQ,CAAA;GAC9C,EAAK,WAAW,CAAC,EAAK,MAAM,SAC3B,kBAAC,GAAD,EAAM,OAAO,EAAI,CAAA,IACf,EAAK,SAAS,CAAC,EAAK,MAAM,SAC5B,kBAAC,GAAD;IAAY,OAAO,EAAK;IAAO,SAAS,EAAE,uBAAuB;IAAG,OAAO,EAAK;GAAS,CAAA,IACtF,EAAK,MAAM,SAGd,kBAAC,MAAD;IAAI,WAAU;IACX,UAAA,EAAK,MAAM,KAAK,MAAM;KACrB,IAAM,IAAI,EAAO,CAAC,GACZ,IAAO,IAAW;MAAE,MAAM,EAAE;MAAc,IAAI,EAAE;KAAW,CAAC;KAClE,OACE,kBAAC,MAAD;MAAe,WAAU;MAAmB,aAAU;MAAgB,eAAa;MAAnF,UAAA;OACE,kBAAC,OAAD;QAAK,WAAU;QAAf,UAAA;SACE,kBAAC,GAAD,EAAc,IAAI,EAAE,WAAa,CAAA;SACjC,kBAAC,GAAD;UAAO,SAAS,MAAM,cAAc,YAAY,MAAM,SAAS,cAAc;UAAgB,UAAA,EAAE,cAAc,GAAG;SAAS,CAAA;SACzH,kBAAC,QAAD,EAAA,UAAA;UACG,EAAE,eAAe;UAAG;UACpB,IACC,kBAAC,KAAD;WAAG,WAAU;WAAwB;WAArC,UAAA;YACG,EAAE;YAAa;YAAE,EAAE;WACnB;UAEH,CAAA,IAAA,GAAG,EAAE,aAAa,GAAG,EAAE;SAErB,EAAA,CAAA;QACH;;OACL,kBAAC,KAAD;QAAG,WAAW,EAAG,gDAAgD,EAAE,WAAW,oCAAoC;QAAI,UAAA,EAAE;OAAQ,CAAA;OAChI,kBAAC,OAAD,EAAA,UACG,EAAE,UACD,kBAAC,GAAD;QAAQ,MAAK;QAAK,SAAQ;QAAU,UAAU,EAAQ;QAAS,eAAe,KAAK,EAAQ,UAAU,EAAO,SAAS,QAAQ,EAAE,EAAE,CAAC;QAC/H,UAAA,EAAE,oBAAoB;OACjB,CAAA,IAER,kBAAC,GAAD;QAAQ,MAAK;QAAK,SAAQ;QAAc,UAAU,EAAI;QAAS,eAAe,KAAK,EAAI,UAAU,EAAO,SAAS,OAAO,EAAE,EAAE,CAAC;QAC1H,UAAA,EAAE,mBAAmB;OAChB,CAAA,EAEP,CAAA;MACH;KA3BK,GAAA,EAAE,EA2BP;IAER,CAAC;GACC,CAAA,IArCJ,kBAAC,KAAD;IAAG,WAAU;IAAyB,UAAA,EAAE,qBAAqB;GAAK,CAAA;GAuCnE,EAAK,WACJ,kBAAC,GAAD;IAAQ,SAAQ;IAAU,WAAU;IAAsB,UAAU,EAAK;IAAa,eAAe,KAAK,EAAK,SAAS;IACrH,UAAA,EAAE,qBAAqB;GAClB,CAAA;EAEH;;AAEb"}
|
|
1
|
+
{"version":3,"file":"user-comment-standing.js","names":[],"sources":["../../../src/components/admin/user-comment-standing.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useState } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport type { AdminComment } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useFlowClient, useStaffAccess, type FlowClientOptions, type StaffAccess } from \"../../react/admin.js\";\nimport { useAdminComments, useGlobalCommentBan } from \"../../react/comments.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Skeleton } from \"#ckui/ui/skeleton\";\nimport { CommentBanDialog } from \"../social/bans.js\";\nimport { ConfirmDialog, ErrorLine, formatDate, RelativeTime, useAction } from \"../social/parts.js\";\nimport { AdminRoot, LoadFailed, NoAccess, Rows, type AdminFlowProps } from \"./shell.js\";\n\nexport interface UserCommentStandingProps extends AdminFlowProps {\n /** The user's id. */\n user: string;\n /** Their name in headings and dialogs; else their username once known. */\n name?: string;\n /** A link to a commented item. */\n itemHref?: (item: { kind: string; id: string }) => string | undefined;\n /** Comments per page. Default 10. */\n pageSize?: number;\n}\n\ntype StandingArea = \"bans\" | \"comments\";\nconst AREAS: readonly StandingArea[] = [\"bans\", \"comments\"];\n\n/** Whether the caller may manage site-wide comment bans and moderate comments: what UserCommentStanding shows. */\nexport function useUserCommentStandingAccess(o: FlowClientOptions = {}): StaffAccess<StandingArea> {\n return useStaffAccess(AREAS, o);\n}\n\n/**\n * A user page's comment panel: the person's site-wide comment ban (ban,\n * replace, lift) and their recent comments (delete, restore), each as the\n * caller's permissions allow.\n */\nexport function UserCommentStanding({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: UserCommentStandingProps) {\n const client = useFlowClient({ client: own, baseUrl, auth });\n return (\n <AdminRoot flow=\"user-comment-standing\" appearance={appearance} className={className} language={language} messages={messages} guard={guard}>\n <Standing {...p} client={client} />\n </AdminRoot>\n );\n}\n\ntype Inner = Omit<UserCommentStandingProps, \"client\" | \"baseUrl\" | \"auth\" | \"guard\" | \"appearance\" | \"className\" | \"language\" | \"messages\"> & { client: ContentKitClient };\n\nfunction Standing(p: Inner) {\n const { t } = useMessages();\n const access = useUserCommentStandingAccess({ client: p.client });\n const title = <h3 className=\"text-base font-semibold\">{t(\"standing.title\")}</h3>;\n if (access.loading)\n return (\n <>\n {title}\n <Rows count={2} />\n </>\n );\n if (!access.any)\n return (\n <>\n {title}\n <NoAccess>{t(\"standing.noAccess\")}</NoAccess>\n </>\n );\n return (\n <>\n {title}\n {access.allowed.bans && <BanCard {...p} />}\n {access.allowed.comments && <Recent {...p} />}\n </>\n );\n}\n\nfunction BanCard({ user, name, client, onError }: Inner) {\n const m = useMessages();\n const { t } = m;\n const g = useGlobalCommentBan(user, { client });\n const [editing, setEditing] = useState(false);\n const [lifting, setLifting] = useState(false);\n const lift = useAction(\"ban.lift\", onError);\n const who = name ?? g.ban?.user?.username ?? user;\n const ban = g.ban;\n return (\n <section className=\"grid gap-2 rounded-lg border border-border p-3\" data-ckui=\"standing-ban\" aria-busy={g.loading}>\n <h4 className=\"font-medium\">{t(\"standing.ban\")}</h4>\n {g.loading && !ban ? (\n <Skeleton className=\"h-10 w-full\" />\n ) : g.error ? (\n <LoadFailed error={g.error} message={t(\"standing.banLoadFailed\")} retry={g.reload} />\n ) : !ban ? (\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <p className=\"text-muted-foreground\">{t(\"standing.notBanned\")}</p>\n <Button size=\"sm\" variant=\"destructive\" onClick={() => setEditing(true)}>\n {t(\"comments.ban\")}\n </Button>\n </div>\n ) : (\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"grid min-w-0 gap-0.5\">\n <div>\n <Badge variant={ban.expired ? \"outline\" : \"destructive\"}>{t(ban.expired ? \"bans.expired\" : \"bans.active\")}</Badge>\n </div>\n {ban.reason && <p className=\"break-words\">{t(\"bans.reason\", { reason: ban.reason })}</p>}\n <p className=\"text-muted-foreground\">\n {ban.until ? t(\"bans.until\", { date: formatDate(m, ban.until) }) : t(\"bans.untilLifted\")} · {t(\"bans.bannedAt\", { date: formatDate(m, ban.banned_at, false) })}\n </p>\n </div>\n <div className=\"flex gap-2\">\n <Button size=\"sm\" variant=\"outline\" onClick={() => setEditing(true)}>\n {t(\"bans.update\")}\n </Button>\n <Button size=\"sm\" variant=\"destructive\" onClick={() => setLifting(true)}>\n {t(\"bans.lift\")}\n </Button>\n </div>\n </div>\n )}\n <ErrorLine error={lift.error} />\n {editing && <CommentBanDialog open onOpenChange={setEditing} user={{ id: user, name: who }} scopes={[\"global\"]} ban={ban} client={client} onError={onError} />}\n <ConfirmDialog\n open={lifting}\n onOpenChange={setLifting}\n title={t(\"bans.liftTitle\", { name: who })}\n description={t(\"bans.liftBody\")}\n confirm={t(\"bans.lift\")}\n onConfirm={() => void lift.run(g.lift)}\n />\n </section>\n );\n}\n\nfunction status(c: AdminComment): \"deleted\" | \"held\" | \"rejected\" | \"published\" {\n if (c.deleted) return \"deleted\";\n return c.moderation === \"held\" ? \"held\" : c.moderation === \"rejected\" ? \"rejected\" : \"published\";\n}\n\nfunction Recent({ user, client, itemHref, pageSize = 10, onError }: Inner) {\n const { t } = useMessages();\n const list = useAdminComments({ author: user, pageSize, client });\n const del = useAction(\"comment.delete\", onError);\n const restore = useAction(\"comment.restore\", onError);\n return (\n <section className=\"grid gap-2\" data-ckui=\"standing-comments\">\n <h4 className=\"font-medium\">{t(\"standing.comments\")}</h4>\n <ErrorLine error={del.error ?? restore.error} />\n {list.loading && !list.items.length ? (\n <Rows count={2} />\n ) : list.error && !list.items.length ? (\n <LoadFailed error={list.error} message={t(\"moderation.loadFailed\")} retry={list.reload} />\n ) : !list.items.length ? (\n <p className=\"text-muted-foreground\">{t(\"standing.noComments\")}</p>\n ) : (\n <ul className=\"grid divide-y divide-border rounded-lg border border-border\">\n {list.items.map((c) => {\n const s = status(c);\n const href = itemHref?.({ kind: c.content_kind, id: c.content_id });\n return (\n <li key={c.id} className=\"grid gap-1.5 p-3\" data-ckui=\"admin-comment\" data-status={s}>\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground\">\n <RelativeTime at={c.created_at} />\n <Badge variant={s === \"published\" ? \"outline\" : s === \"held\" ? \"secondary\" : \"destructive\"}>{t(`moderation.${s}`)}</Badge>\n <span>\n {t(\"moderation.on\")}{\" \"}\n {href ? (\n <a className=\"hover:underline\" href={href}>\n {c.content_kind}/{c.content_id}\n </a>\n ) : (\n `${c.content_kind}/${c.content_id}`\n )}\n </span>\n </div>\n <p className={cn(\"line-clamp-3 break-words whitespace-pre-wrap\", c.deleted && \"text-muted-foreground line-through\")}>{c.body}</p>\n <div>\n {c.deleted ? (\n <Button size=\"sm\" variant=\"outline\" disabled={restore.pending} onClick={() => void restore.run(() => client.comments.restore(c.id))}>\n {t(\"moderation.restore\")}\n </Button>\n ) : (\n <Button size=\"sm\" variant=\"destructive\" disabled={del.pending} onClick={() => void del.run(() => client.comments.delete(c.id))}>\n {t(\"moderation.delete\")}\n </Button>\n )}\n </div>\n </li>\n );\n })}\n </ul>\n )}\n {list.hasMore && (\n <div className=\"grid justify-items-center gap-2\">\n <ErrorLine error={list.error} />\n <Button variant=\"outline\" disabled={list.loadingMore} onClick={() => void list.loadMore()}>\n {list.error ? t(\"common.retry\") : t(\"moderation.loadMore\")}\n </Button>\n </div>\n )}\n </section>\n );\n}\n\n"],"mappings":"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"}
|
|
@@ -94,7 +94,7 @@ function D({ card: i, client: c, href: u, onOpen: d, shape: p = "portrait", aspe
|
|
|
94
94
|
}), [fe, pe] = g(null);
|
|
95
95
|
return /* @__PURE__ */ v("article", {
|
|
96
96
|
ref: pe,
|
|
97
|
-
className: y("group/card grid content-start gap-2", le),
|
|
97
|
+
className: y("group/card relative grid content-start gap-2", le),
|
|
98
98
|
"data-ckui": "content-card",
|
|
99
99
|
"data-id": i.id,
|
|
100
100
|
"data-locked": V ? "" : void 0,
|
|
@@ -276,7 +276,7 @@ function k({ url: e, name: t, variant: n = "chips" }) {
|
|
|
276
276
|
function A({ card: t, href: n, onOpen: i, tiers: a, labels: o, byline: s, owner: l = !0, ownerHref: u, loadMedia: d = !0, showCounts: f = !0, decorate: p, children: h, className: g }) {
|
|
277
277
|
let x = r(), D = n ? n(t) : t.path, k = E(D, m(() => i?.(t), [i, t])), A = T(x, t.access, a, o), j = t.title || t.code;
|
|
278
278
|
return /* @__PURE__ */ v("article", {
|
|
279
|
-
className: y("grid gap-3 rounded-lg border border-border p-3", g),
|
|
279
|
+
className: y("relative grid gap-3 rounded-lg border border-border p-3", g),
|
|
280
280
|
"data-ckui": "post-card",
|
|
281
281
|
"data-id": t.id,
|
|
282
282
|
"data-locked": A ? "" : void 0,
|
|
@@ -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 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?.completed ? 1 : 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.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":"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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?.completed ? 1 : 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 relative 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.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(\"relative 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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"}
|
|
@@ -8,11 +8,11 @@ interface Common {
|
|
|
8
8
|
labels?: Labels;
|
|
9
9
|
}
|
|
10
10
|
/** Listing and the kind's declared attributes. */
|
|
11
|
-
export declare function ContentDetailsEditor(
|
|
11
|
+
export declare function ContentDetailsEditor(p: Common & {
|
|
12
12
|
content: Content;
|
|
13
13
|
}): import('react').JSX.Element;
|
|
14
14
|
/** Who may open the content and what it sells for. */
|
|
15
|
-
export declare function AccessEditor(
|
|
15
|
+
export declare function AccessEditor(p: Common & {
|
|
16
16
|
content: Content;
|
|
17
17
|
}): import('react').JSX.Element;
|
|
18
18
|
/** A version's name in the editor: its label, else its languages and traits. */
|