@openrails/contentkit-ui 0.98.6 → 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +26 -0
  2. package/dist/client/content/comments.d.ts +4 -2
  3. package/dist/client/content/comments.js +10 -6
  4. package/dist/client/content/comments.js.map +1 -1
  5. package/dist/client/generated/routes.d.ts +28 -2
  6. package/dist/client/generated/routes.js +39 -2
  7. package/dist/client/generated/routes.js.map +1 -1
  8. package/dist/client/generated/wire.d.ts +85 -0
  9. package/dist/client/generated/wire.js.map +1 -1
  10. package/dist/client/media/client.d.ts +4 -0
  11. package/dist/client/media/client.js +8 -4
  12. package/dist/client/media/client.js.map +1 -1
  13. package/dist/client/media/queue.js +42 -26
  14. package/dist/client/media/queue.js.map +1 -1
  15. package/dist/client/media/rules.d.ts +6 -0
  16. package/dist/client/media/rules.js +64 -10
  17. package/dist/client/media/rules.js.map +1 -1
  18. package/dist/client/versions.d.ts +6 -2
  19. package/dist/client/versions.js +26 -0
  20. package/dist/client/versions.js.map +1 -1
  21. package/dist/components/admin/page-review.d.ts +13 -0
  22. package/dist/components/admin/page-review.js +216 -0
  23. package/dist/components/admin/page-review.js.map +1 -0
  24. package/dist/components/content/card.js +98 -103
  25. package/dist/components/content/card.js.map +1 -1
  26. package/dist/components/content/daily-releases.d.ts +8 -1
  27. package/dist/components/content/daily-releases.js +291 -133
  28. package/dist/components/content/daily-releases.js.map +1 -1
  29. package/dist/components/content/editors.js +216 -210
  30. package/dist/components/content/editors.js.map +1 -1
  31. package/dist/components/content/filter-dialog.d.ts +11 -0
  32. package/dist/components/content/filter-dialog.js +29 -0
  33. package/dist/components/content/filter-dialog.js.map +1 -0
  34. package/dist/components/content/filters.d.ts +10 -0
  35. package/dist/components/content/filters.js +5 -2
  36. package/dist/components/content/filters.js.map +1 -1
  37. package/dist/components/content/grid.js +19 -14
  38. package/dist/components/content/grid.js.map +1 -1
  39. package/dist/components/content/index.d.ts +2 -1
  40. package/dist/components/content/release-reveal.d.ts +5 -0
  41. package/dist/components/content/release-reveal.js +62 -0
  42. package/dist/components/content/release-reveal.js.map +1 -0
  43. package/dist/components/social/comments.d.ts +1 -1
  44. package/dist/components/social/comments.js +15 -15
  45. package/dist/components/social/comments.js.map +1 -1
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.js +24 -22
  48. package/dist/locales/admin/de.js +23 -0
  49. package/dist/locales/admin/de.js.map +1 -1
  50. package/dist/locales/admin/en.d.ts +23 -0
  51. package/dist/locales/admin/en.js +23 -0
  52. package/dist/locales/admin/en.js.map +1 -1
  53. package/dist/locales/admin/es.js +23 -0
  54. package/dist/locales/admin/es.js.map +1 -1
  55. package/dist/locales/admin/ja.js +23 -0
  56. package/dist/locales/admin/ja.js.map +1 -1
  57. package/dist/locales/admin/ko.js +23 -0
  58. package/dist/locales/admin/ko.js.map +1 -1
  59. package/dist/locales/admin/zh.js +23 -0
  60. package/dist/locales/admin/zh.js.map +1 -1
  61. package/dist/locales/discovery/de.js +11 -0
  62. package/dist/locales/discovery/de.js.map +1 -1
  63. package/dist/locales/discovery/en.d.ts +11 -0
  64. package/dist/locales/discovery/en.js +11 -0
  65. package/dist/locales/discovery/en.js.map +1 -1
  66. package/dist/locales/discovery/es.js +11 -0
  67. package/dist/locales/discovery/es.js.map +1 -1
  68. package/dist/locales/discovery/ja.js +11 -0
  69. package/dist/locales/discovery/ja.js.map +1 -1
  70. package/dist/locales/discovery/ko.js +11 -0
  71. package/dist/locales/discovery/ko.js.map +1 -1
  72. package/dist/locales/discovery/zh.js +11 -0
  73. package/dist/locales/discovery/zh.js.map +1 -1
  74. package/dist/locales/en.d.ts +34 -0
  75. package/dist/locales/social/en.js +2 -2
  76. package/dist/locales/social/en.js.map +1 -1
  77. package/dist/react/comments.d.ts +2 -1
  78. package/dist/react/comments.js +10 -8
  79. package/dist/react/comments.js.map +1 -1
  80. package/dist/react/folder.js +39 -39
  81. package/dist/react/folder.js.map +1 -1
  82. package/dist/react/index.d.ts +1 -0
  83. package/dist/react/page-review.d.ts +14 -0
  84. package/dist/react/page-review.js +36 -0
  85. package/dist/react/page-review.js.map +1 -0
  86. package/dist/react/public.d.ts +3 -1
  87. package/dist/react/public.js +76 -72
  88. package/dist/react/public.js.map +1 -1
  89. package/dist/react/resources.d.ts +5 -13
  90. package/dist/react/resources.js +58 -49
  91. package/dist/react/resources.js.map +1 -1
  92. package/dist/react/use-resource.d.ts +1 -1
  93. package/dist/react/use-resource.js +23 -13
  94. package/dist/react/use-resource.js.map +1 -1
  95. package/dist/react/versions.js +70 -42
  96. package/dist/react/versions.js.map +1 -1
  97. package/dist/react.js +8 -7
  98. package/dist/styles.css +1 -1
  99. package/package.json +1 -1
@@ -80,9 +80,9 @@ var a = 2 ** 31 - 1, o = class {
80
80
  let e = n.findIndex((e) => e.status !== "uploaded" && e.status !== "committed");
81
81
  e >= 0 && (n = n.slice(0, e));
82
82
  }
83
- let r = n.filter((e) => e.status === "uploaded"), i = t.attach ?? [];
84
- if (r.length === 0 && i.length === 0) return [];
85
- let o = r.flatMap((e) => {
83
+ let i = n.filter((e) => e.status === "uploaded"), o = t.attach ?? [];
84
+ if (i.length === 0 && o.length === 0) return [];
85
+ let s = i.flatMap((e) => {
86
86
  let t = {
87
87
  op: "attach",
88
88
  path: e.path,
@@ -93,6 +93,7 @@ var a = 2 ** 31 - 1, o = class {
93
93
  let n = {
94
94
  op: "put",
95
95
  path: e.path,
96
+ source_name: e.result.sourceName,
96
97
  blob: e.result.blob,
97
98
  create_id: e.id,
98
99
  index: a,
@@ -100,44 +101,54 @@ var a = 2 ** 31 - 1, o = class {
100
101
  };
101
102
  return e.result.processOnUpload ? [n, t] : [n];
102
103
  });
103
- for (let e of i) o.push({
104
+ for (let e of o) s.push({
104
105
  op: "attach",
105
106
  path: e,
106
107
  index: a
107
108
  });
108
- let s = Object.fromEntries(r.map((e) => [e.result.blob, {
109
+ let c = Object.fromEntries(i.map((e) => [e.result.blob, {
109
110
  file: e.file,
110
111
  type: e.result.type
111
- }])), c = JSON.stringify(o);
112
- this.batch?.input !== c && (this.batch = {
113
- input: c,
112
+ }])), l = JSON.stringify(s);
113
+ this.batch?.input !== l && (this.batch = {
114
+ input: l,
114
115
  operationID: crypto.randomUUID()
115
116
  });
116
- let l = this.batch, u = await this.client.commit(this.o.ref, o, {
117
+ let u = this.batch, d = await this.client.commit(this.o.ref, s, {
117
118
  signal: e,
118
- sources: s,
119
- operationID: l.operationID,
119
+ sources: c,
120
+ operationID: u.operationID,
120
121
  onState: (e) => {
121
- l.input = JSON.stringify(e.ops), l.operationID = e.operation_id;
122
+ u.input = JSON.stringify(e.ops), u.operationID = e.operation_id;
122
123
  let t = !1;
123
124
  for (let n of e.ops) {
124
125
  if (n.op !== "put" || !n.blob) continue;
125
- let e = r.find((e) => e.id === n.create_id);
126
+ let e = i.find((e) => e.id === n.create_id);
126
127
  if (!e?.result) continue;
127
- let i = this.find(e.id);
128
- i?.result && i.result.blob === e.result.blob && i.result.blob !== n.blob && (t = this.set(i.id, { result: {
129
- ...i.result,
128
+ let r = this.find(e.id);
129
+ r?.result && r.result.blob === e.result.blob && r.result.blob !== n.blob && (t = this.set(r.id, { result: {
130
+ ...r.result,
130
131
  blob: n.blob
131
132
  } }) || t);
132
133
  }
133
134
  t && this.changed();
134
135
  }
135
136
  });
136
- for (let e of r) this.set(e.id, {
137
- status: "committed",
138
- unattached: !1
139
- });
140
- return this.changed(), u.filter((e) => !e.unattached);
137
+ for (let e of i) {
138
+ let t = d.find((t) => t.create_id === e.id) ?? d.find((t) => r(t.path, e.path));
139
+ this.set(e.id, {
140
+ status: "committed",
141
+ unattached: !1,
142
+ ...t ? {
143
+ path: t.path,
144
+ result: {
145
+ ...e.result,
146
+ path: t.path
147
+ }
148
+ } : {}
149
+ });
150
+ }
151
+ return this.changed(), d.filter((e) => !e.unattached);
141
152
  }
142
153
  dispose() {
143
154
  this.started = !1, this.disposed = !0;
@@ -147,21 +158,26 @@ var a = 2 ** 31 - 1, o = class {
147
158
  stage(t) {
148
159
  let n = this.find(t);
149
160
  if (!n?.result) return;
150
- let { blob: a, type: o } = n.result, s = {
161
+ let { blob: a, type: o, sourceName: s } = n.result, c = {
151
162
  op: "put",
152
163
  path: n.path,
164
+ source_name: s,
153
165
  blob: a,
154
166
  create_id: n.id,
155
167
  unattached: !0,
156
168
  ...n.meta ? { meta: n.meta } : {}
157
- }, c = this.client.commit(this.o.ref, [s], { sources: { [a]: {
169
+ }, l = this.client.commit(this.o.ref, [c], { sources: { [a]: {
158
170
  file: n.file,
159
171
  type: o
160
172
  } } }).then((e) => {
161
- let a = e.find((e) => e.unattached && r(e.path, i(n.path)));
173
+ let a = e.find((e) => e.unattached && e.create_id === t) ?? e.find((e) => e.unattached && r(e.path, i(n.path)));
162
174
  return a && this.find(t) && this.patch(t, {
163
175
  unattached: !0,
164
- path: a.path
176
+ path: a.path,
177
+ result: {
178
+ ...n.result,
179
+ path: a.path
180
+ }
165
181
  }), this.schedulePoll(), a;
166
182
  }, (n) => {
167
183
  n instanceof e && n.code === "conflict" && this.patch(t, {
@@ -169,7 +185,7 @@ var a = 2 ** 31 - 1, o = class {
169
185
  error: n
170
186
  });
171
187
  }).finally(() => this.staging.delete(t));
172
- this.staging.set(t, c);
188
+ this.staging.set(t, l);
173
189
  }
174
190
  schedulePoll() {
175
191
  this.poll || this.disposed || (this.poll = setTimeout(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"queue.js","names":[],"sources":["../../../src/client/media/queue.ts"],"sourcesContent":["import { ContentKitError, toContentKitError } from \"../errors.js\";\nimport type { FileInfo, Op, RefBody } from \"../generated/wire.js\";\nimport { fill } from \"../public.js\";\nimport { samePath, stem, type MediaClient, type Progress, type UploadedFile, type UploadState } from \"./client.js\";\n\nexport type ItemStatus = \"queued\" | \"uploading\" | \"uploaded\" | \"failed\" | \"committed\";\n\nexport interface QueueItem {\n readonly id: string;\n readonly file: File;\n /** The upload path the commit puts; the server's canonical one once uploaded. */\n path: string;\n meta?: Record<string, unknown>;\n status: ItemStatus;\n progress?: Progress;\n error?: ContentKitError;\n result?: UploadedFile;\n /** Resumable multipart state while uploading or after a failure. */\n state?: UploadState;\n /**\n * Committed unattached for processing on upload (the host's\n * ProcessOnUpload): commit() attaches it, remove() discards it.\n */\n unattached?: boolean;\n /** The server's view of an unattached upload while it processes (editor read): dims, pending, failed, progress. */\n processing?: FileInfo;\n /** Processing finished (placed and outputs written, or failed). */\n processed?: boolean;\n}\n\nexport interface QueueSnapshot {\n readonly items: readonly QueueItem[];\n /** A rate, quota or permission refusal: no new uploads start until start(). */\n readonly blocked?: ContentKitError;\n /** Every item is uploaded (or committed) and none is in flight. */\n readonly ready: boolean;\n}\n\nexport interface QueueOptions {\n ref: RefBody;\n /** Each file's upload path: a template whose {name} is the file's name, e.g. \"originals/{name}\". */\n path: string;\n /** Files uploading at once. Default 2. */\n concurrency?: number;\n /** Start uploads as files are added. Default true. */\n autoStart?: boolean;\n /** Called with every item state change, to persist resumable state. */\n onState?: (item: QueueItem, state: UploadState | null) => void;\n /** Milliseconds between processing polls of unattached uploads. Default 2000. */\n pollInterval?: number;\n}\n\n/** An op index past the end of the list: the upload is appended. */\nconst END = 2 ** 31 - 1;\n\n/**\n * An ordered file queue for one item: uploads in the background, keeps the\n * order the user arranges, and commits the uploads as puts appended in that\n * order. Framework-free; the React hooks wrap it.\n */\nexport class ItemUploadQueue {\n private items: QueueItem[] = [];\n private blocked?: ContentKitError;\n private running = new Map<string, AbortController>();\n private listeners = new Set<() => void>();\n private snap!: QueueSnapshot;\n private started: boolean;\n /** In-flight unattached commits, by item id. */\n private staging = new Map<string, Promise<FileInfo | undefined>>();\n private poll?: ReturnType<typeof setTimeout>;\n private disposed = false;\n private batch?: { input: string; operationID: string };\n\n constructor(\n private readonly client: MediaClient,\n private readonly o: QueueOptions,\n ) {\n this.started = o.autoStart ?? true;\n this.publish();\n }\n\n subscribe = (fn: () => void): (() => void) => {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n };\n\n getSnapshot = (): QueueSnapshot => this.snap;\n\n /** Queues files; state restores a multipart upload saved through onState before a reload. */\n add(\n files: Iterable<File>,\n opts: { path?: (f: File) => string; state?: (f: File) => UploadState | undefined } = {},\n ): QueueItem[] {\n const added = [...files].map(\n (file): QueueItem => ({\n id: crypto.randomUUID(),\n file,\n path: opts.path?.(file) ?? fill(this.o.path, { name: file.name }),\n status: \"queued\",\n state: opts.state?.(file),\n }),\n );\n this.items.push(...added);\n this.changed();\n return added;\n }\n\n /** Moves an item to index (before commit). */\n move(id: string, index: number): void {\n const from = this.items.findIndex((i) => i.id === id);\n if (from < 0) return;\n const [item] = this.items.splice(from, 1);\n this.items.splice(Math.max(0, Math.min(index, this.items.length)), 0, item!);\n this.changed();\n }\n\n update(id: string, patch: Pick<Partial<QueueItem>, \"path\" | \"meta\">): void {\n const item = this.find(id);\n if (item?.unattached && patch.path && patch.path !== item.path) {\n void this.client.commit(this.o.ref, [{ op: \"rename\", path: item.path, to: patch.path }]).then(\n (files) => {\n const f = files.find((x) => samePath(x.path, stem(patch.path!)));\n if (f) this.patch(id, { path: f.path });\n },\n () => {},\n );\n }\n this.patch(id, patch);\n }\n\n /**\n * Removes an item, discarding its multipart upload if one is open, or its\n * unattached upload (the commit removes it and its outputs).\n */\n remove(id: string): void {\n const item = this.find(id);\n if (!item) return;\n this.running.get(id)?.abort();\n if (item.state) void this.client.discard(item.state).catch(() => {});\n const staged = this.staging.get(id);\n if (item.unattached || staged) {\n void (staged ?? Promise.resolve())\n .then((file) => {\n if (file || item.unattached) {\n return this.client.commit(this.o.ref, [{ op: \"remove\", path: file?.path ?? item.path }]);\n }\n })\n .catch(() => {});\n }\n this.o.onState?.(item, null);\n this.items = this.items.filter((i) => i.id !== id);\n this.changed();\n }\n\n /** Re-queues a failed item; a multipart upload resumes from its state. */\n retry(id: string): void {\n this.patch(id, { status: \"queued\", error: undefined });\n }\n\n /** Starts uploading (autoStart false) or lifts a block after a refusal. */\n start(): void {\n this.started = true;\n this.blocked = undefined;\n this.changed();\n }\n\n /** Stops starting uploads and pauses running ones (they stay resumable). */\n pause(): void {\n this.started = false;\n for (const c of this.running.values()) c.abort();\n this.changed();\n }\n\n /**\n * Commits every uploaded item, in queue order, after the item's uploads:\n * unattached ones are attached (no reprocessing), the rest put. With head,\n * only the uploaded items before the first one still uploading or failed\n * (a draft committing files as they finish). attach adds unattached\n * uploads the queue does not hold (an earlier visit's) after them. Returns\n * the item's attached uploads in order.\n */\n async commit(signal?: AbortSignal, o: { head?: boolean; attach?: readonly string[] } = {}): Promise<FileInfo[]> {\n await Promise.all(this.staging.values());\n let items = this.items;\n if (o.head) {\n // Only the uploaded head of the queue, so files land in the order added.\n const end = items.findIndex((i) => i.status !== \"uploaded\" && i.status !== \"committed\");\n if (end >= 0) items = items.slice(0, end);\n }\n const ready = items.filter((i) => i.status === \"uploaded\");\n const strays = o.attach ?? [];\n if (ready.length === 0 && strays.length === 0) return [];\n const ops = ready.flatMap((i): Op[] => {\n const attach: Op = { op: \"attach\", path: i.path, index: END, ...(i.meta ? { meta: i.meta } : {}) };\n if (i.unattached) return [attach];\n const put: Op = { op: \"put\", path: i.path, blob: i.result!.blob, create_id: i.id, index: END, ...(i.meta ? { meta: i.meta } : {}) };\n // A lost staging response may hide a successful unattached create.\n return i.result!.processOnUpload ? [put, attach] : [put];\n });\n for (const path of strays) ops.push({ op: \"attach\", path, index: END });\n const sources = Object.fromEntries(ready.map((i) => [i.result!.blob, { file: i.file, type: i.result!.type }]));\n const input = JSON.stringify(ops);\n if (this.batch?.input !== input) this.batch = { input, operationID: crypto.randomUUID() };\n const batch = this.batch;\n const files = await this.client.commit(this.o.ref, ops, {\n signal,\n sources,\n operationID: batch.operationID,\n onState: (body) => {\n batch.input = JSON.stringify(body.ops);\n batch.operationID = body.operation_id;\n let changed = false;\n for (const op of body.ops) {\n if (op.op !== \"put\" || !op.blob) continue;\n const original = ready.find((i) => i.id === op.create_id);\n if (!original?.result) continue;\n const current = this.find(original.id);\n if (current?.result && current.result.blob === original.result.blob && current.result.blob !== op.blob) {\n changed = this.set(current.id, { result: { ...current.result, blob: op.blob } }) || changed;\n }\n }\n if (changed) this.changed();\n },\n });\n for (const i of ready) this.set(i.id, { status: \"committed\", unattached: false });\n this.changed();\n return files.filter((f) => !f.unattached);\n }\n\n /** Aborts running uploads and stops polling; call on unmount. */\n dispose(): void {\n this.started = false;\n this.disposed = true;\n for (const c of this.running.values()) c.abort();\n clearTimeout(this.poll);\n this.listeners.clear();\n }\n\n /** Commits an uploaded file unattached, so the host processes it now. */\n private stage(id: string): void {\n const item = this.find(id);\n if (!item?.result) return;\n const { blob, type } = item.result;\n const op: Op = { op: \"put\", path: item.path, blob, create_id: item.id, unattached: true, ...(item.meta ? { meta: item.meta } : {}) };\n const p = this.client\n .commit(this.o.ref, [op], { sources: { [blob]: { file: item.file, type } } })\n .then(\n (files) => {\n const f = files.find((x) => x.unattached && samePath(x.path, stem(item.path)));\n if (f && this.find(id)) this.patch(id, { unattached: true, path: f.path });\n this.schedulePoll();\n return f;\n },\n (error: unknown) => {\n if (error instanceof ContentKitError && error.code === \"conflict\") {\n this.patch(id, { status: \"failed\", error });\n }\n // Other failures leave it uploaded; commit() retries the same create.\n return undefined;\n },\n )\n .finally(() => this.staging.delete(id));\n this.staging.set(id, p);\n }\n\n private schedulePoll(): void {\n if (this.poll || this.disposed) return;\n this.poll = setTimeout(() => {\n this.poll = undefined;\n void this.refresh().finally(() => {\n if (this.items.some((i) => i.unattached && !i.processed)) this.schedulePoll();\n });\n }, this.o.pollInterval ?? 2000);\n }\n\n /** Reads the processing state of unattached uploads still processing. */\n private async refresh(): Promise<void> {\n const pending = this.items.filter((i) => i.unattached && !i.processed);\n if (pending.length === 0) return;\n let files: FileInfo[];\n try {\n files = (await this.client.read(this.o.ref, { editor: true })).files;\n } catch {\n return;\n }\n const byPath = new Map(files.filter((f) => f.upload).map((f) => [f.path, f]));\n let changed = false;\n for (const i of pending) {\n const f = byPath.get(i.path);\n if (!f) continue;\n changed = this.set(i.id, { processing: f, processed: !!f.failed || (!f.pending?.length && !f.staged) }) || changed;\n }\n if (changed) this.changed();\n }\n\n private find(id: string): QueueItem | undefined {\n return this.items.find((i) => i.id === id);\n }\n\n private patch(id: string, patch: Partial<QueueItem>): void {\n if (this.set(id, patch)) this.changed();\n }\n\n private set(id: string, patch: Partial<QueueItem>): boolean {\n const i = this.items.findIndex((x) => x.id === id);\n if (i >= 0) this.items[i] = { ...this.items[i]!, ...patch };\n return i >= 0;\n }\n\n private changed(): void {\n this.pump();\n this.publish();\n for (const fn of this.listeners) fn();\n }\n\n private publish(): void {\n this.snap = {\n items: [...this.items],\n blocked: this.blocked,\n ready:\n this.running.size === 0 &&\n this.items.length > 0 &&\n this.items.every((i) => i.status === \"uploaded\" || i.status === \"committed\"),\n };\n }\n\n private pump(): void {\n if (!this.started || this.blocked) return;\n const limit = this.o.concurrency ?? 2;\n for (const item of this.items) {\n if (this.running.size >= limit) return;\n if (item.status === \"queued\" && !this.running.has(item.id)) this.run(item.id);\n }\n }\n\n private run(id: string): void {\n const item = this.find(id)!;\n const ctl = new AbortController();\n this.running.set(id, ctl);\n this.set(id, { status: \"uploading\", progress: undefined, error: undefined });\n this.client\n .upload(item.file, {\n ref: this.o.ref,\n path: item.path,\n signal: ctl.signal,\n resume: item.state,\n onProgress: (progress) => this.patch(id, { progress }),\n onState: (state) => {\n this.patch(id, { state: state ?? undefined });\n const cur = this.find(id);\n if (cur) this.o.onState?.(cur, state);\n },\n })\n .then(\n (result) => {\n this.running.delete(id);\n this.patch(id, { status: \"uploaded\", result, path: result.path, state: undefined });\n if (result.processOnUpload) this.stage(id);\n },\n (e: unknown) => {\n this.running.delete(id);\n const error = toContentKitError(e);\n if (error.code === \"aborted\") {\n this.patch(id, { status: \"queued\" });\n return;\n }\n if (error.blocksQueue) {\n // Refused for every file: keep it queued behind the block.\n this.blocked = error;\n this.patch(id, { status: \"queued\", error });\n return;\n }\n this.patch(id, { status: \"failed\", error });\n },\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"queue.js","names":[],"sources":["../../../src/client/media/queue.ts"],"sourcesContent":["import { ContentKitError, toContentKitError } from \"../errors.js\";\nimport type { FileInfo, Op, RefBody } from \"../generated/wire.js\";\nimport { fill } from \"../public.js\";\nimport { samePath, stem, type MediaClient, type Progress, type UploadedFile, type UploadState } from \"./client.js\";\n\nexport type ItemStatus = \"queued\" | \"uploading\" | \"uploaded\" | \"failed\" | \"committed\";\n\nexport interface QueueItem {\n readonly id: string;\n readonly file: File;\n /** The upload path the commit puts; the server's canonical one once uploaded. */\n path: string;\n meta?: Record<string, unknown>;\n status: ItemStatus;\n progress?: Progress;\n error?: ContentKitError;\n result?: UploadedFile;\n /** Resumable multipart state while uploading or after a failure. */\n state?: UploadState;\n /**\n * Committed unattached for processing on upload (the host's\n * ProcessOnUpload): commit() attaches it, remove() discards it.\n */\n unattached?: boolean;\n /** The server's view of an unattached upload while it processes (editor read): dims, pending, failed, progress. */\n processing?: FileInfo;\n /** Processing finished (placed and outputs written, or failed). */\n processed?: boolean;\n}\n\nexport interface QueueSnapshot {\n readonly items: readonly QueueItem[];\n /** A rate, quota or permission refusal: no new uploads start until start(). */\n readonly blocked?: ContentKitError;\n /** Every item is uploaded (or committed) and none is in flight. */\n readonly ready: boolean;\n}\n\nexport interface QueueOptions {\n ref: RefBody;\n /** Each file's upload path: a template whose {name} is the file's name, e.g. \"originals/{name}\". */\n path: string;\n /** Files uploading at once. Default 2. */\n concurrency?: number;\n /** Start uploads as files are added. Default true. */\n autoStart?: boolean;\n /** Called with every item state change, to persist resumable state. */\n onState?: (item: QueueItem, state: UploadState | null) => void;\n /** Milliseconds between processing polls of unattached uploads. Default 2000. */\n pollInterval?: number;\n}\n\n/** An op index past the end of the list: the upload is appended. */\nconst END = 2 ** 31 - 1;\n\n/**\n * An ordered file queue for one item: uploads in the background, keeps the\n * order the user arranges, and commits the uploads as puts appended in that\n * order. Framework-free; the React hooks wrap it.\n */\nexport class ItemUploadQueue {\n private items: QueueItem[] = [];\n private blocked?: ContentKitError;\n private running = new Map<string, AbortController>();\n private listeners = new Set<() => void>();\n private snap!: QueueSnapshot;\n private started: boolean;\n /** In-flight unattached commits, by item id. */\n private staging = new Map<string, Promise<FileInfo | undefined>>();\n private poll?: ReturnType<typeof setTimeout>;\n private disposed = false;\n private batch?: { input: string; operationID: string };\n\n constructor(\n private readonly client: MediaClient,\n private readonly o: QueueOptions,\n ) {\n this.started = o.autoStart ?? true;\n this.publish();\n }\n\n subscribe = (fn: () => void): (() => void) => {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n };\n\n getSnapshot = (): QueueSnapshot => this.snap;\n\n /** Queues files; state restores a multipart upload saved through onState before a reload. */\n add(\n files: Iterable<File>,\n opts: { path?: (f: File) => string; state?: (f: File) => UploadState | undefined } = {},\n ): QueueItem[] {\n const added = [...files].map(\n (file): QueueItem => ({\n id: crypto.randomUUID(),\n file,\n path: opts.path?.(file) ?? fill(this.o.path, { name: file.name }),\n status: \"queued\",\n state: opts.state?.(file),\n }),\n );\n this.items.push(...added);\n this.changed();\n return added;\n }\n\n /** Moves an item to index (before commit). */\n move(id: string, index: number): void {\n const from = this.items.findIndex((i) => i.id === id);\n if (from < 0) return;\n const [item] = this.items.splice(from, 1);\n this.items.splice(Math.max(0, Math.min(index, this.items.length)), 0, item!);\n this.changed();\n }\n\n update(id: string, patch: Pick<Partial<QueueItem>, \"path\" | \"meta\">): void {\n const item = this.find(id);\n if (item?.unattached && patch.path && patch.path !== item.path) {\n void this.client.commit(this.o.ref, [{ op: \"rename\", path: item.path, to: patch.path }]).then(\n (files) => {\n const f = files.find((x) => samePath(x.path, stem(patch.path!)));\n if (f) this.patch(id, { path: f.path });\n },\n () => {},\n );\n }\n this.patch(id, patch);\n }\n\n /**\n * Removes an item, discarding its multipart upload if one is open, or its\n * unattached upload (the commit removes it and its outputs).\n */\n remove(id: string): void {\n const item = this.find(id);\n if (!item) return;\n this.running.get(id)?.abort();\n if (item.state) void this.client.discard(item.state).catch(() => {});\n const staged = this.staging.get(id);\n if (item.unattached || staged) {\n void (staged ?? Promise.resolve())\n .then((file) => {\n if (file || item.unattached) {\n return this.client.commit(this.o.ref, [{ op: \"remove\", path: file?.path ?? item.path }]);\n }\n })\n .catch(() => {});\n }\n this.o.onState?.(item, null);\n this.items = this.items.filter((i) => i.id !== id);\n this.changed();\n }\n\n /** Re-queues a failed item; a multipart upload resumes from its state. */\n retry(id: string): void {\n this.patch(id, { status: \"queued\", error: undefined });\n }\n\n /** Starts uploading (autoStart false) or lifts a block after a refusal. */\n start(): void {\n this.started = true;\n this.blocked = undefined;\n this.changed();\n }\n\n /** Stops starting uploads and pauses running ones (they stay resumable). */\n pause(): void {\n this.started = false;\n for (const c of this.running.values()) c.abort();\n this.changed();\n }\n\n /**\n * Commits every uploaded item, in queue order, after the item's uploads:\n * unattached ones are attached (no reprocessing), the rest put. With head,\n * only the uploaded items before the first one still uploading or failed\n * (a draft committing files as they finish). attach adds unattached\n * uploads the queue does not hold (an earlier visit's) after them. Returns\n * the item's attached uploads in order.\n */\n async commit(signal?: AbortSignal, o: { head?: boolean; attach?: readonly string[] } = {}): Promise<FileInfo[]> {\n await Promise.all(this.staging.values());\n let items = this.items;\n if (o.head) {\n // Only the uploaded head of the queue, so files land in the order added.\n const end = items.findIndex((i) => i.status !== \"uploaded\" && i.status !== \"committed\");\n if (end >= 0) items = items.slice(0, end);\n }\n const ready = items.filter((i) => i.status === \"uploaded\");\n const strays = o.attach ?? [];\n if (ready.length === 0 && strays.length === 0) return [];\n const ops = ready.flatMap((i): Op[] => {\n const attach: Op = { op: \"attach\", path: i.path, index: END, ...(i.meta ? { meta: i.meta } : {}) };\n if (i.unattached) return [attach];\n const put: Op = { op: \"put\", path: i.path, source_name: i.result!.sourceName, blob: i.result!.blob, create_id: i.id, index: END, ...(i.meta ? { meta: i.meta } : {}) };\n // A lost staging response may hide a successful unattached create.\n return i.result!.processOnUpload ? [put, attach] : [put];\n });\n for (const path of strays) ops.push({ op: \"attach\", path, index: END });\n const sources = Object.fromEntries(ready.map((i) => [i.result!.blob, { file: i.file, type: i.result!.type }]));\n const input = JSON.stringify(ops);\n if (this.batch?.input !== input) this.batch = { input, operationID: crypto.randomUUID() };\n const batch = this.batch;\n const files = await this.client.commit(this.o.ref, ops, {\n signal,\n sources,\n operationID: batch.operationID,\n onState: (body) => {\n batch.input = JSON.stringify(body.ops);\n batch.operationID = body.operation_id;\n let changed = false;\n for (const op of body.ops) {\n if (op.op !== \"put\" || !op.blob) continue;\n const original = ready.find((i) => i.id === op.create_id);\n if (!original?.result) continue;\n const current = this.find(original.id);\n if (current?.result && current.result.blob === original.result.blob && current.result.blob !== op.blob) {\n changed = this.set(current.id, { result: { ...current.result, blob: op.blob } }) || changed;\n }\n }\n if (changed) this.changed();\n },\n });\n for (const i of ready) {\n const file = files.find((f) => f.create_id === i.id) ?? files.find((f) => samePath(f.path, i.path));\n this.set(i.id, { status: \"committed\", unattached: false, ...(file ? { path: file.path, result: { ...i.result!, path: file.path } } : {}) });\n }\n this.changed();\n return files.filter((f) => !f.unattached);\n }\n\n /** Aborts running uploads and stops polling; call on unmount. */\n dispose(): void {\n this.started = false;\n this.disposed = true;\n for (const c of this.running.values()) c.abort();\n clearTimeout(this.poll);\n this.listeners.clear();\n }\n\n /** Commits an uploaded file unattached, so the host processes it now. */\n private stage(id: string): void {\n const item = this.find(id);\n if (!item?.result) return;\n const { blob, type, sourceName } = item.result;\n const op: Op = { op: \"put\", path: item.path, source_name: sourceName, blob, create_id: item.id, unattached: true, ...(item.meta ? { meta: item.meta } : {}) };\n const p = this.client\n .commit(this.o.ref, [op], { sources: { [blob]: { file: item.file, type } } })\n .then(\n (files) => {\n const f = files.find((x) => x.unattached && x.create_id === id) ?? files.find((x) => x.unattached && samePath(x.path, stem(item.path)));\n if (f && this.find(id)) this.patch(id, { unattached: true, path: f.path, result: { ...item.result!, path: f.path } });\n this.schedulePoll();\n return f;\n },\n (error: unknown) => {\n if (error instanceof ContentKitError && error.code === \"conflict\") {\n this.patch(id, { status: \"failed\", error });\n }\n // Other failures leave it uploaded; commit() retries the same create.\n return undefined;\n },\n )\n .finally(() => this.staging.delete(id));\n this.staging.set(id, p);\n }\n\n private schedulePoll(): void {\n if (this.poll || this.disposed) return;\n this.poll = setTimeout(() => {\n this.poll = undefined;\n void this.refresh().finally(() => {\n if (this.items.some((i) => i.unattached && !i.processed)) this.schedulePoll();\n });\n }, this.o.pollInterval ?? 2000);\n }\n\n /** Reads the processing state of unattached uploads still processing. */\n private async refresh(): Promise<void> {\n const pending = this.items.filter((i) => i.unattached && !i.processed);\n if (pending.length === 0) return;\n let files: FileInfo[];\n try {\n files = (await this.client.read(this.o.ref, { editor: true })).files;\n } catch {\n return;\n }\n const byPath = new Map(files.filter((f) => f.upload).map((f) => [f.path, f]));\n let changed = false;\n for (const i of pending) {\n const f = byPath.get(i.path);\n if (!f) continue;\n changed = this.set(i.id, { processing: f, processed: !!f.failed || (!f.pending?.length && !f.staged) }) || changed;\n }\n if (changed) this.changed();\n }\n\n private find(id: string): QueueItem | undefined {\n return this.items.find((i) => i.id === id);\n }\n\n private patch(id: string, patch: Partial<QueueItem>): void {\n if (this.set(id, patch)) this.changed();\n }\n\n private set(id: string, patch: Partial<QueueItem>): boolean {\n const i = this.items.findIndex((x) => x.id === id);\n if (i >= 0) this.items[i] = { ...this.items[i]!, ...patch };\n return i >= 0;\n }\n\n private changed(): void {\n this.pump();\n this.publish();\n for (const fn of this.listeners) fn();\n }\n\n private publish(): void {\n this.snap = {\n items: [...this.items],\n blocked: this.blocked,\n ready:\n this.running.size === 0 &&\n this.items.length > 0 &&\n this.items.every((i) => i.status === \"uploaded\" || i.status === \"committed\"),\n };\n }\n\n private pump(): void {\n if (!this.started || this.blocked) return;\n const limit = this.o.concurrency ?? 2;\n for (const item of this.items) {\n if (this.running.size >= limit) return;\n if (item.status === \"queued\" && !this.running.has(item.id)) this.run(item.id);\n }\n }\n\n private run(id: string): void {\n const item = this.find(id)!;\n const ctl = new AbortController();\n this.running.set(id, ctl);\n this.set(id, { status: \"uploading\", progress: undefined, error: undefined });\n this.client\n .upload(item.file, {\n ref: this.o.ref,\n path: item.path,\n signal: ctl.signal,\n resume: item.state,\n onProgress: (progress) => this.patch(id, { progress }),\n onState: (state) => {\n this.patch(id, { state: state ?? undefined });\n const cur = this.find(id);\n if (cur) this.o.onState?.(cur, state);\n },\n })\n .then(\n (result) => {\n this.running.delete(id);\n this.patch(id, { status: \"uploaded\", result, path: result.path, state: undefined });\n if (result.processOnUpload) this.stage(id);\n },\n (e: unknown) => {\n this.running.delete(id);\n const error = toContentKitError(e);\n if (error.code === \"aborted\") {\n this.patch(id, { status: \"queued\" });\n return;\n }\n if (error.blocksQueue) {\n // Refused for every file: keep it queued behind the block.\n this.blocked = error;\n this.patch(id, { status: \"queued\", error });\n return;\n }\n this.patch(id, { status: \"failed\", error });\n },\n );\n }\n}\n"],"mappings":"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"}
@@ -33,6 +33,10 @@ export interface Screened {
33
33
  error: ContentKitError;
34
34
  }[];
35
35
  }
36
+ /** ZIP ingestion's numeric, case-insensitive order, then original UTF-8 name. */
37
+ export declare function naturalNameCompare(left: string, right: string): number;
38
+ /** Sort only new page files within each rule, retaining every non-page slot. */
39
+ export declare function sortPageFiles(accepted: readonly Screened["accepted"][number][]): Screened["accepted"];
36
40
  /**
37
41
  * Checks files against the rules before any upload: a type some rule takes
38
42
  * (the first that does), its size, and the rule's file cap counting
@@ -42,6 +46,8 @@ export interface Screened {
42
46
  export declare function screenFiles(files: Iterable<File>, rules: readonly UploadRule[], have?: (rule: UploadRule) => number): Screened;
43
47
  /** name, or name-2, name-3… so its stem is not among taken (names or paths without the directory). */
44
48
  export declare function uniqueName(name: string, taken: Iterable<string>): string;
49
+ /** Preserve numeric page/range identities; a collision must not invent a joined range. */
50
+ export declare function uniquePageName(name: string, taken: Iterable<string>): string;
45
51
  /** "16:9" style label of a width/height ratio: 2.4 → "2.4:1", 0.4167 → "1:2.4". */
46
52
  export declare function ratioLabel(r: number): string;
47
53
  /** The kind's public preset for an upload path: the one named name, else the first with an aspect, else the first. */
@@ -49,7 +49,49 @@ function m(e) {
49
49
  let n = Object.entries(f).filter(([, e]) => t.includes(e)).map(([e]) => `.${e}`);
50
50
  return [...t, ...n].join(",");
51
51
  }
52
- function h(t, n, r = () => 0) {
52
+ var h = new TextEncoder(), g = (e) => e >= 48 && e <= 57, _ = (e) => e >= 65 && e <= 90 ? e + 32 : e;
53
+ function v(e, t) {
54
+ let n = h.encode(e), r = h.encode(t), i = 0, a = 0;
55
+ for (; i < n.length && a < r.length;) if (g(n[i]) && g(r[a])) {
56
+ let e = i, t = a;
57
+ for (; e < n.length && g(n[e]);) e++;
58
+ for (; t < r.length && g(r[t]);) t++;
59
+ let o = i, s = a;
60
+ for (; o < e && n[o] === 48;) o++;
61
+ for (; s < t && r[s] === 48;) s++;
62
+ if (e - o !== t - s) return e - o - (t - s);
63
+ for (; o < e;) {
64
+ if (n[o] !== r[s]) return n[o] - r[s];
65
+ o++, s++;
66
+ }
67
+ i = e, a = t;
68
+ } else {
69
+ let e = _(n[i]) - _(r[a]);
70
+ if (e) return e;
71
+ i++, a++;
72
+ }
73
+ let o = n.length - i - (r.length - a);
74
+ if (o) return o;
75
+ for (let e = 0; e < Math.min(n.length, r.length); e++) if (n[e] !== r[e]) return n[e] - r[e];
76
+ return n.length - r.length;
77
+ }
78
+ function y(e) {
79
+ let t = /* @__PURE__ */ new Map();
80
+ for (let n of e) {
81
+ if (!n.rule.page) continue;
82
+ let e = t.get(n.rule.path);
83
+ e || t.set(n.rule.path, e = {
84
+ files: [],
85
+ next: 0
86
+ }), e.files.push(n);
87
+ }
88
+ for (let e of t.values()) e.files.sort((e, t) => v(e.file.name, t.file.name));
89
+ return e.map((e) => {
90
+ let n = e.rule.page ? t.get(e.rule.path) : void 0;
91
+ return n ? n.files[n.next++] : e;
92
+ });
93
+ }
94
+ function b(t, n, r = () => 0) {
53
95
  let i = {
54
96
  accepted: [],
55
97
  refused: []
@@ -104,7 +146,7 @@ function h(t, n, r = () => 0) {
104
146
  }
105
147
  return i;
106
148
  }
107
- function g(e, t) {
149
+ function x(e, t) {
108
150
  let n = new Set([...t].map(i));
109
151
  if (!n.has(i(e))) return e;
110
152
  let r = e.lastIndexOf("."), a = r > 0 ? e.slice(0, r) : e, o = r > 0 ? e.slice(r) : "";
@@ -113,21 +155,33 @@ function g(e, t) {
113
155
  if (!n.has(i(t))) return t;
114
156
  }
115
157
  }
116
- function _(e) {
158
+ function S(e, t) {
159
+ let n = (e) => {
160
+ let t = i(e);
161
+ return /^\d+(?:-\d+)?$/.test(t) ? t.split("-").map((e) => e.replace(/^0+/, "") || "0").join("-") : t;
162
+ }, r = new Set([...t].map(n));
163
+ if (!r.has(n(e))) return e;
164
+ let a = e.lastIndexOf("."), o = a > 0 ? e.slice(0, a) : e, s = a > 0 ? e.slice(a) : "";
165
+ for (let e = 2;; e++) {
166
+ let t = `${o} (${e})${s}`;
167
+ if (!r.has(n(t))) return t;
168
+ }
169
+ }
170
+ function C(e) {
117
171
  let t = (e) => String(Math.round(e * 100) / 100);
118
172
  return e >= 1 ? `${t(e)}:1` : `1:${t(1 / e)}`;
119
173
  }
120
- function v(e, t, n, r) {
174
+ function w(e, t, n, r) {
121
175
  let i = e.filter((e) => e.kind === t && u([{ path: e.from }], n));
122
176
  return (r ? i.find((e) => e.name === r) : void 0) ?? i.find((e) => e.aspect) ?? i[0];
123
177
  }
124
- function y(e, t, n) {
125
- return !e.default || e.first || e.to.includes(s) ? null : x(e, t, n);
178
+ function T(e, t, n) {
179
+ return !e.default || e.first || e.to.includes(s) ? null : D(e, t, n);
126
180
  }
127
- function b(e, t) {
128
- return e.stable ? x(e, t) : null;
181
+ function E(e, t) {
182
+ return e.stable ? D(e, t) : null;
129
183
  }
130
- function x(e, i, a) {
184
+ function D(e, i, a) {
131
185
  let o = r(e.aspect);
132
186
  return {
133
187
  preset: e.name,
@@ -140,6 +194,6 @@ function x(e, i, a) {
140
194
  };
141
195
  }
142
196
  //#endregion
143
- export { m as acceptOf, y as defaultImage, d as filesFor, c as isPattern, v as presetFor, _ as ratioLabel, l as ruleDir, u as ruleFor, h as screenFiles, b as stableImage, g as uniqueName, o as uploadRules, p as withMediaType };
197
+ export { m as acceptOf, T as defaultImage, d as filesFor, c as isPattern, v as naturalNameCompare, w as presetFor, C as ratioLabel, l as ruleDir, u as ruleFor, b as screenFiles, y as sortPageFiles, E as stableImage, x as uniqueName, S as uniquePageName, o as uploadRules, p as withMediaType };
144
198
 
145
199
  //# sourceMappingURL=rules.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","names":[],"sources":["../../../src/client/media/rules.ts"],"sourcesContent":["import { ContentKitError } from \"../errors.js\";\nimport type { ErrorDetails, PresetRule, ReadResult, UploadRule } from \"../generated/wire.js\";\nimport { fill, publicURL, type PublicPreset } from \"../public.js\";\nimport { ratio } from \"../aspect.js\";\nimport { stem } from \"./client.js\";\n\nconst NO_RULES: UploadRule[] = [];\n\n/** An editor read's upload rules (a viewer read has none): one empty list, so memos keyed on them hold. */\nexport const uploadRules = (read: ReadResult | null | undefined): UploadRule[] => read?.uploads ?? NO_RULES;\n\nconst NAME = \"{name}\";\n\n/** A pattern path (\"originals/{name}\") holds many files; a literal (\"cover\") one. */\nexport const isPattern = (path: string) => path.endsWith(NAME);\n\n/** The directory of a pattern (\"originals/\"), or the literal itself. */\nexport const ruleDir = (path: string) => (isPattern(path) ? path.slice(0, -NAME.length) : path);\n\n/** The rule an upload path (with or without its extension) falls under: a literal wins over a pattern. */\nexport function ruleFor<R extends { path: string }>(rules: readonly R[], path: string): R | undefined {\n const s = stem(path);\n return (\n rules.find((r) => !isPattern(r.path) && r.path === s) ??\n rules.find((r) => {\n if (!isPattern(r.path)) return false;\n const dir = ruleDir(r.path);\n return s.startsWith(dir) && s.length > dir.length && !s.slice(dir.length).includes(\"/\");\n })\n );\n}\n\n/** Uploads of the read under rule (attached, in manifest order). */\nexport function filesFor(read: ReadResult | null | undefined, rule: { path: string }, rules: readonly { path: string }[] = [rule]) {\n return (read?.files ?? []).filter((f) => f.upload && !f.unattached && ruleFor(rules, f.path)?.path === rule.path);\n}\n\nconst BY_EXTENSION: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n webp: \"image/webp\",\n gif: \"image/gif\",\n avif: \"image/avif\",\n mp4: \"video/mp4\",\n m4v: \"video/mp4\",\n mov: \"video/quicktime\",\n mkv: \"video/x-matroska\",\n webm: \"video/webm\",\n m4a: \"audio/mp4\",\n mp3: \"audio/mpeg\",\n vtt: \"text/vtt\",\n srt: \"application/x-subrip\",\n ass: \"text/x-ass\",\n ssa: \"text/x-ssa\",\n zip: \"application/zip\",\n};\n\n/** The file with a content type from its extension when the browser left it empty or nonstandard (.mkv, .mov, .srt). */\nexport function withMediaType(file: File, allowed?: readonly string[]): File {\n if (file.type && (!allowed || allowed.includes(file.type))) return file;\n const type = BY_EXTENSION[file.name.split(\".\").pop()?.toLowerCase() ?? \"\"];\n return type && type !== file.type ? new File([file], file.name, { type, lastModified: file.lastModified }) : file;\n}\n\n/** `accept` for a file input from the rules' types (plus the extensions browsers leave untyped). */\nexport function acceptOf(rules: readonly UploadRule[]): string | undefined {\n const types = [...new Set(rules.flatMap((r) => r.types))];\n if (!types.length) return undefined;\n const exts = Object.entries(BY_EXTENSION).filter(([, t]) => types.includes(t)).map(([e]) => `.${e}`);\n return [...types, ...exts].join(\",\");\n}\n\nexport interface Screened {\n /** Files to upload, each with its rule (typed by extension when needed). */\n accepted: { file: File; rule: UploadRule }[];\n /** Files the server would refuse, with the refusal it would answer. */\n refused: { file: File; error: ContentKitError }[];\n}\n\n/**\n * Checks files against the rules before any upload: a type some rule takes\n * (the first that does), its size, and the rule's file cap counting\n * have(rule) files already there. A rule without types, size or cap lets\n * that check through; the server decides.\n */\nexport function screenFiles(files: Iterable<File>, rules: readonly UploadRule[], have: (rule: UploadRule) => number = () => 0): Screened {\n const out: Screened = { accepted: [], refused: [] };\n const counts = new Map<UploadRule, number>();\n const allowed = [...new Set(rules.flatMap((r) => r.types))];\n for (const picked of files) {\n const file = withMediaType(picked, allowed);\n const rule = rules.find((r) => !r.types.length || r.types.includes(file.type));\n if (!rule) {\n const details: ErrorDetails = { type: file.type || undefined, allowed };\n out.refused.push({ file, error: new ContentKitError(\"type_not_allowed\", `${file.name}: type not allowed`, { status: 415, details }) });\n continue;\n }\n if (rule.max_bytes > 0 && file.size > rule.max_bytes) {\n const details: ErrorDetails = { type: file.type, size: file.size, max_bytes: rule.max_bytes };\n out.refused.push({ file, error: new ContentKitError(\"too_large\", `${file.name}: too large`, { status: 413, details }) });\n continue;\n }\n const n = counts.get(rule) ?? have(rule);\n if (rule.max && n >= rule.max) {\n const details: ErrorDetails = { max: rule.max };\n out.refused.push({ file, error: new ContentKitError(\"too_many_files\", `${file.name}: at most ${rule.max} files`, { status: 409, details }) });\n continue;\n }\n counts.set(rule, n + 1);\n out.accepted.push({ file, rule });\n }\n return out;\n}\n\n/** name, or name-2, name-3… so its stem is not among taken (names or paths without the directory). */\nexport function uniqueName(name: string, taken: Iterable<string>): string {\n const stems = new Set([...taken].map(stem));\n if (!stems.has(stem(name))) return name;\n const dot = name.lastIndexOf(\".\");\n const base = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : \"\";\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}${ext}`;\n if (!stems.has(stem(candidate))) return candidate;\n }\n}\n\n/** \"16:9\" style label of a width/height ratio: 2.4 → \"2.4:1\", 0.4167 → \"1:2.4\". */\nexport function ratioLabel(r: number): string {\n const round = (v: number) => String(Math.round(v * 100) / 100);\n return r >= 1 ? `${round(r)}:1` : `1:${round(1 / r)}`;\n}\n\n/** The kind's public preset for an upload path: the one named name, else the first with an aspect, else the first. */\nexport function presetFor(presets: readonly PresetRule[], kind: string, path: string, name?: string): PresetRule | undefined {\n const at = presets.filter((p) => p.kind === kind && ruleFor([{ path: p.from }], path));\n return (name ? at.find((p) => p.name === name) : undefined) ?? at.find((p) => p.aspect) ?? at[0];\n}\n\n/**\n * The kind's default image for a preset, at every width: what the gateway\n * serves for an item without its own (a URL from the template never names a\n * current file). null when the preset has no default (`rule.default`).\n */\nexport function defaultImage(rule: PresetRule, id: string, version?: string): PublicPreset | null {\n if (!rule.default || rule.first || rule.to.includes(NAME)) return null;\n return logicalImage(rule, id, version);\n}\n\n/**\n * A Stable preset's image of an item (`rule.stable`, e.g. the content\n * model's `avatars`): URLs built from the id that show its current image,\n * else the default. null for any other preset.\n */\nexport function stableImage(rule: PresetRule, id: string): PublicPreset | null {\n return rule.stable ? logicalImage(rule, id) : null;\n}\n\nfunction logicalImage(rule: PresetRule, id: string, version?: string): PublicPreset {\n const r = ratio(rule.aspect);\n return {\n preset: rule.name,\n aspect: rule.aspect,\n renditions: rule.widths.map((w) => ({ url: publicURL(rule.base, rule.namespace, rule.kind, id, fill(rule.to, { w }), version, rule.root), w, ...(r ? { h: Math.round(w / r) } : {}) })),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rules.js","names":[],"sources":["../../../src/client/media/rules.ts"],"sourcesContent":["import { ContentKitError } from \"../errors.js\";\nimport type { ErrorDetails, PresetRule, ReadResult, UploadRule } from \"../generated/wire.js\";\nimport { fill, publicURL, type PublicPreset } from \"../public.js\";\nimport { ratio } from \"../aspect.js\";\nimport { stem } from \"./client.js\";\n\nconst NO_RULES: UploadRule[] = [];\n\n/** An editor read's upload rules (a viewer read has none): one empty list, so memos keyed on them hold. */\nexport const uploadRules = (read: ReadResult | null | undefined): UploadRule[] => read?.uploads ?? NO_RULES;\n\nconst NAME = \"{name}\";\n\n/** A pattern path (\"originals/{name}\") holds many files; a literal (\"cover\") one. */\nexport const isPattern = (path: string) => path.endsWith(NAME);\n\n/** The directory of a pattern (\"originals/\"), or the literal itself. */\nexport const ruleDir = (path: string) => (isPattern(path) ? path.slice(0, -NAME.length) : path);\n\n/** The rule an upload path (with or without its extension) falls under: a literal wins over a pattern. */\nexport function ruleFor<R extends { path: string }>(rules: readonly R[], path: string): R | undefined {\n const s = stem(path);\n return (\n rules.find((r) => !isPattern(r.path) && r.path === s) ??\n rules.find((r) => {\n if (!isPattern(r.path)) return false;\n const dir = ruleDir(r.path);\n return s.startsWith(dir) && s.length > dir.length && !s.slice(dir.length).includes(\"/\");\n })\n );\n}\n\n/** Uploads of the read under rule (attached, in manifest order). */\nexport function filesFor(read: ReadResult | null | undefined, rule: { path: string }, rules: readonly { path: string }[] = [rule]) {\n return (read?.files ?? []).filter((f) => f.upload && !f.unattached && ruleFor(rules, f.path)?.path === rule.path);\n}\n\nconst BY_EXTENSION: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n webp: \"image/webp\",\n gif: \"image/gif\",\n avif: \"image/avif\",\n mp4: \"video/mp4\",\n m4v: \"video/mp4\",\n mov: \"video/quicktime\",\n mkv: \"video/x-matroska\",\n webm: \"video/webm\",\n m4a: \"audio/mp4\",\n mp3: \"audio/mpeg\",\n vtt: \"text/vtt\",\n srt: \"application/x-subrip\",\n ass: \"text/x-ass\",\n ssa: \"text/x-ssa\",\n zip: \"application/zip\",\n};\n\n/** The file with a content type from its extension when the browser left it empty or nonstandard (.mkv, .mov, .srt). */\nexport function withMediaType(file: File, allowed?: readonly string[]): File {\n if (file.type && (!allowed || allowed.includes(file.type))) return file;\n const type = BY_EXTENSION[file.name.split(\".\").pop()?.toLowerCase() ?? \"\"];\n return type && type !== file.type ? new File([file], file.name, { type, lastModified: file.lastModified }) : file;\n}\n\n/** `accept` for a file input from the rules' types (plus the extensions browsers leave untyped). */\nexport function acceptOf(rules: readonly UploadRule[]): string | undefined {\n const types = [...new Set(rules.flatMap((r) => r.types))];\n if (!types.length) return undefined;\n const exts = Object.entries(BY_EXTENSION).filter(([, t]) => types.includes(t)).map(([e]) => `.${e}`);\n return [...types, ...exts].join(\",\");\n}\n\nexport interface Screened {\n /** Files to upload, each with its rule (typed by extension when needed). */\n accepted: { file: File; rule: UploadRule }[];\n /** Files the server would refuse, with the refusal it would answer. */\n refused: { file: File; error: ContentKitError }[];\n}\n\nconst encoder = new TextEncoder();\nconst digit = (n: number) => n >= 48 && n <= 57;\nconst lowerASCII = (n: number) => n >= 65 && n <= 90 ? n + 32 : n;\n\n/** ZIP ingestion's numeric, case-insensitive order, then original UTF-8 name. */\nexport function naturalNameCompare(left: string, right: string): number {\n const a = encoder.encode(left), b = encoder.encode(right);\n let i = 0, j = 0;\n while (i < a.length && j < b.length) {\n if (digit(a[i]!) && digit(b[j]!)) {\n let ae = i, be = j;\n while (ae < a.length && digit(a[ae]!)) ae++;\n while (be < b.length && digit(b[be]!)) be++;\n let av = i, bv = j;\n while (av < ae && a[av] === 48) av++;\n while (bv < be && b[bv] === 48) bv++;\n if (ae - av !== be - bv) return ae - av - (be - bv);\n while (av < ae) {\n if (a[av] !== b[bv]) return a[av]! - b[bv]!;\n av++; bv++;\n }\n i = ae; j = be;\n } else {\n const difference = lowerASCII(a[i]!) - lowerASCII(b[j]!);\n if (difference) return difference;\n i++; j++;\n }\n }\n const remaining = a.length - i - (b.length - j);\n if (remaining) return remaining;\n for (let at = 0; at < Math.min(a.length, b.length); at++) if (a[at] !== b[at]) return a[at]! - b[at]!;\n return a.length - b.length;\n}\n\n/** Sort only new page files within each rule, retaining every non-page slot. */\nexport function sortPageFiles(accepted: readonly Screened[\"accepted\"][number][]): Screened[\"accepted\"] {\n const groups = new Map<string, { files: Screened[\"accepted\"]; next: number }>();\n for (const entry of accepted) {\n if (!entry.rule.page) continue;\n let group = groups.get(entry.rule.path);\n if (!group) groups.set(entry.rule.path, group = { files: [], next: 0 });\n group.files.push(entry);\n }\n for (const group of groups.values()) group.files.sort((a, b) => naturalNameCompare(a.file.name, b.file.name));\n return accepted.map((entry) => {\n const group = entry.rule.page ? groups.get(entry.rule.path) : undefined;\n return group ? group.files[group.next++]! : entry;\n });\n}\n\n/**\n * Checks files against the rules before any upload: a type some rule takes\n * (the first that does), its size, and the rule's file cap counting\n * have(rule) files already there. A rule without types, size or cap lets\n * that check through; the server decides.\n */\nexport function screenFiles(files: Iterable<File>, rules: readonly UploadRule[], have: (rule: UploadRule) => number = () => 0): Screened {\n const out: Screened = { accepted: [], refused: [] };\n const counts = new Map<UploadRule, number>();\n const allowed = [...new Set(rules.flatMap((r) => r.types))];\n for (const picked of files) {\n const file = withMediaType(picked, allowed);\n const rule = rules.find((r) => !r.types.length || r.types.includes(file.type));\n if (!rule) {\n const details: ErrorDetails = { type: file.type || undefined, allowed };\n out.refused.push({ file, error: new ContentKitError(\"type_not_allowed\", `${file.name}: type not allowed`, { status: 415, details }) });\n continue;\n }\n if (rule.max_bytes > 0 && file.size > rule.max_bytes) {\n const details: ErrorDetails = { type: file.type, size: file.size, max_bytes: rule.max_bytes };\n out.refused.push({ file, error: new ContentKitError(\"too_large\", `${file.name}: too large`, { status: 413, details }) });\n continue;\n }\n const n = counts.get(rule) ?? have(rule);\n if (rule.max && n >= rule.max) {\n const details: ErrorDetails = { max: rule.max };\n out.refused.push({ file, error: new ContentKitError(\"too_many_files\", `${file.name}: at most ${rule.max} files`, { status: 409, details }) });\n continue;\n }\n counts.set(rule, n + 1);\n out.accepted.push({ file, rule });\n }\n return out;\n}\n\n/** name, or name-2, name-3… so its stem is not among taken (names or paths without the directory). */\nexport function uniqueName(name: string, taken: Iterable<string>): string {\n const stems = new Set([...taken].map(stem));\n if (!stems.has(stem(name))) return name;\n const dot = name.lastIndexOf(\".\");\n const base = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : \"\";\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}${ext}`;\n if (!stems.has(stem(candidate))) return candidate;\n }\n}\n\n/** Preserve numeric page/range identities; a collision must not invent a joined range. */\nexport function uniquePageName(name: string, taken: Iterable<string>): string {\n const key = (value: string) => {\n const base = stem(value);\n return /^\\d+(?:-\\d+)?$/.test(base) ? base.split(\"-\").map((n) => n.replace(/^0+/, \"\") || \"0\").join(\"-\") : base;\n };\n const names = new Set([...taken].map(key));\n if (!names.has(key(name))) return name;\n const dot = name.lastIndexOf(\".\");\n const base = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : \"\";\n for (let n = 2; ; n++) {\n const candidate = `${base} (${n})${ext}`;\n if (!names.has(key(candidate))) return candidate;\n }\n}\n\n/** \"16:9\" style label of a width/height ratio: 2.4 → \"2.4:1\", 0.4167 → \"1:2.4\". */\nexport function ratioLabel(r: number): string {\n const round = (v: number) => String(Math.round(v * 100) / 100);\n return r >= 1 ? `${round(r)}:1` : `1:${round(1 / r)}`;\n}\n\n/** The kind's public preset for an upload path: the one named name, else the first with an aspect, else the first. */\nexport function presetFor(presets: readonly PresetRule[], kind: string, path: string, name?: string): PresetRule | undefined {\n const at = presets.filter((p) => p.kind === kind && ruleFor([{ path: p.from }], path));\n return (name ? at.find((p) => p.name === name) : undefined) ?? at.find((p) => p.aspect) ?? at[0];\n}\n\n/**\n * The kind's default image for a preset, at every width: what the gateway\n * serves for an item without its own (a URL from the template never names a\n * current file). null when the preset has no default (`rule.default`).\n */\nexport function defaultImage(rule: PresetRule, id: string, version?: string): PublicPreset | null {\n if (!rule.default || rule.first || rule.to.includes(NAME)) return null;\n return logicalImage(rule, id, version);\n}\n\n/**\n * A Stable preset's image of an item (`rule.stable`, e.g. the content\n * model's `avatars`): URLs built from the id that show its current image,\n * else the default. null for any other preset.\n */\nexport function stableImage(rule: PresetRule, id: string): PublicPreset | null {\n return rule.stable ? logicalImage(rule, id) : null;\n}\n\nfunction logicalImage(rule: PresetRule, id: string, version?: string): PublicPreset {\n const r = ratio(rule.aspect);\n return {\n preset: rule.name,\n aspect: rule.aspect,\n renditions: rule.widths.map((w) => ({ url: publicURL(rule.base, rule.namespace, rule.kind, id, fill(rule.to, { w }), version, rule.root), w, ...(r ? { h: Math.round(w / r) } : {}) })),\n };\n}\n"],"mappings":"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"}
@@ -1,11 +1,15 @@
1
- import { FileDownload, ForkBody, Pending, ReaderMeta, ReaderPlan } from './generated/wire.js';
1
+ import { FileDownload, ForkBody, PageReview, PageReviewPatch, Pending, ReaderMeta, ReaderPlan } from './generated/wire.js';
2
2
  import { Http } from './http.js';
3
3
  /** A version's answer while it is prepared (202). */
4
4
  export declare const isPending: (x: unknown) => x is Pending;
5
- /** Content versions: reader plans, reader metadata, forks and downloads. */
5
+ /** Content versions: page review, reader plans, reader metadata, forks and downloads. */
6
6
  export declare class VersionsClient {
7
7
  private readonly http;
8
8
  constructor(http: Http);
9
+ /** Editors: retained page sources, automatic decisions and manual overrides. */
10
+ pages(content: string, version: string, signal?: AbortSignal): Promise<PageReview>;
11
+ /** Editors: changes choices or order against the review's current revision. */
12
+ updatePages(content: string, version: string, patch: PageReviewPatch): Promise<PageReview>;
9
13
  /** The version's reader plan for this viewer; Pending while it has no current publication. */
10
14
  reader(content: string, version: string, signal?: AbortSignal): Promise<ReaderPlan | Pending>;
11
15
  /** Editors: replaces the version's reader metadata (anchors, audio cues, defaults). */
@@ -5,6 +5,32 @@ var n = (e) => !!e && typeof e == "object" && "status" in e && !("pages" in e) &
5
5
  constructor(e) {
6
6
  this.http = e;
7
7
  }
8
+ pages(t, n, r) {
9
+ return e(this.http, "GET", "/contents/{id}/versions/{version}/pages", {
10
+ params: {
11
+ id: t,
12
+ version: n
13
+ },
14
+ signal: r
15
+ });
16
+ }
17
+ async updatePages(t, n, r) {
18
+ let i = this.http.captureChanges(), a = await e(this.http, "PATCH", "/contents/{id}/versions/{version}/pages", {
19
+ params: {
20
+ id: t,
21
+ version: n
22
+ },
23
+ body: r
24
+ });
25
+ return i({
26
+ type: "content.changed",
27
+ id: t
28
+ }), i({
29
+ type: "reader.changed",
30
+ content: t,
31
+ version: n
32
+ }), a;
33
+ }
8
34
  reader(t, n, r) {
9
35
  return e(this.http, "GET", "/contents/{id}/versions/{version}/reader", {
10
36
  params: {
@@ -1 +1 @@
1
- {"version":3,"file":"versions.js","names":[],"sources":["../../src/client/versions.ts"],"sourcesContent":["import type { FileDownload, ForkBody, Pending, ReaderMeta, ReaderPlan } from \"./generated/wire.js\";\nimport type { Http } from \"./http.js\";\nimport { call, routeURL } from \"./route.js\";\n\n/** A version's answer while it is prepared (202). */\nexport const isPending = (x: unknown): x is Pending => !!x && typeof x === \"object\" && \"status\" in x && !(\"pages\" in x) && !(\"url\" in x);\n\n/** Content versions: reader plans, reader metadata, forks and downloads. */\nexport class VersionsClient {\n constructor(private readonly http: Http) {}\n\n /** The version's reader plan for this viewer; Pending while it has no current publication. */\n reader(content: string, version: string, signal?: AbortSignal): Promise<ReaderPlan | Pending> {\n return call(this.http, \"GET\", \"/contents/{id}/versions/{version}/reader\", { params: { id: content, version }, signal });\n }\n /** Editors: replaces the version's reader metadata (anchors, audio cues, defaults). */\n async setReader(content: string, version: string, meta: ReaderMeta): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"PUT\", \"/contents/{id}/versions/{version}/reader\", { params: { id: content, version }, body: meta });\n emit({ type: \"reader.changed\", content, version });\n }\n /** Editors: fills a version without media from a sibling, in a durable job. */\n fork(content: string, version: string, body: ForkBody): Promise<Pending> {\n return call(this.http, \"POST\", \"/contents/{id}/versions/{version}/fork\", { params: { id: content, version }, body });\n }\n /**\n * Grants the version's download, what its kind serves (the original, a\n * rendition, its pages as a zip), charged to the viewer's download quota:\n * `url` (absolute) serves it as an attachment until `expires_at`. Pending\n * while it is prepared (ask again).\n */\n async download(content: string, version: string, signal?: AbortSignal): Promise<FileDownload | Pending> {\n const url = routeURL(this.http, \"GET\", \"/contents/{id}/versions/{version}/download\", { params: { id: content, version } });\n const out = await this.http.request<FileDownload | Pending>(url, { signal });\n if (isPending(out)) return out;\n // A marked zip's URL is the archive route's, relative to this one.\n const base = new URL(url, globalThis.location?.href ?? \"http://localhost/\");\n return { ...out, url: new URL(out.url, base).toString() };\n }\n}\n"],"mappings":";;AAKA,IAAa,KAAa,MAA6B,CAAC,CAAC,KAAK,OAAO,KAAM,YAAY,YAAY,KAAK,EAAE,WAAW,MAAM,EAAE,SAAS,IAGzH,IAAb,MAA4B;CACG;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,OAAO,GAAiB,GAAiB,GAAqD;EAC5F,OAAO,EAAK,KAAK,MAAM,OAAO,4CAA4C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG;EAAO,CAAC;CACxH;CAEA,MAAM,UAAU,GAAiB,GAAiB,GAAiC;EACjF,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,OAAO,4CAA4C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG,MAAM;EAAK,CAAC,GACzH,EAAK;GAAE,MAAM;GAAkB;GAAS;EAAQ,CAAC;CACnD;CAEA,KAAK,GAAiB,GAAiB,GAAkC;EACvE,OAAO,EAAK,KAAK,MAAM,QAAQ,0CAA0C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG;EAAK,CAAC;CACrH;CAOA,MAAM,SAAS,GAAiB,GAAiB,GAAuD;EACtG,IAAM,IAAM,EAAS,KAAK,MAAM,OAAO,8CAA8C,EAAE,QAAQ;GAAE,IAAI;GAAS;EAAQ,EAAE,CAAC,GACnH,IAAM,MAAM,KAAK,KAAK,QAAgC,GAAK,EAAE,UAAO,CAAC;EAC3E,IAAI,EAAU,CAAG,GAAG,OAAO;EAE3B,IAAM,IAAO,IAAI,IAAI,GAAK,WAAW,UAAU,QAAQ,mBAAmB;EAC1E,OAAO;GAAE,GAAG;GAAK,KAAK,IAAI,IAAI,EAAI,KAAK,CAAI,CAAC,CAAC,SAAS;EAAE;CAC1D;AACF"}
1
+ {"version":3,"file":"versions.js","names":[],"sources":["../../src/client/versions.ts"],"sourcesContent":["import type { FileDownload, ForkBody, PageReview, PageReviewPatch, Pending, ReaderMeta, ReaderPlan } from \"./generated/wire.js\";\nimport type { Http } from \"./http.js\";\nimport { call, routeURL } from \"./route.js\";\n\n/** A version's answer while it is prepared (202). */\nexport const isPending = (x: unknown): x is Pending => !!x && typeof x === \"object\" && \"status\" in x && !(\"pages\" in x) && !(\"url\" in x);\n\n/** Content versions: page review, reader plans, reader metadata, forks and downloads. */\nexport class VersionsClient {\n constructor(private readonly http: Http) {}\n\n /** Editors: retained page sources, automatic decisions and manual overrides. */\n pages(content: string, version: string, signal?: AbortSignal): Promise<PageReview> {\n return call(this.http, \"GET\", \"/contents/{id}/versions/{version}/pages\", { params: { id: content, version }, signal });\n }\n /** Editors: changes choices or order against the review's current revision. */\n async updatePages(content: string, version: string, patch: PageReviewPatch): Promise<PageReview> {\n const emit = this.http.captureChanges();\n const next: PageReview = await call(this.http, \"PATCH\", \"/contents/{id}/versions/{version}/pages\", { params: { id: content, version }, body: patch });\n emit({ type: \"content.changed\", id: content });\n emit({ type: \"reader.changed\", content, version });\n return next;\n }\n /** The version's reader plan for this viewer; Pending while it has no current publication. */\n reader(content: string, version: string, signal?: AbortSignal): Promise<ReaderPlan | Pending> {\n return call(this.http, \"GET\", \"/contents/{id}/versions/{version}/reader\", { params: { id: content, version }, signal });\n }\n /** Editors: replaces the version's reader metadata (anchors, audio cues, defaults). */\n async setReader(content: string, version: string, meta: ReaderMeta): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"PUT\", \"/contents/{id}/versions/{version}/reader\", { params: { id: content, version }, body: meta });\n emit({ type: \"reader.changed\", content, version });\n }\n /** Editors: fills a version without media from a sibling, in a durable job. */\n fork(content: string, version: string, body: ForkBody): Promise<Pending> {\n return call(this.http, \"POST\", \"/contents/{id}/versions/{version}/fork\", { params: { id: content, version }, body });\n }\n /**\n * Grants the version's download, what its kind serves (the original, a\n * rendition, its pages as a zip), charged to the viewer's download quota:\n * `url` (absolute) serves it as an attachment until `expires_at`. Pending\n * while it is prepared (ask again).\n */\n async download(content: string, version: string, signal?: AbortSignal): Promise<FileDownload | Pending> {\n const url = routeURL(this.http, \"GET\", \"/contents/{id}/versions/{version}/download\", { params: { id: content, version } });\n const out = await this.http.request<FileDownload | Pending>(url, { signal });\n if (isPending(out)) return out;\n // A marked zip's URL is the archive route's, relative to this one.\n const base = new URL(url, globalThis.location?.href ?? \"http://localhost/\");\n return { ...out, url: new URL(out.url, base).toString() };\n }\n}\n"],"mappings":";;AAKA,IAAa,KAAa,MAA6B,CAAC,CAAC,KAAK,OAAO,KAAM,YAAY,YAAY,KAAK,EAAE,WAAW,MAAM,EAAE,SAAS,IAGzH,IAAb,MAA4B;CACG;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,MAAM,GAAiB,GAAiB,GAA2C;EACjF,OAAO,EAAK,KAAK,MAAM,OAAO,2CAA2C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG;EAAO,CAAC;CACvH;CAEA,MAAM,YAAY,GAAiB,GAAiB,GAA6C;EAC/F,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAmB,MAAM,EAAK,KAAK,MAAM,SAAS,2CAA2C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG,MAAM;EAAM,CAAC;EAGpJ,OAFA,EAAK;GAAE,MAAM;GAAmB,IAAI;EAAQ,CAAC,GAC7C,EAAK;GAAE,MAAM;GAAkB;GAAS;EAAQ,CAAC,GAC1C;CACT;CAEA,OAAO,GAAiB,GAAiB,GAAqD;EAC5F,OAAO,EAAK,KAAK,MAAM,OAAO,4CAA4C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG;EAAO,CAAC;CACxH;CAEA,MAAM,UAAU,GAAiB,GAAiB,GAAiC;EACjF,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,OAAO,4CAA4C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG,MAAM;EAAK,CAAC,GACzH,EAAK;GAAE,MAAM;GAAkB;GAAS;EAAQ,CAAC;CACnD;CAEA,KAAK,GAAiB,GAAiB,GAAkC;EACvE,OAAO,EAAK,KAAK,MAAM,QAAQ,0CAA0C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG;EAAK,CAAC;CACrH;CAOA,MAAM,SAAS,GAAiB,GAAiB,GAAuD;EACtG,IAAM,IAAM,EAAS,KAAK,MAAM,OAAO,8CAA8C,EAAE,QAAQ;GAAE,IAAI;GAAS;EAAQ,EAAE,CAAC,GACnH,IAAM,MAAM,KAAK,KAAK,QAAgC,GAAK,EAAE,UAAO,CAAC;EAC3E,IAAI,EAAU,CAAG,GAAG,OAAO;EAE3B,IAAM,IAAO,IAAI,IAAI,GAAK,WAAW,UAAU,QAAQ,mBAAmB;EAC1E,OAAO;GAAE,GAAG;GAAK,KAAK,IAAI,IAAI,EAAI,KAAK,CAAI,CAAC,CAAC,SAAS;EAAE;CAC1D;AACF"}
@@ -0,0 +1,13 @@
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;