@openrails/contentkit-ui 0.90.0 → 0.90.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 (54) hide show
  1. package/README.md +82 -0
  2. package/dist/client/browser-search-history.d.ts +24 -0
  3. package/dist/client/browser-search-history.js +92 -0
  4. package/dist/client/browser-search-history.js.map +1 -0
  5. package/dist/client/discovery.d.ts +5 -2
  6. package/dist/client/discovery.js +14 -22
  7. package/dist/client/discovery.js.map +1 -1
  8. package/dist/client.js +14 -14
  9. package/dist/components/admin/content-admin.d.ts +28 -0
  10. package/dist/components/admin/content-admin.js +191 -0
  11. package/dist/components/admin/content-admin.js.map +1 -0
  12. package/dist/components/content/index.d.ts +1 -0
  13. package/dist/components/content/reader-settings.d.ts +19 -0
  14. package/dist/components/content/reader-settings.js +95 -0
  15. package/dist/components/content/reader-settings.js.map +1 -0
  16. package/dist/components/content/search.d.ts +2 -0
  17. package/dist/components/content/search.js +346 -137
  18. package/dist/components/content/search.js.map +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +18 -16
  21. package/dist/locales/admin/de.js +10 -0
  22. package/dist/locales/admin/de.js.map +1 -1
  23. package/dist/locales/admin/en.d.ts +10 -0
  24. package/dist/locales/admin/en.js +10 -0
  25. package/dist/locales/admin/en.js.map +1 -1
  26. package/dist/locales/admin/es.js +10 -0
  27. package/dist/locales/admin/es.js.map +1 -1
  28. package/dist/locales/admin/ja.js +10 -0
  29. package/dist/locales/admin/ja.js.map +1 -1
  30. package/dist/locales/admin/ko.js +10 -0
  31. package/dist/locales/admin/ko.js.map +1 -1
  32. package/dist/locales/admin/zh.js +10 -0
  33. package/dist/locales/admin/zh.js.map +1 -1
  34. package/dist/locales/discovery/de.js +9 -0
  35. package/dist/locales/discovery/de.js.map +1 -1
  36. package/dist/locales/discovery/en.d.ts +9 -0
  37. package/dist/locales/discovery/en.js +9 -0
  38. package/dist/locales/discovery/en.js.map +1 -1
  39. package/dist/locales/discovery/es.js +9 -0
  40. package/dist/locales/discovery/es.js.map +1 -1
  41. package/dist/locales/discovery/ja.js +9 -0
  42. package/dist/locales/discovery/ja.js.map +1 -1
  43. package/dist/locales/discovery/ko.js +9 -0
  44. package/dist/locales/discovery/ko.js.map +1 -1
  45. package/dist/locales/discovery/zh.js +9 -0
  46. package/dist/locales/discovery/zh.js.map +1 -1
  47. package/dist/locales/en.d.ts +19 -0
  48. package/dist/react/discovery.d.ts +1 -1
  49. package/dist/react/discovery.js +145 -92
  50. package/dist/react/discovery.js.map +1 -1
  51. package/dist/styles.css +1 -1
  52. package/dist/ui/dialog.js +16 -10
  53. package/dist/ui/dialog.js.map +1 -1
  54. package/package.json +1 -1
package/README.md CHANGED
@@ -473,6 +473,61 @@ const content = useContentGroups(contentId); // content.set(memberships, kinds)
473
473
  show humanized. Covers render `cover.renditions`; `renderCover` stands in
474
474
  until group reads carry them.
475
475
 
476
+ ## Reference administration workbench
477
+
478
+ `ContentAdmin` is an optional library component composing the existing content,
479
+ group, moderation, poll and statistics managers. It is not a standalone server
480
+ portal, a sign-in page or a SaaS control plane. A host can continue to compose
481
+ any individual manager in its own administration area.
482
+
483
+ ```tsx
484
+ import { ContentAdmin } from "@openrails/contentkit-ui";
485
+ import { richTextEditor } from "@openrails/contentkit-ui/editor";
486
+ import "@openrails/contentkit-ui/styles.css";
487
+
488
+ <ContentAdmin
489
+ client={contentkit}
490
+ basePath="/admin/content"
491
+ navigation={{ pathname, search, navigate }}
492
+ appearance={{ theme: "inherit" }}
493
+ sections={["contents", "groups", "moderation", "polls", "statistics"]}
494
+ contentKinds={["article", "video"]}
495
+ labels={{ article: "Articles", video: "Videos" }}
496
+ content={{ renderEditor: richTextEditor }}
497
+ />;
498
+ ```
499
+
500
+ The client is an existing ContentKit client, or pass `baseUrl` and the host's
501
+ `auth` transport. `guard`, `language`, `messages` and `onError` follow the
502
+ individual managers' contracts below. Without `navigation`, the workbench uses
503
+ browser history; without `basePath` as well, navigation stays in memory.
504
+
505
+ `GET /kinds` supplies languages and configured content and group kinds. Omit
506
+ `contentKinds` to offer all content kinds; omit `groupKinds` to offer the
507
+ site-owned group kinds. Explicit lists restrict and order those choices, and
508
+ unknown kinds are omitted. The routes beneath `basePath` are
509
+ `contents/<kind>[/new|/<id>]`, `groups/<kind>[/new|/<id>]`, `moderation`,
510
+ `polls[/new|/<id>]` and `statistics`; each manager retains its own filters and
511
+ subviews. Unknown or excluded routes show an unavailable view and do not load
512
+ a manager. Only the selected manager mounts. Navigation entries describe
513
+ configured sections, not permission grants: a selected manager can show its
514
+ normal no-access state. Statistics uses its own read permission, independently
515
+ of content editing.
516
+
517
+ The optional `content` props are `renderEditor` and `contentHref`; `groups`
518
+ accepts `renderContent`; `moderation` accepts `itemHref`, `userHref` and
519
+ `sections`; `polls` accepts `freeText` and `maxOptions`. These are the existing
520
+ managers' extension points. Without `renderEditor`, body editing uses the
521
+ manager's HTML field; the rich text editor is an optional peer dependency.
522
+ Changing the client or the provider's viewer/access/language scope resets the
523
+ workbench's local navigation and unsaved forms. A URL already selected by the
524
+ host stays selected and is checked again under the new scope. When reusing
525
+ a transport across sign-ins, keep `ContentKitProvider`'s `viewer` and
526
+ `accessRevision` current, or remount the workbench. Media and
527
+ version editing remain in the content editor. Site-specific import,
528
+ review and publishing workflows can use the individual managers and media
529
+ components without adopting this workbench.
530
+
476
531
  ## Admin flows
477
532
 
478
533
  Drop-in staff screens for a host's own admin area: each makes its own reads
@@ -679,3 +734,30 @@ poll by default. Call `client.notifications.refresh()` when your application's
679
734
  event channel announces new data. `useNotifications` and `useNotificationUnread`
680
735
  are available from `/react` for a custom presentation. Keep provider viewer and
681
736
  access revision current; signed-out viewers do not fetch an inbox.
737
+
738
+ ### Reader header controls
739
+
740
+ `SearchBox` is an inline input with a downward suggestions/recent-search panel,
741
+ including loading and empty states. Set `presentation="header"` for a portal
742
+ panel directly below the input, matching its width and following it on resize
743
+ or scroll: a dimmed backdrop, text rows with colored type icons, and no
744
+ thumbnails. Recent-search dates appear when the panel is wide enough. The panel
745
+ slides downward when opened and honors reduced motion. Its mobile/desktop
746
+ scroll limit is 70vh/60vh. The default inline
747
+ presentation is unchanged. Both presentations keep the provider's result order;
748
+ the host owns search-page navigation.
749
+ `ReaderSettings` combines language selection and content filters in one dialog:
750
+
751
+ ```tsx
752
+ <SearchBox presentation="header" onSearch={(query) => navigate(`/search?q=${encodeURIComponent(query)}`)} />
753
+ <ReaderSettings
754
+ languages={[{ value: "en", label: "English" }, { value: "es", label: "Español" }]}
755
+ language={language}
756
+ onLanguageChange={changeLanguage}
757
+ />
758
+ ```
759
+
760
+ Render these under `ContentKitProvider`. Keep its `viewer` current: guests can
761
+ change language, while signed-in readers also see the site's configured content
762
+ filters. Filter toggles save through ContentKit immediately. The language callback
763
+ belongs to the host so it can update its route and stored preference together.
@@ -0,0 +1,24 @@
1
+ import { RecentSearch } from './generated/wire.js';
2
+ /** The number of recent queries displayed, bounded like the server endpoint. */
3
+ export declare const recentSearchLimit: (limit?: number) => number;
4
+ /** Browser-owned anonymous search history. It never reads or copies account history. */
5
+ export declare class BrowserSearchHistory {
6
+ private readonly key;
7
+ private readonly browser?;
8
+ private rows;
9
+ private encoded;
10
+ private storageFailed;
11
+ private readonly listeners;
12
+ constructor(key: string, browser?: Window | undefined);
13
+ readonly snapshot: () => readonly RecentSearch[];
14
+ list(limit?: number): RecentSearch[];
15
+ readonly subscribe: (listener: () => void) => (() => void);
16
+ record(query: string, results: number): void;
17
+ clear(query?: string): void;
18
+ private readonly onStorage;
19
+ private sync;
20
+ private save;
21
+ private replace;
22
+ }
23
+ /** One same-tab store per discovery URL; server rendering never shares a store across requests. */
24
+ export declare function browserSearchHistory(mount: string): BrowserSearchHistory;
@@ -0,0 +1,92 @@
1
+ //#region src/client/browser-search-history.ts
2
+ var e = (e) => e.trim().replace(/\s+/g, " "), t = (t) => e(t).toLowerCase(), n = (e = 10) => Number.isFinite(e) ? Math.max(1, Math.min(100, Math.floor(e))) : 10;
3
+ function r(n) {
4
+ try {
5
+ let r = JSON.parse(n ?? "[]");
6
+ if (!Array.isArray(r)) return [];
7
+ let i = /* @__PURE__ */ new Set(), a = [];
8
+ for (let n of r) {
9
+ if (!n || typeof n.query != "string" || typeof n.results != "number" || !Number.isFinite(n.results) || n.results < 0 || typeof n.searched_at != "string" || !Number.isFinite(Date.parse(n.searched_at))) continue;
10
+ let r = e(n.query), o = t(r);
11
+ if (o && !i.has(o) && (i.add(o), a.push({
12
+ query: r,
13
+ results: Math.floor(n.results),
14
+ searched_at: n.searched_at
15
+ }), a.length === 100)) break;
16
+ }
17
+ return a;
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
22
+ var i = class {
23
+ key;
24
+ browser;
25
+ rows = [];
26
+ encoded = "[]";
27
+ storageFailed = !1;
28
+ listeners = /* @__PURE__ */ new Set();
29
+ constructor(e, t) {
30
+ this.key = e, this.browser = t, this.sync();
31
+ }
32
+ snapshot = () => this.rows;
33
+ list(e) {
34
+ return this.sync(), this.rows.slice(0, n(e)).map((e) => ({ ...e }));
35
+ }
36
+ subscribe = (e) => (this.sync(), this.listeners.size || this.browser?.addEventListener("storage", this.onStorage), this.listeners.add(e), () => {
37
+ this.listeners.delete(e), this.listeners.size || this.browser?.removeEventListener("storage", this.onStorage);
38
+ });
39
+ record(n, r) {
40
+ n = e(n), n && (this.sync(), this.save([{
41
+ query: n,
42
+ results: Number.isFinite(r) ? Math.max(0, Math.floor(r)) : 0,
43
+ searched_at: (/* @__PURE__ */ new Date()).toISOString()
44
+ }, ...this.rows.filter((e) => t(e.query) !== t(n))].slice(0, 100)));
45
+ }
46
+ clear(e) {
47
+ this.sync(), this.save(e ? this.rows.filter((n) => t(n.query) !== t(e)) : []);
48
+ }
49
+ onStorage = (e) => {
50
+ (e.key === null || e.key === this.key) && this.sync();
51
+ };
52
+ sync() {
53
+ if (this.browser && !this.storageFailed) try {
54
+ this.replace(r(this.browser.localStorage.getItem(this.key)));
55
+ } catch {
56
+ this.storageFailed = !0;
57
+ }
58
+ }
59
+ save(e) {
60
+ if (this.browser && !this.storageFailed) try {
61
+ e.length ? this.browser.localStorage.setItem(this.key, JSON.stringify(e)) : this.browser.localStorage.removeItem(this.key);
62
+ } catch {
63
+ this.storageFailed = !0;
64
+ }
65
+ this.replace(e);
66
+ }
67
+ replace(e) {
68
+ let t = JSON.stringify(e);
69
+ if (t !== this.encoded) {
70
+ this.encoded = t, this.rows = Object.freeze(e.map((e) => Object.freeze({ ...e })));
71
+ for (let e of this.listeners) try {
72
+ e();
73
+ } catch {}
74
+ }
75
+ }
76
+ }, a = /* @__PURE__ */ new WeakMap();
77
+ function o(e) {
78
+ let t = typeof window > "u" ? void 0 : window, n = e.replace(/\/+$/, "");
79
+ if (t) try {
80
+ n = new URL(n, t.location.href).href;
81
+ } catch {}
82
+ let r = `contentkit:recent-searches:v1:${n}`;
83
+ if (!t) return new i(r);
84
+ let o = a.get(t);
85
+ o || a.set(t, o = /* @__PURE__ */ new Map());
86
+ let s = o.get(r);
87
+ return s || o.set(r, s = new i(r, t)), s;
88
+ }
89
+ //#endregion
90
+ export { i as BrowserSearchHistory, o as browserSearchHistory, n as recentSearchLimit };
91
+
92
+ //# sourceMappingURL=browser-search-history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-search-history.js","names":[],"sources":["../../src/client/browser-search-history.ts"],"sourcesContent":["import type { RecentSearch } from \"./generated/wire.js\";\n\nconst MAX_SEARCHES = 100;\nconst normalized = (query: string) => query.trim().replace(/\\s+/g, \" \");\nconst identity = (query: string) => normalized(query).toLowerCase();\n\n/** The number of recent queries displayed, bounded like the server endpoint. */\nexport const recentSearchLimit = (limit = 10): number => Number.isFinite(limit) ? Math.max(1, Math.min(MAX_SEARCHES, Math.floor(limit))) : 10;\n\nfunction decode(raw: string | null): RecentSearch[] {\n try {\n const data: unknown = JSON.parse(raw ?? \"[]\");\n if (!Array.isArray(data)) return [];\n const seen = new Set<string>();\n const rows: RecentSearch[] = [];\n for (const entry of data) {\n if (!entry || typeof entry.query !== \"string\" || typeof entry.results !== \"number\" || !Number.isFinite(entry.results) || entry.results < 0 || typeof entry.searched_at !== \"string\" || !Number.isFinite(Date.parse(entry.searched_at))) continue;\n const query = normalized(entry.query), key = identity(query);\n if (!key || seen.has(key)) continue;\n seen.add(key);\n rows.push({ query, results: Math.floor(entry.results), searched_at: entry.searched_at });\n if (rows.length === MAX_SEARCHES) break;\n }\n return rows;\n } catch { return []; }\n}\n\n/** Browser-owned anonymous search history. It never reads or copies account history. */\nexport class BrowserSearchHistory {\n private rows: readonly RecentSearch[] = [];\n private encoded = \"[]\";\n private storageFailed = false;\n private readonly listeners = new Set<() => void>();\n\n constructor(private readonly key: string, private readonly browser?: Window) { this.sync(); }\n\n readonly snapshot = (): readonly RecentSearch[] => this.rows;\n\n list(limit?: number): RecentSearch[] {\n this.sync();\n return this.rows.slice(0, recentSearchLimit(limit)).map((row) => ({ ...row }));\n }\n\n readonly subscribe = (listener: () => void): (() => void) => {\n this.sync();\n if (!this.listeners.size) this.browser?.addEventListener(\"storage\", this.onStorage);\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n if (!this.listeners.size) this.browser?.removeEventListener(\"storage\", this.onStorage);\n };\n };\n\n record(query: string, results: number): void {\n query = normalized(query);\n if (!query) return;\n this.sync();\n this.save([{ query, results: Number.isFinite(results) ? Math.max(0, Math.floor(results)) : 0, searched_at: new Date().toISOString() }, ...this.rows.filter((row) => identity(row.query) !== identity(query))].slice(0, MAX_SEARCHES));\n }\n\n clear(query?: string): void {\n this.sync();\n this.save(query ? this.rows.filter((row) => identity(row.query) !== identity(query)) : []);\n }\n\n private readonly onStorage = (event: StorageEvent): void => {\n if (event.key === null || event.key === this.key) this.sync();\n };\n\n private sync(): void {\n if (!this.browser || this.storageFailed) return;\n try { this.replace(decode(this.browser.localStorage.getItem(this.key))); }\n catch { this.storageFailed = true; }\n }\n\n private save(rows: readonly RecentSearch[]): void {\n if (this.browser && !this.storageFailed) {\n try {\n if (rows.length) this.browser.localStorage.setItem(this.key, JSON.stringify(rows));\n else this.browser.localStorage.removeItem(this.key);\n } catch { this.storageFailed = true; }\n }\n this.replace(rows);\n }\n\n private replace(rows: readonly RecentSearch[]): void {\n const encoded = JSON.stringify(rows);\n if (encoded === this.encoded) return;\n this.encoded = encoded;\n this.rows = Object.freeze(rows.map((row) => Object.freeze({ ...row })));\n for (const listener of this.listeners) {\n try { listener(); } catch { /* A subscriber must not fail the search that completed. */ }\n }\n }\n}\n\nconst stores = new WeakMap<Window, Map<string, BrowserSearchHistory>>();\n\n/** One same-tab store per discovery URL; server rendering never shares a store across requests. */\nexport function browserSearchHistory(mount: string): BrowserSearchHistory {\n const browser = typeof window === \"undefined\" ? undefined : window;\n let scope = mount.replace(/\\/+$/, \"\");\n if (browser) {\n try { scope = new URL(scope, browser.location.href).href; } catch { /* A relative SSR mount remains a distinct local key. */ }\n }\n const key = `contentkit:recent-searches:v1:${scope}`;\n if (!browser) return new BrowserSearchHistory(key);\n let scoped = stores.get(browser);\n if (!scoped) stores.set(browser, (scoped = new Map()));\n let store = scoped.get(key);\n if (!store) scoped.set(key, (store = new BrowserSearchHistory(key, browser)));\n return store;\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { ContentFilters, ContentFiltersUpdate, ContentPage, ExposureInput, RecentSearches, ReleaseDates, SignalInput, Typeahead } from './generated/wire.js';
2
2
  import { Http } from './http.js';
3
+ import { BrowserSearchHistory } from './browser-search-history.js';
3
4
  /** A popularity period. */
4
5
  export type Period = "7d" | "30d" | "90d" | "365d" | "all";
5
6
  /** A list's order; position orders by the first of `groups` (an ordered group). */
@@ -55,11 +56,11 @@ export type SearchQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQue
55
56
  q: string;
56
57
  sort?: "relevance" | ListSort;
57
58
  personalize?: boolean;
58
- /** A signed-out reader's session, which keeps its recent searches; default readerSession(). */
59
+ /** Explicit legacy server-side anonymous history. Omit for public search without server history. */
59
60
  session?: string;
60
61
  };
61
62
  export interface RecentSearchesQuery {
62
- /** A signed-out reader's session; default readerSession(). */
63
+ /** Explicit legacy server-side anonymous history; no anonymous session is created by default. */
63
64
  session?: string;
64
65
  /** 10 by default, at most 100. */
65
66
  limit?: number;
@@ -91,6 +92,8 @@ export declare function query(q: object): Record<string, string | number | boole
91
92
  */
92
93
  export declare class DiscoveryClient {
93
94
  private readonly http;
95
+ /** Local anonymous history, isolated from authenticated server history. */
96
+ readonly browserSearches: BrowserSearchHistory;
94
97
  constructor(http: Http);
95
98
  /** Contents as cards, one per content item with the version that fits the reader. */
96
99
  list(q?: ContentQuery, signal?: AbortSignal): Promise<ContentPage>;
@@ -1,5 +1,5 @@
1
1
  import { call as e } from "./route.js";
2
- import { readerSession as t } from "./beacons.js";
2
+ import { browserSearchHistory as t } from "./browser-search-history.js";
3
3
  //#region src/client/discovery.ts
4
4
  var n = /* @__PURE__ */ new Set([
5
5
  "groups",
@@ -18,8 +18,9 @@ function r(e) {
18
18
  }
19
19
  var i = class {
20
20
  http;
21
+ browserSearches;
21
22
  constructor(e) {
22
- this.http = e;
23
+ this.http = e, this.browserSearches = t(e.mount("discovery"));
23
24
  }
24
25
  list(t = {}, n) {
25
26
  return e(this.http, "GET", "/contents", {
@@ -27,31 +28,22 @@ var i = class {
27
28
  signal: n
28
29
  });
29
30
  }
30
- async search(n, i) {
31
- let a = n.cursor ? null : this.http.captureChanges(), o = await e(this.http, "GET", "/search", {
32
- query: r({
33
- session: t(),
34
- ...n
35
- }),
36
- signal: i
31
+ async search(t, n) {
32
+ let i = t.cursor ? null : this.http.captureChanges(), a = await e(this.http, "GET", "/search", {
33
+ query: r(t),
34
+ signal: n
37
35
  });
38
- return a?.({ type: "searches.changed" }), o;
36
+ return i?.({ type: "searches.changed" }), a;
39
37
  }
40
- recentSearches(n = {}, r) {
38
+ recentSearches(t = {}, n) {
41
39
  return e(this.http, "GET", "/me/searches", {
42
- query: {
43
- session: t(),
44
- ...n
45
- },
46
- signal: r
40
+ query: t,
41
+ signal: n
47
42
  });
48
43
  }
49
- async clearSearches(n = {}) {
50
- let r = this.http.captureChanges();
51
- await e(this.http, "DELETE", "/me/searches", { query: {
52
- session: t(),
53
- ...n
54
- } }), r({ type: "searches.changed" });
44
+ async clearSearches(t = {}) {
45
+ let n = this.http.captureChanges();
46
+ await e(this.http, "DELETE", "/me/searches", { query: t }), n({ type: "searches.changed" });
55
47
  }
56
48
  typeahead(t, n) {
57
49
  return e(this.http, "GET", "/typeahead", {
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","names":[],"sources":["../../src/client/discovery.ts"],"sourcesContent":["import type {\n ContentFilters,\n ContentFiltersUpdate,\n ContentPage,\n ExposureInput,\n Exposures,\n RecentSearches,\n ReleaseDates,\n SignalInput,\n Signals,\n Typeahead,\n} from \"./generated/wire.js\";\nimport { readerSession } from \"./beacons.js\";\nimport type { Http } from \"./http.js\";\nimport { call } from \"./route.js\";\n\n/** A popularity period. */\nexport type Period = \"7d\" | \"30d\" | \"90d\" | \"365d\" | \"all\";\n/** A list's order; position orders by the first of `groups` (an ordered group). */\nexport type ListSort = \"newest\" | \"oldest\" | \"title\" | \"popular\" | `popular:${Period}` | \"top_rated\" | `top_rated:${Period}` | \"most_favorited\" | \"most_viewed\" | \"position\" | \"random\";\n/** How locked contents list: marked (lock, the default) or left out (hide). */\nexport type AccessMode = \"lock\" | \"hide\";\n\n/** Which contents a list holds. */\nexport interface ContentFilterQuery {\n kind?: string;\n /** Group ids every listed version is in. */\n groups?: readonly string[];\n /** Group ids no listed version is in. */\n exclude_groups?: readonly string[];\n /** Only contents this group owns: a channel's feed. */\n owner_group?: string;\n /** User ids: only contents one of them is credited on. */\n creators?: readonly string[];\n /** public (anyone opens it now) or the level it stays locked at. */\n access?: readonly (\"public\" | \"tier\" | \"group\" | \"ppv\" | \"group_ppv\")[];\n /** Only contents locked behind one of these tiers. */\n tiers?: readonly string[];\n /** Version traits every listed version has. */\n traits?: readonly string[];\n /** \"name:value\" of the kind's filterable attributes. */\n attribute?: readonly string[];\n /** Published on this day (YYYY-MM-DD) or month (YYYY-MM). */\n released_on?: string;\n /** IANA time zone of released_on and release dates; UTC by default. */\n tz?: string;\n /** The signed-in reader's unseen, in-progress or completed contents. */\n view_status?: \"unseen\" | \"in_progress\" | \"completed\";\n}\n\n/** How a list reads to its reader. */\nexport interface ReaderQuery {\n /** Only content with a live version in this language. */\n language?: string;\n /** Titles, names and paths in this language, and each card's preferred version's; language by default. */\n display_language?: string;\n mode?: AccessMode;\n}\n\n/** The group kinds cards name; every kind with a page by default. */\nexport interface CardQuery {\n card_groups?: readonly string[];\n}\n\nexport interface PageQuery {\n cursor?: string;\n limit?: number;\n}\n\nexport type ContentQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & { sort?: ListSort };\nexport type SearchQuery = ContentFilterQuery &\n ReaderQuery &\n CardQuery &\n PageQuery & {\n q: string;\n sort?: \"relevance\" | ListSort;\n personalize?: boolean;\n /** A signed-out reader's session, which keeps its recent searches; default readerSession(). */\n session?: string;\n };\nexport interface RecentSearchesQuery {\n /** A signed-out reader's session; default readerSession(). */\n session?: string;\n /** 10 by default, at most 100. */\n limit?: number;\n}\nexport type RailQuery = ContentFilterQuery & ReaderQuery & CardQuery & { limit?: number };\nexport type ReleaseDatesQuery = ContentFilterQuery & ReaderQuery & PageQuery & { by?: \"month\" | \"day\" };\nexport type HistoryQuery = ReaderQuery & CardQuery & PageQuery & { kind?: string; status?: \"seen\" | \"in_progress\" | \"completed\" };\nexport interface TypeaheadQuery extends ReaderQuery {\n q: string;\n /** Only contents of this kind. */\n kind?: string;\n /** The group kinds suggested; every kind with a page by default. */\n group_kinds?: readonly string[];\n /** Per section: 8 by default, at most 20. */\n limit?: number;\n}\n\n// Comma-joined list parameters; `attribute` repeats instead.\nconst LISTS = new Set([\"groups\", \"exclude_groups\", \"creators\", \"access\", \"tiers\", \"traits\", \"card_groups\", \"group_kinds\"]);\n\n/** A query with its list parameters comma-joined, as discovery routes read them. */\nexport function query(q: object): Record<string, string | number | boolean | readonly string[] | undefined> {\n const out: Record<string, string | number | boolean | readonly string[] | undefined> = {};\n for (const [k, v] of Object.entries(q)) out[k] = LISTS.has(k) && Array.isArray(v) ? (v.length ? v.join(\",\") : undefined) : v;\n return out;\n}\n\n/**\n * Discovery: lists, search, typeahead, similar and recommended contents,\n * release dates, the reader's content filters and history, signals and exposures.\n */\nexport class DiscoveryClient {\n constructor(private readonly http: Http) {}\n\n /** Contents as cards, one per content item with the version that fits the reader. */\n list(q: ContentQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents\", { query: query(q) as never, signal });\n }\n /** A first page is kept among the reader's recent searches. */\n async search(q: SearchQuery, signal?: AbortSignal): Promise<ContentPage> {\n const emit = q.cursor ? null : this.http.captureChanges();\n const out: ContentPage = await call(this.http, \"GET\", \"/search\", { query: query({ session: readerSession(), ...q }) as never, signal });\n emit?.({ type: \"searches.changed\" });\n return out;\n }\n /** The reader's recent searches, newest first, since they last cleared them. */\n recentSearches(q: RecentSearchesQuery = {}, signal?: AbortSignal): Promise<RecentSearches> {\n return call(this.http, \"GET\", \"/me/searches\", { query: { session: readerSession(), ...q }, signal });\n }\n /** Clears one recent search, or all of them. */\n async clearSearches(o: { query?: string; session?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/searches\", { query: { session: readerSession(), ...o } });\n emit({ type: \"searches.changed\" });\n }\n /** Contents and groups while the reader types. */\n typeahead(q: TypeaheadQuery, signal?: AbortSignal): Promise<Typeahead> {\n return call(this.http, \"GET\", \"/typeahead\", { query: query(q) as never, signal });\n }\n /** Contents like one, of its kind unless q.kind names another. */\n similar(id: string, q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents/{id}/similar\", { params: { id }, query: query(q) as never, signal });\n }\n /** For the signed-in reader. */\n recommendations(q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/recommendations\", { query: query(q) as never, signal });\n }\n /** Per month or day, newest first: how many contents the reader's listing shows released then, and how many are free. */\n releaseDates(q: ReleaseDatesQuery = {}, signal?: AbortSignal): Promise<ReleaseDates> {\n return call(this.http, \"GET\", \"/release-dates\", { query: query(q) as never, signal });\n }\n contentFilters(language?: string, signal?: AbortSignal): Promise<ContentFilters> {\n return call(this.http, \"GET\", \"/me/content-filters\", { query: { language }, signal });\n }\n /** Replaces the reader's filter settings; every read applies them. */\n async setContentFilters(update: ContentFiltersUpdate, language?: string): Promise<ContentFilters> {\n const emit = this.http.captureChanges();\n const out: ContentFilters = await call(this.http, \"PUT\", \"/me/content-filters\", { query: { language }, body: update });\n emit({ type: \"contentFilters.changed\" });\n return out;\n }\n /** What the reader engaged with, most recent first. */\n history(q: HistoryQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/me/history\", { query: query(q) as never, signal });\n }\n /** Erases the reader's history: of one content item, one kind or everything. */\n async forgetHistory(o: { kind?: string; content_id?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/history\", { query: o });\n emit({ type: \"history.changed\" });\n }\n /** Views and clicks; signed out, of the session named. */\n signals(signals: SignalInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Signals = { session, signals };\n return call(this.http, \"POST\", \"/signals\", { body, keepalive: o.keepalive });\n }\n /** Result lists served, rendered or shown, for click attribution. */\n exposures(exposures: ExposureInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Exposures = { session, exposures };\n return call(this.http, \"POST\", \"/exposures\", { body, keepalive: o.keepalive });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"discovery.js","names":[],"sources":["../../src/client/discovery.ts"],"sourcesContent":["import type {\n ContentFilters,\n ContentFiltersUpdate,\n ContentPage,\n ExposureInput,\n Exposures,\n RecentSearches,\n ReleaseDates,\n SignalInput,\n Signals,\n Typeahead,\n} from \"./generated/wire.js\";\nimport type { Http } from \"./http.js\";\nimport { call } from \"./route.js\";\nimport { browserSearchHistory, type BrowserSearchHistory } from \"./browser-search-history.js\";\n\n/** A popularity period. */\nexport type Period = \"7d\" | \"30d\" | \"90d\" | \"365d\" | \"all\";\n/** A list's order; position orders by the first of `groups` (an ordered group). */\nexport type ListSort = \"newest\" | \"oldest\" | \"title\" | \"popular\" | `popular:${Period}` | \"top_rated\" | `top_rated:${Period}` | \"most_favorited\" | \"most_viewed\" | \"position\" | \"random\";\n/** How locked contents list: marked (lock, the default) or left out (hide). */\nexport type AccessMode = \"lock\" | \"hide\";\n\n/** Which contents a list holds. */\nexport interface ContentFilterQuery {\n kind?: string;\n /** Group ids every listed version is in. */\n groups?: readonly string[];\n /** Group ids no listed version is in. */\n exclude_groups?: readonly string[];\n /** Only contents this group owns: a channel's feed. */\n owner_group?: string;\n /** User ids: only contents one of them is credited on. */\n creators?: readonly string[];\n /** public (anyone opens it now) or the level it stays locked at. */\n access?: readonly (\"public\" | \"tier\" | \"group\" | \"ppv\" | \"group_ppv\")[];\n /** Only contents locked behind one of these tiers. */\n tiers?: readonly string[];\n /** Version traits every listed version has. */\n traits?: readonly string[];\n /** \"name:value\" of the kind's filterable attributes. */\n attribute?: readonly string[];\n /** Published on this day (YYYY-MM-DD) or month (YYYY-MM). */\n released_on?: string;\n /** IANA time zone of released_on and release dates; UTC by default. */\n tz?: string;\n /** The signed-in reader's unseen, in-progress or completed contents. */\n view_status?: \"unseen\" | \"in_progress\" | \"completed\";\n}\n\n/** How a list reads to its reader. */\nexport interface ReaderQuery {\n /** Only content with a live version in this language. */\n language?: string;\n /** Titles, names and paths in this language, and each card's preferred version's; language by default. */\n display_language?: string;\n mode?: AccessMode;\n}\n\n/** The group kinds cards name; every kind with a page by default. */\nexport interface CardQuery {\n card_groups?: readonly string[];\n}\n\nexport interface PageQuery {\n cursor?: string;\n limit?: number;\n}\n\nexport type ContentQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & { sort?: ListSort };\nexport type SearchQuery = ContentFilterQuery &\n ReaderQuery &\n CardQuery &\n PageQuery & {\n q: string;\n sort?: \"relevance\" | ListSort;\n personalize?: boolean;\n /** Explicit legacy server-side anonymous history. Omit for public search without server history. */\n session?: string;\n };\nexport interface RecentSearchesQuery {\n /** Explicit legacy server-side anonymous history; no anonymous session is created by default. */\n session?: string;\n /** 10 by default, at most 100. */\n limit?: number;\n}\nexport type RailQuery = ContentFilterQuery & ReaderQuery & CardQuery & { limit?: number };\nexport type ReleaseDatesQuery = ContentFilterQuery & ReaderQuery & PageQuery & { by?: \"month\" | \"day\" };\nexport type HistoryQuery = ReaderQuery & CardQuery & PageQuery & { kind?: string; status?: \"seen\" | \"in_progress\" | \"completed\" };\nexport interface TypeaheadQuery extends ReaderQuery {\n q: string;\n /** Only contents of this kind. */\n kind?: string;\n /** The group kinds suggested; every kind with a page by default. */\n group_kinds?: readonly string[];\n /** Per section: 8 by default, at most 20. */\n limit?: number;\n}\n\n// Comma-joined list parameters; `attribute` repeats instead.\nconst LISTS = new Set([\"groups\", \"exclude_groups\", \"creators\", \"access\", \"tiers\", \"traits\", \"card_groups\", \"group_kinds\"]);\n\n/** A query with its list parameters comma-joined, as discovery routes read them. */\nexport function query(q: object): Record<string, string | number | boolean | readonly string[] | undefined> {\n const out: Record<string, string | number | boolean | readonly string[] | undefined> = {};\n for (const [k, v] of Object.entries(q)) out[k] = LISTS.has(k) && Array.isArray(v) ? (v.length ? v.join(\",\") : undefined) : v;\n return out;\n}\n\n/**\n * Discovery: lists, search, typeahead, similar and recommended contents,\n * release dates, the reader's content filters and history, signals and exposures.\n */\nexport class DiscoveryClient {\n /** Local anonymous history, isolated from authenticated server history. */\n readonly browserSearches: BrowserSearchHistory;\n\n constructor(private readonly http: Http) {\n this.browserSearches = browserSearchHistory(http.mount(\"discovery\"));\n }\n\n /** Contents as cards, one per content item with the version that fits the reader. */\n list(q: ContentQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents\", { query: query(q) as never, signal });\n }\n /** A first page is kept among the reader's recent searches. */\n async search(q: SearchQuery, signal?: AbortSignal): Promise<ContentPage> {\n const emit = q.cursor ? null : this.http.captureChanges();\n const out: ContentPage = await call(this.http, \"GET\", \"/search\", { query: query(q) as never, signal });\n emit?.({ type: \"searches.changed\" });\n return out;\n }\n /** The reader's recent searches, newest first, since they last cleared them. */\n recentSearches(q: RecentSearchesQuery = {}, signal?: AbortSignal): Promise<RecentSearches> {\n return call(this.http, \"GET\", \"/me/searches\", { query: q, signal });\n }\n /** Clears one recent search, or all of them. */\n async clearSearches(o: { query?: string; session?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/searches\", { query: o });\n emit({ type: \"searches.changed\" });\n }\n /** Contents and groups while the reader types. */\n typeahead(q: TypeaheadQuery, signal?: AbortSignal): Promise<Typeahead> {\n return call(this.http, \"GET\", \"/typeahead\", { query: query(q) as never, signal });\n }\n /** Contents like one, of its kind unless q.kind names another. */\n similar(id: string, q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents/{id}/similar\", { params: { id }, query: query(q) as never, signal });\n }\n /** For the signed-in reader. */\n recommendations(q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/recommendations\", { query: query(q) as never, signal });\n }\n /** Per month or day, newest first: how many contents the reader's listing shows released then, and how many are free. */\n releaseDates(q: ReleaseDatesQuery = {}, signal?: AbortSignal): Promise<ReleaseDates> {\n return call(this.http, \"GET\", \"/release-dates\", { query: query(q) as never, signal });\n }\n contentFilters(language?: string, signal?: AbortSignal): Promise<ContentFilters> {\n return call(this.http, \"GET\", \"/me/content-filters\", { query: { language }, signal });\n }\n /** Replaces the reader's filter settings; every read applies them. */\n async setContentFilters(update: ContentFiltersUpdate, language?: string): Promise<ContentFilters> {\n const emit = this.http.captureChanges();\n const out: ContentFilters = await call(this.http, \"PUT\", \"/me/content-filters\", { query: { language }, body: update });\n emit({ type: \"contentFilters.changed\" });\n return out;\n }\n /** What the reader engaged with, most recent first. */\n history(q: HistoryQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/me/history\", { query: query(q) as never, signal });\n }\n /** Erases the reader's history: of one content item, one kind or everything. */\n async forgetHistory(o: { kind?: string; content_id?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/history\", { query: o });\n emit({ type: \"history.changed\" });\n }\n /** Views and clicks; signed out, of the session named. */\n signals(signals: SignalInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Signals = { session, signals };\n return call(this.http, \"POST\", \"/signals\", { body, keepalive: o.keepalive });\n }\n /** Result lists served, rendered or shown, for click attribution. */\n exposures(exposures: ExposureInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Exposures = { session, exposures };\n return call(this.http, \"POST\", \"/exposures\", { body, keepalive: o.keepalive });\n }\n}\n"],"mappings":"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"}
package/dist/client.js CHANGED
@@ -25,18 +25,18 @@ import { CodesClient as He } from "./client/codes.js";
25
25
  import { ContentsClient as Ue, IMAGE_SCHEME as We, imageRef as Ge, imageRefs as Ke } from "./client/contents.js";
26
26
  import { BansClient as qe } from "./client/content/bans.js";
27
27
  import { CommentsClient as Je } from "./client/content/comments.js";
28
- import { Beacons as Ye, eventId as Xe, readerSession as Ze } from "./client/beacons.js";
29
- import { DiscoveryClient as Qe } from "./client/discovery.js";
30
- import { FavoritesClient as $e } from "./client/content/favorites.js";
31
- import { ModerationClient as et } from "./client/content/moderation.js";
32
- import { PollsClient as tt } from "./client/content/polls.js";
33
- import { ReactionsClient as nt } from "./client/content/reactions.js";
34
- import { GroupsClient as rt } from "./client/groups.js";
35
- import { OffersClient as it } from "./client/offers.js";
36
- import { StatsClient as at } from "./client/stats.js";
37
- import { VersionsClient as ot, isPending as st } from "./client/versions.js";
38
- import { NotificationsClient as ct } from "./client/notifications.js";
39
- import { createContentKitClient as lt } from "./client/client.js";
40
- import { LETTERS as ut, attributeInput as dt, fold as ft, fromLocalInput as pt, humanize as mt, letterOf as ht, parseAttribute as gt, parseAttributes as _t, slugify as vt, toLocalInput as yt } from "./client/group-fields.js";
28
+ import { DiscoveryClient as Ye } from "./client/discovery.js";
29
+ import { FavoritesClient as Xe } from "./client/content/favorites.js";
30
+ import { ModerationClient as Ze } from "./client/content/moderation.js";
31
+ import { PollsClient as Qe } from "./client/content/polls.js";
32
+ import { ReactionsClient as $e } from "./client/content/reactions.js";
33
+ import { GroupsClient as et } from "./client/groups.js";
34
+ import { OffersClient as tt } from "./client/offers.js";
35
+ import { StatsClient as nt } from "./client/stats.js";
36
+ import { VersionsClient as rt, isPending as it } from "./client/versions.js";
37
+ import { NotificationsClient as at } from "./client/notifications.js";
38
+ import { createContentKitClient as ot } from "./client/client.js";
39
+ import { LETTERS as st, attributeInput as ct, fold as lt, fromLocalInput as ut, humanize as dt, letterOf as ft, parseAttribute as pt, parseAttributes as mt, slugify as ht, toLocalInput as gt } from "./client/group-fields.js";
40
+ import { Beacons as _t, eventId as vt, readerSession as yt } from "./client/beacons.js";
41
41
  import { emptyReaderMeta as bt, matchAudio as xt, readerMetaProblems as St, readerNames as Ct } from "./client/reader-meta.js";
42
- export { he as ABR_DEFAULTS, qe as BansClient, Ye as Beacons, i as CLIENT_ERROR_CODES, Ve as CatalogClient, He as CodesClient, Je as CommentsClient, a as ContentKitError, Ue as ContentsClient, e as DEFAULT_DENSITY, Qe as DiscoveryClient, o as ERROR_CODES, $e as FavoritesClient, rt as GroupsClient, We as IMAGE_SCHEME, ae as ItemUploadQueue, ut as LETTERS, T as MAX_PART_SIZE, E as MAX_SINGLE_PUT, D as MIN_PART_SIZE, w as MediaApi, N as MediaClient, et as ModerationClient, ge as NETWORK_FAILURES_BEFORE_ERROR, ct as NotificationsClient, it as OffersClient, Fe as PLAYBACK_SPEEDS, Ie as PLAYER_TRACKS_KEY, Le as PLAYER_VOLUME_KEY, k as Pacer, tt as PollsClient, ue as READ_CHUNK, _e as REFRESH_BEFORE_EXPIRY, nt as ReactionsClient, at as StatsClient, ot as VersionsClient, ve as abrHlsConfig, L as acceptOf, g as aspectOf, dt as attributeInput, P as backoff, v as call, ye as capRung, Y as centeredCrop, b as checkRef, de as chunkOffsets, be as classifyHlsError, xe as classifyMediaError, X as constrainCrop, lt as createContentKitClient, oe as decodeImage, R as defaultImage, A as defaultTransport, t as densityFor, Z as editOf, Q as editedSize, bt as emptyReaderMeta, ie as encodeRemaining, Xe as eventId, Se as expiryDelay, s as failureError, j as fetchTransport, z as filesFor, f as fill, ft as fold, Oe as formatDuration, pt as fromLocalInput, $ as fromRotated, ke as galleryItems, Ce as hlsConfig, mt as humanize, Ge as imageRef, Ke as imageRefs, we as initialEstimate, se as isAnimatedImage, Ae as isAudioType, x as isContentId, c as isContentKitError, je as isImageType, B as isPattern, st as isPending, fe as isProcessing, Me as isSubtitleType, Ne as isVideoType, S as itemKey, ht as letterOf, xt as matchAudio, pe as mergeReads, gt as parseAttribute, _t as parseAttributes, Re as parseSpriteVtt, n as pickRendition, ze as playerKeyAction, me as processing, p as publicRenditions, m as publicURL, _ as ratio, V as ratioLabel, ce as reactionVerb, l as readContentKitError, St as readerMetaProblems, Ct as readerNames, Ze as readerSession, Te as refreshable, Be as resumeAt, ee as rotation, y as routeURL, H as ruleDir, U as ruleFor, te as sameEdit, C as sameItem, F as samePath, le as sameTarget, W as screenFiles, O as sha256Hex, vt as slugify, r as sortRenditions, h as srcSet, G as stableImage, Pe as stageAspect, Ee as startRung, De as statusKind, I as stem, u as toContentKitError, yt as toLocalInput, ne as toOriginal, re as toRotated, d as transportFailure, K as uniqueName, q as uploadRules, J as withMediaType, M as xhrTransport };
42
+ export { he as ABR_DEFAULTS, qe as BansClient, _t as Beacons, i as CLIENT_ERROR_CODES, Ve as CatalogClient, He as CodesClient, Je as CommentsClient, a as ContentKitError, Ue as ContentsClient, e as DEFAULT_DENSITY, Ye as DiscoveryClient, o as ERROR_CODES, Xe as FavoritesClient, et as GroupsClient, We as IMAGE_SCHEME, ae as ItemUploadQueue, st as LETTERS, T as MAX_PART_SIZE, E as MAX_SINGLE_PUT, D as MIN_PART_SIZE, w as MediaApi, N as MediaClient, Ze as ModerationClient, ge as NETWORK_FAILURES_BEFORE_ERROR, at as NotificationsClient, tt as OffersClient, Fe as PLAYBACK_SPEEDS, Ie as PLAYER_TRACKS_KEY, Le as PLAYER_VOLUME_KEY, k as Pacer, Qe as PollsClient, ue as READ_CHUNK, _e as REFRESH_BEFORE_EXPIRY, $e as ReactionsClient, nt as StatsClient, rt as VersionsClient, ve as abrHlsConfig, L as acceptOf, g as aspectOf, ct as attributeInput, P as backoff, v as call, ye as capRung, Y as centeredCrop, b as checkRef, de as chunkOffsets, be as classifyHlsError, xe as classifyMediaError, X as constrainCrop, ot as createContentKitClient, oe as decodeImage, R as defaultImage, A as defaultTransport, t as densityFor, Z as editOf, Q as editedSize, bt as emptyReaderMeta, ie as encodeRemaining, vt as eventId, Se as expiryDelay, s as failureError, j as fetchTransport, z as filesFor, f as fill, lt as fold, Oe as formatDuration, ut as fromLocalInput, $ as fromRotated, ke as galleryItems, Ce as hlsConfig, dt as humanize, Ge as imageRef, Ke as imageRefs, we as initialEstimate, se as isAnimatedImage, Ae as isAudioType, x as isContentId, c as isContentKitError, je as isImageType, B as isPattern, it as isPending, fe as isProcessing, Me as isSubtitleType, Ne as isVideoType, S as itemKey, ft as letterOf, xt as matchAudio, pe as mergeReads, pt as parseAttribute, mt as parseAttributes, Re as parseSpriteVtt, n as pickRendition, ze as playerKeyAction, me as processing, p as publicRenditions, m as publicURL, _ as ratio, V as ratioLabel, ce as reactionVerb, l as readContentKitError, St as readerMetaProblems, Ct as readerNames, yt as readerSession, Te as refreshable, Be as resumeAt, ee as rotation, y as routeURL, H as ruleDir, U as ruleFor, te as sameEdit, C as sameItem, F as samePath, le as sameTarget, W as screenFiles, O as sha256Hex, ht as slugify, r as sortRenditions, h as srcSet, G as stableImage, Pe as stageAspect, Ee as startRung, De as statusKind, I as stem, u as toContentKitError, gt as toLocalInput, ne as toOriginal, re as toRotated, d as transportFailure, K as uniqueName, q as uploadRules, J as withMediaType, M as xhrTransport };
@@ -0,0 +1,28 @@
1
+ import { Labels } from '../groups/parts.js';
2
+ import { CommentModerationProps } from '../social/moderation.js';
3
+ import { ContentManagerProps } from './content-manager.js';
4
+ import { GroupManagerProps } from './group-manager.js';
5
+ import { PollManagerProps } from './poll-manager.js';
6
+ import { AdminFlowProps, AdminRouteProps } from './shell.js';
7
+ export type ContentAdminSection = "contents" | "groups" | "moderation" | "polls" | "statistics";
8
+ /** An optional reference workbench; each section remains usable as its own component. */
9
+ export interface ContentAdminProps extends AdminFlowProps, AdminRouteProps {
10
+ /** Sections in navigation order. Default all. Navigation does not grant access. */
11
+ sections?: readonly ContentAdminSection[];
12
+ /** Content kinds in picker order; default all configured kinds. Unknown kinds are omitted. */
13
+ contentKinds?: readonly string[];
14
+ /** Group kinds in picker order; default the configured site-owned kinds. Unknown kinds are omitted. */
15
+ groupKinds?: readonly string[];
16
+ labels?: Labels;
17
+ content?: Pick<ContentManagerProps, "renderEditor" | "contentHref">;
18
+ groups?: Pick<GroupManagerProps, "renderContent">;
19
+ moderation?: Pick<CommentModerationProps, "itemHref" | "userHref" | "sections">;
20
+ polls?: Pick<PollManagerProps, "freeText" | "maxOptions">;
21
+ }
22
+ /**
23
+ * Model-driven content, group, moderation, poll and statistics screens, with
24
+ * the host's transport, router and theme. Only the selected manager mounts;
25
+ * its native permissions and refusals remain authoritative. This component
26
+ * does not mount a server portal, sign users in or define a publishing workflow.
27
+ */
28
+ export declare function ContentAdmin({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: ContentAdminProps): import('react').JSX.Element;
@@ -0,0 +1,191 @@
1
+ import { useMessages as e } from "../../i18n/context.js";
2
+ import { useReadScope as t } from "../../react/context.js";
3
+ import { useKinds as n } from "../../react/groups.js";
4
+ import { Button as r } from "../../ui/button.js";
5
+ import { AdminRoot as i, LoadFailed as a, Rows as o } from "./shell.js";
6
+ import { useAdminLocation as s, useFlowClient as c } from "../../react/admin.js";
7
+ import { CommentModeration as l } from "../social/moderation.js";
8
+ import { labelOf as u } from "../groups/parts.js";
9
+ import { ContentManager as d } from "./content-manager.js";
10
+ import { GroupManager as f } from "./group-manager.js";
11
+ import { PollManager as p } from "./poll-manager.js";
12
+ import { StatsDashboard as m } from "./stats-dashboard.js";
13
+ import { useId as h, useState as g } from "react";
14
+ import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
15
+ //#region src/components/admin/content-admin.tsx
16
+ var b = [
17
+ "contents",
18
+ "groups",
19
+ "moderation",
20
+ "polls",
21
+ "statistics"
22
+ ], x = [
23
+ "comments",
24
+ "queue",
25
+ "bans",
26
+ "words",
27
+ "ips"
28
+ ];
29
+ function S({ client: e, baseUrl: n, auth: r, guard: a, appearance: o, className: s, language: l, messages: u, ...d }) {
30
+ let f = c({
31
+ client: e,
32
+ baseUrl: n,
33
+ auth: r
34
+ }), p = t(), [m, h] = g({
35
+ client: f,
36
+ scope: p,
37
+ revision: 0
38
+ });
39
+ return (m.client !== f || m.scope !== p) && h({
40
+ client: f,
41
+ scope: p,
42
+ revision: m.revision + 1
43
+ }), /* @__PURE__ */ v(i, {
44
+ flow: "content-admin",
45
+ guard: a,
46
+ appearance: o,
47
+ className: s,
48
+ language: l,
49
+ messages: u,
50
+ children: /* @__PURE__ */ v(D, {
51
+ ...d,
52
+ client: f,
53
+ guard: a
54
+ }, m.revision)
55
+ });
56
+ }
57
+ function C(e, t) {
58
+ return [...new Set(e ?? Object.keys(t).sort())].filter((e) => Object.hasOwn(t, e));
59
+ }
60
+ function w(e, t) {
61
+ return C(e ?? Object.keys(t.group_kinds).filter((e) => t.group_kinds[e]?.owner !== "user").sort(), t.group_kinds);
62
+ }
63
+ function T(e) {
64
+ if (e !== void 0) try {
65
+ return decodeURIComponent(e);
66
+ } catch {
67
+ return;
68
+ }
69
+ }
70
+ function E(e, t, n) {
71
+ return {
72
+ pathname: `${e}/${t}`,
73
+ search: n.query.size ? `?${n.query}` : "",
74
+ navigate(t, r) {
75
+ let i = new URL(t, "https://contentkit.invalid");
76
+ if (i.origin !== "https://contentkit.invalid") return;
77
+ let a = `${e}/`;
78
+ (i.pathname === e || i.pathname.startsWith(a)) && n.go(i.pathname.slice(a.length), Object.fromEntries(i.searchParams), r);
79
+ }
80
+ };
81
+ }
82
+ function D({ client: t, basePath: i, navigation: c, sections: g, contentKinds: S, groupKinds: D, labels: O, guard: k, onError: A, content: j, groups: M, moderation: N, polls: P }) {
83
+ let { t: F } = e(), I = h(), L = s(i, c), R = n({ client: t });
84
+ if (R.loading) return /* @__PURE__ */ v(o, {});
85
+ if (!R.kinds) return /* @__PURE__ */ v(a, {
86
+ error: R.error,
87
+ message: F("contentAdmin.modelFailed"),
88
+ retry: R.reload
89
+ });
90
+ let z = C(S, R.kinds.kinds), B = w(D, R.kinds), V = [...new Set(g ?? b)].filter((e) => b.includes(e) && (e !== "contents" && e !== "statistics" || z.length > 0) && (e !== "groups" || B.length > 0) && (e !== "moderation" || N?.sections?.length !== 0));
91
+ if (!V.length) return /* @__PURE__ */ v("p", {
92
+ role: "status",
93
+ className: "text-muted-foreground",
94
+ children: F("contentAdmin.empty")
95
+ });
96
+ let H = L.view.split("/"), U = L.view ? H[0] : V[0], W = H[1] === void 0 ? void 0 : T(H[1]), G = W ?? z[0], K = N?.sections ?? x, q = V.includes(U) && (U !== "contents" || (H[1] === void 0 || W !== void 0) && z.includes(G)) && (U !== "groups" || H[1] === void 0 || W !== void 0 && B.includes(W)) && (U !== "moderation" || !H[1] || K.includes(H[1])) && (U !== "statistics" || H.length === 1 || !L.view), J = i?.replace(/\/+$/, "") ?? "", Y = U === "contents" ? `contents/${encodeURIComponent(G)}` : U, X = L.view || Y, Z = E(J, U === "contents" && H[1] === void 0 ? Y : X, L), Q = {
97
+ client: t,
98
+ guard: k,
99
+ onError: A
100
+ }, $ = {
101
+ contents: F("contentManager.title"),
102
+ groups: F("contentAdmin.groups"),
103
+ moderation: F("moderation.title"),
104
+ polls: F("pollManager.title"),
105
+ statistics: F("stats.title")
106
+ }, ee = (e) => e === "contents" ? `contents/${encodeURIComponent(z[0])}` : e;
107
+ return /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v("h1", {
108
+ className: "text-lg font-semibold",
109
+ children: F("contentAdmin.title")
110
+ }), /* @__PURE__ */ y("div", {
111
+ className: "grid min-w-0 gap-4 sm:grid-cols-[12rem_minmax(0,1fr)]",
112
+ children: [/* @__PURE__ */ v("nav", {
113
+ "aria-label": F("contentAdmin.navigation"),
114
+ className: "flex gap-1 overflow-x-auto sm:grid sm:content-start",
115
+ children: V.map((e) => /* @__PURE__ */ v(r, {
116
+ variant: U === e ? "secondary" : "ghost",
117
+ className: "justify-start whitespace-nowrap",
118
+ "aria-current": U === e ? "page" : void 0,
119
+ onClick: () => L.go(ee(e)),
120
+ children: $[e]
121
+ }, e))
122
+ }), /* @__PURE__ */ v("div", {
123
+ className: "grid min-w-0 content-start gap-4",
124
+ "data-ckui": "content-admin-view",
125
+ children: q ? U === "contents" ? /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ y("label", {
126
+ className: "grid justify-items-start gap-1 text-sm",
127
+ htmlFor: `${I}-kind`,
128
+ children: [F("contentAdmin.kind"), /* @__PURE__ */ v("select", {
129
+ id: `${I}-kind`,
130
+ className: "h-8 rounded-md border border-input bg-background px-2 text-sm",
131
+ value: G,
132
+ onChange: (e) => L.go(`contents/${encodeURIComponent(e.target.value)}`),
133
+ children: z.map((e) => /* @__PURE__ */ v("option", {
134
+ value: e,
135
+ children: u(O, e)
136
+ }, e))
137
+ })]
138
+ }), /* @__PURE__ */ v(d, {
139
+ ...Q,
140
+ ...j,
141
+ kind: G,
142
+ labels: O,
143
+ languages: R.kinds.languages,
144
+ basePath: `${J}/${Y}`,
145
+ navigation: Z
146
+ }, G)] }) : U === "groups" ? /* @__PURE__ */ v(f, {
147
+ ...Q,
148
+ ...M,
149
+ labels: O,
150
+ groupKinds: B,
151
+ basePath: `${J}/groups`,
152
+ navigation: Z
153
+ }) : U === "moderation" ? /* @__PURE__ */ v(l, {
154
+ ...Q,
155
+ ...N,
156
+ contentKinds: z.map((e) => ({
157
+ value: e,
158
+ label: u(O, e)
159
+ })),
160
+ basePath: `${J}/moderation`,
161
+ navigation: Z
162
+ }) : U === "polls" ? /* @__PURE__ */ v(p, {
163
+ ...Q,
164
+ ...P,
165
+ languages: R.kinds.languages,
166
+ basePath: `${J}/polls`,
167
+ navigation: Z
168
+ }) : /* @__PURE__ */ v(m, {
169
+ ...Q,
170
+ kinds: z.map((e) => ({
171
+ value: e,
172
+ label: u(O, e)
173
+ }))
174
+ }) : /* @__PURE__ */ y("div", {
175
+ className: "grid justify-items-start gap-2",
176
+ children: [/* @__PURE__ */ v("p", {
177
+ role: "status",
178
+ children: F("contentAdmin.unavailable")
179
+ }), /* @__PURE__ */ v(r, {
180
+ variant: "outline",
181
+ onClick: () => L.go(""),
182
+ children: F("contentAdmin.back")
183
+ })]
184
+ })
185
+ })]
186
+ })] });
187
+ }
188
+ //#endregion
189
+ export { S as ContentAdmin };
190
+
191
+ //# sourceMappingURL=content-admin.js.map