@murumets-ee/media 0.71.0 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +1 -1
- package/dist/backfill-B1StTnxg.mjs +2 -0
- package/dist/backfill-B1StTnxg.mjs.map +1 -0
- package/dist/backfill-trigger-BFL1L7_j.mjs +2 -0
- package/dist/backfill-trigger-BFL1L7_j.mjs.map +1 -0
- package/dist/backlog-D-H5VUQA.mjs +2 -0
- package/dist/backlog-D-H5VUQA.mjs.map +1 -0
- package/dist/client.d.mts +65 -7
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/crop-editor.d.mts +61 -0
- package/dist/crop-editor.d.mts.map +1 -0
- package/dist/crop-editor.mjs +2 -0
- package/dist/crop-editor.mjs.map +1 -0
- package/dist/crop-rect-B-AJ2pWm.mjs +2 -0
- package/dist/crop-rect-B-AJ2pWm.mjs.map +1 -0
- package/dist/crop-rect-C-SbjoxK.d.mts +59 -0
- package/dist/crop-rect-C-SbjoxK.d.mts.map +1 -0
- package/dist/crop.d.mts +2 -1
- package/dist/crop.mjs +1 -1
- package/dist/definitions-LJpgrdxd.mjs +2 -0
- package/dist/definitions-LJpgrdxd.mjs.map +1 -0
- package/dist/deps-DaGjHO16.mjs +2 -0
- package/dist/deps-DaGjHO16.mjs.map +1 -0
- package/dist/derive-media-type-C38ZA_Cl.mjs +2 -0
- package/dist/derive-media-type-C38ZA_Cl.mjs.map +1 -0
- package/dist/en-Duj_Pp-O.mjs +2 -0
- package/dist/en-Duj_Pp-O.mjs.map +1 -0
- package/dist/entity-CsDdjKz6.mjs +2 -0
- package/dist/entity-CsDdjKz6.mjs.map +1 -0
- package/dist/et-BFdSZnb5.mjs +2 -0
- package/dist/et-BFdSZnb5.mjs.map +1 -0
- package/dist/generate-variants-Dpy4aT2o.mjs +2 -0
- package/dist/generate-variants-Dpy4aT2o.mjs.map +1 -0
- package/dist/i18n.mjs +1 -1
- package/dist/i18n.mjs.map +1 -1
- package/dist/image-styles-settings.d.mts +8 -1
- package/dist/image-styles-settings.d.mts.map +1 -1
- package/dist/image-styles-settings.mjs +1 -1
- package/dist/image-styles-settings.mjs.map +1 -1
- package/dist/image-styles.d.mts +35 -21
- package/dist/image-styles.d.mts.map +1 -1
- package/dist/image-styles.mjs +1 -2
- package/dist/image-styles.mjs.map +1 -1
- package/dist/index.d.mts +50 -42
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/media-config-DkbnXIjC.mjs +2 -0
- package/dist/media-config-DkbnXIjC.mjs.map +1 -0
- package/dist/owned-delete-Cn_KFFjS.mjs +2 -0
- package/dist/owned-delete-Cn_KFFjS.mjs.map +1 -0
- package/dist/picker.d.mts +1 -94
- package/dist/picker.d.mts.map +1 -1
- package/dist/picker.mjs +1 -2
- package/dist/picker.mjs.map +1 -1
- package/dist/plugin.d.mts +2 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/process-image-wQwiRhyk.mjs +2 -0
- package/dist/process-image-wQwiRhyk.mjs.map +1 -0
- package/dist/processing.d.mts +201 -7
- package/dist/processing.d.mts.map +1 -1
- package/dist/processing.mjs +1 -1
- package/dist/public-resolver.d.mts +80 -16
- package/dist/public-resolver.d.mts.map +1 -1
- package/dist/public-resolver.mjs +1 -1
- package/dist/public-resolver.mjs.map +1 -1
- package/dist/query-client.d.mts +1 -1
- package/dist/query-client.mjs +1 -1
- package/dist/regenerate-variants-RwV_AQSo.mjs +2 -0
- package/dist/regenerate-variants-RwV_AQSo.mjs.map +1 -0
- package/dist/register-CJKjNl2w.mjs +2 -0
- package/dist/register-CJKjNl2w.mjs.map +1 -0
- package/dist/resolve-image-styles-YExI329D.mjs +2 -0
- package/dist/resolve-image-styles-YExI329D.mjs.map +1 -0
- package/dist/routes-DaE0M8h8.mjs +2 -0
- package/dist/routes-DaE0M8h8.mjs.map +1 -0
- package/dist/ru-ojvshRJy.mjs +2 -0
- package/dist/ru-ojvshRJy.mjs.map +1 -0
- package/dist/schedule-JlvfUEVA.mjs +2 -0
- package/dist/schedule-JlvfUEVA.mjs.map +1 -0
- package/dist/shapes-BpOEJeLx.mjs +2 -0
- package/dist/shapes-BpOEJeLx.mjs.map +1 -0
- package/dist/shapes-C51rXSDJ.d.mts +90 -0
- package/dist/shapes-C51rXSDJ.d.mts.map +1 -0
- package/dist/shapes-CqppzFzb.mjs +2 -0
- package/dist/shapes-CqppzFzb.mjs.map +1 -0
- package/dist/slot-CY4isqmN.mjs +2 -0
- package/dist/slot-CY4isqmN.mjs.map +1 -0
- package/dist/{types-CnORVkLZ.d.mts → types-6PZQ4Zq6.d.mts} +82 -5
- package/dist/types-6PZQ4Zq6.d.mts.map +1 -0
- package/dist/types-Lj3SkZQ_.d.mts +107 -0
- package/dist/types-Lj3SkZQ_.d.mts.map +1 -0
- package/dist/variant-plan-86tJKtVi.mjs +2 -0
- package/dist/variant-plan-86tJKtVi.mjs.map +1 -0
- package/dist/variant-state-CBtqb9Cb.mjs +2 -0
- package/dist/variant-state-CBtqb9Cb.mjs.map +1 -0
- package/package.json +21 -8
- package/dist/client-CSOH7Zun.mjs +0 -2
- package/dist/client-CSOH7Zun.mjs.map +0 -1
- package/dist/en-0YnQ_Qz-.mjs +0 -2
- package/dist/en-0YnQ_Qz-.mjs.map +0 -1
- package/dist/entity-CNxAERYG.mjs +0 -2
- package/dist/entity-CNxAERYG.mjs.map +0 -1
- package/dist/entity-DKFCdQJT.mjs +0 -2
- package/dist/entity-DKFCdQJT.mjs.map +0 -1
- package/dist/et-BIxGYWVK.mjs +0 -2
- package/dist/et-BIxGYWVK.mjs.map +0 -1
- package/dist/image-styles-settings-DEnu-V2E.mjs +0 -2
- package/dist/image-styles-settings-DEnu-V2E.mjs.map +0 -1
- package/dist/plugin-B_ZtfiAc.mjs +0 -2
- package/dist/plugin-B_ZtfiAc.mjs.map +0 -1
- package/dist/process-image-WByEHTXQ.mjs +0 -2
- package/dist/process-image-WByEHTXQ.mjs.map +0 -1
- package/dist/regenerate-variants-2Lmo5kf8.mjs +0 -2
- package/dist/regenerate-variants-2Lmo5kf8.mjs.map +0 -1
- package/dist/regenerate-variants-B9AXXN6N.mjs +0 -2
- package/dist/regenerate-variants-B9AXXN6N.mjs.map +0 -1
- package/dist/resolve-image-styles-Cakg1Qpp.mjs +0 -2
- package/dist/resolve-image-styles-Cakg1Qpp.mjs.map +0 -1
- package/dist/resolve-image-styles-iN9JbZYf.mjs +0 -2
- package/dist/resolve-image-styles-iN9JbZYf.mjs.map +0 -1
- package/dist/routes-C7Fx-055.mjs +0 -2
- package/dist/routes-C7Fx-055.mjs.map +0 -1
- package/dist/ru-UUTBfsMF.mjs +0 -2
- package/dist/ru-UUTBfsMF.mjs.map +0 -1
- package/dist/types-CnORVkLZ.d.mts.map +0 -1
- package/dist/usage-CVAqkS6h.mjs +0 -2
- package/dist/usage-CVAqkS6h.mjs.map +0 -1
- package/dist/variant-key-JBTJXPL1.mjs +0 -2
- package/dist/variant-key-JBTJXPL1.mjs.map +0 -1
- package/dist/variant-key-RKke2RfS.mjs +0 -2
- package/dist/variant-key-RKke2RfS.mjs.map +0 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { r as FractionalRect } from "./focal-point-C_0drV03.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/crop/crop-rect.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Is `value` a storable crop rectangle — exactly `{x, y, w, h}`, fractional,
|
|
6
|
+
* non-degenerate, and inside the unit square?
|
|
7
|
+
*
|
|
8
|
+
* STRICT, because it guards a WRITE (a `crops[shape]` entry). Mirrors
|
|
9
|
+
* `isFocalPoint`'s reasoning: an extra key is refused rather than stripped.
|
|
10
|
+
*/
|
|
11
|
+
declare function isFractionalRect(value: unknown): value is FractionalRect;
|
|
12
|
+
/**
|
|
13
|
+
* Is `value` a storable `crops` map — every value a {@link isFractionalRect},
|
|
14
|
+
* keyed by shape name?
|
|
15
|
+
*
|
|
16
|
+
* Shape names are NOT validated against the configured vocabulary here: that
|
|
17
|
+
* vocabulary is runtime operator data (`media.imageStyles`, C7) that can be
|
|
18
|
+
* renamed or removed after a crop was saved against it. A stale key is inert
|
|
19
|
+
* — nothing reads a shape name absent from the current vocabulary — so
|
|
20
|
+
* refusing it here would only make renaming a style destroy data it need not
|
|
21
|
+
* touch.
|
|
22
|
+
*/
|
|
23
|
+
declare function isCropsRecord(value: unknown): value is Record<string, FractionalRect>;
|
|
24
|
+
/**
|
|
25
|
+
* The `crops` map, LENIENT — a read path (images PR04's variant-key
|
|
26
|
+
* fingerprint, or any future consumer). Drops any entry that isn't a valid
|
|
27
|
+
* {@link isFractionalRect} rather than refusing the whole value — a render
|
|
28
|
+
* must not fail over one bad shape (mirrors `readFocalPoint`'s reasoning).
|
|
29
|
+
*
|
|
30
|
+
* Returns a `Map`, not a plain object, on purpose: a shape literally named
|
|
31
|
+
* `"__proto__"` or `"constructor"` is a legal `field.json()` key today (shape
|
|
32
|
+
* names are operator data, not a fixed enum — C7), and building a plain
|
|
33
|
+
* accumulator with `result[shape] = …` from an attacker- or
|
|
34
|
+
* accident-supplied shape name is exactly the classic prototype-pollution
|
|
35
|
+
* shape. A `Map`'s keys never touch `Object.prototype`, so the question
|
|
36
|
+
* cannot come up for any caller that reads through this rather than
|
|
37
|
+
* re-deriving its own `Object.entries` walk.
|
|
38
|
+
*/
|
|
39
|
+
declare function readCrops(value: unknown): ReadonlyMap<string, FractionalRect>;
|
|
40
|
+
/**
|
|
41
|
+
* Keep a hand-edited rect inside the unit square.
|
|
42
|
+
*
|
|
43
|
+
* `pinned` names the field the caller just typed. On ITS axis, the pinned
|
|
44
|
+
* field wins and its sibling (the extent, if position was pinned; the
|
|
45
|
+
* position, if extent was pinned) yields — so typing `x` slides the window
|
|
46
|
+
* rather than eating the `w` the editor deliberately set, and typing `w`
|
|
47
|
+
* grows/shrinks in place rather than silently moving `x` — EXCEPT in the
|
|
48
|
+
* narrow band where `position > 1 - MIN_EXTENT`, where position must yield by
|
|
49
|
+
* the minimum needed to leave room for a non-degenerate window; see
|
|
50
|
+
* {@link resolveAxis}. The other axis is untouched by this edit and is only
|
|
51
|
+
* clamped defensively (position kept, extent shrunk to fit — the same rule a
|
|
52
|
+
* position-pinned edit uses on its own axis). Used by the crop editor's
|
|
53
|
+
* numeric fallback inputs (F015): each field commits through this rather
|
|
54
|
+
* than the raw value.
|
|
55
|
+
*/
|
|
56
|
+
declare function clampFractionalRect(rect: FractionalRect, pinned: 'x' | 'y' | 'w' | 'h'): FractionalRect;
|
|
57
|
+
//#endregion
|
|
58
|
+
export { readCrops as i, isCropsRecord as n, isFractionalRect as r, clampFractionalRect as t };
|
|
59
|
+
//# sourceMappingURL=crop-rect-C-SbjoxK.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crop-rect-C-SbjoxK.d.mts","names":[],"sources":["../src/crop/crop-rect.ts"],"mappings":";;;;;;AAqG6E;AA2D7E;;;iBA9GgB,gBAAA,CAAiB,KAAA,YAAiB,KAAA,IAAS,cAAc;;;;;;AAiHxD;;;;;;iBAvFD,aAAA,CAAc,KAAA,YAAiB,KAAA,IAAS,MAAM,SAAS,cAAA;;;;;;;;;;;;;;;;iBAyBvD,SAAA,CAAU,KAAA,YAAiB,WAAW,SAAS,cAAA;;;;;;;;;;;;;;;;;iBA2D/C,mBAAA,CACd,IAAA,EAAM,cAAA,EACN,MAAA,0BACC,cAAc"}
|
package/dist/crop.d.mts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { a as formatObjectPosition, c as objectPositionFor, i as focalCropRect, l as readFocalPoint, n as FocalPoint, o as isCentreFocalPoint, r as FractionalRect, s as isFocalPoint, t as CENTRE_FOCAL_POINT, u as visibleWindow } from "./focal-point-C_0drV03.mjs";
|
|
2
|
-
|
|
2
|
+
import { i as readCrops, n as isCropsRecord, r as isFractionalRect, t as clampFractionalRect } from "./crop-rect-C-SbjoxK.mjs";
|
|
3
|
+
export { CENTRE_FOCAL_POINT, type FocalPoint, type FractionalRect, clampFractionalRect, focalCropRect, formatObjectPosition, isCentreFocalPoint, isCropsRecord, isFocalPoint, isFractionalRect, objectPositionFor, readCrops, readFocalPoint, visibleWindow };
|
package/dist/crop.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{i as e,n as t,r as n,t as r}from"./crop-rect-B-AJ2pWm.mjs";import{a as i,c as a,i as o,n as s,o as c,r as l,s as u,t as d}from"./focal-point-BWhtJzef.mjs";export{d as CENTRE_FOCAL_POINT,r as clampFractionalRect,s as focalCropRect,l as formatObjectPosition,o as isCentreFocalPoint,t as isCropsRecord,i as isFocalPoint,n as isFractionalRect,c as objectPositionFor,e as readCrops,u as readFocalPoint,a as visibleWindow};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{z as e}from"zod";const t=`@murumets-ee/queue`,n=`media:generate-variants`,r=`media:variants-backfill`,i=e.object({mediaId:e.string().uuid(),epoch:e.string().regex(/^[a-z0-9-]{1,64}$/)}),a=e.object({}).strict();function o(e){return`media-variants:${e.mediaId}:${e.epoch}`}function s(e){let t=e instanceof Date?e.getTime():typeof e==`string`?Date.parse(e):NaN;return Number.isFinite(t)?`e${t}`:null}function c(e){return`w${e.toLowerCase()}`}const l=`upload`;export{c as a,i as c,r as i,a as l,t as n,s as o,l as r,o as s,n as t};
|
|
2
|
+
//# sourceMappingURL=definitions-LJpgrdxd.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions-LJpgrdxd.mjs","names":[],"sources":["../src/jobs/definitions.ts"],"sourcesContent":["/**\n * The media queue jobs' names, payloads and idempotency keys — pure, so the\n * upload path, the backfill and the tests share one definition of each.\n *\n * Nothing here imports `@murumets-ee/queue`: the `defineJob` handles are built\n * in `register.ts`, behind a dynamic import, so an app without the queue plugin\n * (`apps/web-astro`) never loads it.\n */\n\nimport { z } from 'zod'\n\n/** The queue plugin's name — media degrades to inline generation without it. */\nexport const QUEUE_PLUGIN_NAME = '@murumets-ee/queue'\n\n/** One generation pass for one photograph. `pluginName:action`. */\nexport const GENERATE_VARIANTS_JOB_NAME = 'media:generate-variants'\n\n/** Walks the library and enqueues a pass for every photograph that needs one. */\nexport const VARIANTS_BACKFILL_JOB_NAME = 'media:variants-backfill'\n\nexport const generateVariantsPayloadSchema = z.object({\n mediaId: z.string().uuid(),\n /**\n * Which REQUEST for a pass this is: `upload`, `e<ms>` for an edit (the row's\n * `updatedAt`), or `w<backfill job id>` for one walk of the library. See\n * {@link generateVariantsJobKey} for why the key needs it.\n */\n epoch: z.string().regex(/^[a-z0-9-]{1,64}$/),\n})\nexport type GenerateVariantsPayload = z.infer<typeof generateVariantsPayloadSchema>\n\n/** No parameters: the backfill always walks the whole (capped) image library. */\nexport const variantsBackfillPayloadSchema = z.object({}).strict()\nexport type VariantsBackfillPayload = z.infer<typeof variantsBackfillPayloadSchema>\n\n/**\n * `media-variants:<mediaId>:<epoch>` — deterministic in the payload, no PII (a\n * UUID and a timestamp or UUID), at most 116 characters against the queue's 255.\n *\n * ⚠️ Why not a key derived from WHAT the pass produces — the plan asked for\n * `media-variants:<mediaId>:<shape>:<rung>:<fingerprint>`: the queue's key is\n * HISTORICAL. The worker records it on success and skips any later job carrying\n * it for 30 days (`toolkit_idempotency_keys`). A photograph's target state can\n * recur — focal point A, then B, then back to A — and the B pass deletes A's\n * files as superseded. A content-derived key would then skip the return to A as\n * \"already done\", leaving the photograph pointing at B's crop with A's files\n * gone (images F018). So the key names the REQUEST: one upload, one edit, one\n * walk of the library. What makes a pass cheap when there is nothing to do is\n * the pass itself — it renders only keys that are not already recorded.\n *\n * What the key still buys: a walk retried after a crash re-queues nothing it\n * already queued, and a pass retried after a partial failure is not run twice\n * once one attempt has succeeded.\n */\nexport function generateVariantsJobKey(payload: GenerateVariantsPayload): string {\n return `media-variants:${payload.mediaId}:${payload.epoch}`\n}\n\n/** The epoch of an edit: the updated row's `updatedAt`, as `e<ms>`. */\nexport function editEpoch(updatedAt: unknown): string | null {\n const ms =\n updatedAt instanceof Date\n ? updatedAt.getTime()\n : typeof updatedAt === 'string'\n ? Date.parse(updatedAt)\n : Number.NaN\n return Number.isFinite(ms) ? `e${ms}` : null\n}\n\n/** The epoch of one backfill walk: `w<its job id>`. */\nexport function backfillEpoch(jobId: string): string {\n return `w${jobId.toLowerCase()}`\n}\n\n/** A freshly uploaded photograph's one pass. */\nexport const UPLOAD_EPOCH = 'upload'\n"],"mappings":"wBAYA,MAAa,EAAoB,qBAGpB,EAA6B,0BAG7B,EAA6B,0BAE7B,EAAgC,EAAE,OAAO,CACpD,QAAS,EAAE,OAAO,CAAC,CAAC,KAAK,EAMzB,MAAO,EAAE,OAAO,CAAC,CAAC,MAAM,mBAAmB,CAC7C,CAAC,EAIY,EAAgC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,EAsBjE,SAAgB,EAAuB,EAA0C,CAC/E,MAAO,kBAAkB,EAAQ,QAAQ,GAAG,EAAQ,OACtD,CAGA,SAAgB,EAAU,EAAmC,CAC3D,IAAM,EACJ,aAAqB,KACjB,EAAU,QAAQ,EAClB,OAAO,GAAc,SACnB,KAAK,MAAM,CAAS,EACpB,IACR,OAAO,OAAO,SAAS,CAAE,EAAI,IAAI,IAAO,IAC1C,CAGA,SAAgB,EAAc,EAAuB,CACnD,MAAO,IAAI,EAAM,YAAY,GAC/B,CAGA,MAAa,EAAe"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"./entity-CsDdjKz6.mjs";import{n as t,t as n}from"./resolve-image-styles-YExI329D.mjs";async function r(e,r){let[i,a]=await Promise.all([t(e,e.logger,{strict:!0}),n(e,e.logger,{strict:!0})]);return{media:r.media,storage:r.storage,config:i,styles:a,logger:e.logger}}async function i(t){let n=await o(t);return r(t,{media:t.getClient(e),storage:n})}let a=null;function o(e){return a??=(async()=>{let{createStorageClient:t}=await import(`@murumets-ee/storage`),{getStorageConfig:n}=await import(`@murumets-ee/storage/plugin`);return t(n(),{app:e})})(),a.catch(()=>{a=null}),a}export{i as createJobVariantDeps,r as createVariantDeps};
|
|
2
|
+
//# sourceMappingURL=deps-DaGjHO16.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deps-DaGjHO16.mjs","names":[],"sources":["../src/jobs/deps.ts"],"sourcesContent":["/**\n * Building a {@link VariantGenerationDeps} — the configuration a pass needs and\n * the clients it reads and writes through.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport type { StorageClient } from '@murumets-ee/storage'\nimport { Media } from '../entity.js'\nimport { resolveImageStyles, resolveVariantConfig } from '../resolve-image-styles.js'\nimport type { VariantGenerationDeps } from './generate-variants.js'\n\n/** The media-row slice of the deps: an `AdminClient<Media>` satisfies it. */\nexport type VariantMediaAccess = VariantGenerationDeps['media']\n\n/**\n * Deps for a pass run with the CALLER's clients — the inline fallback inside an\n * upload or regenerate request, which already passed that route's permission\n * gate. The configuration is read fresh (it is two settings reads), strictly.\n */\nexport async function createVariantDeps(\n app: ToolkitApp,\n clients: { media: VariantMediaAccess; storage: VariantGenerationDeps['storage'] },\n): Promise<VariantGenerationDeps> {\n // STRICT: a pass deletes what the configuration no longer names, so a failed\n // settings read must fail the pass (the queue retries), never fall back to\n // the code defaults and delete the operator's shapes.\n const [config, styles] = await Promise.all([\n resolveVariantConfig(app, app.logger, { strict: true }),\n resolveImageStyles(app, app.logger, { strict: true }),\n ])\n return { media: clients.media, storage: clients.storage, config, styles, logger: app.logger }\n}\n\n/**\n * Deps for a pass run by the worker — call INSIDE `runAsCli`, so the\n * `AdminClient` binds to that elevated context.\n */\nexport async function createJobVariantDeps(app: ToolkitApp): Promise<VariantGenerationDeps> {\n const storage = await sharedStorage(app)\n return createVariantDeps(app, { media: app.getClient(Media), storage })\n}\n\n// One per worker process: a `StorageClient` carries no security context (the\n// gate is in whichever client reads the media row), so it is safe to share.\nlet storagePromise: Promise<StorageClient> | null = null\n\nfunction sharedStorage(app: ToolkitApp): Promise<StorageClient> {\n storagePromise ??= (async () => {\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n return createStorageClient(getStorageConfig(), { app })\n })()\n // A failed init is not pinned — the next pass tries again.\n storagePromise.catch(() => {\n storagePromise = null\n })\n return storagePromise\n}\n"],"mappings":"yGAmBA,eAAsB,EACpB,EACA,EACgC,CAIhC,GAAM,CAAC,EAAQ,GAAU,MAAM,QAAQ,IAAI,CACzC,EAAqB,EAAK,EAAI,OAAQ,CAAE,OAAQ,EAAK,CAAC,EACtD,EAAmB,EAAK,EAAI,OAAQ,CAAE,OAAQ,EAAK,CAAC,CACtD,CAAC,EACD,MAAO,CAAE,MAAO,EAAQ,MAAO,QAAS,EAAQ,QAAS,SAAQ,SAAQ,OAAQ,EAAI,MAAO,CAC9F,CAMA,eAAsB,EAAqB,EAAiD,CAC1F,IAAM,EAAU,MAAM,EAAc,CAAG,EACvC,OAAO,EAAkB,EAAK,CAAE,MAAO,EAAI,UAAU,CAAK,EAAG,SAAQ,CAAC,CACxE,CAIA,IAAI,EAAgD,KAEpD,SAAS,EAAc,EAAyC,CAU9D,MATA,MAAoB,SAAY,CAC9B,GAAM,CAAE,uBAAwB,MAAM,OAAO,wBACvC,CAAE,oBAAqB,MAAM,OAAO,+BAC1C,OAAO,EAAoB,EAAiB,EAAG,CAAE,KAAI,CAAC,CACxD,EAAA,CAAG,EAEH,EAAe,UAAY,CACzB,EAAiB,IACnB,CAAC,EACM,CACT"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e){return e.startsWith(`image/`)?`image`:e.startsWith(`video/`)?`video`:e.startsWith(`audio/`)?`audio`:e===`application/pdf`||e.startsWith(`application/msword`)||e.startsWith(`application/vnd.`)?`document`:`other`}export{e as t};
|
|
2
|
+
//# sourceMappingURL=derive-media-type-C38ZA_Cl.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive-media-type-C38ZA_Cl.mjs","names":[],"sources":["../src/derive-media-type.ts"],"sourcesContent":["import type { MediaType } from './types.js'\n\n/**\n * Classify a MIME type into the coarse `MediaType` an upload is stored under.\n *\n * Pure and dependency-free, and in its own module on purpose: `client.ts`\n * (`import 'server-only'`) needs it to set `mediaType` at upload, and the admin\n * list's MIME-type cell (`picker/media-type-icon.ts`, a CLIENT entry) needs the\n * same answer to pick an icon. One rule in one file means the icon a row shows\n * cannot disagree with the type it was stored as — which two copies would, the\n * first time someone taught one of them about a new document type.\n */\nexport function deriveMediaType(mimeType: string): MediaType {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (\n mimeType === 'application/pdf' ||\n mimeType.startsWith('application/msword') ||\n mimeType.startsWith('application/vnd.')\n ) {\n return 'document'\n }\n return 'other'\n}\n"],"mappings":"AAYA,SAAgB,EAAgB,EAA6B,CAW3D,OAVI,EAAS,WAAW,QAAQ,EAAU,QACtC,EAAS,WAAW,QAAQ,EAAU,QACtC,EAAS,WAAW,QAAQ,EAAU,QAExC,IAAa,mBACb,EAAS,WAAW,oBAAoB,GACxC,EAAS,WAAW,kBAAkB,EAE/B,WAEF,OACT"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";var t=e({Media:()=>r,MediaCropEditor:()=>i,PluginNav:()=>n,default:()=>a}),n={media:`Media`},r={imageVocabulary:{description:`Every photograph is generated in each shape below, at each width. A slot whose ratio matches a shape draws that shape’s files; a slot with any other ratio is cropped in the browser instead.`,usingDefaults:`Nothing is saved here yet. Shown are the toolkit defaults; until you save, the site uses them — or the shapes configured in its code, if it has any.`,widthsLabel:`Widths (px)`,widthsHelp:`Ascending, separated by commas. A photograph is never enlarged — a width it cannot supply is skipped.`,widthsInvalid:`Use up to {count} ascending whole numbers between {min} and {max}.`,shapesLabel:`Shapes`,naturalNote:`Plus “natural” — each photograph at its own shape, over the same widths.`,columnName:`Name`,columnRatio:`Ratio (W:H)`,columnWidths:`Own widths`,ownWidthsPlaceholder:`All widths`,ratioInvalid:`Write the ratio as W:H, for example 3:2.`,nameInvalid:`Lowercase letters, digits, - and _, starting with a letter — and not “natural”.`,nameTaken:`A shape named “{name}” already exists.`,tooMany:`At most {count} shapes.`,add:`Add shape`,remove:`Remove {name}`,tooManyFiles:`That makes {count} files for every photograph; the most is {max}. Remove a shape or a width first.`},variantBacklog:{waiting:`Images waiting for their sizes: {count}`,workerRunning:`The queue worker is generating them. Until it finishes, those images are shown at full size.`,workerMissing:`No queue worker is running, so these images are shown at full size — slower pages, especially on phones. Image sizes are made by the queue worker, a separate process that has to run beside the site (pnpm worker). Ask whoever hosts this site to start it.`,waitingSince:`The oldest was queued {since}.`,failed:`Size generation failed {count} times and was given up on — the Queue page shows why.`,catchUp:`A catch-up of the whole library is queued: every image is checked against the current shapes and styles.`,catchUpWorkerMissing:`No queue worker is running, so it has not started. Image sizes are made by the queue worker, a separate process that has to run beside the site (pnpm worker). Ask whoever hosts this site to start it.`,openQueue:`Open the queue`},variantGeneration:{workerRequired:`Image sizes are generated in the background by the queue worker — a separate process that has to run beside the site (pnpm worker). Without it, the site still works, but new and changed images are shown at full size.`,regenerate:`Catch up all images`,regenerating:`Starting…`,queued:`Started. The queue worker catches the library up in the background; the site keeps serving meanwhile. Follow it on the Queue page.`,alreadyRunning:`A catch-up is already running — follow it on the Queue page.`,inlineDone:`Processed {processed} of {total}; skipped {skipped}; errors {errors}.`,failed:`Could not start the catch-up: {reason}`}},i={tabListLabel:`Crop shapes`,description:`Drag to position the photograph, scroll or pinch to zoom. Every slot using this shape will show this crop.`,xLabel:`Left (%)`,yLabel:`Top (%)`,widthLabel:`Width (%)`,heightLabel:`Height (%)`,clearShape:`Clear this crop`,noShapes:`No configured image style has a fixed ratio yet, so there is nothing to crop against.`,hasCropSuffix:`— custom crop saved`},a={PluginNav:n,Media:r,MediaCropEditor:i};export{t as n,a as t};
|
|
2
|
+
//# sourceMappingURL=en-Duj_Pp-O.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"en-Duj_Pp-O.mjs","names":[],"sources":["../src/messages/en.json"],"sourcesContent":[""],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";import{n as t}from"./crop-rect-B-AJ2pWm.mjs";import{a as n}from"./focal-point-BWhtJzef.mjs";import{o as r}from"./definitions-LJpgrdxd.mjs";import{t as i}from"./slot-CY4isqmN.mjs";import{behavior as a,defineEntity as o,field as s,searchable as c}from"@murumets-ee/entity";import{z as l}from"zod";function u(e){if(!(`crops`in e))return e;let n=e.crops;if(n==null||t(n))return e;throw new l.ZodError([{code:`custom`,path:[`crops`],message:`crops must be a map of shape name to { x, y, w, h } fractions in [0, 1], or null.`}])}function d(e){if(e.keys.includes(`crops`))throw new l.ZodError([{code:`custom`,path:[`crops`],message:`bulk update on '${e.entityName}' cannot write 'crops': updateMany runs no per-row hooks, so the value would bypass its shape check. Update each row individually.`}])}function f(){return{name:`cropsValidation`,hooks:{beforeCreate:async e=>u(e),beforeUpdate:async(e,t)=>u(t)},assertBulkUpdate:d}}function p(e){if(!(`focalPoint`in e))return e;let t=e.focalPoint;if(t==null||n(t))return e;throw new l.ZodError([{code:`custom`,path:[`focalPoint`],message:`focalPoint must be { x, y } with both between 0 and 1, or null.`}])}function m(e){if(e.keys.includes(`focalPoint`))throw new l.ZodError([{code:`custom`,path:[`focalPoint`],message:`bulk update on '${e.entityName}' cannot write 'focalPoint': updateMany runs no per-row hooks, so the value would bypass its shape check. Update each row individually.`}])}function h(){return{name:`focalPointValidation`,hooks:{beforeCreate:async e=>p(e),beforeUpdate:async(e,t)=>p(t)},assertBulkUpdate:m}}function g(e,t){return JSON.stringify(e??null)===JSON.stringify(t??null)}function _(){return{name:`variantRegeneration`,hooks:{afterUpdate:async(e,t)=>{if(!t||t.viaInternal||!t.enqueueOnCommit||!t.loadCurrent||e.mediaType!==`image`||typeof e.id!=`string`)return;let n=i();if(!n)return;let a=await t.loadCurrent();if(!a||g(a.focalPoint,e.focalPoint)&&g(a.crops,e.crops))return;let o=r(e.updatedAt);o&&t.enqueueOnCommit(n.generateVariants,{mediaId:e.id,epoch:o})}}}}var v=e({Media:()=>y});const y=o({name:`media`,fields:{title:s.text({translatable:!0}),alt:s.text({translatable:!0}),description:s.text({translatable:!0}),fileKey:s.text({required:!0,indexed:!0}),filename:s.text({required:!0}),mimeType:s.text({required:!0,indexed:!0,admin:{display:{type:`mimeType`}}}),size:s.number({required:!0,integer:!0,admin:{display:{type:`filesize`}}}),width:s.number({integer:!0}),height:s.number({integer:!0}),focalPoint:s.json(),crops:s.json(),mediaType:s.select({options:[`image`,`video`,`audio`,`document`,`other`],required:!0,indexed:!0})},behaviors:[a.auditable(),h(),f(),_(),c({fields:[`filename`,`fileKey`,`mimeType`,`title`,`alt`,`description`],fts:{fields:[`filename`,`title`,`alt`,`description`],language:`simple`},projection:e=>({id:e.id,label:e.title??e.filename,description:e.mimeType})})],admin:{list:{fields:[`title`,`mimeType`,`size`,`createdAt`],layouts:[`table`,`grid`]}},scope:`global`,access:{view:`public`,create:`group.editor`,update:`group.editor`,delete:`group.admin`}});export{v as n,y as t};
|
|
2
|
+
//# sourceMappingURL=entity-CsDdjKz6.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-CsDdjKz6.mjs","names":[],"sources":["../src/crop/crops-validation.ts","../src/crop/focal-point-validation.ts","../src/jobs/regenerate-on-edit.ts","../src/entity.ts"],"sourcesContent":["/**\n * `cropsValidation()` — the write guard on `media.crops`.\n *\n * Same reasoning as `focalPointValidation()`, one column over: `field.json()`\n * accepts any JSON, so without this hook an editor's PATCH could park\n * anything in `crops` — a bad shape name's worth of megabyte string, a\n * malformed rect — and every shaped slot resolving that shape would carry it\n * (images D002).\n */\n\nimport type { Behavior, BulkUpdateInfo } from '@murumets-ee/entity'\nimport { z } from 'zod'\nimport { isCropsRecord } from './crop-rect.js'\n\n/**\n * `null` is a legitimate value — no shape has an override, so every shape\n * falls back to the focal point (D002's \"a missing crop is a fallback, never\n * a hole\"). Anything else must be a map of valid rects.\n */\nfunction validateCrops(data: Record<string, unknown>): Record<string, unknown> {\n // A payload without the key is a partial update that does not touch the\n // column — see `validateFocalPoint` for why defaulting it here would be wrong.\n if (!('crops' in data)) return data\n const value = data.crops\n if (value === null || value === undefined || isCropsRecord(value)) return data\n throw new z.ZodError([\n {\n code: 'custom',\n path: ['crops'],\n message: 'crops must be a map of shape name to { x, y, w, h } fractions in [0, 1], or null.',\n },\n ])\n}\n\n/** Same reasoning as `assertNoBulkFocalPointWrite` — `updateMany` runs no per-row hooks. */\nfunction assertNoBulkCropsWrite(info: BulkUpdateInfo): void {\n if (!info.keys.includes('crops')) return\n throw new z.ZodError([\n {\n code: 'custom',\n path: ['crops'],\n message:\n `bulk update on '${info.entityName}' cannot write 'crops': updateMany runs no per-row ` +\n 'hooks, so the value would bypass its shape check. Update each row individually.',\n },\n ])\n}\n\n/** Contributes NO fields — see `focalPointValidation`'s note on `Record<never, never>`. */\nexport function cropsValidation(): Behavior<Record<never, never>, 'cropsValidation'> {\n return {\n name: 'cropsValidation',\n hooks: {\n beforeCreate: async (data) => validateCrops(data),\n beforeUpdate: async (_id, data) => validateCrops(data),\n },\n assertBulkUpdate: assertNoBulkCropsWrite,\n }\n}\n","/**\n * `focalPointValidation()` — the write guard on `media.focalPoint`.\n *\n * `field.json()` validates as the permissive JSON union on purpose, so without\n * this hook an editor's PATCH could park any shape in the column — a megabyte\n * string, a nested object — and every public page drawing the photograph would\n * carry it, because the projection hands the value to the render. CLAUDE.md's\n * \"validate at the boundary\", on the one column this package adds.\n *\n * Errors are `ZodError`s, so the admin API answers 400 with an issue pathed at\n * `focalPoint` rather than a 500 — the contract `menuEntityTypesValidation()`\n * already relies on.\n */\n\nimport type { Behavior, BulkUpdateInfo } from '@murumets-ee/entity'\nimport { z } from 'zod'\nimport { isFocalPoint } from './focal-point.js'\n\n/**\n * `null` is a legitimate value — it means \"no focal point\", which renders as the\n * centre (D010). Anything else must be exactly `{x, y}` in `[0, 1]`.\n */\nfunction validateFocalPoint(data: Record<string, unknown>): Record<string, unknown> {\n // A payload without the key is a partial update that does not touch the\n // column. Defaulting it here would let a title-only PATCH reset the point.\n if (!('focalPoint' in data)) return data\n const value = data.focalPoint\n if (value === null || value === undefined || isFocalPoint(value)) return data\n throw new z.ZodError([\n {\n code: 'custom',\n path: ['focalPoint'],\n message: 'focalPoint must be { x, y } with both between 0 and 1, or null.',\n },\n ])\n}\n\n/**\n * `updateMany` runs no per-row hooks, so `beforeUpdate` never sees a bulk write.\n * Refused rather than validated: `BulkUpdateInfo` is deliberately shape-only (it\n * names the columns, never the values), and \"every photograph has the same\n * subject position\" is not an intent anyone has. A migration is the tool for a\n * one-off backfill.\n */\nfunction assertNoBulkFocalPointWrite(info: BulkUpdateInfo): void {\n if (!info.keys.includes('focalPoint')) return\n throw new z.ZodError([\n {\n code: 'custom',\n path: ['focalPoint'],\n message:\n `bulk update on '${info.entityName}' cannot write 'focalPoint': updateMany runs no ` +\n 'per-row hooks, so the value would bypass its shape check. Update each row individually.',\n },\n ])\n}\n\n/**\n * Contributes NO fields — the empty map `Record<never, never>`, not\n * `Record<string, never>`, which would merge into the entity's field map and\n * collapse every field to `never`.\n */\nexport function focalPointValidation(): Behavior<Record<never, never>, 'focalPointValidation'> {\n return {\n name: 'focalPointValidation',\n hooks: {\n beforeCreate: async (data) => validateFocalPoint(data),\n beforeUpdate: async (_id, data) => validateFocalPoint(data),\n },\n assertBulkUpdate: assertNoBulkFocalPointWrite,\n }\n}\n","/**\n * `variantRegeneration()` — when an editor moves a photograph's focal point (or,\n * with images PR03's crop editor, redraws a crop), queue a pass for it.\n *\n * The shaped variants are cut around the point and the crop (`planVariants`),\n * and their keys carry a fingerprint of the cut (D005). So an edit changes WHICH\n * files the photograph should have; without a pass, the recorded set would keep\n * describing the old cut while the page's `object-position` follows the new one.\n *\n * Skipped when:\n * - the write came through `updateInternal` (`ctx.viaInternal`) — that is the\n * pass itself writing the drawn size or a seeded point back, and it has\n * already planned with them, so a hook here would only queue a duplicate;\n * - neither `focalPoint` nor `crops` changed — a title or alt edit changes no\n * pixels;\n * - the app has no queue (`ctx.enqueueOnCommit` absent, or no registered job):\n * an inline pass on every PATCH is the stall this plan removes. The\n * regenerate button catches such an app up.\n *\n * Enqueued through `ctx.enqueueOnCommit`, so the job row commits (or rolls\n * back) with the update. Its epoch is the updated row's `updatedAt`, so a\n * return to an earlier point is a new request, not a skipped repeat (F018).\n *\n * Imports nothing heavier than the slot: this module is reachable from\n * `entity.ts`, which client code imports for `toEntityMeta(Media)`.\n */\n\nimport type { Behavior } from '@murumets-ee/entity'\nimport { editEpoch } from './definitions.js'\nimport { getMediaJobs } from './slot.js'\n\nfunction sameJson(a: unknown, b: unknown): boolean {\n return JSON.stringify(a ?? null) === JSON.stringify(b ?? null)\n}\n\nexport function variantRegeneration(): Behavior<Record<never, never>, 'variantRegeneration'> {\n return {\n name: 'variantRegeneration',\n hooks: {\n afterUpdate: async (entity, ctx) => {\n if (!ctx || ctx.viaInternal || !ctx.enqueueOnCommit || !ctx.loadCurrent) return\n if (entity.mediaType !== 'image' || typeof entity.id !== 'string') return\n const jobs = getMediaJobs()\n if (!jobs) return\n const before = await ctx.loadCurrent()\n if (!before) return\n if (\n sameJson(before.focalPoint, entity.focalPoint) &&\n sameJson(before.crops, entity.crops)\n ) {\n return\n }\n const epoch = editEpoch(entity.updatedAt)\n if (!epoch) return\n ctx.enqueueOnCommit(jobs.generateVariants, { mediaId: entity.id, epoch })\n },\n },\n }\n}\n","/**\n * Pre-defined Media entity.\n *\n * Auto-registered by the media() plugin — users do NOT add this to their entities array.\n * Connected to toolkit_files via the fileKey field (stores the StorageClient file key).\n *\n * @example\n * ```typescript\n * import { media } from '@murumets-ee/media/plugin'\n *\n * export default defineConfig({\n * plugins: [storage(), media()],\n * // Media entity is auto-added — no need to list it here\n * })\n * ```\n */\n\nimport { behavior, defineEntity, field, searchable } from '@murumets-ee/entity'\nimport { cropsValidation } from './crop/crops-validation.js'\nimport { focalPointValidation } from './crop/focal-point-validation.js'\nimport { variantRegeneration } from './jobs/regenerate-on-edit.js'\n\nexport const Media = defineEntity({\n name: 'media',\n fields: {\n // --- Display metadata (editable, translatable) ---\n title: field.text({ translatable: true }),\n alt: field.text({ translatable: true }),\n description: field.text({ translatable: true }),\n\n // --- File linkage (set on upload, immutable in practice) ---\n /** Key into toolkit_files table (e.g., 'uploads/2026/02/uuid/photo.jpg') */\n fileKey: field.text({ required: true, indexed: true }),\n\n // --- File metadata (set on upload) ---\n filename: field.text({ required: true }),\n // Drawn as an icon in the admin list (a film for `video/mp4`), not the raw\n // string — declared on the field, so any list of media reads the same\n // (entity-table D008).\n mimeType: field.text({\n required: true,\n indexed: true,\n admin: { display: { type: 'mimeType' } },\n }),\n // Bytes, drawn as \"1.5 MB\" in the operator's locale in the admin list\n // (entity-table D008). Hidden by default — an unindexed number is — and one\n // click away in the View menu.\n size: field.number({\n required: true,\n integer: true,\n admin: { display: { type: 'filesize' } },\n }),\n\n // --- Image-specific metadata (client-measured on upload) ---\n width: field.number({ integer: true }),\n height: field.number({ integer: true }),\n /**\n * Where the subject is, as `{ x, y }` fractions of the image — never pixels,\n * so it survives the file being replaced at another resolution (images\n * D002). A shaped slot crops around it (`@murumets-ee/media/crop`).\n *\n * Nullable, and `null` means the centre: an absent point and `{0.5, 0.5}`\n * render identically, so no reader branches on a legacy row (D010). The\n * shape is enforced by `focalPointValidation()` below — `field.json()` on\n * its own accepts any JSON.\n */\n focalPoint: field.json(),\n /**\n * Per-shape crop overrides, keyed by shape name — `{x, y, w, h}` fractions\n * of the image, `[0,1]` (images D002). Optional: a shape absent from this\n * map falls back to the rect `focalPoint` implies for that shape's ratio,\n * which is why a missing crop is a fallback and never a hole. Written by\n * the crop editor (`@murumets-ee/media/crop-editor`, images PR03).\n *\n * `null` and `{}` both mean \"no shape has an override\" — the same reading\n * `readFocalPoint` gives an absent focal point (D010). Shape is enforced\n * by `cropsValidation()` below.\n */\n crops: field.json(),\n\n // --- Classification (derived from mimeType on upload) ---\n mediaType: field.select({\n options: ['image', 'video', 'audio', 'document', 'other'] as const,\n required: true,\n indexed: true,\n }),\n },\n behaviors: [\n behavior.auditable(),\n focalPointValidation(),\n cropsValidation(),\n // A moved focal point (or a redrawn crop) changes which variant files the\n // photograph should have — queue a pass for it (images PR05).\n variantRegeneration(),\n // filename / fileKey / mimeType live on the main table.\n // title/alt/description are translatable — their tsvector goes on\n // media_translations so non-default-locale callers still get FTS.\n searchable({\n fields: ['filename', 'fileKey', 'mimeType', 'title', 'alt', 'description'],\n fts: {\n fields: ['filename', 'title', 'alt', 'description'],\n language: 'simple',\n },\n projection: (row) => ({\n id: row.id as string,\n label: (row.title as string | undefined) ?? (row.filename as string),\n description: row.mimeType as string,\n }),\n }),\n ],\n admin: {\n list: {\n // Title first (its own row-link column), then the type — the icon\n // display already draws it, so the raw `mimeType` string is not a\n // second column — size and when it was added. `fileKey` (the raw\n // storage path) and `mediaType` (redundant with the icon) stay off by\n // default, one View-menu click away (entity-table PR06 §6).\n fields: ['title', 'mimeType', 'size', 'createdAt'],\n layouts: ['table', 'grid'],\n },\n },\n scope: 'global',\n access: {\n view: 'public',\n create: 'group.editor',\n update: 'group.editor',\n delete: 'group.admin',\n },\n})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e={PluginNav:{media:`Meedia`},Media:{imageVocabulary:{description:`Iga foto genereeritakse igas allolevas kujus ja igas laiuses. Pesa, mille kuvasuhe ühtib mõne kujuga, kasutab selle kuju faile; muu kuvasuhtega pesa kärbitakse hoopis brauseris.`,usingDefaults:`Siin pole veel midagi salvestatud. Näidatud on tööriista vaikeväärtused; kuni salvestad, kasutab sait neid — või koodis seadistatud kujusid, kui neid on.`,widthsLabel:`Laiused (px)`,widthsHelp:`Kasvavas järjekorras, komadega eraldatud. Fotot ei suurendata kunagi — laius, mida see ei suuda pakkuda, jäetakse vahele.`,widthsInvalid:`Kasuta kuni {count} kasvavat täisarvu vahemikus {min}–{max}.`,shapesLabel:`Kujud`,naturalNote:`Lisaks „natural“ — iga foto oma kujus, samades laiustes.`,columnName:`Nimi`,columnRatio:`Kuvasuhe (L:K)`,columnWidths:`Oma laiused`,ownWidthsPlaceholder:`Kõik laiused`,ratioInvalid:`Kirjuta kuvasuhe kujul L:K, näiteks 3:2.`,nameInvalid:`Väiketähed, numbrid, - ja _, algab tähega — ja mitte „natural“.`,nameTaken:`Kuju nimega „{name}“ on juba olemas.`,tooMany:`Kõige rohkem {count} kuju.`,add:`Lisa kuju`,remove:`Eemalda {name}`,tooManyFiles:`See teeks iga foto kohta {count} faili; lubatud on kõige rohkem {max}. Eemalda enne kuju või laius.`},variantBacklog:{waiting:`Suuruste loomist ootavaid pilte: {count}`,workerRunning:`Järjekorratöötleja loob neid praegu. Kuni see pole valmis, näidatakse neid pilte täissuuruses.`,workerMissing:`Ükski järjekorratöötleja ei tööta, seega näidatakse neid pilte täissuuruses — lehed on aeglasemad, eriti telefonis. Pildisuurused loob järjekorratöötleja, eraldi protsess, mis peab töötama saidi kõrval (pnpm worker). Palu saidi haldajal see käivitada.`,waitingSince:`Vanim lisati järjekorda {since}.`,failed:`Suuruste loomine ebaõnnestus {count} korda ja jäeti pooleli — põhjuse leiad järjekorra lehelt.`,catchUp:`Kogu meediakogu uuendamine on järjekorras: iga pilti võrreldakse praeguste kujude ja stiilidega.`,catchUpWorkerMissing:`Ükski järjekorratöötleja ei tööta, seega pole see alanud. Pildisuurused loob järjekorratöötleja, eraldi protsess, mis peab töötama saidi kõrval (pnpm worker). Palu saidi haldajal see käivitada.`,openQueue:`Ava järjekord`},variantGeneration:{workerRequired:`Pildisuurused luuakse taustal järjekorratöötleja poolt — see on eraldi protsess, mis peab töötama saidi kõrval (pnpm worker). Ilma selleta sait töötab, kuid uusi ja muudetud pilte näidatakse täissuuruses.`,regenerate:`Uuenda kõik pildid`,regenerating:`Käivitan…`,queued:`Käivitatud. Järjekorratöötleja uuendab meediakogu taustal; sait töötab samal ajal edasi. Jälgi seda järjekorra lehel.`,alreadyRunning:`Uuendamine juba käib — jälgi seda järjekorra lehel.`,inlineDone:`Töödeldud {processed} / {total}; vahele jäetud {skipped}; vigu {errors}.`,failed:`Uuendamist ei õnnestunud käivitada: {reason}`}},MediaCropEditor:{tabListLabel:`Kärpimise kujud`,description:`Lohista foto paigutamiseks, kerimine või näpuliigutus suumib. Iga seda kuju kasutav koht näitab seda kärbet.`,xLabel:`Vasakult (%)`,yLabel:`Ülevalt (%)`,widthLabel:`Laius (%)`,heightLabel:`Kõrgus (%)`,clearShape:`Tühista see kärbe`,noShapes:`Ühelgi seadistatud pildistiilil pole veel kindlat proportsiooni, seega pole midagi kärpida.`,hasCropSuffix:`— kohandatud kärbe salvestatud`}};export{e as default};
|
|
2
|
+
//# sourceMappingURL=et-BFdSZnb5.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"et-BFdSZnb5.mjs","names":[],"sources":["../src/messages/et.json"],"sourcesContent":[""],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as e,i as t,n}from"./variant-plan-86tJKtVi.mjs";import{t as r}from"./owned-delete-Cn_KFFjS.mjs";import{c as i,o as a,s as o,t as s}from"./variant-state-CBtqb9Cb.mjs";import{a as ee,i as te,r as ne,s as re,t as c}from"./process-image-wQwiRhyk.mjs";var l=class extends Error{failed;constructor(e,t){super(`media ${e}: ${t} variant(s) failed to upload; the pass will be retried`),this.name=`VariantUploadError`,this.failed=t}};function u(e,t,n={}){return{mediaId:e,status:t,uploaded:0,reused:0,deleted:0,dropped:0,dimensionsCorrected:!1,focalPointSeeded:!1,...n}}async function ie(e){if(Buffer.isBuffer(e))return e;let t=[],n=e.getReader();for(;;){let{done:e,value:r}=await n.read();if(e)break;r&&t.push(r)}return Buffer.concat(t)}async function d(e,t,n){let r=[...e];await Promise.all(Array.from({length:Math.min(t,e.length)},async()=>{for(let e=r.shift();e!==void 0;e=r.shift())await n(e)}))}function f(e,t){return JSON.stringify(e??null)===JSON.stringify(t??null)}function p(e,n){return t(e,{shape:n.shape,width:n.width,fingerprint:n.fingerprint,format:n.format})}function ae(e,t,n){let{width:r,height:i}=e.metadata??{};return typeof r!=`number`||typeof i!=`number`||r<=0||i<=0?null:{shape:t.shape,rung:t.width,width:r,height:i,format:t.format,key:n}}async function m(t,m){let{media:v,storage:y,config:b,styles:oe,logger:x}=m,S=await v.findById(t);if(!S)return u(t,`missing`);let C=S;if(C.mediaType!==`image`||!te(C.mimeType))return u(t,`skipped`);let w=await y.getMetadata(C.fileKey);if(!w)return x?.warn({mediaId:t},`media variants: original file record is missing; nothing to do`),u(t,`skipped`);let T=await ie((await y.download(C.fileKey)).body),E=!1,D=await ne(T),O={};if(D.width>0&&(C.width!==D.width||C.height!==D.height)&&(O.width=D.width,O.height=D.height),C.focalPoint===null||C.focalPoint===void 0){let e=await c(T).catch(e=>(x?.warn({mediaId:t,err:e},`media variants: focal-point detection failed; left unset`),null));e?O.focalPoint=e:E=!0}if(`focalPoint`in O){let e=await v.findById(t);if(!e)return u(t,`missing`);e.focalPoint!==null&&e.focalPoint!==void 0&&(delete O.focalPoint,C={...C,focalPoint:e.focalPoint,crops:e.crops})}Object.keys(O).length>0&&(await v.updateInternal(t,O),C={...C,...O});let k=n(D,b,{focalPoint:C.focalPoint,crops:C.crops});k.dropped>0&&x?.warn({mediaId:t,dropped:k.dropped},`media variants: the per-image cap cut the largest variants off this photograph`);let A=o(w.metadata),j=new Map(A.shaped.map(e=>[e.key,e])),M=[],N=[];for(let e of k.variants){let t=p(C.fileKey,e),n=j.get(t);if(n){M.push({...n,shape:e.shape,rung:e.width});continue}let r=await y.getMetadata(t),i=r?ae(r,e,t):null;i?M.push(i):N.push({planned:e,key:t,replace:r!==null})}let se=M.length,P=0,F=0,I=[],L=[],ce=await re(T,N.map(e=>e.planned));await d(N.map((e,t)=>({...e,variant:ce[t]})),4,async({planned:e,key:n,replace:r,variant:i})=>{if(i)try{r&&await y.delete(n),await y.upload(i.buffer,{key:n,filename:`${e.shape}_${e.width}w_${C.filename}`,mimeType:i.mimeType,size:i.buffer.byteLength,visibility:w.visibility,metadata:{variantOf:C.fileKey,shape:e.shape,rung:e.width,width:i.width,height:i.height}}),M.push({shape:e.shape,rung:e.width,width:i.width,height:i.height,format:e.format,key:n}),I.push(n),P++}catch(e){F++,x?.warn({mediaId:t,key:n,err:e},`media variants: shaped variant upload failed`)}});let R=new Map,z=new Map,B={},V=new Set;for(let[t,n]of Object.entries(oe)){let r=e(C.fileKey,t,n.format??`webp`),i=a(n);A.legacy.get(t)===r&&A.legacyRecipes.get(t)===i?(R.set(t,r),z.set(t,i)):B[t]=n}Object.keys(B).length>0&&await d([...(await ee(T,B)).variants],4,async([n,r])=>{let i=B[n];if(!i)return;let o=e(C.fileKey,n,r.format),s=!1;try{await y.getMetadata(o)&&(await y.delete(o),s=!0),await y.upload(r.buffer,{key:o,filename:`${n}_${C.filename}`,mimeType:r.mimeType,size:r.buffer.byteLength,visibility:w.visibility,metadata:{variantOf:C.fileKey,style:n}}),R.set(n,o),z.set(n,a(i)),L.push(o),P++}catch(e){F++,s&&V.add(n),x?.warn({mediaId:t,key:o,err:e},`media variants: legacy style upload failed`)}});let H=await v.findById(t);if(!H)return await r(y,C.fileKey,[...I,...L],x),u(t,`missing`,{uploaded:P});let U=!f(H.focalPoint,C.focalPoint)||!f(H.crops,C.crops),W=await y.getMetadata(C.fileKey)??w,G=o(W.metadata),K=g(h(G.legacy,R),V),q=g(h(G.legacyRecipes,z),V),le=[...G.legacy].filter(([e,t])=>K.has(e)&&K.get(e)!==t).map(([,e])=>e),J=U?new Set(n(D,b,{focalPoint:H.focalPoint,crops:H.crops}).variants.map(e=>p(C.fileKey,e))):null,Y=(e,t)=>{let n=new Set(e.map(e=>e.key));return[...new Set([...G.unconfirmed,...t,...le])].filter(e=>!n.has(e))},X;if(U){let e=I.filter(e=>J?.has(e));X={legacy:K,legacyRecipes:q,shaped:G.shaped,unconfirmed:Y(G.shaped,e)}}else if(F>0){let e=new Set(G.shaped.map(e=>`${e.shape}@${e.rung}`)),t=M.filter(t=>!e.has(`${t.shape}@${t.rung}`)),n=_([...G.shaped,...t]);X={legacy:K,legacyRecipes:q,shaped:n,unconfirmed:Y(n,M.map(e=>e.key))}}else X={legacy:R,legacyRecipes:z,shaped:_(M),unconfirmed:[]};let Z=i(W.metadata,X);await y.updateMetadata(C.fileKey,{metadata:E?{...Z,[s]:!0}:Z});let Q=new Set;if(U){let e=new Set(G.shaped.map(e=>e.key));for(let t of I)!J?.has(t)&&!e.has(t)&&Q.add(t)}else if(F===0){let e=new Set([...M.map(e=>e.key),...R.values()]);for(let t of G.shaped)e.has(t.key)||Q.add(t.key);for(let t of G.legacy.values())e.has(t)||Q.add(t);for(let t of G.unconfirmed)e.has(t)||Q.add(t)}let ue=await r(y,C.fileKey,Q,x),$=u(t,U?`stale`:`generated`,{uploaded:P,reused:se,deleted:ue,dropped:k.dropped,dimensionsCorrected:`width`in O,focalPointSeeded:`focalPoint`in O});if(F>0&&!U)throw new l(t,F);return x?.debug($,`media variants: pass complete`),$}function h(e,t){return new Map([...e,...t])}function g(e,t){for(let n of t)e.delete(n);return e}function _(e){return[...e].sort((e,t)=>e.shape===t.shape?e.rung-t.rung||e.width-t.width:e.shape<t.shape?-1:1)}export{m as generateMediaVariants};
|
|
2
|
+
//# sourceMappingURL=generate-variants-Dpy4aT2o.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-variants-Dpy4aT2o.mjs","names":[],"sources":["../src/jobs/generate-variants.ts"],"sourcesContent":["/**\n * One generation pass for one photograph — the body of the\n * `media:generate-variants` job (images D004), and of the inline fallback an\n * app without the queue plugin runs at upload.\n *\n * ## The order, which is the point (F002)\n *\n * 1. Read the row and the ORIGINAL; correct the row's drawn size and seed a\n * missing focal point (F014, below).\n * 2. Plan the shaped set from the FILE's drawn size (F016) and render only the\n * variants whose key does not exist yet. A shaped key is recipe-addressed\n * (D005), so an existing key already holds the right bytes.\n * 3. Upload the new files.\n * 4. Record the new set on the original's metadata — the pointer readers use.\n * 5. Only then delete what the new set superseded.\n *\n * Today's `regenerateAllVariants` deleted first, so removing a style destroyed\n * its files before anything replaced them and every variant URL 404'd for the\n * length of the run. Here a reader holding a stale URL keeps getting the old\n * picture until the pointer has moved.\n *\n * Legacy `{style}_{base}` keys cannot be additive — the key has no recipe in it\n * and `StorageClient.upload` refuses to overwrite a key (R014). So a legacy file\n * is re-rendered only when its style's recipe CHANGED (`legacyRecipes`), and\n * then replaced one key at a time with the new bytes already in memory: a\n * window of one upload, not of the whole library.\n *\n * ## The row, while the original is open anyway (F014)\n *\n * Rows uploaded before images PR02 hold sensor-oriented `width`/`height` — an\n * EXIF-rotated phone portrait stored 4000×3000 is drawn 3000×4000, and its focal\n * crop is positioned on the wrong axis. Rows uploaded before PR02 also have no\n * focal point. The pass already decodes the original, so it writes the drawn\n * size back and seeds a `null` focal point with `detectFocalPoint` — never\n * overwriting a point someone set (the picker's Reset writes the centre, not\n * `null`, so `null` means \"never set\"). Both go through `updateInternal`, which\n * the regenerate-on-edit behavior ignores, so this write does not enqueue\n * another pass of itself.\n *\n * ## Failure\n *\n * A variant that fails to upload is logged and the pass THROWS after recording\n * and deleting nothing a reader resolves — so the queue retries it, and the\n * retry only renders what is still missing. The shaped set readers see is left\n * exactly as it was; what did land is recorded as `unconfirmed` (adopted by the\n * retry, and deleted with the photograph). The one exception is a legacy file\n * replaced in place whose new upload failed after the old was deleted: its\n * mapping is dropped, so readers fall back to the original rather than a 404.\n *\n * ## Concurrency, and what is accepted\n *\n * Two passes for one photograph converge: a key already written by the other\n * fails the unique-key upload and is adopted on retry. A pass that sees the\n * row's point or crop change between planning and recording returns `stale`\n * and records nothing shaped. What is NOT closed: a newer pass recording\n * between this pass's freshness check and its own record — a window of two\n * round trips. The loser's set is then recorded for the winner's point until\n * the next backfill finds the mismatch (`isUpToDate`) and queues a repair.\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { FileRecord, StorageClient } from '@murumets-ee/storage'\nimport {\n detectFocalPoint,\n getImageDimensions,\n isProcessableImage,\n processImage,\n renderPlannedVariants,\n} from '../process-image.js'\nimport type { VariantConfig } from '../shapes.js'\nimport type { ImageStyle } from '../types.js'\nimport { deriveShapedVariantKey, deriveVariantKey } from '../variant-key.js'\nimport { type PlannedVariant, planVariants } from '../variant-plan.js'\nimport { deleteOwnedVariants } from './owned-delete.js'\nimport {\n FOCAL_SEED_TRIED,\n legacyRecipe,\n readVariantState,\n type StoredShapedVariant,\n type VariantState,\n writeVariantState,\n} from './variant-state.js'\n\n/** The slice of a media row a pass reads. `crops` arrives with images PR03; read leniently. */\nexport interface VariantSourceRow {\n readonly id: string\n readonly fileKey: string\n readonly filename: string\n readonly mimeType: string\n readonly mediaType: string\n readonly width?: number | null\n readonly height?: number | null\n readonly focalPoint?: unknown\n readonly crops?: unknown\n}\n\nexport interface VariantGenerationDeps {\n /** Reads the media row, and writes the corrected size / seeded focal point. */\n readonly media: {\n findById(id: string): Promise<VariantSourceRow | null>\n updateInternal(id: string, data: Record<string, unknown>): Promise<unknown>\n }\n readonly storage: Pick<\n StorageClient,\n 'download' | 'getMetadata' | 'upload' | 'updateMetadata' | 'delete'\n >\n /** The shape model — `resolveVariantConfig`. */\n readonly config: VariantConfig\n /** The legacy fixed styles — `resolveImageStyles`. */\n readonly styles: Readonly<Record<string, ImageStyle>>\n readonly logger?: Pick<Logger, 'info' | 'warn' | 'debug'> | undefined\n}\n\nexport type VariantGenerationStatus =\n /** The pass ran — possibly finding nothing to do. */\n | 'generated'\n /** Not a raster image this pipeline processes (SVG, GIF, a PDF), or its original is gone. */\n | 'skipped'\n /** The row no longer exists. */\n | 'missing'\n /**\n * The row's focal point or crops changed while the pass ran, so what it\n * rendered is already out of date. Nothing shaped was recorded; the edit\n * enqueued its own pass, which will.\n */\n | 'stale'\n\nexport interface VariantGenerationResult {\n readonly mediaId: string\n readonly status: VariantGenerationStatus\n /** Files written this pass (shaped + legacy). */\n readonly uploaded: number\n /** Planned shaped variants that already existed and were kept as they were. */\n readonly reused: number\n /** Superseded files removed after the new set was recorded. */\n readonly deleted: number\n /** Variants the per-image cap cut off (`MAX_VARIANTS_PER_IMAGE`). */\n readonly dropped: number\n readonly dimensionsCorrected: boolean\n readonly focalPointSeeded: boolean\n}\n\n/** Uploads run a few at a time — each is a DB insert plus an object write. */\nconst UPLOAD_CONCURRENCY = 4\n\n/** Thrown when some variants failed to upload — the queue retries the pass. */\nexport class VariantUploadError extends Error {\n readonly failed: number\n constructor(mediaId: string, failed: number) {\n super(`media ${mediaId}: ${failed} variant(s) failed to upload; the pass will be retried`)\n this.name = 'VariantUploadError'\n this.failed = failed\n }\n}\n\nfunction result(\n mediaId: string,\n status: VariantGenerationStatus,\n partial: Partial<Omit<VariantGenerationResult, 'mediaId' | 'status'>> = {},\n): VariantGenerationResult {\n return {\n mediaId,\n status,\n uploaded: 0,\n reused: 0,\n deleted: 0,\n dropped: 0,\n dimensionsCorrected: false,\n focalPointSeeded: false,\n ...partial,\n }\n}\n\n/** A downloaded body as one Buffer. */\nasync function toBuffer(body: Buffer | ReadableStream<Uint8Array>): Promise<Buffer> {\n if (Buffer.isBuffer(body)) return body\n const chunks: Uint8Array[] = []\n const reader = body.getReader()\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (value) chunks.push(value)\n }\n return Buffer.concat(chunks)\n}\n\n/** `Promise.all(items.map(fn))` with at most `limit` in flight. */\nasync function eachBounded<T>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<void>,\n): Promise<void> {\n const queue = [...items]\n const worker = async (): Promise<void> => {\n for (let item = queue.shift(); item !== undefined; item = queue.shift()) await fn(item)\n }\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))\n}\n\nfunction sameJson(a: unknown, b: unknown): boolean {\n return JSON.stringify(a ?? null) === JSON.stringify(b ?? null)\n}\n\nfunction shapedKey(fileKey: string, planned: PlannedVariant): string {\n return deriveShapedVariantKey(fileKey, {\n shape: planned.shape,\n width: planned.width,\n fingerprint: planned.fingerprint,\n format: planned.format,\n })\n}\n\n/**\n * A shaped file that exists but was not recorded — a previous pass uploaded it\n * and then failed before recording, or a concurrent pass did. The job writes the\n * real size onto every file it uploads, so it can be recorded without decoding.\n */\nfunction recordedFromFile(\n file: FileRecord,\n planned: PlannedVariant,\n key: string,\n): StoredShapedVariant | null {\n const meta = file.metadata ?? {}\n const { width, height } = meta\n if (typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0) {\n return null\n }\n return { shape: planned.shape, rung: planned.width, width, height, format: planned.format, key }\n}\n\n/** Generate — and record, and clean up after — every variant one photograph should have. */\nexport async function generateMediaVariants(\n mediaId: string,\n deps: VariantGenerationDeps,\n): Promise<VariantGenerationResult> {\n const { media, storage, config, styles, logger } = deps\n\n const found = await media.findById(mediaId)\n if (!found) return result(mediaId, 'missing')\n let row = found\n if (row.mediaType !== 'image' || !isProcessableImage(row.mimeType)) {\n return result(mediaId, 'skipped')\n }\n const original = await storage.getMetadata(row.fileKey)\n if (!original) {\n logger?.warn({ mediaId }, 'media variants: original file record is missing; nothing to do')\n return result(mediaId, 'skipped')\n }\n const buffer = await toBuffer((await storage.download(row.fileKey)).body)\n\n // ── 1. The row: drawn size, and a seed for a point nobody has set (F014) ──\n let seedTried = false\n const drawn = await getImageDimensions(buffer)\n const patch: Record<string, unknown> = {}\n if (drawn.width > 0 && (row.width !== drawn.width || row.height !== drawn.height)) {\n patch.width = drawn.width\n patch.height = drawn.height\n }\n if (row.focalPoint === null || row.focalPoint === undefined) {\n const seeded = await detectFocalPoint(buffer).catch((err: unknown) => {\n logger?.warn({ mediaId, err }, 'media variants: focal-point detection failed; left unset')\n return null\n })\n if (seeded) patch.focalPoint = seeded\n // Nothing to guess about (a strip, a flat image): say so on the original,\n // or every walk would queue this photograph again to guess once more.\n else seedTried = true\n }\n if ('focalPoint' in patch) {\n // The seed took a download and a saliency pass; an editor may have set a\n // point meanwhile. Re-read right before writing, and never overwrite one\n // that is now set (a window of one round trip remains — accepted).\n const current = await media.findById(mediaId)\n if (!current) return result(mediaId, 'missing')\n if (current.focalPoint !== null && current.focalPoint !== undefined) {\n delete patch.focalPoint\n row = { ...row, focalPoint: current.focalPoint, crops: current.crops }\n }\n }\n if (Object.keys(patch).length > 0) {\n await media.updateInternal(mediaId, patch)\n row = { ...row, ...patch }\n }\n\n // ── 2. The shaped plan, from the FILE's drawn size ──\n const plan = planVariants(drawn, config, { focalPoint: row.focalPoint, crops: row.crops })\n if (plan.dropped > 0) {\n logger?.warn(\n { mediaId, dropped: plan.dropped },\n 'media variants: the per-image cap cut the largest variants off this photograph',\n )\n }\n\n const before = readVariantState(original.metadata)\n const recorded = new Map(before.shaped.map((v) => [v.key, v]))\n const present: StoredShapedVariant[] = []\n const toRender: Array<{ planned: PlannedVariant; key: string; replace: boolean }> = []\n for (const planned of plan.variants) {\n const key = shapedKey(row.fileKey, planned)\n const known = recorded.get(key)\n if (known) {\n present.push({ ...known, shape: planned.shape, rung: planned.width })\n continue\n }\n const existing = await storage.getMetadata(key)\n const fromFile = existing ? recordedFromFile(existing, planned, key) : null\n if (fromFile) present.push(fromFile)\n else toRender.push({ planned, key, replace: existing !== null })\n }\n const reused = present.length\n\n // ── 3. Render what is missing, and upload it ──\n let uploaded = 0\n let failed = 0\n const uploadedKeys: string[] = []\n /** Legacy files written this pass — only for the photograph-deleted-mid-pass cleanup. */\n const uploadedLegacyKeys: string[] = []\n const rendered = await renderPlannedVariants(\n buffer,\n toRender.map((t) => t.planned),\n )\n const work = toRender.map((t, i) => ({ ...t, variant: rendered[i] }))\n await eachBounded(work, UPLOAD_CONCURRENCY, async ({ planned, key, replace, variant }) => {\n if (!variant) return\n try {\n // An unrecorded file with no size on it predates this module's metadata\n // and cannot be trusted as the recipe's bytes — replaced, not kept.\n if (replace) await storage.delete(key)\n await storage.upload(variant.buffer, {\n key,\n filename: `${planned.shape}_${planned.width}w_${row.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility: original.visibility,\n metadata: {\n variantOf: row.fileKey,\n shape: planned.shape,\n rung: planned.width,\n width: variant.width,\n height: variant.height,\n },\n })\n present.push({\n shape: planned.shape,\n rung: planned.width,\n width: variant.width,\n height: variant.height,\n format: planned.format,\n key,\n })\n uploadedKeys.push(key)\n uploaded++\n } catch (err) {\n failed++\n logger?.warn({ mediaId, key, err }, 'media variants: shaped variant upload failed')\n }\n })\n\n // ── Legacy fixed styles: re-rendered only when the recipe changed ──\n const legacy = new Map<string, string>()\n const legacyRecipes = new Map<string, string>()\n const legacyDue: Record<string, ImageStyle> = {}\n /** Styles whose old file was deleted for an in-place replacement whose upload then failed. */\n const legacyGone = new Set<string>()\n for (const [name, style] of Object.entries(styles)) {\n const key = deriveVariantKey(row.fileKey, name, style.format ?? 'webp')\n const recipe = legacyRecipe(style)\n if (before.legacy.get(name) === key && before.legacyRecipes.get(name) === recipe) {\n legacy.set(name, key)\n legacyRecipes.set(name, recipe)\n } else {\n legacyDue[name] = style\n }\n }\n if (Object.keys(legacyDue).length > 0) {\n const processed = await processImage(buffer, legacyDue)\n await eachBounded([...processed.variants], UPLOAD_CONCURRENCY, async ([name, variant]) => {\n const style = legacyDue[name]\n if (!style) return\n const key = deriveVariantKey(row.fileKey, name, variant.format)\n let oldDeleted = false\n try {\n // In place: a legacy key names no recipe, so new bytes need the old row\n // gone first. The bytes are already rendered — the gap is one upload.\n if (await storage.getMetadata(key)) {\n await storage.delete(key)\n oldDeleted = true\n }\n await storage.upload(variant.buffer, {\n key,\n filename: `${name}_${row.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility: original.visibility,\n metadata: { variantOf: row.fileKey, style: name },\n })\n legacy.set(name, key)\n legacyRecipes.set(name, legacyRecipe(style))\n uploadedLegacyKeys.push(key)\n uploaded++\n } catch (err) {\n failed++\n // The old file is gone and its replacement is not there: stop pointing\n // at it, so readers fall back to the original until the retry lands.\n if (oldDeleted) legacyGone.add(name)\n logger?.warn({ mediaId, key, err }, 'media variants: legacy style upload failed')\n }\n })\n }\n\n // ── Was the plan still the right one? ──\n const fresh = await media.findById(mediaId)\n if (!fresh) {\n // Deleted while the pass ran. `MediaClient.delete` removed what was\n // recorded; what this pass wrote was never recorded, so it goes here.\n await deleteOwnedVariants(\n storage,\n row.fileKey,\n [...uploadedKeys, ...uploadedLegacyKeys],\n logger,\n )\n return result(mediaId, 'missing', { uploaded })\n }\n const stale = !sameJson(fresh.focalPoint, row.focalPoint) || !sameJson(fresh.crops, row.crops)\n\n // ── 4. Record — merged over a fresh read, so a concurrent writer's other keys survive ──\n const latest = (await storage.getMetadata(row.fileKey)) ?? original\n const latestState = readVariantState(latest.metadata)\n const mergedLegacy = without(merged(latestState.legacy, legacy), legacyGone)\n const mergedRecipes = without(merged(latestState.legacyRecipes, legacyRecipes), legacyGone)\n // A legacy style whose key CHANGED (its format did) now points at the new\n // file; the old one is recorded nowhere else. Outside a successful pass it\n // must stay findable, so it goes to `unconfirmed` (deleted by the next\n // successful pass, and with the photograph).\n const replacedLegacy = [...latestState.legacy]\n .filter(([name, key]) => mergedLegacy.has(name) && mergedLegacy.get(name) !== key)\n .map(([, key]) => key)\n const freshKeys = stale\n ? new Set(\n planVariants(drawn, config, {\n focalPoint: fresh.focalPoint,\n crops: fresh.crops,\n }).variants.map((p) => shapedKey(row.fileKey, p)),\n )\n : null\n /** Every file this pass holds that the recorded `shaped` set will not name. */\n const leftovers = (shaped: readonly StoredShapedVariant[], extra: readonly string[]) => {\n const named = new Set(shaped.map((v) => v.key))\n return [...new Set([...latestState.unconfirmed, ...extra, ...replacedLegacy])].filter(\n (k) => !named.has(k),\n )\n }\n let next: VariantState\n if (stale) {\n // The shaped set belongs to the edit's own pass. Record the legacy files\n // (independent of focal point and crops). What this pass rendered that the\n // NEW point also plans is kept as unconfirmed for the edit's pass to adopt;\n // the rest is deleted below.\n const keepForEdit = uploadedKeys.filter((k) => freshKeys?.has(k))\n next = {\n legacy: mergedLegacy,\n legacyRecipes: mergedRecipes,\n shaped: latestState.shaped,\n unconfirmed: leftovers(latestState.shaped, keepForEdit),\n }\n } else if (failed > 0) {\n // Never two crops for one (shape, rung): a variant that landed is promoted\n // only where readers have nothing for its (shape, rung) yet — so a photo\n // whose one format always fails still gets every other size. The rest is\n // UNCONFIRMED: the retry adopts it, and deleting the photograph finds it.\n const taken = new Set(latestState.shaped.map((v) => `${v.shape}@${v.rung}`))\n const promoted = present.filter((v) => !taken.has(`${v.shape}@${v.rung}`))\n const shaped = sortShaped([...latestState.shaped, ...promoted])\n next = {\n legacy: mergedLegacy,\n legacyRecipes: mergedRecipes,\n shaped,\n unconfirmed: leftovers(\n shaped,\n present.map((v) => v.key),\n ),\n }\n } else {\n next = { legacy, legacyRecipes, shaped: sortShaped(present), unconfirmed: [] }\n }\n const written = writeVariantState(latest.metadata, next)\n await storage.updateMetadata(row.fileKey, {\n metadata: seedTried ? { ...written, [FOCAL_SEED_TRIED]: true } : written,\n })\n\n // ── 5. Only now delete what the new set superseded — and only files that are\n // variants OF this original (`deleteOwnedVariants`): the lists come from\n // caller-writable metadata. ──\n const toDelete = new Set<string>()\n if (stale) {\n const stillRecorded = new Set(latestState.shaped.map((v) => v.key))\n for (const key of uploadedKeys) {\n if (!freshKeys?.has(key) && !stillRecorded.has(key)) toDelete.add(key)\n }\n } else if (failed === 0) {\n const keep = new Set([...present.map((v) => v.key), ...legacy.values()])\n for (const v of latestState.shaped) if (!keep.has(v.key)) toDelete.add(v.key)\n for (const key of latestState.legacy.values()) if (!keep.has(key)) toDelete.add(key)\n for (const key of latestState.unconfirmed) if (!keep.has(key)) toDelete.add(key)\n }\n const deleted = await deleteOwnedVariants(storage, row.fileKey, toDelete, logger)\n\n const outcome = result(mediaId, stale ? 'stale' : 'generated', {\n uploaded,\n reused,\n deleted,\n dropped: plan.dropped,\n dimensionsCorrected: 'width' in patch,\n focalPointSeeded: 'focalPoint' in patch,\n })\n if (failed > 0 && !stale) throw new VariantUploadError(mediaId, failed)\n logger?.debug(outcome, 'media variants: pass complete')\n return outcome\n}\n\nfunction merged<V>(base: ReadonlyMap<string, V>, over: ReadonlyMap<string, V>): Map<string, V> {\n return new Map([...base, ...over])\n}\n\nfunction without<V>(map: Map<string, V>, names: ReadonlySet<string>): Map<string, V> {\n for (const name of names) map.delete(name)\n return map\n}\n\n/** Shape, then width — a stable order, so an unchanged set writes unchanged JSON. */\nfunction sortShaped(list: readonly StoredShapedVariant[]): StoredShapedVariant[] {\n return [...list].sort((x, y) =>\n x.shape === y.shape ? x.rung - y.rung || x.width - y.width : x.shape < y.shape ? -1 : 1,\n )\n}\n"],"mappings":"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"}
|
package/dist/i18n.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
async function e(e){switch(e){case`et`:return(await import(`./et-
|
|
1
|
+
async function e(e){switch(e){case`et`:return(await import(`./et-BFdSZnb5.mjs`)).default;case`ru`:return(await import(`./ru-ojvshRJy.mjs`)).default;default:return(await import(`./en-Duj_Pp-O.mjs`).then(e=>e.n)).default}}export{e as getMediaMessages};
|
|
2
2
|
//# sourceMappingURL=i18n.mjs.map
|
package/dist/i18n.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.mjs","names":[],"sources":["../src/i18n.ts"],"sourcesContent":["/**\n * Load `@murumets-ee/media` messages for a given locale.\n *\n * Mirrors `@murumets-ee/rent`/`@murumets-ee/ticketing`'s `./i18n` subpath\n * exactly. `src/i18n.ts` is a sibling entry of `src/plugin.ts` in the same\n * tsdown config, so `plugin.ts` imports this RELATIVELY (`./i18n.js`) rather\n * than through the package's own subpath — the loader lands in a shared\n * chunk instead of a duplicated one.\n *\n * @example\n * ```typescript\n * import { getMediaMessages } from '@murumets-ee/media/i18n'\n * import { mergeMessages } from '@murumets-ee/i18n'\n *\n * const messages = mergeMessages(\n * appMessages,\n * await getMediaMessages(locale),\n * )\n * ```\n */\nexport async function getMediaMessages(locale: string): Promise<Record<string, unknown>> {\n switch (locale) {\n case 'et':\n return (await import('./messages/et.json')).default\n case 'ru':\n return (await import('./messages/ru.json')).default\n default:\n return (await import('./messages/en.json')).default\n }\n}\n"],"mappings":"AAoBA,eAAsB,EAAiB,EAAkD,CACvF,OAAQ,EAAR,CACE,IAAK,KACH,OAAQ,MAAM,OAAO,qBAAA,CAAuB,QAC9C,IAAK,KACH,OAAQ,MAAM,OAAO,qBAAA,CAAuB,QAC9C,QACE,OAAQ,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"i18n.mjs","names":[],"sources":["../src/i18n.ts"],"sourcesContent":["/**\n * Load `@murumets-ee/media` messages for a given locale.\n *\n * Mirrors `@murumets-ee/rent`/`@murumets-ee/ticketing`'s `./i18n` subpath\n * exactly. `src/i18n.ts` is a sibling entry of `src/plugin.ts` in the same\n * tsdown config, so `plugin.ts` imports this RELATIVELY (`./i18n.js`) rather\n * than through the package's own subpath — the loader lands in a shared\n * chunk instead of a duplicated one.\n *\n * @example\n * ```typescript\n * import { getMediaMessages } from '@murumets-ee/media/i18n'\n * import { mergeMessages } from '@murumets-ee/i18n'\n *\n * const messages = mergeMessages(\n * appMessages,\n * await getMediaMessages(locale),\n * )\n * ```\n */\nexport async function getMediaMessages(locale: string): Promise<Record<string, unknown>> {\n switch (locale) {\n case 'et':\n return (await import('./messages/et.json')).default\n case 'ru':\n return (await import('./messages/ru.json')).default\n default:\n return (await import('./messages/en.json')).default\n }\n}\n"],"mappings":"AAoBA,eAAsB,EAAiB,EAAkD,CACvF,OAAQ,EAAR,CACE,IAAK,KACH,OAAQ,MAAM,OAAO,qBAAA,CAAuB,QAC9C,IAAK,KACH,OAAQ,MAAM,OAAO,qBAAA,CAAuB,QAC9C,QACE,OAAQ,MAAM,OAAO,oBAAqB,CAAA,KAAA,GAAA,EAAA,CAAA,EAAA,CAAE,OAChD,CACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as ImageVocabulary, i as ImageStyle } from "./types-6PZQ4Zq6.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/image-styles-settings.d.ts
|
|
4
4
|
/**
|
|
@@ -16,6 +16,13 @@ import { t as ImageStyle } from "./types-CnORVkLZ.mjs";
|
|
|
16
16
|
*/
|
|
17
17
|
declare const defaultImageStyles: Record<string, ImageStyle>;
|
|
18
18
|
declare const imageStylesSettings: import("@murumets-ee/settings").SettingsDefinition<{
|
|
19
|
+
/**
|
|
20
|
+
* The shape vocabulary and width ladder (images D003). Deliberately NO
|
|
21
|
+
* `default`: an unset key reads as `null`, which is what lets the waterfall
|
|
22
|
+
* reach the plugin config (`media({ vocabulary })`) and then the seeded
|
|
23
|
+
* `DEFAULT_IMAGE_VOCABULARY` — see `resolveVariantConfig`.
|
|
24
|
+
*/
|
|
25
|
+
readonly vocabulary: import("@murumets-ee/settings").JsonSettingConfig<ImageVocabulary>;
|
|
19
26
|
readonly imageStyles: import("@murumets-ee/settings").JsonSettingConfig<Record<string, ImageStyle>>;
|
|
20
27
|
}, "media.imageStyles">;
|
|
21
28
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-styles-settings.d.mts","names":[],"sources":["../src/image-styles-settings.ts"],"mappings":";;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"image-styles-settings.d.mts","names":[],"sources":["../src/image-styles-settings.ts"],"mappings":";;;;;;;;;;;;;;;;cA4Ia,kBAAA,EAAoB,MAAM,SAAS,UAAA;AAAA,cAOnC,mBAAA,kCAAmB,kBAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{a as e,c as t,i as n,l as r,o as i}from"./shapes-CqppzFzb.mjs";import"./variant-plan-86tJKtVi.mjs";import{z as a}from"zod";import{defineSettings as o,setting as s}from"@murumets-ee/settings/define";const c=a.object({width:a.number().int().positive().optional(),height:a.number().int().positive().optional(),fit:a.enum([`cover`,`contain`,`inside`,`outside`,`fill`]).optional(),format:a.enum([`webp`,`jpeg`,`png`,`avif`]).optional(),quality:a.number().int().min(1).max(100).optional()}).refine(e=>e.width!==void 0||e.height!==void 0,{message:`Style must have at least width or height`}),l=/^[a-z][a-z0-9_-]{0,63}$/,u=a.record(a.string(),c).superRefine((e,t)=>{for(let n of Object.keys(e))l.test(n)||t.addIssue({code:a.ZodIssueCode.custom,path:[n],message:`Invalid style name "${n}": must be lowercase alphanumeric (a-z, 0-9, -, _)`})}),d=a.number().int().min(16).max(n);function f(e){return e.every((t,n)=>n===0||t>(e[n-1]??-1/0))}const p=a.array(d).min(1).max(12).refine(f,{message:`Widths must be ascending, with no repeats`}),m=a.object({ratio:a.string().refine(e=>t(e)!==null,{message:`Ratio must be W:H in positive whole numbers, e.g. 3:2`}),widths:p.optional(),format:a.enum([`webp`,`jpeg`,`png`,`avif`]).optional(),quality:a.number().int().min(1).max(100).optional()}).strict(),h=a.object({shapes:a.record(a.string(),m).superRefine((t,n)=>{let r=Object.keys(t);r.length>16&&n.addIssue({code:a.ZodIssueCode.custom,message:`At most 16 shapes`});for(let t of r)(!i.test(t)||t===`natural`)&&n.addIssue({code:a.ZodIssueCode.custom,path:[t],message:`Invalid shape name "${t}": lowercase alphanumeric (a-z, 0-9, -, _), and not "${e}"`})}),ladder:p}).strict().superRefine((e,t)=>{let n=r(e);n>64&&t.addIssue({code:a.ZodIssueCode.custom,message:`These shapes and widths make ${n} files per photograph; the most is 64. Remove a shape or a width.`})}),g={thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80},card:{width:600,height:400,fit:`cover`,format:`webp`,quality:85},hero:{width:1920,height:800,fit:`cover`,format:`webp`,quality:85},full:{width:2400,fit:`inside`,format:`webp`,quality:90}},_=o({namespace:`media.imageStyles`,scope:`global`,label:`Image Styles`,iconName:`image-down`,schema:{vocabulary:s.json({label:`Shapes and widths`,schema:h,renderer:`media.imageVocabulary`}),imageStyles:s.json({default:{thumbnail:g.thumbnail},label:`Image processing presets`,schema:u,renderer:`media.imageStyles`})}});export{g as defaultImageStyles,_ as imageStylesSettings};
|
|
2
2
|
//# sourceMappingURL=image-styles-settings.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-styles-settings.mjs","names":[],"sources":["../src/image-styles-settings.ts"],"sourcesContent":["/**\n * Settings definition for media image styles.\n *\n * Stores the full Record<string, ImageStyle> as a single JSON key\n * in the toolkit_settings table under namespace 'media.imageStyles'.\n *\n * The media() plugin seeds this from config on first boot.\n * After that, the settings DB is the source of truth.\n */\n\nimport { defineSettings, setting } from '@murumets-ee/settings/define'\nimport { z } from 'zod'\nimport type { ImageStyle } from './types.js'\n\n/**\n * Zod schema for the `imageStyles` value (Record<styleName, ImageStyle>).\n *\n * Drives both server-side validation (settings PATCH route) and\n * client-side resolverless surfacing of field errors. The keys (style\n * names) are constrained at validation time via `.superRefine` because\n * Zod records do not support per-key regex.\n */\nconst styleSchema = z\n .object({\n width: z.number().int().positive().optional(),\n height: z.number().int().positive().optional(),\n fit: z.enum(['cover', 'contain', 'inside', 'outside', 'fill']).optional(),\n format: z.enum(['webp', 'jpeg', 'png', 'avif']).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .refine((s) => s.width !== undefined || s.height !== undefined, {\n message: 'Style must have at least width or height',\n })\n\nconst STYLE_NAME_RE = /^[a-z][a-z0-9_-]
|
|
1
|
+
{"version":3,"file":"image-styles-settings.mjs","names":[],"sources":["../src/image-styles-settings.ts"],"sourcesContent":["/**\n * Settings definition for media image styles.\n *\n * Stores the full Record<string, ImageStyle> as a single JSON key\n * in the toolkit_settings table under namespace 'media.imageStyles'.\n *\n * The media() plugin seeds this from config on first boot.\n * After that, the settings DB is the source of truth.\n */\n\nimport { defineSettings, setting } from '@murumets-ee/settings/define'\nimport { z } from 'zod'\nimport {\n MAX_LADDER_RUNGS,\n MAX_SHAPES,\n MAX_VARIANT_WIDTH,\n MIN_VARIANT_WIDTH,\n NATURAL_SHAPE,\n parseShapeRatio,\n plannedFileCount,\n SHAPE_NAME_PATTERN,\n} from './shapes.js'\nimport type { ImageStyle, ImageVocabulary } from './types.js'\nimport { MAX_VARIANTS_PER_IMAGE } from './variant-plan.js'\n\n/**\n * Zod schema for the `imageStyles` value (Record<styleName, ImageStyle>).\n *\n * Drives both server-side validation (settings PATCH route) and\n * client-side resolverless surfacing of field errors. The keys (style\n * names) are constrained at validation time via `.superRefine` because\n * Zod records do not support per-key regex.\n */\nconst styleSchema = z\n .object({\n width: z.number().int().positive().optional(),\n height: z.number().int().positive().optional(),\n fit: z.enum(['cover', 'contain', 'inside', 'outside', 'fill']).optional(),\n format: z.enum(['webp', 'jpeg', 'png', 'avif']).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .refine((s) => s.width !== undefined || s.height !== undefined, {\n message: 'Style must have at least width or height',\n })\n\n// Bounded at 64 like `SHAPE_NAME_PATTERN`: an edited style is a shape, and its name a crops key.\nconst STYLE_NAME_RE = /^[a-z][a-z0-9_-]{0,63}$/\n\nconst imageStylesSchema = z.record(z.string(), styleSchema).superRefine((styles, ctx) => {\n for (const name of Object.keys(styles)) {\n if (!STYLE_NAME_RE.test(name)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [name],\n message: `Invalid style name \"${name}\": must be lowercase alphanumeric (a-z, 0-9, -, _)`,\n })\n }\n }\n})\n\nconst widthSchema = z.number().int().min(MIN_VARIANT_WIDTH).max(MAX_VARIANT_WIDTH)\n\n/** Ascending and distinct — the order a `srcset` and the generator both assume. */\nfunction isAscendingDistinct(widths: readonly number[]): boolean {\n return widths.every((w, i) => i === 0 || w > (widths[i - 1] ?? Number.NEGATIVE_INFINITY))\n}\n\nconst widthListSchema = z\n .array(widthSchema)\n .min(1)\n .max(MAX_LADDER_RUNGS)\n .refine(isAscendingDistinct, { message: 'Widths must be ascending, with no repeats' })\n\nconst shapeSchema = z\n .object({\n ratio: z.string().refine((r) => parseShapeRatio(r) !== null, {\n message: 'Ratio must be W:H in positive whole numbers, e.g. 3:2',\n }),\n widths: widthListSchema.optional(),\n format: z.enum(['webp', 'jpeg', 'png', 'avif']).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .strict()\n\n/**\n * Zod schema for the `vocabulary` value — the shape vocabulary and the width\n * ladder (images D003). `.strict()` so a stored value carries nothing the\n * generator does not read.\n */\nconst vocabularySchema = z\n .object({\n shapes: z.record(z.string(), shapeSchema).superRefine((shapes, ctx) => {\n const names = Object.keys(shapes)\n if (names.length > MAX_SHAPES) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `At most ${MAX_SHAPES} shapes`,\n })\n }\n for (const name of names) {\n if (!SHAPE_NAME_PATTERN.test(name) || name === NATURAL_SHAPE) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [name],\n message: `Invalid shape name \"${name}\": lowercase alphanumeric (a-z, 0-9, -, _), and not \"${NATURAL_SHAPE}\"`,\n })\n }\n }\n }),\n ladder: widthListSchema,\n })\n .strict()\n .superRefine((vocabulary, ctx) => {\n // Every (shape × width) is a file per photograph. Refuse a vocabulary that\n // on its OWN plans more than the per-image cap, rather than store it and\n // cut it on every upload. `natural` counts: it runs over the ladder too.\n // The fixed styles are a separate key and add one each on top; the\n // planner trims those (largest first) and `resolveVariantConfig` warns.\n const planned = plannedFileCount(vocabulary)\n if (planned > MAX_VARIANTS_PER_IMAGE) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `These shapes and widths make ${planned} files per photograph; the most is ${MAX_VARIANTS_PER_IMAGE}. Remove a shape or a width.`,\n })\n }\n })\n\n/**\n * Curated default image style presets for new projects.\n *\n * Consumers can register all of these via the media plugin config:\n * media({ imageStyles: defaultImageStyles })\n *\n * Or pick a subset:\n * media({ imageStyles: { thumbnail: defaultImageStyles.thumbnail } })\n *\n * The `imageStylesSettings` default below intentionally only includes\n * `thumbnail` — adding more here would auto-create them for every project\n * that uses the plugin without opting in.\n */\nexport const defaultImageStyles: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n card: { width: 600, height: 400, fit: 'cover', format: 'webp', quality: 85 },\n hero: { width: 1920, height: 800, fit: 'cover', format: 'webp', quality: 85 },\n full: { width: 2400, fit: 'inside', format: 'webp', quality: 90 },\n}\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n iconName: 'image-down',\n schema: {\n /**\n * The shape vocabulary and width ladder (images D003). Deliberately NO\n * `default`: an unset key reads as `null`, which is what lets the waterfall\n * reach the plugin config (`media({ vocabulary })`) and then the seeded\n * `DEFAULT_IMAGE_VOCABULARY` — see `resolveVariantConfig`.\n */\n vocabulary: setting.json<ImageVocabulary>({\n label: 'Shapes and widths',\n schema: vocabularySchema,\n renderer: 'media.imageVocabulary',\n }),\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: defaultImageStyles.thumbnail,\n },\n label: 'Image processing presets',\n schema: imageStylesSchema,\n renderer: 'media.imageStyles',\n }),\n },\n})\n"],"mappings":"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"}
|
package/dist/image-styles.d.mts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
+
import { a as ImageVocabulary, i as ImageStyle } from "./types-6PZQ4Zq6.mjs";
|
|
1
2
|
|
|
2
|
-
//#region src/types.d.ts
|
|
3
|
-
/** Named image processing preset — defines how to resize/convert an image variant */
|
|
4
|
-
interface ImageStyle {
|
|
5
|
-
/** Max width in pixels (omit for height-only constraint) */
|
|
6
|
-
width?: number;
|
|
7
|
-
/** Max height in pixels (omit for height-only constraint) */
|
|
8
|
-
height?: number;
|
|
9
|
-
/** Resize strategy (default: 'cover') */
|
|
10
|
-
fit?: 'cover' | 'contain' | 'inside' | 'outside' | 'fill';
|
|
11
|
-
/** Output format (default: 'webp') */
|
|
12
|
-
format?: 'webp' | 'jpeg' | 'png' | 'avif';
|
|
13
|
-
/** Output quality 1–100 (default: 80) */
|
|
14
|
-
quality?: number;
|
|
15
|
-
}
|
|
16
|
-
//#endregion
|
|
17
3
|
//#region src/image-styles/types.d.ts
|
|
18
4
|
interface ImageStylesManagerLabels {
|
|
19
5
|
title?: string;
|
|
@@ -70,6 +56,17 @@ interface RegenerateVariantsActionProps {
|
|
|
70
56
|
/** i18n labels with English defaults. */
|
|
71
57
|
labels?: RegenerateVariantsActionLabels;
|
|
72
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Controlled props for the {@link ImageVocabularyEditor} — the
|
|
61
|
+
* `SettingFieldRendererProps<ImageVocabulary>` contract, so it drops into the
|
|
62
|
+
* `vocabulary` setting's `renderer` slot. `value` is `null` until saved.
|
|
63
|
+
*/
|
|
64
|
+
interface ImageVocabularyEditorProps {
|
|
65
|
+
value: ImageVocabulary | null | undefined;
|
|
66
|
+
onChange: (value: ImageVocabulary) => void;
|
|
67
|
+
error?: string | undefined;
|
|
68
|
+
className?: string | undefined;
|
|
69
|
+
}
|
|
73
70
|
//#endregion
|
|
74
71
|
//#region src/image-styles/image-styles-manager.d.ts
|
|
75
72
|
/**
|
|
@@ -86,18 +83,35 @@ declare function ImageStylesManager({
|
|
|
86
83
|
classNames
|
|
87
84
|
}: ImageStylesManagerProps): import("react/jsx-runtime").JSX.Element;
|
|
88
85
|
//#endregion
|
|
86
|
+
//#region src/image-styles/image-vocabulary-editor.d.ts
|
|
87
|
+
/** Every catalogue key this component reads — exported for the catalogue test. */
|
|
88
|
+
declare const IMAGE_VOCABULARY_MESSAGE_KEYS: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* The editor for `media.imageStyles` → `vocabulary`: the shape vocabulary and
|
|
91
|
+
* the width ladder (images D003). Controlled — every valid edit commits through
|
|
92
|
+
* `onChange`, and the settings form's Save persists it; an invalid one stays in
|
|
93
|
+
* its own field with the reason beside it and commits nothing.
|
|
94
|
+
*
|
|
95
|
+
* `value` is `null` until something is saved, because the key deliberately
|
|
96
|
+
* has no settings default (so the plugin-config tier stays reachable). The
|
|
97
|
+
* editor then shows the seeded vocabulary and says so.
|
|
98
|
+
*/
|
|
99
|
+
declare function ImageVocabularyEditor(props: ImageVocabularyEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
100
|
+
//#endregion
|
|
89
101
|
//#region src/image-styles/regenerate-variants-action.d.ts
|
|
90
102
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
103
|
+
* The header action on `/admin/settings/media.imageStyles`.
|
|
104
|
+
*
|
|
105
|
+
* Says, always and before anyone clicks, that sizes come from the queue
|
|
106
|
+
* worker — an operator who has never heard of the worker must learn it here,
|
|
107
|
+
* not from a slow page (images C6). The button then asks the server to catch
|
|
108
|
+
* the library up: with a queue that QUEUES a background backfill and returns at
|
|
109
|
+
* once (202); only an app with no queue runs it inside the request, as before.
|
|
96
110
|
*/
|
|
97
111
|
declare function RegenerateVariantsAction({
|
|
98
112
|
apiBasePath,
|
|
99
113
|
labels: userLabels
|
|
100
114
|
}: RegenerateVariantsActionProps): import("react/jsx-runtime").JSX.Element;
|
|
101
115
|
//#endregion
|
|
102
|
-
export { ImageStylesManager, type ImageStylesManagerClassNames, type ImageStylesManagerLabels, type ImageStylesManagerProps, RegenerateVariantsAction, type RegenerateVariantsActionLabels, type RegenerateVariantsActionProps };
|
|
116
|
+
export { IMAGE_VOCABULARY_MESSAGE_KEYS, ImageStylesManager, type ImageStylesManagerClassNames, type ImageStylesManagerLabels, type ImageStylesManagerProps, ImageVocabularyEditor, type ImageVocabularyEditorProps, RegenerateVariantsAction, type RegenerateVariantsActionLabels, type RegenerateVariantsActionProps };
|
|
103
117
|
//# sourceMappingURL=image-styles.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-styles.d.mts","names":[],"sources":["../src/types.ts","../src/image-styles/
|
|
1
|
+
{"version":3,"file":"image-styles.d.mts","names":[],"sources":["../src/image-styles/types.ts","../src/image-styles/image-styles-manager.tsx","../src/image-styles/image-vocabulary-editor.tsx","../src/image-styles/regenerate-variants-action.tsx"],"mappings":";;;UAEiB,wBAAA;EACf,KAAA;EACA,WAAA;EACA,QAAA;EACA,SAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,GAAA;EACA,MAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA;EACA,MAAA;EACA,kBAAA;EACA,wBAAA;EACA,WAAA;EACA,QAAA;EACA,EAAA;AAAA;AAAA,UAGe,4BAAA;EACf,IAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;;UASe,uBAAA;EATf;EAWA,KAAA,EAAO,MAAA,SAAe,UAAA;EAXf;EAaP,QAAA,GAAW,KAAA,EAAO,MAAA,SAAe,UAAA;EAJK;EAMtC,KAAA;EAJsB;EAMtB,MAAA,GAAS,wBAAA;EAJwB;EAMjC,UAAA,GAAa,4BAAA;AAAA;AAAA,UAGE,8BAAA;EACf,UAAA;EACA,YAAY;AAAA;AAAA,UAGG,6BAAA;EAhBO;EAkBtB,WAAA;EAhBkB;EAkBlB,MAAA,GAAS,8BAA8B;AAAA;;;;;;UAQxB,0BAAA;EACf,KAAA,EAAO,eAAA;EACP,QAAA,GAAW,KAAA,EAAO,eAAe;EACjC,KAAA;EACA,SAAA;AAAA;;;;;AArEF;;;;iBCwDgB,kBAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,KAAA;EACA,MAAA,EAAQ,UAAA;EACR;AAAA,GACC,uBAAA,+BAAuB,GAAA,CAAA,OAAA;;;;cCKb,6BAAA;;;;;;;;;;;iBAsFG,qBAAA,CAAsB,KAAA,EAAO,0BAA0B,+BAAA,GAAA,CAAA,OAAA;;;AFzJvE;;;;;;;;;AAAA,iBGkEgB,wBAAA,CAAA;EACd,WAAA;EACA,MAAA,EAAQ;AAAA,GACP,6BAAA,+BAA6B,GAAA,CAAA,OAAA"}
|
package/dist/image-styles.mjs
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import{Badge as e,Button as t,Dialog as n,DialogContent as r,DialogDescription as i,DialogFooter as a,DialogHeader as o,DialogTitle as s,Input as c,Label as l,Select as u,Table as d,TableBody as f,TableCell as p,TableHead as m,TableHeader as ee,TableRow as h,cn as g}from"@murumets-ee/ui";import{Loader2 as _,Pencil as v,Plus as y,RefreshCw as b,Trash2 as x}from"lucide-react";import{useCallback as S,useState as C}from"react";import{Fragment as w,jsx as T,jsxs as E}from"react/jsx-runtime";const te=[`cover`,`contain`,`inside`,`outside`,`fill`],ne=[`webp`,`jpeg`,`png`,`avif`],re={title:`Image Styles`,description:`Configure image processing presets. Variants are generated on upload for each style. Save the form to apply changes.`,addStyle:`Add Style`,editStyle:`Edit Style`,styleName:`Style Name`,width:`Width`,height:`Height`,fit:`Fit`,format:`Format`,quality:`Quality`,save:`Save`,cancel:`Cancel`,delete:`Delete`,deleteConfirmTitle:`Delete Image Style`,deleteConfirmDescription:`Are you sure? This will not remove existing variant files.`,systemBadge:`system`,noStyles:`No image styles configured.`,px:`px`};function D({value:_,onChange:b,error:w,labels:D,classNames:O}){let k={...re,...D},A=_??{},[ie,j]=C(!1),[M,N]=C(null),[P,F]=C(``),[I,L]=C(``),[R,z]=C(``),[B,V]=C(`cover`),[H,U]=C(`webp`),[W,G]=C(`80`),[K,q]=C(null),[J,Y]=C(!1),[X,Z]=C(null),ae=S(()=>{N(null),F(``),L(``),z(``),V(`cover`),U(`webp`),G(`80`),q(null),j(!0)},[]),Q=S(e=>{let t=A[e];t&&(N(e),F(e),L(t.width?.toString()??``),z(t.height?.toString()??``),V(t.fit??`cover`),U(t.format??`webp`),G((t.quality??80).toString()),q(null),j(!0))},[A]),oe=S(()=>{q(null);let e=P.trim().toLowerCase();if(!e||!/^[a-z][a-z0-9_-]*$/.test(e)){q(`Name must be lowercase alphanumeric (a-z, 0-9, -, _)`);return}if(e!==M&&A[e]){q(`A style named "${e}" already exists`);return}let t=I?Number.parseInt(I,10):void 0,n=R?Number.parseInt(R,10):void 0;if(!t&&!n){q(`At least width or height is required`);return}if(t!==void 0&&(Number.isNaN(t)||t<=0)){q(`Width must be a positive number`);return}if(n!==void 0&&(Number.isNaN(n)||n<=0)){q(`Height must be a positive number`);return}let r=Number.parseInt(W,10);if(Number.isNaN(r)||r<1||r>100){q(`Quality must be 1-100`);return}let i=B,a=H,o={...t?{width:t}:{},...n?{height:n}:{},fit:i,format:a,quality:r},s={...A};M&&M!==e&&delete s[M],s[e]=o,b(s),j(!1)},[A,M,P,I,R,B,H,W,b]),se=S(e=>{Z(e),Y(!0)},[]),ce=S(()=>{if(!X)return;let e={...A};delete e[X],b(e),Y(!1),Z(null)},[X,A,b]),$=Object.entries(A).sort(([e],[t])=>e.localeCompare(t));return E(`div`,{className:g(`space-y-4`,O?.root),children:[E(`div`,{className:g(`flex items-start justify-between`,O?.header),children:[T(`p`,{className:`text-sm text-muted-foreground`,children:k.description}),E(t,{onClick:ae,size:`sm`,type:`button`,children:[T(y,{className:`h-4 w-4 mr-1`}),k.addStyle]})]}),w&&T(`div`,{className:`rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive`,children:w}),$.length===0?T(`p`,{className:`text-sm text-muted-foreground py-8 text-center`,children:k.noStyles}):T(`div`,{className:g(`rounded-md border`,O?.table),children:E(d,{children:[T(ee,{children:E(h,{children:[T(m,{children:k.styleName}),T(m,{className:`text-center`,children:k.width}),T(m,{className:`text-center`,children:k.height}),T(m,{className:`text-center`,children:k.fit}),T(m,{className:`text-center`,children:k.format}),T(m,{className:`text-center`,children:k.quality}),T(m,{className:`w-24`})]})}),T(f,{children:$.map(([n,r])=>E(h,{children:[E(p,{className:`font-mono text-sm`,children:[n,n===`thumbnail`&&T(e,{variant:`secondary`,className:`ml-2 text-xs`,children:k.systemBadge})]}),T(p,{className:`text-center tabular-nums`,children:r.width?`${r.width}${k.px}`:`—`}),T(p,{className:`text-center tabular-nums`,children:r.height?`${r.height}${k.px}`:`—`}),T(p,{className:`text-center text-sm text-muted-foreground`,children:r.fit??`cover`}),T(p,{className:`text-center text-sm text-muted-foreground`,children:r.format??`webp`}),T(p,{className:`text-center tabular-nums`,children:r.quality??80}),T(p,{children:E(`div`,{className:g(`flex items-center gap-1 justify-end`,O?.actions),children:[T(t,{variant:`ghost`,size:`sm`,type:`button`,onClick:()=>Q(n),title:k.editStyle,children:T(v,{className:`h-3.5 w-3.5`})}),T(t,{variant:`ghost`,size:`sm`,type:`button`,onClick:()=>se(n),title:k.delete,className:`text-destructive hover:text-destructive`,children:T(x,{className:`h-3.5 w-3.5`})})]})})]},n))})]})}),T(n,{open:ie,onOpenChange:j,children:E(r,{className:O?.dialog,children:[E(o,{children:[T(s,{children:M?k.editStyle:k.addStyle}),T(i,{children:M?`Editing "${M}" image style.`:`Create a new image processing preset.`})]}),E(`div`,{className:`space-y-4 py-2`,children:[K&&T(`div`,{className:`text-sm text-destructive`,children:K}),E(`div`,{className:`space-y-2`,children:[T(l,{htmlFor:`style-name`,children:k.styleName}),T(c,{id:`style-name`,value:P,onChange:e=>F(e.target.value),placeholder:`e.g. thumbnail, medium, large`,disabled:!!M})]}),E(`div`,{className:`grid grid-cols-2 gap-4`,children:[E(`div`,{className:`space-y-2`,children:[E(l,{htmlFor:`style-width`,children:[k.width,` (`,k.px,`)`]}),T(c,{id:`style-width`,type:`number`,value:I,onChange:e=>L(e.target.value),placeholder:`e.g. 200`,min:1})]}),E(`div`,{className:`space-y-2`,children:[E(l,{htmlFor:`style-height`,children:[k.height,` (`,k.px,`)`]}),T(c,{id:`style-height`,type:`number`,value:R,onChange:e=>z(e.target.value),placeholder:`e.g. 200`,min:1})]})]}),E(`div`,{className:`grid grid-cols-2 gap-4`,children:[E(`div`,{className:`space-y-2`,children:[T(l,{htmlFor:`style-fit`,children:k.fit}),T(u,{id:`style-fit`,value:B,onChange:e=>V(e.target.value),children:te.map(e=>T(`option`,{value:e,children:e},e))})]}),E(`div`,{className:`space-y-2`,children:[T(l,{htmlFor:`style-format`,children:k.format}),T(u,{id:`style-format`,value:H,onChange:e=>U(e.target.value),children:ne.map(e=>T(`option`,{value:e,children:e},e))})]})]}),E(`div`,{className:`space-y-2`,children:[E(l,{htmlFor:`style-quality`,children:[k.quality,` (1-100)`]}),T(c,{id:`style-quality`,type:`number`,value:W,onChange:e=>G(e.target.value),min:1,max:100})]})]}),E(a,{children:[T(t,{variant:`outline`,type:`button`,onClick:()=>j(!1),children:k.cancel}),T(t,{type:`button`,onClick:oe,children:k.save})]})]})}),T(n,{open:J,onOpenChange:Y,children:E(r,{children:[E(o,{children:[T(s,{children:k.deleteConfirmTitle}),E(i,{children:[k.deleteConfirmDescription,X&&T(`span`,{className:`block mt-2 font-mono text-foreground`,children:X})]})]}),E(a,{children:[T(t,{variant:`outline`,type:`button`,onClick:()=>Y(!1),children:k.cancel}),T(t,{variant:`destructive`,type:`button`,onClick:ce,children:k.delete})]})]})})]})}const O={regenerate:`Regenerate All Variants`,regenerating:`Regenerating...`};function k({apiBasePath:e,labels:n}){let r={...O,...n},[i,a]=C(!1),[o,s]=C(null),[c,l]=C(null),u=S(async()=>{a(!0),s(null),l(null);try{let t=await fetch(`${e}/media/regenerate-variants`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({})});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(e.error??`Regeneration failed (${t.status})`)}s(await t.json())}catch(e){l(e instanceof Error?e.message:`Regeneration failed`)}finally{a(!1)}},[e]);return E(`div`,{className:`flex items-center gap-3`,children:[o&&E(`span`,{className:`text-xs text-muted-foreground`,children:[`Processed `,o.processed,` of `,o.total,o.skipped>0&&`, ${o.skipped} skipped`,o.errors>0&&E(`span`,{className:`text-destructive`,children:[`, `,o.errors,` errors`]})]}),c&&T(`span`,{className:`text-xs text-destructive`,children:c}),T(t,{variant:`outline`,size:`sm`,type:`button`,onClick:u,disabled:i,children:i?E(w,{children:[T(_,{className:`h-4 w-4 mr-1 animate-spin`}),r.regenerating]}):E(w,{children:[T(b,{className:`h-4 w-4 mr-1`}),r.regenerate]})})]})}export{D as ImageStylesManager,k as RegenerateVariantsAction};
|
|
1
|
+
"use client";import{c as e,i as t,l as n,o as r,r as i}from"./shapes-CqppzFzb.mjs";import"./variant-plan-86tJKtVi.mjs";import{Loader2 as a,Pencil as o,Plus as s,RefreshCw as c,Trash2 as l}from"lucide-react";import{Fragment as u,jsx as d,jsxs as f}from"react/jsx-runtime";import{useCallback as p,useId as m,useRef as h,useState as g}from"react";import{Badge as _,Button as v,Dialog as y,DialogContent as b,DialogDescription as x,DialogFooter as S,DialogHeader as C,DialogTitle as w,Input as T,Label as E,Select as D,Table as O,TableBody as k,TableCell as A,TableHead as j,TableHeader as M,TableRow as N,cn as P}from"@murumets-ee/ui";import{useTranslations as F}from"next-intl";const I=[`cover`,`contain`,`inside`,`outside`,`fill`],L=[`webp`,`jpeg`,`png`,`avif`],ee={title:`Image Styles`,description:`Configure image processing presets. Every image gets a file for each style, generated in the background by the queue worker. Save the form to apply changes.`,addStyle:`Add Style`,editStyle:`Edit Style`,styleName:`Style Name`,width:`Width`,height:`Height`,fit:`Fit`,format:`Format`,quality:`Quality`,save:`Save`,cancel:`Cancel`,delete:`Delete`,deleteConfirmTitle:`Delete Image Style`,deleteConfirmDescription:`Are you sure? This will not remove existing variant files.`,systemBadge:`system`,noStyles:`No image styles configured.`,px:`px`};function R({value:e,onChange:t,error:n,labels:r,classNames:i}){let a={...ee,...r},c=e??{},[u,m]=g(!1),[h,F]=g(null),[R,z]=g(``),[B,V]=g(``),[H,U]=g(``),[W,G]=g(`cover`),[K,q]=g(`webp`),[J,Y]=g(`80`),[X,Z]=g(null),[te,Q]=g(!1),[$,ne]=g(null),re=p(()=>{F(null),z(``),V(``),U(``),G(`cover`),q(`webp`),Y(`80`),Z(null),m(!0)},[]),ie=p(e=>{let t=c[e];t&&(F(e),z(e),V(t.width?.toString()??``),U(t.height?.toString()??``),G(t.fit??`cover`),q(t.format??`webp`),Y((t.quality??80).toString()),Z(null),m(!0))},[c]),ae=p(()=>{Z(null);let e=R.trim().toLowerCase();if(!e||!/^[a-z][a-z0-9_-]*$/.test(e)){Z(`Name must be lowercase alphanumeric (a-z, 0-9, -, _)`);return}if(e!==h&&c[e]){Z(`A style named "${e}" already exists`);return}let n=B?Number.parseInt(B,10):void 0,r=H?Number.parseInt(H,10):void 0;if(!n&&!r){Z(`At least width or height is required`);return}if(n!==void 0&&(Number.isNaN(n)||n<=0)){Z(`Width must be a positive number`);return}if(r!==void 0&&(Number.isNaN(r)||r<=0)){Z(`Height must be a positive number`);return}let i=Number.parseInt(J,10);if(Number.isNaN(i)||i<1||i>100){Z(`Quality must be 1-100`);return}let a=W,o=K,s={...n?{width:n}:{},...r?{height:r}:{},fit:a,format:o,quality:i},l={...c};h&&h!==e&&delete l[h],l[e]=s,t(l),m(!1)},[c,h,R,B,H,W,K,J,t]),oe=p(e=>{ne(e),Q(!0)},[]),se=p(()=>{if(!$)return;let e={...c};delete e[$],t(e),Q(!1),ne(null)},[$,c,t]),ce=Object.entries(c).sort(([e],[t])=>e.localeCompare(t));return f(`div`,{className:P(`space-y-4`,i?.root),children:[f(`div`,{className:P(`flex items-start justify-between`,i?.header),children:[d(`p`,{className:`text-sm text-muted-foreground`,children:a.description}),f(v,{onClick:re,size:`sm`,type:`button`,children:[d(s,{className:`h-4 w-4 mr-1`}),a.addStyle]})]}),n&&d(`div`,{className:`rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive`,children:n}),ce.length===0?d(`p`,{className:`text-sm text-muted-foreground py-8 text-center`,children:a.noStyles}):d(`div`,{className:P(`rounded-md border`,i?.table),children:f(O,{children:[d(M,{children:f(N,{children:[d(j,{children:a.styleName}),d(j,{className:`text-center`,children:a.width}),d(j,{className:`text-center`,children:a.height}),d(j,{className:`text-center`,children:a.fit}),d(j,{className:`text-center`,children:a.format}),d(j,{className:`text-center`,children:a.quality}),d(j,{className:`w-24`})]})}),d(k,{children:ce.map(([e,t])=>f(N,{children:[f(A,{className:`font-mono text-sm`,children:[e,e===`thumbnail`&&d(_,{variant:`secondary`,className:`ml-2 text-xs`,children:a.systemBadge})]}),d(A,{className:`text-center tabular-nums`,children:t.width?`${t.width}${a.px}`:`—`}),d(A,{className:`text-center tabular-nums`,children:t.height?`${t.height}${a.px}`:`—`}),d(A,{className:`text-center text-sm text-muted-foreground`,children:t.fit??`cover`}),d(A,{className:`text-center text-sm text-muted-foreground`,children:t.format??`webp`}),d(A,{className:`text-center tabular-nums`,children:t.quality??80}),d(A,{children:f(`div`,{className:P(`flex items-center gap-1 justify-end`,i?.actions),children:[d(v,{variant:`ghost`,size:`sm`,type:`button`,onClick:()=>ie(e),title:a.editStyle,children:d(o,{className:`h-3.5 w-3.5`})}),d(v,{variant:`ghost`,size:`sm`,type:`button`,onClick:()=>oe(e),title:a.delete,className:`text-destructive hover:text-destructive`,children:d(l,{className:`h-3.5 w-3.5`})})]})})]},e))})]})}),d(y,{open:u,onOpenChange:m,children:f(b,{className:i?.dialog,children:[f(C,{children:[d(w,{children:h?a.editStyle:a.addStyle}),d(x,{children:h?`Editing "${h}" image style.`:`Create a new image processing preset.`})]}),f(`div`,{className:`space-y-4 py-2`,children:[X&&d(`div`,{className:`text-sm text-destructive`,children:X}),f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`style-name`,children:a.styleName}),d(T,{id:`style-name`,value:R,onChange:e=>z(e.target.value),placeholder:`e.g. thumbnail, medium, large`,disabled:!!h})]}),f(`div`,{className:`grid grid-cols-2 gap-4`,children:[f(`div`,{className:`space-y-2`,children:[f(E,{htmlFor:`style-width`,children:[a.width,` (`,a.px,`)`]}),d(T,{id:`style-width`,type:`number`,value:B,onChange:e=>V(e.target.value),placeholder:`e.g. 200`,min:1})]}),f(`div`,{className:`space-y-2`,children:[f(E,{htmlFor:`style-height`,children:[a.height,` (`,a.px,`)`]}),d(T,{id:`style-height`,type:`number`,value:H,onChange:e=>U(e.target.value),placeholder:`e.g. 200`,min:1})]})]}),f(`div`,{className:`grid grid-cols-2 gap-4`,children:[f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`style-fit`,children:a.fit}),d(D,{id:`style-fit`,value:W,onChange:e=>G(e.target.value),children:I.map(e=>d(`option`,{value:e,children:e},e))})]}),f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`style-format`,children:a.format}),d(D,{id:`style-format`,value:K,onChange:e=>q(e.target.value),children:L.map(e=>d(`option`,{value:e,children:e},e))})]})]}),f(`div`,{className:`space-y-2`,children:[f(E,{htmlFor:`style-quality`,children:[a.quality,` (1-100)`]}),d(T,{id:`style-quality`,type:`number`,value:J,onChange:e=>Y(e.target.value),min:1,max:100})]})]}),f(S,{children:[d(v,{variant:`outline`,type:`button`,onClick:()=>m(!1),children:a.cancel}),d(v,{type:`button`,onClick:ae,children:a.save})]})]})}),d(y,{open:te,onOpenChange:Q,children:f(b,{children:[f(C,{children:[d(w,{children:a.deleteConfirmTitle}),f(x,{children:[a.deleteConfirmDescription,$&&d(`span`,{className:`block mt-2 font-mono text-foreground`,children:$})]})]}),f(S,{children:[d(v,{variant:`outline`,type:`button`,onClick:()=>Q(!1),children:a.cancel}),d(v,{variant:`destructive`,type:`button`,onClick:se,children:a.delete})]})]})})]})}const z={description:[`Media.imageVocabulary.description`,`Every photograph is generated in each shape below, at each width. A slot whose ratio matches a shape draws that shape’s files; a slot with any other ratio is cropped in the browser instead.`],usingDefaults:[`Media.imageVocabulary.usingDefaults`,`Nothing is saved here yet. Shown are the toolkit defaults; until you save, the site uses them — or the shapes configured in its code, if it has any.`],widthsLabel:[`Media.imageVocabulary.widthsLabel`,`Widths (px)`],widthsHelp:[`Media.imageVocabulary.widthsHelp`,`Ascending, separated by commas. A photograph is never enlarged — a width it cannot supply is skipped.`],widthsInvalid:[`Media.imageVocabulary.widthsInvalid`,`Use up to {count} ascending whole numbers between {min} and {max}.`],shapesLabel:[`Media.imageVocabulary.shapesLabel`,`Shapes`],naturalNote:[`Media.imageVocabulary.naturalNote`,`Plus “natural” — each photograph at its own shape, over the same widths.`],columnName:[`Media.imageVocabulary.columnName`,`Name`],columnRatio:[`Media.imageVocabulary.columnRatio`,`Ratio (W:H)`],columnWidths:[`Media.imageVocabulary.columnWidths`,`Own widths`],ownWidthsPlaceholder:[`Media.imageVocabulary.ownWidthsPlaceholder`,`All widths`],ratioInvalid:[`Media.imageVocabulary.ratioInvalid`,`Write the ratio as W:H, for example 3:2.`],nameInvalid:[`Media.imageVocabulary.nameInvalid`,`Lowercase letters, digits, - and _, starting with a letter — and not “natural”.`],nameTaken:[`Media.imageVocabulary.nameTaken`,`A shape named “{name}” already exists.`],tooMany:[`Media.imageVocabulary.tooMany`,`At most {count} shapes.`],tooManyFiles:[`Media.imageVocabulary.tooManyFiles`,`That makes {count} files for every photograph; the most is {max}. Remove a shape or a width first.`],add:[`Media.imageVocabulary.add`,`Add shape`],remove:[`Media.imageVocabulary.remove`,`Remove {name}`]},B=Object.values(z).map(([e])=>e);function V(e,t){return t?e.replace(/\{(\w+)\}/g,(e,n)=>n in t?String(t[n]):e):e}function H(){let e=F();return(t,n)=>{let[r,i]=z[t];return e.has(r)?e(r,n):V(i,n)}}function U(e){let t=e.split(`,`).map(e=>e.trim()).filter(e=>e!==``);if(t.length===0||t.length>12)return null;let n=[];for(let e of t){if(!/^\d+$/.test(e))return null;let t=Number(e);if(t<16||t>8192)return null;let r=n[n.length-1];if(r!==void 0&&t<=r)return null;n.push(t)}return n}function W(e,t){return e.length===t.length&&e.every((e,n)=>e===t[n])}function G(e){return e?e.join(`, `):``}function K(e){return t=>{t.key!==`Enter`||t.nativeEvent.isComposing||t.keyCode===229||(t.preventDefault(),e())}}function q(){return{shapes:Object.fromEntries(Object.entries(i.shapes).map(([e,t])=>[e,{...t}])),ladder:[...i.ladder]}}function J(e){let t=JSON.stringify(e.value??null),[n,r]=g(null),[i,a]=g(t),[o,s]=g(0);t!==i&&(a(t),t!==n&&s(o+1));let c=t=>{r(JSON.stringify(t)),e.onChange(t)};return d(Y,{...e,onChange:c},o)}function Y({value:i,onChange:a,error:o,className:c}){let l=H(),u=m(),p=i??q(),_=Object.keys(p.shapes).sort(),[y,b]=g(G(p.ladder)),[x,S]=g(!1),[C,w]=g(``),[D,O]=g(``),[k,A]=g(null),[j,M]=g(null),N=h(null),F=e=>{let t=n(e);return t>64?(M(l(`tooManyFiles`,{count:t,max:64})),!1):(M(null),a(e),!0)},I=()=>{let e=U(y);S(e===null),e&&!W(e,p.ladder)&&F({...p,ladder:e})},L=()=>{let t=C.trim(),n=D.trim();if(!r.test(t)||t===`natural`){A(l(`nameInvalid`));return}if(Object.hasOwn(p.shapes,t)){A(l(`nameTaken`,{name:t}));return}if(_.length>=16){A(l(`tooMany`,{count:16}));return}if(e(n)===null){A(l(`ratioInvalid`));return}A(null),F({...p,shapes:{...p.shapes,[t]:{ratio:n}}})&&(w(``),O(``))},ee=(e,t)=>F({...p,shapes:{...p.shapes,[e]:t}}),R=e=>{let{[e]:t,...n}=p.shapes;F({...p,shapes:n})};return f(`div`,{className:P(`space-y-6`,c),children:[f(`div`,{className:`space-y-1 text-sm text-muted-foreground`,children:[d(`p`,{children:l(`description`)}),i==null?d(`p`,{children:l(`usingDefaults`)}):null]}),j?d(`p`,{role:`alert`,className:`text-sm text-destructive`,children:j}):null,o?d(`div`,{className:`rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive`,children:o}):null,f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`${u}-ladder`,children:l(`widthsLabel`)}),d(T,{id:`${u}-ladder`,value:y,onChange:e=>b(e.target.value),onBlur:I,onKeyDown:K(I),"aria-invalid":x,"aria-describedby":`${u}-ladder-help`,inputMode:`numeric`}),d(`p`,{id:`${u}-ladder-help`,className:P(`text-xs`,x?`text-destructive`:`text-muted-foreground`),children:x?l(`widthsInvalid`,{count:12,min:16,max:t}):l(`widthsHelp`)})]}),f(`fieldset`,{className:`space-y-2`,children:[d(`legend`,{className:`text-sm font-medium`,children:l(`shapesLabel`)}),f(`div`,{className:`grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] gap-2 text-xs text-muted-foreground`,children:[d(`span`,{children:l(`columnName`)}),d(`span`,{children:l(`columnRatio`)}),d(`span`,{children:l(`columnWidths`)}),d(`span`,{})]}),_.map(e=>{let t=p.shapes[e];return t?d(X,{name:e,shape:t,copy:l,onReplace:t=>ee(e,t),onRemove:()=>R(e)},e):null}),d(`p`,{className:`text-xs text-muted-foreground`,children:l(`naturalNote`)}),f(`div`,{className:`grid grid-cols-[minmax(6rem,1fr)_8rem_auto] items-start gap-2 pt-2`,children:[d(T,{"aria-label":l(`columnName`),value:C,onChange:e=>w(e.target.value),onKeyDown:K(()=>N.current?.focus())}),d(T,{ref:N,"aria-label":l(`columnRatio`),value:D,onChange:e=>O(e.target.value),onKeyDown:K(L),placeholder:`3:2`}),f(v,{type:`button`,size:`sm`,onClick:L,children:[d(s,{className:`mr-1 h-4 w-4`}),l(`add`)]})]}),k?d(`p`,{role:`alert`,className:`text-xs text-destructive`,children:k}):null]})]})}function X({name:n,shape:r,copy:i,onReplace:a,onRemove:o}){let[s,c]=g(r.ratio),[u,p]=g(G(r.widths)),[m,h]=g(null),_=()=>{let t=e(s.trim())!==null;h(t?null:`ratioInvalid`),t&&s.trim()!==r.ratio&&a({...r,ratio:s.trim()})},y=()=>{if(u.trim()===``){h(null);let{widths:e,...t}=r;r.widths&&a(t);return}let e=U(u);h(e?null:`widthsInvalid`),e&&!W(e,r.widths??[])&&a({...r,widths:e})};return f(`div`,{className:`space-y-1`,children:[f(`div`,{className:`grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] items-center gap-2`,children:[d(`span`,{className:`font-mono text-sm`,children:n}),d(T,{"aria-label":`${n} ${i(`columnRatio`)}`,value:s,onChange:e=>c(e.target.value),onBlur:_,onKeyDown:K(_),"aria-invalid":m===`ratioInvalid`}),d(T,{"aria-label":`${n} ${i(`columnWidths`)}`,value:u,onChange:e=>p(e.target.value),onBlur:y,onKeyDown:K(y),placeholder:i(`ownWidthsPlaceholder`),"aria-invalid":m===`widthsInvalid`}),d(v,{type:`button`,variant:`ghost`,size:`sm`,onClick:o,title:i(`remove`,{name:n}),"aria-label":i(`remove`,{name:n}),className:`text-destructive hover:text-destructive`,children:d(l,{className:`h-3.5 w-3.5`})})]}),m?d(`p`,{role:`alert`,className:`text-xs text-destructive`,children:i(m,{count:12,min:16,max:t})}):null]})}const Z={workerRequired:[`Media.variantGeneration.workerRequired`,`Image sizes are generated in the background by the queue worker — a separate process that has to run beside the site (pnpm worker). Without it, the site still works, but new and changed images are shown at full size.`],regenerate:[`Media.variantGeneration.regenerate`,`Catch up all images`],regenerating:[`Media.variantGeneration.regenerating`,`Starting…`],queued:[`Media.variantGeneration.queued`,`Started. The queue worker catches the library up in the background; the site keeps serving meanwhile. Follow it on the Queue page.`],alreadyRunning:[`Media.variantGeneration.alreadyRunning`,`A catch-up is already running — follow it on the Queue page.`],inlineDone:[`Media.variantGeneration.inlineDone`,`Processed {processed} of {total}; skipped {skipped}; errors {errors}.`],failed:[`Media.variantGeneration.failed`,`Could not start the catch-up: {reason}`]};Object.values(Z).map(([e])=>e);function te(e,t){return t?e.replace(/\{(\w+)\}/g,(e,n)=>n in t?String(t[n]):e):e}function Q({apiBasePath:e,labels:t}){let n=F(),r=(e,t)=>{let[r,i]=Z[e];return n.has(r)?n(r,t):te(i,t)},[i,o]=g(!1),[s,l]=g(null),m=p(async()=>{o(!0),l(null);try{let t=await fetch(`${e}/media/regenerate-variants`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({})});if(!t.ok){l({kind:`error`,reason:(await t.json().catch(()=>({}))).error??String(t.status)});return}let n=await t.json();l(n.mode===`queued`?{kind:`queued`,alreadyRunning:n.alreadyRunning}:{kind:`inline`,...n})}catch(e){l({kind:`error`,reason:e instanceof Error?e.message:String(e)})}finally{o(!1)}},[e]);return f(`div`,{className:`flex max-w-xl flex-col items-end gap-2`,children:[d(`p`,{className:`text-right text-xs text-muted-foreground`,"data-variant-worker-required":``,children:r(`workerRequired`)}),f(`div`,{className:`flex items-center gap-3`,children:[s?.kind===`queued`&&d(`span`,{className:`text-xs text-muted-foreground`,role:`status`,children:r(s.alreadyRunning?`alreadyRunning`:`queued`)}),s?.kind===`inline`&&d(`span`,{className:`text-xs text-muted-foreground`,role:`status`,children:r(`inlineDone`,{processed:s.processed,total:s.total,skipped:s.skipped,errors:s.errors})}),s?.kind===`error`&&d(`span`,{className:`text-xs text-destructive`,role:`alert`,children:r(`failed`,{reason:s.reason})}),d(v,{variant:`outline`,size:`sm`,type:`button`,onClick:m,disabled:i,children:i?f(u,{children:[d(a,{className:`h-4 w-4 mr-1 animate-spin`}),t?.regenerating??r(`regenerating`)]}):f(u,{children:[d(c,{className:`h-4 w-4 mr-1`}),t?.regenerate??r(`regenerate`)]})})]})]})}export{B as IMAGE_VOCABULARY_MESSAGE_KEYS,R as ImageStylesManager,J as ImageVocabularyEditor,Q as RegenerateVariantsAction};
|
|
3
2
|
//# sourceMappingURL=image-styles.mjs.map
|