@openrails/contentkit-ui 0.71.0 → 0.72.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.
Files changed (120) hide show
  1. package/README.md +98 -10
  2. package/dist/client/content/bans.d.ts +2 -0
  3. package/dist/client/content/bans.js +24 -12
  4. package/dist/client/content/bans.js.map +1 -1
  5. package/dist/client/content/comments.d.ts +2 -0
  6. package/dist/client/content/comments.js.map +1 -1
  7. package/dist/client/content/polls.d.ts +9 -2
  8. package/dist/client/content/polls.js.map +1 -1
  9. package/dist/client/generated/routes.d.ts +28 -2
  10. package/dist/client/generated/routes.js +40 -0
  11. package/dist/client/generated/routes.js.map +1 -1
  12. package/dist/client/generated/wire.d.ts +4 -0
  13. package/dist/client/generated/wire.js.map +1 -1
  14. package/dist/client/index.d.ts +2 -2
  15. package/dist/client/taxonomy.d.ts +5 -1
  16. package/dist/client/taxonomy.js +7 -0
  17. package/dist/client/taxonomy.js.map +1 -1
  18. package/dist/components/admin/poll-manager.d.ts +28 -0
  19. package/dist/components/admin/poll-manager.js +297 -0
  20. package/dist/components/admin/poll-manager.js.map +1 -0
  21. package/dist/components/admin/post-manager.d.ts +45 -0
  22. package/dist/components/admin/post-manager.js +602 -0
  23. package/dist/components/admin/post-manager.js.map +1 -0
  24. package/dist/components/admin/shell.d.ts +73 -0
  25. package/dist/components/admin/shell.js +148 -0
  26. package/dist/components/admin/shell.js.map +1 -0
  27. package/dist/components/admin/taxonomy-manager.d.ts +54 -0
  28. package/dist/components/admin/taxonomy-manager.js +976 -0
  29. package/dist/components/admin/taxonomy-manager.js.map +1 -0
  30. package/dist/components/admin/user-comment-standing.d.ts +25 -0
  31. package/dist/components/admin/user-comment-standing.js +229 -0
  32. package/dist/components/admin/user-comment-standing.js.map +1 -0
  33. package/dist/components/social/bans.d.ts +5 -2
  34. package/dist/components/social/bans.js +132 -134
  35. package/dist/components/social/bans.js.map +1 -1
  36. package/dist/components/social/index.d.ts +1 -1
  37. package/dist/components/social/moderation.d.ts +11 -13
  38. package/dist/components/social/moderation.js +299 -218
  39. package/dist/components/social/moderation.js.map +1 -1
  40. package/dist/components/social/parts.d.ts +3 -2
  41. package/dist/components/social/parts.js +74 -48
  42. package/dist/components/social/parts.js.map +1 -1
  43. package/dist/components/social/poll-editor.js +1 -0
  44. package/dist/components/social/poll-editor.js.map +1 -1
  45. package/dist/editor/index.d.ts +1 -0
  46. package/dist/editor/rich-text-editor.d.ts +13 -0
  47. package/dist/editor/rich-text-editor.js +228 -0
  48. package/dist/editor/rich-text-editor.js.map +1 -0
  49. package/dist/editor.js +2 -0
  50. package/dist/index.d.ts +6 -0
  51. package/dist/index.js +13 -8
  52. package/dist/locales/admin/de.d.ts +2 -0
  53. package/dist/locales/admin/de.js +176 -0
  54. package/dist/locales/admin/de.js.map +1 -0
  55. package/dist/locales/admin/en.d.ts +171 -0
  56. package/dist/locales/admin/en.js +176 -0
  57. package/dist/locales/admin/en.js.map +1 -0
  58. package/dist/locales/admin/es.d.ts +2 -0
  59. package/dist/locales/admin/es.js +176 -0
  60. package/dist/locales/admin/es.js.map +1 -0
  61. package/dist/locales/admin/ja.d.ts +2 -0
  62. package/dist/locales/admin/ja.js +176 -0
  63. package/dist/locales/admin/ja.js.map +1 -0
  64. package/dist/locales/admin/ko.d.ts +2 -0
  65. package/dist/locales/admin/ko.js +176 -0
  66. package/dist/locales/admin/ko.js.map +1 -0
  67. package/dist/locales/admin/zh.d.ts +2 -0
  68. package/dist/locales/admin/zh.js +176 -0
  69. package/dist/locales/admin/zh.js.map +1 -0
  70. package/dist/locales/de.js +5 -3
  71. package/dist/locales/de.js.map +1 -1
  72. package/dist/locales/en.d.ts +176 -0
  73. package/dist/locales/en.js +5 -3
  74. package/dist/locales/en.js.map +1 -1
  75. package/dist/locales/es.js +5 -3
  76. package/dist/locales/es.js.map +1 -1
  77. package/dist/locales/ja.js +5 -3
  78. package/dist/locales/ja.js.map +1 -1
  79. package/dist/locales/ko.js +5 -3
  80. package/dist/locales/ko.js.map +1 -1
  81. package/dist/locales/social/de.js +8 -1
  82. package/dist/locales/social/de.js.map +1 -1
  83. package/dist/locales/social/en.d.ts +7 -0
  84. package/dist/locales/social/en.js +8 -1
  85. package/dist/locales/social/en.js.map +1 -1
  86. package/dist/locales/social/es.js +8 -1
  87. package/dist/locales/social/es.js.map +1 -1
  88. package/dist/locales/social/ja.js +8 -1
  89. package/dist/locales/social/ja.js.map +1 -1
  90. package/dist/locales/social/ko.js +8 -1
  91. package/dist/locales/social/ko.js.map +1 -1
  92. package/dist/locales/social/zh.js +8 -1
  93. package/dist/locales/social/zh.js.map +1 -1
  94. package/dist/locales/zh.js +5 -3
  95. package/dist/locales/zh.js.map +1 -1
  96. package/dist/provider.d.ts +2 -0
  97. package/dist/provider.js +23 -19
  98. package/dist/provider.js.map +1 -1
  99. package/dist/react/admin.d.ts +55 -0
  100. package/dist/react/admin.js +97 -0
  101. package/dist/react/admin.js.map +1 -0
  102. package/dist/react/comments.d.ts +18 -2
  103. package/dist/react/comments.js +24 -8
  104. package/dist/react/comments.js.map +1 -1
  105. package/dist/react/context.d.ts +7 -1
  106. package/dist/react/context.js +6 -1
  107. package/dist/react/context.js.map +1 -1
  108. package/dist/react/index.d.ts +4 -3
  109. package/dist/react/polls.js +88 -87
  110. package/dist/react/polls.js.map +1 -1
  111. package/dist/react/posts.js +123 -122
  112. package/dist/react/posts.js.map +1 -1
  113. package/dist/react/resources.js +3 -0
  114. package/dist/react/resources.js.map +1 -1
  115. package/dist/react/use-resource.d.ts +16 -0
  116. package/dist/react/use-resource.js +20 -1
  117. package/dist/react/use-resource.js.map +1 -1
  118. package/dist/react.js +21 -20
  119. package/dist/styles.css +1 -1
  120. package/package.json +37 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-manager.js","names":[],"sources":["../../../src/components/admin/post-manager.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { toContentKitError, type ContentKitError } from \"../../client/errors.js\";\nimport { imageRefs } from \"../../client/content/posts.js\";\nimport type { Post, PostInput } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useAdminLocation, useFlowClient, useStaffAccess, type AdminLocation, type FlowClientOptions, type StaffAccess } from \"../../react/admin.js\";\nimport { useErrorReporter } from \"../../react/context.js\";\nimport { usePost } from \"../../react/posts.js\";\nimport { keyOf, useContentScope, usePaged } from \"../../react/use-resource.js\";\nimport { Alert, AlertDescription } from \"#ckui/ui/alert\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button, buttonVariants } 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 { Skeleton } from \"#ckui/ui/skeleton\";\nimport { Textarea } from \"#ckui/ui/textarea\";\nimport { localInput } from \"../social/bans.js\";\nimport { contentError, ErrorLine, formatDate, RelativeTime, useAction } from \"../social/parts.js\";\nimport { AdminRoot, LoadFailed, NoAccess, Pager, Rows, pageOf, useConfirm, type AdminFlowProps, type AdminRouteProps } from \"./shell.js\";\n\ntype Choice = string | { value: string; label: string };\n\n/** What PostManager hands its body editor. */\nexport interface PostEditorProps {\n /** The body as HTML, its images at URLs that show them now. */\n value: string;\n onChange: (html: string) => void;\n /** Uploads an image to the post and resolves with a URL that shows it; absent until the post exists. */\n uploadImage?: (file: File) => Promise<string>;\n disabled?: boolean;\n placeholder?: string;\n \"aria-label\"?: string;\n}\n\nexport interface PostManagerProps extends AdminFlowProps, AdminRouteProps {\n /** Languages to filter by and write in: codes, or codes with labels; the first is a new post's. */\n languages?: readonly Choice[];\n /**\n * The body editor: `richTextEditor` from `@openrails/contentkit-ui/editor`,\n * or the host's own. Without it, the body is a plain HTML field.\n */\n renderEditor?: (props: PostEditorProps) => ReactNode;\n /** A link to a published post's page. */\n postHref?: (post: Post) => string | undefined;\n pageSize?: number;\n}\n\n/** Whether the caller may manage posts (root:posts:write). */\nexport function usePostManagerAccess(o: FlowClientOptions = {}): StaffAccess<\"posts\"> {\n return useStaffAccess([\"posts\"] as const, o);\n}\n\nexport type PostStatus = \"draft\" | \"scheduled\" | \"published\" | \"held\" | \"rejected\";\n\n/** A post's status as the staff list names it. */\nexport function postStatus(p: Post, now = Date.now()): PostStatus {\n if (p.moderation === \"rejected\") return \"rejected\";\n if (p.moderation === \"held\") return \"held\";\n if (p.is_draft) return \"draft\";\n if (p.live_at && Date.parse(p.live_at) > now) return \"scheduled\";\n return \"published\";\n}\n\nconst choices = (list?: readonly Choice[]) => (list ?? []).map((c) => (typeof c === \"string\" ? { value: c, label: c } : c));\nconst badge: Record<PostStatus, \"default\" | \"secondary\" | \"outline\" | \"destructive\"> = { published: \"default\", scheduled: \"secondary\", draft: \"outline\", held: \"secondary\", rejected: \"destructive\" };\n\n/**\n * Staff: every post, by language, draft state and text, deleted ones on\n * their own to restore; create and edit title, slug, language, excerpt,\n * schedule, cover and body with inline images, publish or keep a draft, and\n * delete. The list is at basePath, a new post at basePath/new and each post\n * at basePath/{id}, filters in the query.\n */\nexport function PostManager({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: PostManagerProps) {\n const client = useFlowClient({ client: own, baseUrl, auth });\n return (\n <AdminRoot flow=\"post-manager\" appearance={appearance} className={className} language={language} messages={messages} guard={guard}>\n <Posts {...p} client={client} />\n </AdminRoot>\n );\n}\n\ntype Inner = Omit<PostManagerProps, \"client\" | \"baseUrl\" | \"auth\" | \"guard\" | \"appearance\" | \"className\" | \"language\" | \"messages\"> & { client: ContentKitClient };\n\nfunction Posts({ basePath, navigation, ...p }: Inner) {\n const { t } = useMessages();\n const loc = useAdminLocation(basePath, navigation);\n const access = usePostManagerAccess({ client: p.client });\n if (access.loading) return <Rows />;\n if (!access.allowed.posts) return <NoAccess>{t(\"postManager.noAccess\")}</NoAccess>;\n if (!loc.view) return <List {...p} loc={loc} />;\n return (\n <>\n <Button variant=\"ghost\" size=\"sm\" className=\"justify-self-start\" onClick={() => loc.go(\"\")}>\n {t(\"postManager.back\")}\n </Button>\n <PostForm key={loc.view} {...p} loc={loc} id={loc.view === \"new\" ? null : loc.view} />\n </>\n );\n}\n\nfunction List({ loc, client, languages: offered, postHref, pageSize = 20, onError }: Inner & { loc: AdminLocation }) {\n const m = useMessages();\n const { t } = m;\n const ids = useId();\n const languages = choices(offered);\n const query = loc.query.get(\"q\") ?? \"\";\n const language = loc.query.get(\"language\") ?? \"\";\n const status = loc.query.get(\"status\") === \"draft\" ? \"draft\" : loc.query.get(\"status\") === \"live\" ? \"live\" : \"\";\n const deleted = loc.query.get(\"deleted\") === \"1\";\n const page = pageOf(loc.query);\n const filters = { q: query, language, status, deleted: deleted ? \"1\" : undefined };\n const filter = (next: Partial<typeof filters>) => loc.go(\"\", { ...filters, ...next });\n const [text, setText] = useState(query);\n useEffect(() => setText(query), [query]);\n const { store, scope } = useContentScope(client);\n const list = usePaged(store, keyOf(\"posts-page\", scope, query, language, status, deleted, pageSize), { type: \"posts\", scope: deleted ? \"deleted\" : undefined }, page, pageSize, (q, signal) =>\n client.posts.adminList(\n { ...q, q: query || undefined, language: language || undefined, draft: deleted || !status ? undefined : status === \"draft\", deleted: deleted || undefined },\n signal,\n ),\n );\n const del = useAction(\"post.save\", onError);\n const { confirm, dialog } = useConfirm();\n const remove = async (post: Post) => {\n if (await confirm({ title: t(\"postManager.deleteTitle\", { title: post.title }), description: t(\"postManager.deleteBody\"), confirm: t(\"postManager.delete\") })) {\n await del.run(() => client.posts.delete(post.id));\n }\n };\n return (\n <div className=\"grid gap-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h2 className=\"text-base font-semibold\">{t(\"postManager.title\")}</h2>\n <Button size=\"sm\" onClick={() => loc.go(\"new\")}>\n {t(\"postManager.new\")}\n </Button>\n </div>\n <div className=\"flex gap-2\" role=\"group\" aria-label={t(\"postManager.title\")}>\n <Button size=\"sm\" variant={deleted ? \"outline\" : \"default\"} aria-pressed={!deleted} onClick={() => loc.go(\"\", { q: query, language })}>\n {t(\"postManager.posts\")}\n </Button>\n <Button size=\"sm\" variant={deleted ? \"default\" : \"outline\"} aria-pressed={deleted} onClick={() => loc.go(\"\", { q: query, language, deleted: \"1\" })}>\n {t(\"postManager.deleted\")}\n </Button>\n </div>\n <div className=\"grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto]\">\n <form\n className=\"flex gap-2\"\n role=\"search\"\n onSubmit={(e) => {\n e.preventDefault();\n filter({ q: text.trim() });\n }}\n >\n <Label htmlFor={`${ids}-q`} className=\"sr-only\">\n {t(\"postManager.search\")}\n </Label>\n <Input id={`${ids}-q`} className=\"h-8\" type=\"search\" value={text} placeholder={t(\"postManager.search\")} onChange={(e) => setText(e.target.value)} />\n <Button type=\"submit\" size=\"sm\" variant=\"outline\">\n {t(\"moderation.filter\")}\n </Button>\n </form>\n <div className=\"flex flex-wrap gap-2\">\n {languages.length > 0 && (\n <Select value={language} onValueChange={(v) => filter({ language: String(v ?? \"\") })}>\n <SelectTrigger size=\"sm\" aria-label={t(\"postManager.language\")}>\n <SelectValue>{(v: string) => (v ? (languages.find((l) => l.value === v)?.label ?? v) : t(\"pollManager.allLanguages\"))}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"\">{t(\"pollManager.allLanguages\")}</SelectItem>\n {languages.map((l) => (\n <SelectItem key={l.value} value={l.value}>\n {l.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n {!deleted && (\n <Select value={status} onValueChange={(v) => filter({ status: String(v ?? \"\") })}>\n <SelectTrigger size=\"sm\" aria-label={t(\"postManager.status\")}>\n <SelectValue>{(v: string) => t(v === \"draft\" ? \"postManager.drafts\" : v === \"live\" ? \"postManager.notDrafts\" : \"postManager.allStatuses\")}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"\">{t(\"postManager.allStatuses\")}</SelectItem>\n <SelectItem value=\"draft\">{t(\"postManager.drafts\")}</SelectItem>\n <SelectItem value=\"live\">{t(\"postManager.notDrafts\")}</SelectItem>\n </SelectContent>\n </Select>\n )}\n </div>\n </div>\n <ErrorLine error={del.error} />\n {list.loading && !list.items.length ? (\n <Rows />\n ) : list.error && !list.items.length ? (\n <LoadFailed error={list.error} message={t(\"postManager.loadFailed\")} retry={list.reload} />\n ) : !list.items.length ? (\n <p className=\"text-muted-foreground\">{t(deleted ? \"postManager.noDeleted\" : \"postManager.empty\")}</p>\n ) : (\n <ul className=\"grid divide-y divide-border rounded-lg border border-border\" aria-busy={list.loading}>\n {list.items.map((post) => {\n const s = postStatus(post);\n const href = s === \"published\" ? postHref?.(post) : undefined;\n return (\n <li key={post.id} className=\"flex flex-wrap items-start gap-3 p-3\" data-ckui=\"post-row\" data-status={deleted ? \"deleted\" : s}>\n <div className=\"grid min-w-0 flex-1 gap-1\">\n {deleted ? (\n <span className=\"font-medium break-words\">{post.title}</span>\n ) : (\n <button type=\"button\" className=\"text-left font-medium break-words hover:underline\" onClick={() => loc.go(post.id)}>\n {post.title}\n </button>\n )}\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground\">\n {!deleted && <Badge variant={badge[s]}>{t(`postManager.${s}`)}</Badge>}\n <span>{post.language}</span>\n <RelativeTime at={deleted && post.deleted_at ? post.deleted_at : post.updated_at} />\n {s === \"scheduled\" && post.live_at && <span>{t(\"pollManager.goesLive\", { date: formatDate(m, post.live_at) })}</span>}\n </div>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n {deleted ? (\n <Button size=\"sm\" variant=\"outline\" disabled={del.pending} onClick={() => void del.run(() => client.posts.restore(post.id))}>\n {t(\"moderation.restore\")}\n </Button>\n ) : (\n <>\n {href && (\n <a className={buttonVariants({ size: \"sm\", variant: \"ghost\" })} href={href}>\n {t(\"postManager.view\")}\n </a>\n )}\n <Button size=\"sm\" variant=\"outline\" onClick={() => loc.go(post.id)}>\n {t(\"comments.edit\")}\n </Button>\n <Button size=\"sm\" variant=\"destructive\" disabled={del.pending} onClick={() => void remove(post)}>\n {t(\"comments.delete\")}\n </Button>\n </>\n )}\n </div>\n </li>\n );\n })}\n </ul>\n )}\n <Pager page={page} hasNext={list.hasNext} disabled={list.loading} onPage={(n) => loc.go(\"\", { ...filters, page: n > 1 ? n : undefined })} />\n {dialog}\n </div>\n );\n}\n\ninterface Form {\n title: string;\n slug: string;\n language: string;\n excerpt: string;\n liveAt: string;\n body: string;\n}\n\nconst fromPost = (p: Post): Form => ({\n title: p.title,\n slug: p.slug ?? \"\",\n language: p.language,\n excerpt: p.excerpt ?? \"\",\n liveAt: p.live_at ? localInput(new Date(p.live_at)) : \"\",\n body: p.body,\n});\n\nfunction PostForm({ id, loc, client, languages: offered, renderEditor, postHref, onError }: Inner & { id: string | null; loc: AdminLocation }) {\n const m = useMessages();\n const { t } = m;\n const ids = useId();\n const languages = choices(offered);\n const ed = usePost(id, { client });\n const report = useErrorReporter(onError);\n // The body is kept as stored (image references); the editor shows it with URLs.\n const [form, setForm] = useState<Form | null>(id ? null : { title: \"\", slug: \"\", language: languages[0]?.value ?? \"en\", excerpt: \"\", liveAt: \"\", body: \"\" });\n const [error, setError] = useState<ContentKitError | string | null>(null);\n const [notice, setNotice] = useState<string | null>(null);\n const { confirm, dialog } = useConfirm();\n useEffect(() => {\n if (ed.post) setForm((f) => f ?? fromPost(ed.post!));\n }, [ed.post]);\n const { imageSrc } = ed;\n const shown = useMemo(\n () => (form?.body ?? \"\").replace(imageRefs(), (ref, name: string) => {\n const src = imageSrc(name);\n return src && src !== name ? src.replaceAll(\"&\", \"&amp;\") : ref;\n }),\n [form?.body, imageSrc],\n );\n if (id && !form) {\n if (ed.error) return <LoadFailed error={ed.error} message={t(\"postManager.loadFailed\")} retry={() => loc.go(id, undefined, { replace: true })} />;\n return (\n <div className=\"grid gap-3\" aria-busy>\n <Skeleton className=\"h-9 w-full\" />\n <Skeleton className=\"h-48 w-full\" />\n </div>\n );\n }\n const f = form!;\n const set = (patch: Partial<Form>) => setForm((cur) => ({ ...cur!, ...patch }));\n const fail = (e: unknown) => {\n const err = toContentKitError(e);\n setError(err);\n report(err, \"post.save\");\n };\n const input = (draft?: boolean): PostInput => ({\n title: f.title.trim(),\n slug: f.slug.trim() || undefined,\n language: f.language,\n excerpt: f.excerpt.trim(),\n body: f.body,\n live_at: f.liveAt ? new Date(f.liveAt).toISOString() : undefined,\n ...(draft === undefined ? {} : { is_draft: draft }),\n });\n const save = async (draft?: boolean) => {\n setError(null);\n setNotice(null);\n if (!f.title.trim()) return setError(t(\"postManager.titleRequired\"));\n try {\n if (!ed.post) {\n const post = await ed.create({ ...input(), is_draft: true });\n loc.go(post.id, undefined, { replace: true });\n return;\n }\n const post = await ed.update(input(draft));\n setNotice(t(draft === false ? \"postManager.publishedNotice\" : draft === true ? \"postManager.unpublishedNotice\" : \"pollEditor.saved\"));\n setForm((cur) => ({ ...cur!, slug: post.slug ?? \"\" }));\n } catch (e) {\n fail(e);\n }\n };\n const remove = async () => {\n if (!ed.post || !(await confirm({ title: t(\"postManager.deleteTitle\", { title: ed.post.title }), description: t(\"postManager.deleteBody\"), confirm: t(\"postManager.delete\") }))) return;\n try {\n await ed.remove();\n loc.go(\"\", undefined, { replace: true });\n } catch (e) {\n fail(e);\n }\n };\n const post = ed.post;\n const s = post ? postStatus(post) : \"draft\";\n const href = post && s === \"published\" ? postHref?.(post) : undefined;\n const editor: PostEditorProps = {\n value: shown,\n onChange: (html) => set({ body: ed.storedBody(html) }),\n uploadImage: post ? (file) => ed.uploadImage(file) : undefined,\n disabled: ed.saving,\n placeholder: t(\"postManager.bodyPlaceholder\"),\n \"aria-label\": t(\"postManager.body\"),\n };\n return (\n <form\n className=\"grid gap-4\"\n data-ckui=\"post-form\"\n onSubmit={(e) => {\n e.preventDefault();\n void save();\n }}\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"text-base font-semibold\">{t(post ? \"postManager.editTitle\" : \"postManager.newTitle\")}</h2>\n {post && <Badge variant={badge[s]}>{t(`postManager.${s}`)}</Badge>}\n {href && (\n <a className=\"text-xs text-muted-foreground hover:underline\" href={href}>\n {t(\"postManager.view\")}\n </a>\n )}\n </div>\n {post?.moderation && (\n <Alert variant={post.moderation === \"rejected\" ? \"destructive\" : \"default\"}>\n <AlertDescription>\n {post.moderation_reason ? t(`postManager.${post.moderation === \"rejected\" ? \"rejectedBecause\" : \"heldBecause\"}`, { reason: post.moderation_reason }) : t(`postManager.${post.moderation === \"rejected\" ? \"rejectedNote\" : \"heldNote\"}`)}\n </AlertDescription>\n </Alert>\n )}\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-title`}>{t(\"postManager.postTitle\")}</Label>\n <Input id={`${ids}-title`} value={f.title} required maxLength={300} onChange={(e) => set({ title: e.target.value })} />\n </div>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-slug`}>{t(\"postManager.slug\")}</Label>\n <Input id={`${ids}-slug`} value={f.slug} placeholder={t(\"postManager.slugAuto\")} onChange={(e) => set({ slug: e.target.value })} />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-lang`}>{t(\"postManager.language\")}</Label>\n {languages.length ? (\n <Select value={f.language} onValueChange={(v) => set({ language: 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={f.language} onChange={(e) => set({ language: e.target.value })} />\n )}\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-live`}>{t(\"postManager.liveAt\")}</Label>\n <Input id={`${ids}-live`} type=\"datetime-local\" value={f.liveAt} onChange={(e) => set({ liveAt: e.target.value })} />\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-excerpt`}>{t(\"postManager.excerpt\")}</Label>\n <Textarea id={`${ids}-excerpt`} rows={2} value={f.excerpt} onChange={(e) => set({ excerpt: e.target.value })} />\n </div>\n <Cover post={post} src={ed.coverSrc} saving={ed.saving} onFile={(file) => void ed.setCover(file).catch(fail)} />\n <div className=\"grid gap-1.5\">\n <span className=\"text-sm font-medium\">{t(\"postManager.body\")}</span>\n {renderEditor ? renderEditor(editor) : <Textarea aria-label={editor[\"aria-label\"]} rows={12} value={f.body} placeholder={editor.placeholder} onChange={(e) => set({ body: e.target.value })} />}\n {!post && <p className=\"text-xs text-muted-foreground\">{t(\"postManager.imagesAfterCreate\")}</p>}\n </div>\n {error && <ErrorLine>{typeof error === \"string\" ? error : contentError(m, error)}</ErrorLine>}\n {notice && !error && (\n <p role=\"status\" className=\"text-sm text-muted-foreground\">\n {notice}\n </p>\n )}\n <div className=\"flex flex-wrap gap-2 border-t border-border pt-4\">\n <Button type=\"submit\" disabled={ed.saving}>\n {ed.saving ? t(\"common.saving\") : t(post ? \"common.save\" : \"postManager.createDraft\")}\n </Button>\n {post && (\n <Button type=\"button\" variant=\"outline\" disabled={ed.saving} onClick={() => void save(!post.is_draft)}>\n {t(post.is_draft ? \"postManager.publish\" : \"postManager.unpublish\")}\n </Button>\n )}\n {post && (\n <Button type=\"button\" variant=\"destructive\" className=\"ms-auto\" disabled={ed.saving} onClick={() => void remove()}>\n {t(\"postManager.delete\")}\n </Button>\n )}\n </div>\n {dialog}\n </form>\n );\n}\n\nfunction Cover({ post, src, saving, onFile }: { post: Post | null; src?: string; saving: boolean; onFile: (file: File | null) => void }) {\n const { t } = useMessages();\n const file = useRef<HTMLInputElement>(null);\n return (\n <div className=\"grid gap-1.5\" data-ckui=\"post-cover\">\n <span className=\"text-sm font-medium\">{t(\"postManager.cover\")}</span>\n {post ? (\n <div className=\"flex flex-wrap items-center gap-3\">\n {src ? <img src={src} alt=\"\" className=\"h-20 w-36 rounded-md border border-border object-cover\" /> : <span className=\"text-muted-foreground\">{t(\"postManager.noCover\")}</span>}\n <Button type=\"button\" size=\"sm\" variant=\"outline\" disabled={saving} onClick={() => file.current?.click()}>\n {t(src ? \"pollEditor.replaceImage\" : \"postManager.addCover\")}\n </Button>\n {src && (\n <Button type=\"button\" size=\"sm\" variant=\"ghost\" disabled={saving} onClick={() => onFile(null)}>\n {t(\"pollEditor.removeImage\")}\n </Button>\n )}\n <input\n ref={file}\n type=\"file\"\n accept=\"image/*\"\n hidden\n aria-label={t(\"postManager.cover\")}\n onChange={(e) => {\n const f = e.target.files?.[0];\n e.target.value = \"\";\n if (f) onFile(f);\n }}\n />\n </div>\n ) : (\n <p className=\"text-xs text-muted-foreground\">{t(\"postManager.imagesAfterCreate\")}</p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { ReactNode } from 'react';
2
+ import { ContentKitUiAppearance } from '../../appearance.js';
3
+ import { ContentKitClient } from '../../client/client.js';
4
+ import { ContentKitError } from '../../client/errors.js';
5
+ import { ContentKitUiMessageBundle } from '../../i18n/messages.js';
6
+ import { AdminNavigation, ContentKitAuth } from '../../react/admin.js';
7
+ import { useGuarded, ContentKitErrorHandler, ContentKitGuard } from '../../react/context.js';
8
+ export { useGuarded, type ContentKitGuard };
9
+ /** What every drop-in admin flow takes. */
10
+ export interface AdminFlowProps {
11
+ /** The ContentKit client; or give baseUrl and auth. Without either, the provider's. */
12
+ client?: ContentKitClient;
13
+ /** Where the host mounts ContentKit, with auth. */
14
+ baseUrl?: string;
15
+ /** The host's auth (auth-ui's AuthClient or IssuerClient): its authFetch carries the session. */
16
+ auth?: ContentKitAuth;
17
+ /** Runs every write (auth-ui's useStepUpGuard()). */
18
+ guard?: ContentKitGuard;
19
+ /** Default theme "inherit": the host's shadcn variables. */
20
+ appearance?: ContentKitUiAppearance;
21
+ className?: string;
22
+ /** BCP 47; its built-in bundle loads. Default the provider's. */
23
+ language?: string;
24
+ messages?: ContentKitUiMessageBundle | readonly ContentKitUiMessageBundle[];
25
+ onError?: ContentKitErrorHandler;
26
+ }
27
+ /** A multi-view flow's place: views and filters in the URL under basePath, through the host router when given. */
28
+ export interface AdminRouteProps {
29
+ /** Where the host mounts the flow, e.g. "/admin/comments". Without it, views live in memory. */
30
+ basePath?: string;
31
+ /** The host router; without it the history API. */
32
+ navigation?: AdminNavigation;
33
+ }
34
+ /** A flow's root: its words, its look (the host's theme unless set), the guard for writes, and the `.ckui` scope. */
35
+ export declare function AdminRoot({ appearance, language, messages, guard, className, flow, children, }: Pick<AdminFlowProps, "appearance" | "language" | "messages" | "guard" | "className"> & {
36
+ flow: string;
37
+ children: ReactNode;
38
+ }): import('react').JSX.Element;
39
+ /** Said when the caller may do nothing here. */
40
+ export declare function NoAccess({ children }: {
41
+ children?: ReactNode;
42
+ }): import('react').JSX.Element;
43
+ /** Rows loading. */
44
+ export declare function Rows({ count }: {
45
+ count?: number;
46
+ }): import('react').JSX.Element;
47
+ /** A failed read with a retry; a refusal says so. */
48
+ export declare function LoadFailed({ error, message, retry }: {
49
+ error?: ContentKitError;
50
+ message: string;
51
+ retry: () => void;
52
+ }): import('react').JSX.Element;
53
+ /** Previous and next around the current page number, announced as it changes. */
54
+ export declare function Pager({ page, hasNext, onPage, disabled }: {
55
+ page: number;
56
+ hasNext: boolean;
57
+ onPage: (page: number) => void;
58
+ disabled?: boolean;
59
+ }): import('react').JSX.Element | null;
60
+ /** A positive page number from the query; 1 otherwise. */
61
+ export declare function pageOf(q: URLSearchParams): number;
62
+ export interface ConfirmOptions {
63
+ title: ReactNode;
64
+ description?: ReactNode;
65
+ confirm: ReactNode;
66
+ /** A word to type first: for acts that cannot be undone. */
67
+ typed?: string;
68
+ }
69
+ /** A promise-based confirmation: `if (await confirm({ … })) …`; render `dialog` once. */
70
+ export declare function useConfirm(): {
71
+ confirm: (o: ConfirmOptions) => Promise<boolean>;
72
+ dialog: ReactNode;
73
+ };
@@ -0,0 +1,148 @@
1
+ import { createTranslator as e, resolveMessages as t } from "../../i18n/messages.js";
2
+ import { MessagesContext as n, useMessages as r } from "../../i18n/context.js";
3
+ import { AppearanceContext as i, ContentKitUiRoot as a } from "../../scope.js";
4
+ import { useTranslator as o } from "../../provider.js";
5
+ import { GuardContext as s, useGuarded as c } from "../../react/context.js";
6
+ import { Button as l } from "../../ui/button.js";
7
+ import { Skeleton as u } from "../../ui/skeleton.js";
8
+ import { ConfirmDialog as d } from "../social/parts.js";
9
+ import { useCallback as f, useContext as p, useMemo as m, useState as h } from "react";
10
+ import { jsx as g, jsxs as _ } from "react/jsx-runtime";
11
+ import { cn as v } from "cn";
12
+ //#region src/components/admin/shell.tsx
13
+ function y({ language: i, messages: a, children: s }) {
14
+ let c = r(), l = o(i, a), u = m(() => i ? l : e(t([c.messages, ...a ? Array.isArray(a) ? a : [a] : []]), void 0, c.language), [
15
+ i,
16
+ l,
17
+ c,
18
+ a
19
+ ]);
20
+ return /* @__PURE__ */ g(n.Provider, {
21
+ value: u,
22
+ children: s
23
+ });
24
+ }
25
+ function b({ appearance: e, language: t, messages: n, guard: r, className: o, flow: c, children: l }) {
26
+ let u = p(i), d = m(() => ({
27
+ ...u,
28
+ ...e,
29
+ theme: e?.theme ?? u?.theme ?? "inherit",
30
+ variables: {
31
+ ...u?.variables,
32
+ ...e?.variables
33
+ }
34
+ }), [u, e]), f = /* @__PURE__ */ g(i.Provider, {
35
+ value: d,
36
+ children: /* @__PURE__ */ g(s.Provider, {
37
+ value: r,
38
+ children: /* @__PURE__ */ g(a, {
39
+ appearance: d,
40
+ className: v("grid gap-4 text-sm text-foreground", o),
41
+ "data-ckui": c,
42
+ children: l
43
+ })
44
+ })
45
+ });
46
+ return t || n ? /* @__PURE__ */ g(y, {
47
+ language: t,
48
+ messages: n,
49
+ children: f
50
+ }) : f;
51
+ }
52
+ function x({ children: e }) {
53
+ let { t } = r();
54
+ return /* @__PURE__ */ _("div", {
55
+ className: "grid gap-1 rounded-lg border border-dashed border-border p-6 text-center",
56
+ "data-ckui": "no-access",
57
+ role: "status",
58
+ children: [/* @__PURE__ */ g("p", {
59
+ className: "font-medium",
60
+ children: t("admin.noAccessTitle")
61
+ }), /* @__PURE__ */ g("p", {
62
+ className: "text-muted-foreground",
63
+ children: e ?? t("admin.noAccess")
64
+ })]
65
+ });
66
+ }
67
+ function S({ count: e = 3 }) {
68
+ return /* @__PURE__ */ g("div", {
69
+ className: "grid gap-3",
70
+ "aria-busy": !0,
71
+ children: Array.from({ length: e }, (e, t) => /* @__PURE__ */ g(u, { className: "h-14 w-full" }, t))
72
+ });
73
+ }
74
+ function C({ error: e, message: t, retry: n }) {
75
+ let { t: i } = r(), a = e?.code === "forbidden" || e?.code === "unauthorized";
76
+ return /* @__PURE__ */ _("div", {
77
+ className: "grid justify-items-start gap-2",
78
+ role: "alert",
79
+ children: [/* @__PURE__ */ g("p", {
80
+ className: "text-destructive",
81
+ children: a ? i(e.code === "forbidden" ? "contentErrors.forbidden" : "contentErrors.unauthorized") : t
82
+ }), /* @__PURE__ */ g(l, {
83
+ size: "sm",
84
+ variant: "outline",
85
+ onClick: n,
86
+ children: i("common.retry")
87
+ })]
88
+ });
89
+ }
90
+ function w({ page: e, hasNext: t, onPage: n, disabled: i }) {
91
+ let { t: a } = r();
92
+ return e <= 1 && !t ? null : /* @__PURE__ */ _("nav", {
93
+ className: "flex items-center justify-center gap-3",
94
+ "aria-label": a("admin.pages"),
95
+ "data-ckui": "pager",
96
+ children: [
97
+ /* @__PURE__ */ g(l, {
98
+ size: "sm",
99
+ variant: "outline",
100
+ disabled: i || e <= 1,
101
+ onClick: () => n(e - 1),
102
+ children: a("admin.previous")
103
+ }),
104
+ /* @__PURE__ */ g("span", {
105
+ className: "text-muted-foreground",
106
+ "aria-live": "polite",
107
+ children: a("admin.page", { page: e })
108
+ }),
109
+ /* @__PURE__ */ g(l, {
110
+ size: "sm",
111
+ variant: "outline",
112
+ disabled: i || !t,
113
+ onClick: () => n(e + 1),
114
+ children: a("admin.next")
115
+ })
116
+ ]
117
+ });
118
+ }
119
+ function T(e) {
120
+ let t = Number(e.get("page"));
121
+ return Number.isInteger(t) && t > 1 ? t : 1;
122
+ }
123
+ function E() {
124
+ let [e, t] = h(null);
125
+ return {
126
+ confirm: f((e) => new Promise((n) => t({
127
+ ...e,
128
+ resolve: n
129
+ })), []),
130
+ dialog: /* @__PURE__ */ g(d, {
131
+ open: !!e,
132
+ onOpenChange: (n) => {
133
+ !n && e && (e.resolve(!1), t(null));
134
+ },
135
+ title: e?.title,
136
+ description: e?.description,
137
+ confirm: e?.confirm,
138
+ typed: e?.typed,
139
+ onConfirm: () => {
140
+ e?.resolve(!0), t(null);
141
+ }
142
+ })
143
+ };
144
+ }
145
+ //#endregion
146
+ export { b as AdminRoot, C as LoadFailed, x as NoAccess, w as Pager, S as Rows, T as pageOf, E as useConfirm, c as useGuarded };
147
+
148
+ //# sourceMappingURL=shell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.js","names":[],"sources":["../../../src/components/admin/shell.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useCallback, useContext, useMemo, useState, type ReactNode } from \"react\";\nimport type { ContentKitUiAppearance } from \"../../appearance.js\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport type { ContentKitError } from \"../../client/errors.js\";\nimport { MessagesContext, useMessages } from \"../../i18n/context.js\";\nimport { createTranslator, resolveMessages, type ContentKitUiMessageBundle } from \"../../i18n/messages.js\";\nimport { useTranslator } from \"../../provider.js\";\nimport type { AdminNavigation, ContentKitAuth } from \"../../react/admin.js\";\nimport { GuardContext, useGuarded, type ContentKitErrorHandler, type ContentKitGuard } from \"../../react/context.js\";\nimport { AppearanceContext, ContentKitUiRoot } from \"../../scope.js\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Skeleton } from \"#ckui/ui/skeleton\";\nimport { ConfirmDialog } from \"../social/parts.js\";\n\nexport { useGuarded, type ContentKitGuard };\n\n/** What every drop-in admin flow takes. */\nexport interface AdminFlowProps {\n /** The ContentKit client; or give baseUrl and auth. Without either, the provider's. */\n client?: ContentKitClient;\n /** Where the host mounts ContentKit, with auth. */\n baseUrl?: string;\n /** The host's auth (auth-ui's AuthClient or IssuerClient): its authFetch carries the session. */\n auth?: ContentKitAuth;\n /** Runs every write (auth-ui's useStepUpGuard()). */\n guard?: ContentKitGuard;\n /** Default theme \"inherit\": the host's shadcn variables. */\n appearance?: ContentKitUiAppearance;\n className?: string;\n /** BCP 47; its built-in bundle loads. Default the provider's. */\n language?: string;\n messages?: ContentKitUiMessageBundle | readonly ContentKitUiMessageBundle[];\n onError?: ContentKitErrorHandler;\n}\n\n/** A multi-view flow's place: views and filters in the URL under basePath, through the host router when given. */\nexport interface AdminRouteProps {\n /** Where the host mounts the flow, e.g. \"/admin/comments\". Without it, views live in memory. */\n basePath?: string;\n /** The host router; without it the history API. */\n navigation?: AdminNavigation;\n}\n\nfunction Words({ language, messages, children }: { language?: string; messages?: AdminFlowProps[\"messages\"]; children: ReactNode }) {\n const parent = useMessages();\n const own = useTranslator(language, messages);\n const layered = useMemo(\n () => (language ? own : createTranslator(resolveMessages([parent.messages as ContentKitUiMessageBundle, ...(messages ? (Array.isArray(messages) ? messages : [messages]) : [])]), undefined, parent.language)),\n [language, own, parent, messages],\n );\n return <MessagesContext.Provider value={layered}>{children}</MessagesContext.Provider>;\n}\n\n/** A flow's root: its words, its look (the host's theme unless set), the guard for writes, and the `.ckui` scope. */\nexport function AdminRoot({\n appearance,\n language,\n messages,\n guard,\n className,\n flow,\n children,\n}: Pick<AdminFlowProps, \"appearance\" | \"language\" | \"messages\" | \"guard\" | \"className\"> & { flow: string; children: ReactNode }) {\n const parent = useContext(AppearanceContext);\n const look = useMemo<ContentKitUiAppearance>(\n () => ({ ...parent, ...appearance, theme: appearance?.theme ?? parent?.theme ?? \"inherit\", variables: { ...parent?.variables, ...appearance?.variables } }),\n [parent, appearance],\n );\n const body = (\n <AppearanceContext.Provider value={look}>\n <GuardContext.Provider value={guard}>\n <ContentKitUiRoot appearance={look} className={cn(\"grid gap-4 text-sm text-foreground\", className)} data-ckui={flow}>\n {children}\n </ContentKitUiRoot>\n </GuardContext.Provider>\n </AppearanceContext.Provider>\n );\n return language || messages ? (\n <Words language={language} messages={messages}>\n {body}\n </Words>\n ) : (\n body\n );\n}\n\n/** Said when the caller may do nothing here. */\nexport function NoAccess({ children }: { children?: ReactNode }) {\n const { t } = useMessages();\n return (\n <div className=\"grid gap-1 rounded-lg border border-dashed border-border p-6 text-center\" data-ckui=\"no-access\" role=\"status\">\n <p className=\"font-medium\">{t(\"admin.noAccessTitle\")}</p>\n <p className=\"text-muted-foreground\">{children ?? t(\"admin.noAccess\")}</p>\n </div>\n );\n}\n\n/** Rows loading. */\nexport function Rows({ count = 3 }: { count?: number }) {\n return (\n <div className=\"grid gap-3\" aria-busy>\n {Array.from({ length: count }, (_, i) => (\n <Skeleton key={i} className=\"h-14 w-full\" />\n ))}\n </div>\n );\n}\n\n/** A failed read with a retry; a refusal says so. */\nexport function LoadFailed({ error, message, retry }: { error?: ContentKitError; message: string; retry: () => void }) {\n const { t } = useMessages();\n const refused = error?.code === \"forbidden\" || error?.code === \"unauthorized\";\n return (\n <div className=\"grid justify-items-start gap-2\" role=\"alert\">\n <p className=\"text-destructive\">{refused ? t(error.code === \"forbidden\" ? \"contentErrors.forbidden\" : \"contentErrors.unauthorized\") : message}</p>\n <Button size=\"sm\" variant=\"outline\" onClick={retry}>\n {t(\"common.retry\")}\n </Button>\n </div>\n );\n}\n\n/** Previous and next around the current page number, announced as it changes. */\nexport function Pager({ page, hasNext, onPage, disabled }: { page: number; hasNext: boolean; onPage: (page: number) => void; disabled?: boolean }) {\n const { t } = useMessages();\n if (page <= 1 && !hasNext) return null;\n return (\n <nav className=\"flex items-center justify-center gap-3\" aria-label={t(\"admin.pages\")} data-ckui=\"pager\">\n <Button size=\"sm\" variant=\"outline\" disabled={disabled || page <= 1} onClick={() => onPage(page - 1)}>\n {t(\"admin.previous\")}\n </Button>\n <span className=\"text-muted-foreground\" aria-live=\"polite\">\n {t(\"admin.page\", { page })}\n </span>\n <Button size=\"sm\" variant=\"outline\" disabled={disabled || !hasNext} onClick={() => onPage(page + 1)}>\n {t(\"admin.next\")}\n </Button>\n </nav>\n );\n}\n\n/** A positive page number from the query; 1 otherwise. */\nexport function pageOf(q: URLSearchParams): number {\n const n = Number(q.get(\"page\"));\n return Number.isInteger(n) && n > 1 ? n : 1;\n}\n\nexport interface ConfirmOptions {\n title: ReactNode;\n description?: ReactNode;\n confirm: ReactNode;\n /** A word to type first: for acts that cannot be undone. */\n typed?: string;\n}\n\n/** A promise-based confirmation: `if (await confirm({ … })) …`; render `dialog` once. */\nexport function useConfirm(): { confirm: (o: ConfirmOptions) => Promise<boolean>; dialog: ReactNode } {\n const [asking, setAsking] = useState<(ConfirmOptions & { resolve: (ok: boolean) => void }) | null>(null);\n const confirm = useCallback((o: ConfirmOptions) => new Promise<boolean>((resolve) => setAsking({ ...o, resolve })), []);\n const dialog = (\n <ConfirmDialog\n open={!!asking}\n onOpenChange={(open) => {\n if (!open && asking) {\n asking.resolve(false);\n setAsking(null);\n }\n }}\n title={asking?.title}\n description={asking?.description}\n confirm={asking?.confirm}\n typed={asking?.typed}\n onConfirm={() => {\n asking?.resolve(true);\n setAsking(null);\n }}\n />\n );\n return { confirm, dialog };\n}\n"],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import { ReactNode } from 'react';
2
+ import { Assignment, ContentRef, Edge, NodeDetail } from '../../client/generated/wire.js';
3
+ import { FlowClientOptions, StaffAccess } from '../../react/admin.js';
4
+ import { AdminFlowProps, AdminRouteProps } from './shell.js';
5
+ type Choice = string | {
6
+ value: string;
7
+ label: string;
8
+ };
9
+ export type TaxonomyRelation = Edge["relation"];
10
+ /** One node kind the flow manages. */
11
+ export interface TaxonomyKind {
12
+ /** The kind as the store registers it, e.g. "tag", "series". */
13
+ kind: string;
14
+ label: string;
15
+ /** Edges a node of this kind may have, "node is <relation> of a <to> node": e.g. a character member_of a series. */
16
+ relations?: readonly {
17
+ relation: TaxonomyRelation;
18
+ to: string;
19
+ label: string;
20
+ inverseLabel?: string;
21
+ }[];
22
+ /** A cover in the host's media: the media kind whose items are node ids, its upload path ("cover") and public preset ("cover"). */
23
+ cover?: {
24
+ kind: string;
25
+ path?: string;
26
+ preset?: string;
27
+ };
28
+ }
29
+ export interface TaxonomyManagerProps extends AdminFlowProps, AdminRouteProps {
30
+ /** The kinds, in tab order; the first is at basePath. */
31
+ kinds: readonly TaxonomyKind[];
32
+ /** Languages names are written in; the first resolves the list's names. */
33
+ languages?: readonly Choice[];
34
+ /** Counts of this content kind; default every kind. */
35
+ contentKind?: string;
36
+ /** Host-owned fields of a node (a creator's type, a tag's restricted flag), below its names. */
37
+ renderFields?: (node: NodeDetail) => ReactNode;
38
+ /** One assigned content, e.g. a link with its title. Default its kind and id. */
39
+ renderContent?: (ref: ContentRef, assignment: Assignment) => ReactNode;
40
+ pageSize?: number;
41
+ }
42
+ /** Whether the caller may manage the taxonomy (root:taxonomy:write). */
43
+ export declare function useTaxonomyManagerAccess(o?: FlowClientOptions): StaffAccess<"taxonomy">;
44
+ /** A slug from a name: lowercase words joined by dashes. */
45
+ export declare function slugify(name: string): string;
46
+ /**
47
+ * Staff: the taxonomy per kind. Each kind's nodes by name, state, language,
48
+ * minimum count and order; a node's names and aliases per language, host
49
+ * fields, relations (with a typeahead), cover, counts and assigned contents;
50
+ * create, delete, restore and merge. Kinds are at basePath/{kind}, a node at
51
+ * basePath/{kind}/{id}, filters in the query.
52
+ */
53
+ export declare function TaxonomyManager({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: TaxonomyManagerProps): import('react').JSX.Element;
54
+ export {};