@openrails/contentkit-ui 0.104.0-rc.1 → 0.104.0-rc.2

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 (210) hide show
  1. package/README.md +6 -6
  2. package/dist/client/beacons.js +4 -1
  3. package/dist/client/beacons.js.map +1 -1
  4. package/dist/client/contents.d.ts +7 -1
  5. package/dist/client/contents.js +8 -0
  6. package/dist/client/contents.js.map +1 -1
  7. package/dist/client/discovery.d.ts +6 -2
  8. package/dist/client/discovery.js +1 -1
  9. package/dist/client/discovery.js.map +1 -1
  10. package/dist/client/generated/routes.d.ts +41 -28
  11. package/dist/client/generated/routes.js +63 -61
  12. package/dist/client/generated/routes.js.map +1 -1
  13. package/dist/client/generated/wire.d.ts +36 -74
  14. package/dist/client/generated/wire.js.map +1 -1
  15. package/dist/client/index.d.ts +1 -1
  16. package/dist/client/media/windows.d.ts +2 -0
  17. package/dist/client/media/windows.js +4 -1
  18. package/dist/client/media/windows.js.map +1 -1
  19. package/dist/client/stats.d.ts +4 -20
  20. package/dist/client/stats.js +0 -11
  21. package/dist/client/stats.js.map +1 -1
  22. package/dist/client/versions.d.ts +8 -6
  23. package/dist/client/versions.js +15 -10
  24. package/dist/client/versions.js.map +1 -1
  25. package/dist/components/admin/bulk-drafts.d.ts +17 -0
  26. package/dist/components/admin/bulk-drafts.js +41 -0
  27. package/dist/components/admin/bulk-drafts.js.map +1 -0
  28. package/dist/components/admin/content-admin.d.ts +1 -1
  29. package/dist/components/admin/content-admin.js.map +1 -1
  30. package/dist/components/admin/content-manager.d.ts +11 -3
  31. package/dist/components/admin/content-manager.js +739 -380
  32. package/dist/components/admin/content-manager.js.map +1 -1
  33. package/dist/components/admin/group-manager.d.ts +8 -3
  34. package/dist/components/admin/group-manager.js +218 -146
  35. package/dist/components/admin/group-manager.js.map +1 -1
  36. package/dist/components/admin/page-notices.d.ts +16 -0
  37. package/dist/components/admin/page-notices.js +113 -0
  38. package/dist/components/admin/page-notices.js.map +1 -0
  39. package/dist/components/admin/page-picker.d.ts +38 -0
  40. package/dist/components/admin/page-picker.js +159 -0
  41. package/dist/components/admin/page-picker.js.map +1 -0
  42. package/dist/components/admin/reader-content-editor.js +537 -500
  43. package/dist/components/admin/reader-content-editor.js.map +1 -1
  44. package/dist/components/admin/reader-manifest.js +181 -179
  45. package/dist/components/admin/reader-manifest.js.map +1 -1
  46. package/dist/components/admin/shell.d.ts +10 -2
  47. package/dist/components/admin/shell.js +114 -42
  48. package/dist/components/admin/shell.js.map +1 -1
  49. package/dist/components/admin/stats-dashboard.d.ts +4 -9
  50. package/dist/components/admin/stats-dashboard.js +300 -202
  51. package/dist/components/admin/stats-dashboard.js.map +1 -1
  52. package/dist/components/admin/upload-queue.d.ts +14 -0
  53. package/dist/components/admin/upload-queue.js +115 -98
  54. package/dist/components/admin/upload-queue.js.map +1 -1
  55. package/dist/components/content/card.d.ts +31 -3
  56. package/dist/components/content/card.js +324 -158
  57. package/dist/components/content/card.js.map +1 -1
  58. package/dist/components/content/daily-releases.js +14 -12
  59. package/dist/components/content/daily-releases.js.map +1 -1
  60. package/dist/components/content/editor-save.js +27 -25
  61. package/dist/components/content/editor-save.js.map +1 -1
  62. package/dist/components/content/editors.js +274 -253
  63. package/dist/components/content/editors.js.map +1 -1
  64. package/dist/components/content/favorites.d.ts +1 -1
  65. package/dist/components/content/favorites.js +36 -22
  66. package/dist/components/content/favorites.js.map +1 -1
  67. package/dist/components/content/grid.d.ts +11 -4
  68. package/dist/components/content/grid.js +52 -43
  69. package/dist/components/content/grid.js.map +1 -1
  70. package/dist/components/content/history.js +83 -63
  71. package/dist/components/content/history.js.map +1 -1
  72. package/dist/components/content/index.d.ts +5 -3
  73. package/dist/components/content/listing.d.ts +14 -3
  74. package/dist/components/content/listing.js +65 -56
  75. package/dist/components/content/listing.js.map +1 -1
  76. package/dist/components/content/offer-fields.js +6 -6
  77. package/dist/components/content/paged-grid.d.ts +10 -0
  78. package/dist/components/content/paged-grid.js +116 -0
  79. package/dist/components/content/paged-grid.js.map +1 -0
  80. package/dist/components/content/pagination.d.ts +19 -0
  81. package/dist/components/content/pagination.js +97 -0
  82. package/dist/components/content/pagination.js.map +1 -0
  83. package/dist/components/content/paywall.d.ts +9 -0
  84. package/dist/components/content/paywall.js +27 -3
  85. package/dist/components/content/paywall.js.map +1 -1
  86. package/dist/components/content/post-list.d.ts +37 -0
  87. package/dist/components/content/post-list.js +203 -0
  88. package/dist/components/content/post-list.js.map +1 -0
  89. package/dist/components/content/reader-settings.d.ts +2 -0
  90. package/dist/components/content/reader-settings.js +1 -1
  91. package/dist/components/content/reader-settings.js.map +1 -1
  92. package/dist/components/groups/banner.d.ts +6 -2
  93. package/dist/components/groups/banner.js +79 -61
  94. package/dist/components/groups/banner.js.map +1 -1
  95. package/dist/components/groups/directory.d.ts +12 -2
  96. package/dist/components/groups/directory.js +168 -105
  97. package/dist/components/groups/directory.js.map +1 -1
  98. package/dist/components/groups/editor.js +10 -10
  99. package/dist/components/groups/editor.js.map +1 -1
  100. package/dist/components/groups/entity-chips.d.ts +61 -0
  101. package/dist/components/groups/entity-chips.js +382 -0
  102. package/dist/components/groups/entity-chips.js.map +1 -0
  103. package/dist/components/groups/group-members.d.ts +29 -0
  104. package/dist/components/groups/group-members.js +474 -0
  105. package/dist/components/groups/group-members.js.map +1 -0
  106. package/dist/components/groups/header.js +53 -45
  107. package/dist/components/groups/header.js.map +1 -1
  108. package/dist/components/groups/index.d.ts +3 -1
  109. package/dist/components/groups/memberships.d.ts +3 -36
  110. package/dist/components/groups/memberships.js +105 -480
  111. package/dist/components/groups/memberships.js.map +1 -1
  112. package/dist/components/groups/scoped.d.ts +23 -0
  113. package/dist/components/groups/scoped.js +92 -0
  114. package/dist/components/groups/scoped.js.map +1 -0
  115. package/dist/components/media-folder-editor.d.ts +5 -0
  116. package/dist/components/media-folder-editor.js +418 -230
  117. package/dist/components/media-folder-editor.js.map +1 -1
  118. package/dist/components/sortable-list.d.ts +5 -1
  119. package/dist/components/sortable-list.js +97 -81
  120. package/dist/components/sortable-list.js.map +1 -1
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.js +41 -37
  123. package/dist/locales/admin/de.js +78 -45
  124. package/dist/locales/admin/de.js.map +1 -1
  125. package/dist/locales/admin/en.d.ts +74 -41
  126. package/dist/locales/admin/en.js +78 -45
  127. package/dist/locales/admin/en.js.map +1 -1
  128. package/dist/locales/admin/es.js +78 -45
  129. package/dist/locales/admin/es.js.map +1 -1
  130. package/dist/locales/admin/ja.js +78 -45
  131. package/dist/locales/admin/ja.js.map +1 -1
  132. package/dist/locales/admin/ko.js +78 -45
  133. package/dist/locales/admin/ko.js.map +1 -1
  134. package/dist/locales/admin/zh.js +78 -45
  135. package/dist/locales/admin/zh.js.map +1 -1
  136. package/dist/locales/de.js +1 -0
  137. package/dist/locales/de.js.map +1 -1
  138. package/dist/locales/discovery/de.js +25 -1
  139. package/dist/locales/discovery/de.js.map +1 -1
  140. package/dist/locales/discovery/en.d.ts +24 -0
  141. package/dist/locales/discovery/en.js +25 -1
  142. package/dist/locales/discovery/en.js.map +1 -1
  143. package/dist/locales/discovery/es.js +25 -1
  144. package/dist/locales/discovery/es.js.map +1 -1
  145. package/dist/locales/discovery/ja.js +25 -1
  146. package/dist/locales/discovery/ja.js.map +1 -1
  147. package/dist/locales/discovery/ko.js +25 -1
  148. package/dist/locales/discovery/ko.js.map +1 -1
  149. package/dist/locales/discovery/zh.js +25 -1
  150. package/dist/locales/discovery/zh.js.map +1 -1
  151. package/dist/locales/en.d.ts +119 -43
  152. package/dist/locales/en.js +1 -0
  153. package/dist/locales/en.js.map +1 -1
  154. package/dist/locales/es.js +1 -0
  155. package/dist/locales/es.js.map +1 -1
  156. package/dist/locales/groups/de.js +22 -4
  157. package/dist/locales/groups/de.js.map +1 -1
  158. package/dist/locales/groups/en.d.ts +20 -2
  159. package/dist/locales/groups/en.js +22 -4
  160. package/dist/locales/groups/en.js.map +1 -1
  161. package/dist/locales/groups/es.js +22 -4
  162. package/dist/locales/groups/es.js.map +1 -1
  163. package/dist/locales/groups/ja.js +22 -4
  164. package/dist/locales/groups/ja.js.map +1 -1
  165. package/dist/locales/groups/ko.js +22 -4
  166. package/dist/locales/groups/ko.js.map +1 -1
  167. package/dist/locales/groups/zh.js +22 -4
  168. package/dist/locales/groups/zh.js.map +1 -1
  169. package/dist/locales/ja.js +1 -0
  170. package/dist/locales/ja.js.map +1 -1
  171. package/dist/locales/ko.js +1 -0
  172. package/dist/locales/ko.js.map +1 -1
  173. package/dist/locales/zh.js +1 -0
  174. package/dist/locales/zh.js.map +1 -1
  175. package/dist/react/discovery.d.ts +16 -0
  176. package/dist/react/discovery.js +69 -37
  177. package/dist/react/discovery.js.map +1 -1
  178. package/dist/react/groups.d.ts +19 -3
  179. package/dist/react/groups.js +49 -22
  180. package/dist/react/groups.js.map +1 -1
  181. package/dist/react/index.d.ts +6 -4
  182. package/dist/react/page-notices.d.ts +13 -0
  183. package/dist/react/page-notices.js +38 -0
  184. package/dist/react/page-notices.js.map +1 -0
  185. package/dist/react/post-reads.d.ts +23 -0
  186. package/dist/react/post-reads.js +90 -0
  187. package/dist/react/post-reads.js.map +1 -0
  188. package/dist/react/remembered-sort.d.ts +5 -0
  189. package/dist/react/remembered-sort.js +29 -0
  190. package/dist/react/remembered-sort.js.map +1 -0
  191. package/dist/react/resources.js +2 -2
  192. package/dist/react/resources.js.map +1 -1
  193. package/dist/react/stats.d.ts +3 -7
  194. package/dist/react/stats.js +2 -6
  195. package/dist/react/stats.js.map +1 -1
  196. package/dist/react/use-engagement.d.ts +2 -0
  197. package/dist/react/use-engagement.js +8 -3
  198. package/dist/react/use-engagement.js.map +1 -1
  199. package/dist/react.js +29 -27
  200. package/dist/styles.css +1 -1
  201. package/dist/urls/index.d.ts +1 -1
  202. package/dist/urls.js +2 -2
  203. package/dist/urls.js.map +1 -1
  204. package/package.json +1 -1
  205. package/dist/components/admin/page-review.d.ts +0 -13
  206. package/dist/components/admin/page-review.js +0 -225
  207. package/dist/components/admin/page-review.js.map +0 -1
  208. package/dist/react/page-review.d.ts +0 -14
  209. package/dist/react/page-review.js +0 -37
  210. package/dist/react/page-review.js.map +0 -1
@@ -43,7 +43,7 @@ export interface PathOptions {
43
43
  * language. Mirrors Go's contenturl.Prefix.
44
44
  */
45
45
  export declare function languagePrefix(languages: readonly string[], language?: string): string;
46
- /** Joins /{language}/{route}/{code}[/{slug}][?query]; ContentURLs.path() picks the language. */
46
+ /** Joins /{language}/{route}/{code}[/{slug}][?query]; slug is the "/"-joined decorative path. ContentURLs.path() picks the language. */
47
47
  export declare function contentPath(route: string, code: string, slug?: string, options?: PathOptions): string;
48
48
  export interface ContentURLConfig {
49
49
  /** Content kind -> the host's route segment ({ video: "watch", gallery: "g" }). */
package/dist/urls.js CHANGED
@@ -95,14 +95,14 @@ function v(e) {
95
95
  let n = t.split("/");
96
96
  if (n.some((e) => e === "")) return null;
97
97
  let o = "";
98
- if (n.length > 2 && i.includes(n[0]) && (o = n[0], n = n.slice(1)), n.length < 2 || n.length > 3 || !a.has(n[0])) return null;
98
+ if (n.length > 2 && i.includes(n[0]) && (o = n[0], n = n.slice(1)), !o && n.length > 2 && !a.has(n[0]) && a.has(n[1]) && (n = n.slice(1)), n.length < 2 || !a.has(n[0])) return null;
99
99
  let s = r(n[1]);
100
100
  return s === null ? null : {
101
101
  language: o,
102
102
  route: n[0],
103
103
  code: s,
104
104
  rawCode: n[1],
105
- slug: n[2] ?? ""
105
+ slug: n.slice(2).join("/")
106
106
  };
107
107
  }, c = (e, n = {}) => {
108
108
  let r = t[e.content_kind];
package/dist/urls.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"urls.js","names":[],"sources":["../src/urls/index.ts"],"sourcesContent":["// ContentKit content URLs: /{lang}/{route}/{CODE}[/{slug}][?v={version}]. Every\n// page is under its language, the default included; the code is the only part\n// of the path the server reads, the slug is decoration, and ?v= selects a\n// version. Mirrors Go's contenturl (the two share contenturl/testdata/vectors.json).\n\nexport const CODE_LENGTH = 9;\n\n/** Crockford base32: digits and uppercase letters without I, L, O, U. */\nexport const CODE_ALPHABET = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\n\nfunction parseCrockford(input: string, length: number): string | null {\n let out = \"\";\n for (const ch of input) {\n if (ch === \"-\") continue;\n if (ch.length !== 1 || ch.charCodeAt(0) > 0x7f) return null;\n let c = ch.toUpperCase();\n if (c === \"O\") c = \"0\";\n else if (c === \"I\" || c === \"L\") c = \"1\";\n if (!CODE_ALPHABET.includes(c) || out.length === length) return null;\n out += c;\n }\n if (out.length !== length || !/[A-Z]/.test(out)) return null;\n return out;\n}\n\n/**\n * Reads a code with Crockford's decoding rules (any case, O as 0, I and L as 1,\n * hyphens ignored) and returns its canonical form, or null when the input is\n * not a code. An all-digit result is never a code (it reads as a legacy id).\n */\nexport function parseCode(input: string): string | null {\n return parseCrockford(input, CODE_LENGTH);\n}\n\n/** The query parameter that selects a version on its content's page. */\nexport const VERSION_PARAM = \"v\";\n\nexport const VERSION_CODE_LENGTH = 4;\n\n/** Reads a version code (four characters, parseCode's rules); null when the input is not one. */\nexport function parseVersionCode(input: string): string | null {\n return parseCrockford(input, VERSION_CODE_LENGTH);\n}\n\n/** The version code a location's (or search string's) ?v= names; null when it names none. */\nexport function versionParam(location: string | URLSearchParams): string | null {\n const v = (typeof location === \"string\" ? searchOf(location) : location).get(VERSION_PARAM);\n return v === null ? null : parseVersionCode(v);\n}\n\nfunction searchOf(location: string): URLSearchParams {\n const noHash = location.split(\"#\", 1)[0]!;\n const q = noHash.indexOf(\"?\");\n return new URLSearchParams(q < 0 ? \"\" : noHash.slice(q + 1));\n}\n\n/** location with ?v= set to code, or removed for null; its other parameters and hash stay. */\nexport function withVersion(location: string, code: string | null): string {\n const hashAt = location.indexOf(\"#\");\n const hash = hashAt < 0 ? \"\" : location.slice(hashAt);\n const rest = hashAt < 0 ? location : location.slice(0, hashAt);\n const q = rest.indexOf(\"?\");\n const params = new URLSearchParams(q < 0 ? \"\" : rest.slice(q + 1));\n if (code === null) params.delete(VERSION_PARAM);\n else params.set(VERSION_PARAM, code);\n const search = params.toString();\n return (q < 0 ? rest : rest.slice(0, q)) + (search ? `?${search}` : \"\") + hash;\n}\n\n/** Reports whether input is a code in canonical form. */\nexport function isCode(input: string): boolean {\n return parseCode(input) === input;\n}\n\n/** A resolved content link, as ContentKit and host APIs return it. */\nexport interface ContentLink {\n content_kind: string;\n code: string;\n slug: string;\n /** Localized slugs keyed by language; the default slug applies otherwise. */\n slugs?: Record<string, string>;\n}\n\n/** The slug for language, falling back to the default slug. */\nexport function slugFor(link: Pick<ContentLink, \"slug\" | \"slugs\">, language = \"\"): string {\n return (language && link.slugs?.[language]) || link.slug;\n}\n\nexport type QueryInit = string | URLSearchParams | Record<string, string | number | boolean | null | undefined>;\n\nexport interface PathOptions {\n /** The page's language; ContentURLs use the site's default when unset. */\n language?: string;\n /** Query string for sub-pages, e.g. { p: 12 }. Null and undefined values are dropped. */\n query?: QueryInit;\n /** A version code (Version.code): ?v= selects that version on the page. */\n version?: string | null;\n}\n\nfunction queryString(query: QueryInit | undefined, version?: string | null): string {\n let params: URLSearchParams;\n if (query === undefined) {\n params = new URLSearchParams();\n } else if (typeof query === \"string\" || query instanceof URLSearchParams) {\n params = new URLSearchParams(query);\n } else {\n params = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) {\n if (v !== null && v !== undefined) params.append(k, String(v));\n }\n }\n if (version) params.set(VERSION_PARAM, version);\n const s = params.toString();\n return s ? `?${s}` : \"\";\n}\n\n/**\n * The language of a page asked for in language on a site of languages, its\n * default first: language, else the default. Every content page is under its\n * language. Mirrors Go's contenturl.Prefix.\n */\nexport function languagePrefix(languages: readonly string[], language = \"\"): string {\n return language || languages[0] || \"\";\n}\n\n/** Joins /{language}/{route}/{code}[/{slug}][?query]; ContentURLs.path() picks the language. */\nexport function contentPath(route: string, code: string, slug = \"\", options: PathOptions = {}): string {\n const prefix = options.language ? `/${options.language}` : \"\";\n return `${prefix}/${route}/${code}${slug ? `/${slug}` : \"\"}${queryString(options.query, options.version)}`;\n}\n\nexport interface ContentURLConfig {\n /** Content kind -> the host's route segment ({ video: \"watch\", gallery: \"g\" }). */\n routes: Record<string, string>;\n /**\n * The site's languages, its default first (the content model's). Required:\n * every content path starts with one; an unprefixed location is not canonical.\n */\n languages: string[];\n /** The site's origin (\"https://example.com\") for absolute URLs (canonical links, og:url). */\n origin?: string;\n}\n\nexport interface ParsedContentPath {\n /** \"\" when unprefixed. */\n language: string;\n route: string;\n /** Canonical form. */\n code: string;\n rawCode: string;\n /** As written; \"\" when absent. */\n slug: string;\n}\n\nexport interface CanonicalResult {\n /** The canonical path. */\n path: string;\n /** path plus the location's query. */\n location: string;\n /** The location's path is not canonical: replace it with location. */\n redirect: boolean;\n}\n\nexport interface CanonicalOptions {\n /** The language an unprefixed location redirects to (the reader's); default the site's default. */\n language?: string;\n}\n\nexport interface HreflangOptions {\n /** Languages the content exists in; default every configured language. */\n languages?: readonly string[];\n /** The language x-default points at, listed or not; default the site's default language. */\n defaultLanguage?: string;\n /** The origin for absolute URLs; default the configured one (throws without either). */\n origin?: string;\n}\n\nexport interface ContentURLs {\n /** The configured language segments. */\n readonly languages: readonly string[];\n /** The configured origin, without a trailing slash. */\n readonly origin?: string;\n /** link's page in a language (the default when unset), with ?v= for a version; throws when its kind has no route. */\n path(link: ContentLink, options?: PathOptions): string;\n /** path() on the configured origin; throws without one. */\n url(link: ContentLink, options?: PathOptions): string;\n /** Splits a pathname; null when it is not a content path (or its code is invalid). */\n parse(pathname: string): ParsedContentPath | null;\n /**\n * Compares a location (pathname plus optional ?query and #hash) with link's\n * canonical path, for a client-side history.replaceState after the page's\n * data arrives; the query (?v= included) stays. null when the location is\n * not a content path or link's kind has no route.\n */\n canonical(location: string, link: ContentLink, options?: CanonicalOptions): CanonicalResult | null;\n /**\n * hreflang alternates: each language's absolute URL of link (its own\n * slug), plus x-default, the default language's. Never a version's: a\n * version's canonical URL is its language page.\n */\n hreflang(link: ContentLink, options?: HreflangOptions): Record<string, string>;\n}\n\n/** Query parameters that only track a visit: never part of a canonical URL. */\nexport const TRACKING_PARAMS = [\"gclid\", \"gbraid\", \"wbraid\", \"dclid\", \"fbclid\", \"msclkid\", \"yclid\", \"twclid\", \"igshid\", \"mc_cid\", \"mc_eid\", \"_ga\", \"_gl\"];\n\n/**\n * href without tracking parameters (utm_* and TRACKING_PARAMS, plus drop),\n * its fragment or a trailing slash: the canonical URL of a page that is not\n * a content page.\n */\nexport function canonicalURL(href: string, options: { drop?: readonly string[] } = {}): string {\n const url = new URL(href);\n const drop = new Set([...TRACKING_PARAMS, ...(options.drop ?? [])]);\n for (const key of [...url.searchParams.keys()]) if (key.startsWith(\"utm_\") || drop.has(key)) url.searchParams.delete(key);\n url.hash = \"\";\n if (url.pathname.length > 1 && url.pathname.endsWith(\"/\")) url.pathname = url.pathname.replace(/\\/+$/, \"\");\n return url.toString();\n}\n\nconst SEGMENT = /^[a-z0-9][a-z0-9_-]*$/;\n\n/** Builds the URL helpers for one host's routes. */\nexport function createContentURLs(config: ContentURLConfig): ContentURLs {\n const routes = { ...config.routes };\n const origin = config.origin?.replace(/\\/+$/, \"\");\n if (origin !== undefined && !/^https?:\\/\\/[^/?#]+$/.test(origin)) throw new Error(`contentkit-ui/urls: origin ${JSON.stringify(config.origin)} is not an http(s) origin`);\n const languages = [...(config.languages ?? [])];\n const routeSet = new Set(Object.values(routes));\n if (routeSet.size === 0) throw new Error(\"contentkit-ui/urls: routes is empty\");\n if (languages.length === 0) throw new Error(\"contentkit-ui/urls: languages is empty\");\n for (const r of [...routeSet, ...languages]) {\n if (!SEGMENT.test(r)) throw new Error(`contentkit-ui/urls: ${JSON.stringify(r)} is not one lowercase path segment`);\n }\n for (const l of languages) {\n if (routeSet.has(l)) throw new Error(`contentkit-ui/urls: language ${JSON.stringify(l)} is also a route`);\n }\n\n const canonicalPath = (link: ContentLink, language: string): string | null => {\n const route = routes[link.content_kind];\n const lang = languagePrefix(languages, language);\n return route === undefined ? null : contentPath(route, link.code, slugFor(link, lang), { language: lang });\n };\n\n const parse = (pathname: string): ParsedContentPath | null => {\n if (!pathname.startsWith(\"/\")) return null;\n let rest = pathname.slice(1);\n if (rest.endsWith(\"/\")) rest = rest.slice(0, -1);\n let segs = rest.split(\"/\");\n if (segs.some((s) => s === \"\")) return null;\n let language = \"\";\n if (segs.length > 2 && languages.includes(segs[0]!)) {\n language = segs[0]!;\n segs = segs.slice(1);\n }\n if (segs.length < 2 || segs.length > 3 || !routeSet.has(segs[0]!)) return null;\n const code = parseCode(segs[1]!);\n if (code === null) return null;\n return { language, route: segs[0]!, code, rawCode: segs[1]!, slug: segs[2] ?? \"\" };\n };\n\n const path = (link: ContentLink, options: PathOptions = {}): string => {\n const route = routes[link.content_kind];\n if (route === undefined) throw new Error(`contentkit-ui/urls: no route for kind ${JSON.stringify(link.content_kind)}`);\n const language = languagePrefix(languages, options.language);\n return contentPath(route, link.code, slugFor(link, language), { ...options, language });\n };\n\n return {\n languages,\n origin,\n path,\n url(link, options) {\n if (origin === undefined) throw new Error(\"contentkit-ui/urls: url() needs ContentURLConfig.origin\");\n return origin + path(link, options);\n },\n parse,\n canonical(location, link, options = {}) {\n const noHash = location.split(\"#\", 1)[0]!;\n const q = noHash.indexOf(\"?\");\n const pathname = q < 0 ? noHash : noHash.slice(0, q);\n const search = q < 0 ? \"\" : noHash.slice(q + 1);\n const parsed = parse(pathname);\n if (parsed === null) return null;\n const language = parsed.language || (options.language && languages.includes(options.language) ? options.language : \"\");\n const path = canonicalPath(link, language);\n if (path === null) return null;\n return { path, location: search ? `${path}?${search}` : path, redirect: pathname !== path };\n },\n hreflang(link, options = {}) {\n const base = options.origin?.replace(/\\/+$/, \"\") ?? origin;\n if (base === undefined) throw new Error(\"contentkit-ui/urls: hreflang() needs an origin\");\n const out: Record<string, string> = {};\n const langs = (options.languages ?? languages).filter((l) => languages.includes(l));\n for (const language of langs) out[language] = base + path(link, { language });\n const def = options.defaultLanguage ?? languages[0];\n if (langs.length) out[\"x-default\"] = base + path(link, { language: def });\n return out;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"urls.js","names":[],"sources":["../src/urls/index.ts"],"sourcesContent":["// ContentKit content URLs: /{lang}/{route}/{CODE}[/{slug}][?v={version}]. Every\n// page is under its language, the default included; the code is the only part\n// of the path the server reads, the slug is decoration, and ?v= selects a\n// version. Mirrors Go's contenturl (the two share contenturl/testdata/vectors.json).\n\nexport const CODE_LENGTH = 9;\n\n/** Crockford base32: digits and uppercase letters without I, L, O, U. */\nexport const CODE_ALPHABET = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\n\nfunction parseCrockford(input: string, length: number): string | null {\n let out = \"\";\n for (const ch of input) {\n if (ch === \"-\") continue;\n if (ch.length !== 1 || ch.charCodeAt(0) > 0x7f) return null;\n let c = ch.toUpperCase();\n if (c === \"O\") c = \"0\";\n else if (c === \"I\" || c === \"L\") c = \"1\";\n if (!CODE_ALPHABET.includes(c) || out.length === length) return null;\n out += c;\n }\n if (out.length !== length || !/[A-Z]/.test(out)) return null;\n return out;\n}\n\n/**\n * Reads a code with Crockford's decoding rules (any case, O as 0, I and L as 1,\n * hyphens ignored) and returns its canonical form, or null when the input is\n * not a code. An all-digit result is never a code (it reads as a legacy id).\n */\nexport function parseCode(input: string): string | null {\n return parseCrockford(input, CODE_LENGTH);\n}\n\n/** The query parameter that selects a version on its content's page. */\nexport const VERSION_PARAM = \"v\";\n\nexport const VERSION_CODE_LENGTH = 4;\n\n/** Reads a version code (four characters, parseCode's rules); null when the input is not one. */\nexport function parseVersionCode(input: string): string | null {\n return parseCrockford(input, VERSION_CODE_LENGTH);\n}\n\n/** The version code a location's (or search string's) ?v= names; null when it names none. */\nexport function versionParam(location: string | URLSearchParams): string | null {\n const v = (typeof location === \"string\" ? searchOf(location) : location).get(VERSION_PARAM);\n return v === null ? null : parseVersionCode(v);\n}\n\nfunction searchOf(location: string): URLSearchParams {\n const noHash = location.split(\"#\", 1)[0]!;\n const q = noHash.indexOf(\"?\");\n return new URLSearchParams(q < 0 ? \"\" : noHash.slice(q + 1));\n}\n\n/** location with ?v= set to code, or removed for null; its other parameters and hash stay. */\nexport function withVersion(location: string, code: string | null): string {\n const hashAt = location.indexOf(\"#\");\n const hash = hashAt < 0 ? \"\" : location.slice(hashAt);\n const rest = hashAt < 0 ? location : location.slice(0, hashAt);\n const q = rest.indexOf(\"?\");\n const params = new URLSearchParams(q < 0 ? \"\" : rest.slice(q + 1));\n if (code === null) params.delete(VERSION_PARAM);\n else params.set(VERSION_PARAM, code);\n const search = params.toString();\n return (q < 0 ? rest : rest.slice(0, q)) + (search ? `?${search}` : \"\") + hash;\n}\n\n/** Reports whether input is a code in canonical form. */\nexport function isCode(input: string): boolean {\n return parseCode(input) === input;\n}\n\n/** A resolved content link, as ContentKit and host APIs return it. */\nexport interface ContentLink {\n content_kind: string;\n code: string;\n slug: string;\n /** Localized slugs keyed by language; the default slug applies otherwise. */\n slugs?: Record<string, string>;\n}\n\n/** The slug for language, falling back to the default slug. */\nexport function slugFor(link: Pick<ContentLink, \"slug\" | \"slugs\">, language = \"\"): string {\n return (language && link.slugs?.[language]) || link.slug;\n}\n\nexport type QueryInit = string | URLSearchParams | Record<string, string | number | boolean | null | undefined>;\n\nexport interface PathOptions {\n /** The page's language; ContentURLs use the site's default when unset. */\n language?: string;\n /** Query string for sub-pages, e.g. { p: 12 }. Null and undefined values are dropped. */\n query?: QueryInit;\n /** A version code (Version.code): ?v= selects that version on the page. */\n version?: string | null;\n}\n\nfunction queryString(query: QueryInit | undefined, version?: string | null): string {\n let params: URLSearchParams;\n if (query === undefined) {\n params = new URLSearchParams();\n } else if (typeof query === \"string\" || query instanceof URLSearchParams) {\n params = new URLSearchParams(query);\n } else {\n params = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) {\n if (v !== null && v !== undefined) params.append(k, String(v));\n }\n }\n if (version) params.set(VERSION_PARAM, version);\n const s = params.toString();\n return s ? `?${s}` : \"\";\n}\n\n/**\n * The language of a page asked for in language on a site of languages, its\n * default first: language, else the default. Every content page is under its\n * language. Mirrors Go's contenturl.Prefix.\n */\nexport function languagePrefix(languages: readonly string[], language = \"\"): string {\n return language || languages[0] || \"\";\n}\n\n/** Joins /{language}/{route}/{code}[/{slug}][?query]; slug is the \"/\"-joined decorative path. ContentURLs.path() picks the language. */\nexport function contentPath(route: string, code: string, slug = \"\", options: PathOptions = {}): string {\n const prefix = options.language ? `/${options.language}` : \"\";\n return `${prefix}/${route}/${code}${slug ? `/${slug}` : \"\"}${queryString(options.query, options.version)}`;\n}\n\nexport interface ContentURLConfig {\n /** Content kind -> the host's route segment ({ video: \"watch\", gallery: \"g\" }). */\n routes: Record<string, string>;\n /**\n * The site's languages, its default first (the content model's). Required:\n * every content path starts with one; an unprefixed location is not canonical.\n */\n languages: string[];\n /** The site's origin (\"https://example.com\") for absolute URLs (canonical links, og:url). */\n origin?: string;\n}\n\nexport interface ParsedContentPath {\n /** \"\" when unprefixed. */\n language: string;\n route: string;\n /** Canonical form. */\n code: string;\n rawCode: string;\n /** As written; \"\" when absent. */\n slug: string;\n}\n\nexport interface CanonicalResult {\n /** The canonical path. */\n path: string;\n /** path plus the location's query. */\n location: string;\n /** The location's path is not canonical: replace it with location. */\n redirect: boolean;\n}\n\nexport interface CanonicalOptions {\n /** The language an unprefixed location redirects to (the reader's); default the site's default. */\n language?: string;\n}\n\nexport interface HreflangOptions {\n /** Languages the content exists in; default every configured language. */\n languages?: readonly string[];\n /** The language x-default points at, listed or not; default the site's default language. */\n defaultLanguage?: string;\n /** The origin for absolute URLs; default the configured one (throws without either). */\n origin?: string;\n}\n\nexport interface ContentURLs {\n /** The configured language segments. */\n readonly languages: readonly string[];\n /** The configured origin, without a trailing slash. */\n readonly origin?: string;\n /** link's page in a language (the default when unset), with ?v= for a version; throws when its kind has no route. */\n path(link: ContentLink, options?: PathOptions): string;\n /** path() on the configured origin; throws without one. */\n url(link: ContentLink, options?: PathOptions): string;\n /** Splits a pathname; null when it is not a content path (or its code is invalid). */\n parse(pathname: string): ParsedContentPath | null;\n /**\n * Compares a location (pathname plus optional ?query and #hash) with link's\n * canonical path, for a client-side history.replaceState after the page's\n * data arrives; the query (?v= included) stays. null when the location is\n * not a content path or link's kind has no route.\n */\n canonical(location: string, link: ContentLink, options?: CanonicalOptions): CanonicalResult | null;\n /**\n * hreflang alternates: each language's absolute URL of link (its own\n * slug), plus x-default, the default language's. Never a version's: a\n * version's canonical URL is its language page.\n */\n hreflang(link: ContentLink, options?: HreflangOptions): Record<string, string>;\n}\n\n/** Query parameters that only track a visit: never part of a canonical URL. */\nexport const TRACKING_PARAMS = [\"gclid\", \"gbraid\", \"wbraid\", \"dclid\", \"fbclid\", \"msclkid\", \"yclid\", \"twclid\", \"igshid\", \"mc_cid\", \"mc_eid\", \"_ga\", \"_gl\"];\n\n/**\n * href without tracking parameters (utm_* and TRACKING_PARAMS, plus drop),\n * its fragment or a trailing slash: the canonical URL of a page that is not\n * a content page.\n */\nexport function canonicalURL(href: string, options: { drop?: readonly string[] } = {}): string {\n const url = new URL(href);\n const drop = new Set([...TRACKING_PARAMS, ...(options.drop ?? [])]);\n for (const key of [...url.searchParams.keys()]) if (key.startsWith(\"utm_\") || drop.has(key)) url.searchParams.delete(key);\n url.hash = \"\";\n if (url.pathname.length > 1 && url.pathname.endsWith(\"/\")) url.pathname = url.pathname.replace(/\\/+$/, \"\");\n return url.toString();\n}\n\nconst SEGMENT = /^[a-z0-9][a-z0-9_-]*$/;\n\n/** Builds the URL helpers for one host's routes. */\nexport function createContentURLs(config: ContentURLConfig): ContentURLs {\n const routes = { ...config.routes };\n const origin = config.origin?.replace(/\\/+$/, \"\");\n if (origin !== undefined && !/^https?:\\/\\/[^/?#]+$/.test(origin)) throw new Error(`contentkit-ui/urls: origin ${JSON.stringify(config.origin)} is not an http(s) origin`);\n const languages = [...(config.languages ?? [])];\n const routeSet = new Set(Object.values(routes));\n if (routeSet.size === 0) throw new Error(\"contentkit-ui/urls: routes is empty\");\n if (languages.length === 0) throw new Error(\"contentkit-ui/urls: languages is empty\");\n for (const r of [...routeSet, ...languages]) {\n if (!SEGMENT.test(r)) throw new Error(`contentkit-ui/urls: ${JSON.stringify(r)} is not one lowercase path segment`);\n }\n for (const l of languages) {\n if (routeSet.has(l)) throw new Error(`contentkit-ui/urls: language ${JSON.stringify(l)} is also a route`);\n }\n\n const canonicalPath = (link: ContentLink, language: string): string | null => {\n const route = routes[link.content_kind];\n const lang = languagePrefix(languages, language);\n return route === undefined ? null : contentPath(route, link.code, slugFor(link, lang), { language: lang });\n };\n\n const parse = (pathname: string): ParsedContentPath | null => {\n if (!pathname.startsWith(\"/\")) return null;\n let rest = pathname.slice(1);\n if (rest.endsWith(\"/\")) rest = rest.slice(0, -1);\n let segs = rest.split(\"/\");\n if (segs.some((s) => s === \"\")) return null;\n let language = \"\";\n if (segs.length > 2 && languages.includes(segs[0]!)) {\n language = segs[0]!;\n segs = segs.slice(1);\n }\n // A stray first segment before a known route (an unserved language) is dropped, so it redirects.\n if (!language && segs.length > 2 && !routeSet.has(segs[0]!) && routeSet.has(segs[1]!)) segs = segs.slice(1);\n if (segs.length < 2 || !routeSet.has(segs[0]!)) return null;\n const code = parseCode(segs[1]!);\n if (code === null) return null;\n // Decorative segments (an entity and a title); stale or extra ones redirect.\n return { language, route: segs[0]!, code, rawCode: segs[1]!, slug: segs.slice(2).join(\"/\") };\n };\n\n const path = (link: ContentLink, options: PathOptions = {}): string => {\n const route = routes[link.content_kind];\n if (route === undefined) throw new Error(`contentkit-ui/urls: no route for kind ${JSON.stringify(link.content_kind)}`);\n const language = languagePrefix(languages, options.language);\n return contentPath(route, link.code, slugFor(link, language), { ...options, language });\n };\n\n return {\n languages,\n origin,\n path,\n url(link, options) {\n if (origin === undefined) throw new Error(\"contentkit-ui/urls: url() needs ContentURLConfig.origin\");\n return origin + path(link, options);\n },\n parse,\n canonical(location, link, options = {}) {\n const noHash = location.split(\"#\", 1)[0]!;\n const q = noHash.indexOf(\"?\");\n const pathname = q < 0 ? noHash : noHash.slice(0, q);\n const search = q < 0 ? \"\" : noHash.slice(q + 1);\n const parsed = parse(pathname);\n if (parsed === null) return null;\n const language = parsed.language || (options.language && languages.includes(options.language) ? options.language : \"\");\n const path = canonicalPath(link, language);\n if (path === null) return null;\n return { path, location: search ? `${path}?${search}` : path, redirect: pathname !== path };\n },\n hreflang(link, options = {}) {\n const base = options.origin?.replace(/\\/+$/, \"\") ?? origin;\n if (base === undefined) throw new Error(\"contentkit-ui/urls: hreflang() needs an origin\");\n const out: Record<string, string> = {};\n const langs = (options.languages ?? languages).filter((l) => languages.includes(l));\n for (const language of langs) out[language] = base + path(link, { language });\n const def = options.defaultLanguage ?? languages[0];\n if (langs.length) out[\"x-default\"] = base + path(link, { language: def });\n return out;\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openrails/contentkit-ui",
3
- "version": "0.104.0-rc.1",
3
+ "version": "0.104.0-rc.2",
4
4
  "description": "ContentKit in the browser: client, React hooks and styled components for media, uploads and content URLs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,13 +0,0 @@
1
- import { ContentKitUiAppearance } from '../../appearance.js';
2
- import { ContentKitClient } from '../../client/client.js';
3
- import { ContentKitErrorHandler } from '../../react/context.js';
4
- export interface PageReviewProps {
5
- content: string;
6
- version: string;
7
- client?: ContentKitClient;
8
- onError?: ContentKitErrorHandler;
9
- className?: string;
10
- appearance?: ContentKitUiAppearance;
11
- }
12
- /** Review automatic page choices without holding up ordinary publication. */
13
- export declare function PageReview({ content, version, client, onError, className, appearance }: PageReviewProps): import('react').JSX.Element;
@@ -1,225 +0,0 @@
1
- import { useMessages as e } from "../../i18n/context.js";
2
- import { ContentKitUiRoot as t } from "../../scope.js";
3
- import { Button as n } from "../../ui/button.js";
4
- import { SortableList as r } from "../sortable-list.js";
5
- import { Badge as i } from "../../ui/badge.js";
6
- import { Select as a, SelectContent as o, SelectItem as s, SelectTrigger as c, SelectValue as l } from "../../ui/select.js";
7
- import { ErrorLine as u, useAction as d } from "../social/parts.js";
8
- import { LoadFailed as f, Rows as p } from "./shell.js";
9
- import { useEditorSaveTask as m } from "../content/editor-save.js";
10
- import { usePageReview as h } from "../../react/page-review.js";
11
- import { useState as g } from "react";
12
- import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
13
- import { cn as b } from "cn";
14
- //#region src/components/admin/page-review.tsx
15
- function x(e) {
16
- if (e) try {
17
- let t = new URL(e, globalThis.location?.href ?? "http://localhost/");
18
- if (t.protocol === "http:" || t.protocol === "https:") return e;
19
- } catch {}
20
- }
21
- var S = (e) => e.replace(/[_-]+/g, " "), C = (e) => e.source_entry || e.source_name;
22
- function w({ content: w, version: T, client: E, onError: D, className: O, appearance: k }) {
23
- let { t: A } = e(), j = h(w, T, { client: E }), M = d("reader.save", D), [N, P] = g(!1);
24
- m(`page-review:${T}`, {
25
- label: A("pageReview.title"),
26
- dirty: !1,
27
- pending: M.pending,
28
- valid: !0,
29
- save: async () => !0
30
- });
31
- let F = j.review, I = F?.pages.some((e) => e.flags.includes("segmented_selection_preserved")) ?? !1, L = (e) => {
32
- !F || I || M.pending || j.loading || (P(!1), M.run(() => j.update({
33
- ...e,
34
- revision: F.revision
35
- })).then((e) => e && P(!0)));
36
- }, R = (() => {
37
- if (!F) return j.error ? /* @__PURE__ */ v(f, {
38
- error: j.error,
39
- message: A("pageReview.loadFailed"),
40
- retry: j.reload
41
- }) : /* @__PURE__ */ v(p, { count: 2 });
42
- if (!F.pages.length) return /* @__PURE__ */ v("p", {
43
- className: "text-muted-foreground",
44
- children: A("pageReview.empty")
45
- });
46
- let e = I || M.pending || j.loading;
47
- return /* @__PURE__ */ y(_, { children: [
48
- /* @__PURE__ */ v("p", {
49
- className: "text-muted-foreground",
50
- children: A("pageReview.summary", {
51
- selected: F.selected,
52
- total: F.pages.length,
53
- flagged: F.flagged
54
- })
55
- }),
56
- I ? /* @__PURE__ */ v("p", {
57
- role: "status",
58
- className: "rounded-md border border-border p-2 text-muted-foreground",
59
- children: A("pageReview.chapterGuard")
60
- }) : /* @__PURE__ */ v("p", {
61
- className: "text-xs text-muted-foreground",
62
- children: A("pageReview.help")
63
- }),
64
- /* @__PURE__ */ v("div", {
65
- className: "max-h-[32rem] overflow-y-auto rounded-md border border-border p-2",
66
- children: /* @__PURE__ */ v(r, {
67
- items: F.pages,
68
- id: (e) => e.id,
69
- name: C,
70
- label: A("pageReview.order"),
71
- disabled: e || F.pages.length < 2,
72
- onMove: (e, t) => {
73
- if (e === t) return;
74
- let n = F.pages.map((e) => e.id), [r] = n.splice(e, 1);
75
- n.splice(t, 0, r), L({ order: n });
76
- },
77
- children: (t, n, r) => {
78
- let u = x(t.preview_url), d = x(t.source_url);
79
- return /* @__PURE__ */ y("div", {
80
- className: "flex gap-2 rounded-md border border-border p-2",
81
- "data-ckui": "page-review-row",
82
- "data-included": t.included,
83
- "data-mode": t.mode,
84
- children: [/* @__PURE__ */ y("div", {
85
- className: "flex shrink-0 flex-col items-center gap-1",
86
- children: [
87
- /* @__PURE__ */ v("span", {
88
- className: "text-xs text-muted-foreground",
89
- children: r + 1
90
- }),
91
- n,
92
- u && /* @__PURE__ */ v("img", {
93
- src: u,
94
- alt: "",
95
- loading: "lazy",
96
- decoding: "async",
97
- className: "h-16 w-12 rounded object-contain"
98
- })
99
- ]
100
- }), /* @__PURE__ */ y("div", {
101
- className: "grid min-w-0 flex-1 gap-1",
102
- children: [
103
- /* @__PURE__ */ y("p", {
104
- className: "break-words",
105
- children: [/* @__PURE__ */ y("span", {
106
- className: "text-muted-foreground",
107
- children: [A("pageReview.original"), " "]
108
- }), C(t)]
109
- }),
110
- /* @__PURE__ */ y("p", {
111
- className: "break-words",
112
- children: [/* @__PURE__ */ y("span", {
113
- className: "text-muted-foreground",
114
- children: [A("pageReview.canonical"), " "]
115
- }), t.id]
116
- }),
117
- /* @__PURE__ */ y("div", {
118
- className: "flex flex-wrap items-center gap-2",
119
- children: [
120
- /* @__PURE__ */ v(i, {
121
- variant: t.included ? "outline" : "secondary",
122
- children: A(t.included ? "pageReview.included" : "pageReview.excluded")
123
- }),
124
- !!t.flags.length && /* @__PURE__ */ v(i, {
125
- variant: "outline",
126
- children: A("pageReview.flagged")
127
- }),
128
- d && /* @__PURE__ */ v("a", {
129
- href: d,
130
- target: "_blank",
131
- rel: "noopener noreferrer",
132
- className: "text-xs underline",
133
- children: A("pageReview.source")
134
- })
135
- ]
136
- }),
137
- /* @__PURE__ */ y("p", {
138
- className: "text-xs text-muted-foreground",
139
- children: [
140
- S(t.decision.reason),
141
- " · ",
142
- A("pageReview.confidence", { confidence: S(t.decision.confidence) })
143
- ]
144
- }),
145
- !!t.flags.length && /* @__PURE__ */ v("p", {
146
- className: "text-xs text-muted-foreground",
147
- children: t.flags.map(S).join(" · ")
148
- }),
149
- /* @__PURE__ */ y(a, {
150
- value: t.mode,
151
- onValueChange: (e) => {
152
- e !== t.mode && (e === "auto" || e === "include" || e === "exclude") && L({ pages: [{
153
- id: t.id,
154
- mode: e
155
- }] });
156
- },
157
- children: [/* @__PURE__ */ v(c, {
158
- size: "sm",
159
- className: "w-44",
160
- disabled: e,
161
- "aria-label": A("pageReview.choice", { name: C(t) }),
162
- children: /* @__PURE__ */ v(l, { children: (e) => A(e === "include" ? "pageReview.include" : e === "exclude" ? "pageReview.exclude" : "pageReview.automatic") })
163
- }), /* @__PURE__ */ y(o, { children: [
164
- /* @__PURE__ */ v(s, {
165
- value: "auto",
166
- children: A("pageReview.automatic")
167
- }),
168
- /* @__PURE__ */ v(s, {
169
- value: "include",
170
- children: A("pageReview.include")
171
- }),
172
- /* @__PURE__ */ v(s, {
173
- value: "exclude",
174
- children: A("pageReview.exclude")
175
- })
176
- ] })]
177
- })
178
- ]
179
- })]
180
- });
181
- }
182
- })
183
- }),
184
- M.error?.status === 409 ? /* @__PURE__ */ v("p", {
185
- role: "alert",
186
- className: "text-destructive",
187
- children: A("pageReview.conflict")
188
- }) : /* @__PURE__ */ v(u, { error: M.error }),
189
- j.error && /* @__PURE__ */ v(f, {
190
- error: j.error,
191
- message: A("pageReview.loadFailed"),
192
- retry: j.reload
193
- }),
194
- N && /* @__PURE__ */ v("span", {
195
- role: "status",
196
- className: "text-muted-foreground",
197
- children: A("pageReview.saved")
198
- })
199
- ] });
200
- })();
201
- return /* @__PURE__ */ y(t, {
202
- appearance: k,
203
- className: b("grid gap-3 text-sm", O),
204
- "data-ckui": "page-review",
205
- "aria-busy": M.pending || j.loading,
206
- children: [/* @__PURE__ */ y("div", {
207
- className: "flex items-center justify-between gap-2",
208
- children: [/* @__PURE__ */ v("h4", {
209
- className: "font-medium",
210
- children: A("pageReview.title")
211
- }), /* @__PURE__ */ v(n, {
212
- type: "button",
213
- size: "xs",
214
- variant: "ghost",
215
- disabled: M.pending || j.loading,
216
- onClick: j.reload,
217
- children: A("pageReview.refresh")
218
- })]
219
- }), R]
220
- });
221
- }
222
- //#endregion
223
- export { w as PageReview };
224
-
225
- //# sourceMappingURL=page-review.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"page-review.js","names":[],"sources":["../../../src/components/admin/page-review.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useState } from \"react\";\nimport type { ContentKitUiAppearance } from \"../../appearance.js\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport type { PageReviewEntry, PageReviewPatch } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport type { ContentKitErrorHandler } from \"../../react/context.js\";\nimport { usePageReview } from \"../../react/page-review.js\";\nimport { ContentKitUiRoot } from \"../../scope.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"#ckui/ui/select\";\nimport { SortableList } from \"../sortable-list.js\";\nimport { ErrorLine, useAction } from \"../social/parts.js\";\nimport { useEditorSaveTask } from \"../content/editor-save.js\";\nimport { LoadFailed, Rows } from \"./shell.js\";\n\nexport interface PageReviewProps {\n content: string;\n version: string;\n client?: ContentKitClient;\n onError?: ContentKitErrorHandler;\n className?: string;\n appearance?: ContentKitUiAppearance;\n}\n\n/** Only link the editor media URLs supplied by the server, never upload paths. */\nfunction imageURL(value?: string): string | undefined {\n if (!value) return;\n try {\n const url = new URL(value, globalThis.location?.href ?? \"http://localhost/\");\n if (url.protocol === \"http:\" || url.protocol === \"https:\") return value;\n } catch { /* An invalid or unsupported URL has no preview. */ }\n}\n\nconst words = (value: string) => value.replace(/[_-]+/g, \" \");\nconst originalName = (page: PageReviewEntry) => page.source_entry || page.source_name;\n\n/** Review automatic page choices without holding up ordinary publication. */\nexport function PageReview({ content, version, client, onError, className, appearance }: PageReviewProps) {\n const { t } = useMessages();\n const r = usePageReview(content, version, { client });\n const save = useAction(\"reader.save\", onError);\n const [saved, setSaved] = useState(false);\n useEditorSaveTask(`page-review:${version}`,{label:t(\"pageReview.title\"),dirty:false,pending:save.pending,valid:true,save:async()=>true});\n const review = r.review;\n const chapterGuarded = review?.pages.some((page) => page.flags.includes(\"segmented_selection_preserved\")) ?? false;\n const apply = (patch: Omit<PageReviewPatch, \"revision\">) => {\n if (!review || chapterGuarded || save.pending || r.loading) return;\n setSaved(false);\n void save.run(() => r.update({ ...patch, revision: review.revision })).then((result) => result && setSaved(true));\n };\n const body = (() => {\n if (!review) return r.error ? <LoadFailed error={r.error} message={t(\"pageReview.loadFailed\")} retry={r.reload} /> : <Rows count={2} />;\n if (!review.pages.length) return <p className=\"text-muted-foreground\">{t(\"pageReview.empty\")}</p>;\n const disabled = chapterGuarded || save.pending || r.loading;\n return <>\n <p className=\"text-muted-foreground\">{t(\"pageReview.summary\", { selected: review.selected, total: review.pages.length, flagged: review.flagged })}</p>\n {chapterGuarded\n ? <p role=\"status\" className=\"rounded-md border border-border p-2 text-muted-foreground\">{t(\"pageReview.chapterGuard\")}</p>\n : <p className=\"text-xs text-muted-foreground\">{t(\"pageReview.help\")}</p>}\n <div className=\"max-h-[32rem] overflow-y-auto rounded-md border border-border p-2\">\n <SortableList items={review.pages} id={(page) => page.id} name={originalName} label={t(\"pageReview.order\")} disabled={disabled || review.pages.length < 2}\n onMove={(from, to) => {\n if (from === to) return;\n const order = review.pages.map((page) => page.id);\n const [moved] = order.splice(from, 1);\n order.splice(to, 0, moved!);\n apply({ order });\n }}>\n {(page, handle, index) => {\n const preview = imageURL(page.preview_url);\n const source = imageURL(page.source_url);\n return <div className=\"flex gap-2 rounded-md border border-border p-2\" data-ckui=\"page-review-row\" data-included={page.included} data-mode={page.mode}>\n <div className=\"flex shrink-0 flex-col items-center gap-1\">\n <span className=\"text-xs text-muted-foreground\">{index + 1}</span>\n {handle}\n {preview && <img src={preview} alt=\"\" loading=\"lazy\" decoding=\"async\" className=\"h-16 w-12 rounded object-contain\" />}\n </div>\n <div className=\"grid min-w-0 flex-1 gap-1\">\n <p className=\"break-words\"><span className=\"text-muted-foreground\">{t(\"pageReview.original\")} </span>{originalName(page)}</p>\n <p className=\"break-words\"><span className=\"text-muted-foreground\">{t(\"pageReview.canonical\")} </span>{page.id}</p>\n <div className=\"flex flex-wrap items-center gap-2\">\n <Badge variant={page.included ? \"outline\" : \"secondary\"}>{t(page.included ? \"pageReview.included\" : \"pageReview.excluded\")}</Badge>\n {!!page.flags.length && <Badge variant=\"outline\">{t(\"pageReview.flagged\")}</Badge>}\n {source && <a href={source} target=\"_blank\" rel=\"noopener noreferrer\" className=\"text-xs underline\">{t(\"pageReview.source\")}</a>}\n </div>\n <p className=\"text-xs text-muted-foreground\">{words(page.decision.reason)} · {t(\"pageReview.confidence\", { confidence: words(page.decision.confidence) })}</p>\n {!!page.flags.length && <p className=\"text-xs text-muted-foreground\">{page.flags.map(words).join(\" · \")}</p>}\n <Select value={page.mode} onValueChange={(mode) => {\n if (mode !== page.mode && (mode === \"auto\" || mode === \"include\" || mode === \"exclude\")) apply({ pages: [{ id: page.id, mode }] });\n }}>\n <SelectTrigger size=\"sm\" className=\"w-44\" disabled={disabled} aria-label={t(\"pageReview.choice\", { name: originalName(page) })}>\n <SelectValue>{(mode: string) => t(mode === \"include\" ? \"pageReview.include\" : mode === \"exclude\" ? \"pageReview.exclude\" : \"pageReview.automatic\")}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"auto\">{t(\"pageReview.automatic\")}</SelectItem>\n <SelectItem value=\"include\">{t(\"pageReview.include\")}</SelectItem>\n <SelectItem value=\"exclude\">{t(\"pageReview.exclude\")}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>;\n }}\n </SortableList>\n </div>\n {save.error?.status === 409 ? <p role=\"alert\" className=\"text-destructive\">{t(\"pageReview.conflict\")}</p> : <ErrorLine error={save.error} />}\n {r.error && <LoadFailed error={r.error} message={t(\"pageReview.loadFailed\")} retry={r.reload} />}\n {saved && <span role=\"status\" className=\"text-muted-foreground\">{t(\"pageReview.saved\")}</span>}\n </>;\n })();\n return <ContentKitUiRoot appearance={appearance} className={cn(\"grid gap-3 text-sm\", className)} data-ckui=\"page-review\" aria-busy={save.pending || r.loading}>\n <div className=\"flex items-center justify-between gap-2\">\n <h4 className=\"font-medium\">{t(\"pageReview.title\")}</h4>\n <Button type=\"button\" size=\"xs\" variant=\"ghost\" disabled={save.pending || r.loading} onClick={r.reload}>{t(\"pageReview.refresh\")}</Button>\n </div>\n {body}\n </ContentKitUiRoot>;\n}\n"],"mappings":"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"}
@@ -1,14 +0,0 @@
1
- import { ContentKitClient } from '../client/client.js';
2
- import { ContentKitError } from '../client/errors.js';
3
- import { PageReview, PageReviewPatch } from '../client/generated/wire.js';
4
- export interface UsePageReview {
5
- review: PageReview | null;
6
- loading: boolean;
7
- error?: ContentKitError;
8
- reload: () => void;
9
- update: (patch: PageReviewPatch) => Promise<PageReview>;
10
- }
11
- /** A version's retained page sources and reversible review choices. */
12
- export declare function usePageReview(content: string | null | undefined, version: string | null | undefined, o?: {
13
- client?: ContentKitClient;
14
- }): UsePageReview;
@@ -1,37 +0,0 @@
1
- import { toContentKitError as e } from "../client/errors.js";
2
- import { keyOf as t, useContentScope as n, useResource as r } from "./use-resource.js";
3
- import { useCallback as i } from "react";
4
- //#region src/react/page-review.ts
5
- function a(a, o, s = {}) {
6
- let { client: c, store: l, scope: u } = n(s.client), d = a && o ? t("page-review", u, a, o) : null, f = r(l, d, {
7
- type: "page-review",
8
- id: o ?? void 0,
9
- scope: a ?? void 0
10
- }, (e) => c.versions.pages(a, o, e)), { reload: p } = f, m = i(async (t) => {
11
- if (!a || !o) throw Error("contentkit-ui: no reader version");
12
- try {
13
- let e = await c.versions.updatePages(a, o, t);
14
- return d && l.set(d, e), e;
15
- } catch (t) {
16
- throw e(t).status === 409 && p(), t;
17
- }
18
- }, [
19
- c,
20
- a,
21
- o,
22
- l,
23
- d,
24
- p
25
- ]);
26
- return {
27
- review: f.data ?? null,
28
- loading: !!d && (f.loading || !f.loaded),
29
- error: f.error,
30
- reload: f.reload,
31
- update: m
32
- };
33
- }
34
- //#endregion
35
- export { a as usePageReview };
36
-
37
- //# sourceMappingURL=page-review.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"page-review.js","names":[],"sources":["../../src/react/page-review.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport type { ContentKitClient } from \"../client/client.js\";\nimport { toContentKitError, type ContentKitError } from \"../client/errors.js\";\nimport type { PageReview, PageReviewPatch } from \"../client/generated/wire.js\";\nimport { keyOf, useContentScope, useResource } from \"./use-resource.js\";\n\nexport interface UsePageReview {\n review: PageReview | null;\n loading: boolean;\n error?: ContentKitError;\n reload: () => void;\n update: (patch: PageReviewPatch) => Promise<PageReview>;\n}\n\n/** A version's retained page sources and reversible review choices. */\nexport function usePageReview(content: string | null | undefined, version: string | null | undefined, o: { client?: ContentKitClient } = {}): UsePageReview {\n const { client, store, scope } = useContentScope(o.client);\n const key = content && version ? keyOf(\"page-review\", scope, content, version) : null;\n const r = useResource<PageReview>(store, key, { type: \"page-review\", id: version ?? undefined, scope: content ?? undefined }, (signal) => client.versions.pages(content!, version!, signal));\n const { reload } = r;\n const update = useCallback(async (patch: PageReviewPatch) => {\n if(!content||!version)throw new Error(\"contentkit-ui: no reader version\");\n try {\n const next = await client.versions.updatePages(content, version, patch);\n if(key)store.set(key, next);\n return next;\n } catch (error) {\n if (toContentKitError(error).status === 409) reload();\n throw error;\n }\n }, [client, content, version, store, key, reload]);\n return { review: r.data ?? null, loading: !!key && (r.loading || !r.loaded), error: r.error, reload: r.reload, update };\n}\n"],"mappings":";;;;AAeA,SAAgB,EAAc,GAAoC,GAAoC,IAAmC,CAAC,GAAkB;CAC1J,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAM,KAAW,IAAU,EAAM,eAAe,GAAO,GAAS,CAAO,IAAI,MAC3E,IAAI,EAAwB,GAAO,GAAK;EAAE,MAAM;EAAe,IAAI,KAAW,KAAA;EAAW,OAAO,KAAW,KAAA;CAAU,IAAI,MAAW,EAAO,SAAS,MAAM,GAAU,GAAU,CAAM,CAAC,GACrL,EAAE,cAAW,GACb,IAAS,EAAY,OAAO,MAA2B;EAC3D,IAAG,CAAC,KAAS,CAAC,GAAQ,MAAU,MAAM,kCAAkC;EACxE,IAAI;GACF,IAAM,IAAO,MAAM,EAAO,SAAS,YAAY,GAAS,GAAS,CAAK;GAEtE,OADG,KAAI,EAAM,IAAI,GAAK,CAAI,GACnB;EACT,SAAS,GAAO;GAEd,MADI,EAAkB,CAAK,CAAC,CAAC,WAAW,OAAK,EAAO,GAC9C;EACR;CACF,GAAG;EAAC;EAAQ;EAAS;EAAS;EAAO;EAAK;CAAM,CAAC;CACjD,OAAO;EAAE,QAAQ,EAAE,QAAQ;EAAM,SAAS,CAAC,CAAC,MAAQ,EAAE,WAAW,CAAC,EAAE;EAAS,OAAO,EAAE;EAAO,QAAQ,EAAE;EAAQ;CAAO;AACxH"}