@murumets-ee/media 0.70.0 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +1 -1
- package/dist/backfill-D2Ubblh3.mjs +2 -0
- package/dist/backfill-D2Ubblh3.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 +55 -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 +60 -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 +3 -0
- package/dist/crop.mjs +1 -0
- package/dist/definitions-LJpgrdxd.mjs +2 -0
- package/dist/definitions-LJpgrdxd.mjs.map +1 -0
- package/dist/deps-CzHEhyJK.mjs +2 -0
- package/dist/deps-CzHEhyJK.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-A8YzaZ51.mjs +2 -0
- package/dist/en-A8YzaZ51.mjs.map +1 -0
- package/dist/entity-CsDdjKz6.mjs +2 -0
- package/dist/entity-CsDdjKz6.mjs.map +1 -0
- package/dist/et-CXvu8U31.mjs +2 -0
- package/dist/et-CXvu8U31.mjs.map +1 -0
- package/dist/focal-point-BWhtJzef.mjs +2 -0
- package/dist/focal-point-BWhtJzef.mjs.map +1 -0
- package/dist/focal-point-C_0drV03.d.mts +125 -0
- package/dist/focal-point-C_0drV03.d.mts.map +1 -0
- package/dist/generate-variants-zyPjaGcy.mjs +2 -0
- package/dist/generate-variants-zyPjaGcy.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 +16 -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-DmcTxuDM.mjs +2 -0
- package/dist/media-config-DmcTxuDM.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 +108 -99
- 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-Deedtkh4.mjs +2 -0
- package/dist/process-image-Deedtkh4.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 +14 -1
- 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-Cp3sNHkT.mjs +2 -0
- package/dist/regenerate-variants-Cp3sNHkT.mjs.map +1 -0
- package/dist/register-BSGkELj0.mjs +2 -0
- package/dist/register-BSGkELj0.mjs.map +1 -0
- package/dist/resolve-image-styles-pf7Nd3Zf.mjs +2 -0
- package/dist/resolve-image-styles-pf7Nd3Zf.mjs.map +1 -0
- package/dist/routes-BJ23Cp0g.mjs +2 -0
- package/dist/routes-BJ23Cp0g.mjs.map +1 -0
- package/dist/ru-Dj-ax8s_.mjs +2 -0
- package/dist/ru-Dj-ax8s_.mjs.map +1 -0
- package/dist/schedule-DREjg2ji.mjs +2 -0
- package/dist/schedule-DREjg2ji.mjs.map +1 -0
- package/dist/shapes-40dHtTb-.d.mts +90 -0
- package/dist/shapes-40dHtTb-.d.mts.map +1 -0
- package/dist/shapes-BlKW-0C1.mjs +2 -0
- package/dist/shapes-BlKW-0C1.mjs.map +1 -0
- package/dist/slot-CY4isqmN.mjs +2 -0
- package/dist/slot-CY4isqmN.mjs.map +1 -0
- package/dist/types-CgkJF5dc.d.mts +261 -0
- package/dist/types-CgkJF5dc.d.mts.map +1 -0
- package/dist/types-DwzblZfW.d.mts +97 -0
- package/dist/types-DwzblZfW.d.mts.map +1 -0
- package/dist/variant-key-Cki4n-42.mjs +2 -0
- package/dist/variant-key-Cki4n-42.mjs.map +1 -0
- package/dist/variant-plan-DV8hsMdz.mjs +2 -0
- package/dist/variant-plan-DV8hsMdz.mjs.map +1 -0
- package/package.json +25 -8
- package/dist/client-YlSoGADl.mjs +0 -2
- package/dist/client-YlSoGADl.mjs.map +0 -1
- package/dist/en-0YnQ_Qz-.mjs +0 -2
- package/dist/en-0YnQ_Qz-.mjs.map +0 -1
- package/dist/entity-Caba_NFw.mjs +0 -2
- package/dist/entity-Caba_NFw.mjs.map +0 -1
- package/dist/entity-fxw-Qywj.mjs +0 -2
- package/dist/entity-fxw-Qywj.mjs.map +0 -1
- package/dist/et-BIxGYWVK.mjs +0 -2
- package/dist/et-BIxGYWVK.mjs.map +0 -1
- package/dist/image-styles-settings-ClIA2MJI.mjs +0 -2
- package/dist/image-styles-settings-ClIA2MJI.mjs.map +0 -1
- package/dist/plugin-BkEED6qj.mjs +0 -2
- package/dist/plugin-BkEED6qj.mjs.map +0 -1
- package/dist/process-image-DYDTMGUJ.mjs +0 -2
- package/dist/process-image-DYDTMGUJ.mjs.map +0 -1
- package/dist/regenerate-variants-BysyeIoA.mjs +0 -2
- package/dist/regenerate-variants-BysyeIoA.mjs.map +0 -1
- package/dist/regenerate-variants-sit6LbUo.mjs +0 -2
- package/dist/regenerate-variants-sit6LbUo.mjs.map +0 -1
- package/dist/resolve-image-styles-BzSRuXUs.mjs +0 -2
- package/dist/resolve-image-styles-BzSRuXUs.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-RCy0SYsK.mjs +0 -2
- package/dist/routes-RCy0SYsK.mjs.map +0 -1
- package/dist/ru-UUTBfsMF.mjs +0 -2
- package/dist/ru-UUTBfsMF.mjs.map +0 -1
- package/dist/types-1idCpe1k.d.mts +0 -160
- package/dist/types-1idCpe1k.d.mts.map +0 -1
- package/dist/usage-CVAqkS6h.mjs +0 -2
- package/dist/usage-CVAqkS6h.mjs.map +0 -1
- package/dist/variant-key-CLlac3_1.mjs +0 -2
- package/dist/variant-key-CLlac3_1.mjs.map +0 -1
- package/dist/variant-key-JBTJXPL1.mjs +0 -2
- package/dist/variant-key-JBTJXPL1.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":";;;AAyEA;;;;;;;AAAA,iBA1BgB,gBAAA,CAAiB,KAAA,YAAiB,KAAA,IAAS,cAAc;;AA0BY;AAyBrF;;;;;;;;AAA6E;iBAzB7D,aAAA,CAAc,KAAA,YAAiB,KAAA,IAAS,MAAM,SAAS,cAAA;;;;;;;;;AAuFtD;;;;;;;iBA9DD,SAAA,CAAU,KAAA,YAAiB,WAAW,SAAS,cAAA;;;;;;;;;;;;;;;;;iBA2D/C,mBAAA,CACd,IAAA,EAAM,cAAA,EACN,MAAA,0BACC,cAAc"}
|
package/dist/crop.d.mts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
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
|
+
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
ADDED
|
@@ -0,0 +1 @@
|
|
|
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-pf7Nd3Zf.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-CzHEhyJK.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deps-CzHEhyJK.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 that names a shape draws these files; a slot with its own 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-A8YzaZ51.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"en-A8YzaZ51.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, mis nimetab kuju, kasutab neid faile; oma 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-CXvu8U31.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"et-CXvu8U31.mjs","names":[],"sources":["../src/messages/et.json"],"sourcesContent":[""],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=Object.freeze({x:.5,y:.5});function t(e){return typeof e==`number`&&Number.isFinite(e)&&e>=0&&e<=1}function n(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let n=Object.keys(e);if(n.length!==2||!n.includes(`x`)||!n.includes(`y`))return!1;let{x:r,y:i}=e;return t(r)&&t(i)}function r(t){return n(t)?t:e}function i(e){return e.x===.5&&e.y===.5}function a(e,t,n){return Math.min(Math.max(e,t),n)}function o(e,t){return e>0&&Number.isFinite(e)&&t>0&&Number.isFinite(t)?e>t?{w:t/e,h:1}:{w:1,h:e/t}:{w:1,h:1}}function s(e,t){return a(e-t/2,0,1-t)}function c(e,t,n){let r=o(t,n);return{x:s(e.x,r.w),y:s(e.y,r.h),w:r.w,h:r.h}}function l(e,t,n){let r=c(e,t,n);return{x:r.w<1?r.x/(1-r.w):.5,y:r.h<1?r.y/(1-r.h):.5}}function u(e){return`${d(e.x)} ${d(e.y)}`}function d(e){return`${Math.round(e*1e4)/100}%`}export{n as a,o as c,i,c as n,l as o,u as r,r as s,e as t};
|
|
2
|
+
//# sourceMappingURL=focal-point-BWhtJzef.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focal-point-BWhtJzef.mjs","names":[],"sources":["../src/crop/focal-point.ts"],"sourcesContent":["/**\n * The focal point, and every number derived from it — ONE pure module.\n *\n * A photograph's focal point is stored once, on the media row, as a fraction of\n * the image (`{x, y}` in `[0, 1]`, images D002). Two consumers turn it into a\n * crop: the browser, through `object-position` on a shaped slot (this PR), and\n * sharp, through `extract()` before a shaped variant is resized (images PR04).\n * Both import this file, so the generated crop and the CSS crop cannot\n * disagree — they are the same arithmetic, not two copies of it.\n *\n * No DOM, no sharp, no database. It is imported by `@murumets-ee/blocks`' render\n * spine, which ships to every visitor's browser, so it must stay small and must\n * never import anything.\n *\n * ## Fractions, never pixels\n *\n * A pixel rect breaks the moment the source file is replaced at a different\n * resolution, and this toolkit's stored `width`/`height` are client-measured and\n * can be wrong (images R003). A fraction survives both.\n *\n * ## Why `object-position` is NOT `focal × 100%`\n *\n * `object-position` is an ALIGNMENT percentage: `75%` puts the image's 75% point\n * at the BOX's 75% point, not at its centre (css-backgrounds-3, \"the point 75%\n * across … is to be placed at the point 75% across the area\"). The naive\n * `object-position: {f·100}%` is therefore wrong by `|f − 0.5| × box_width` —\n * exactly zero for a centred subject and 50% of the box at the edges, which is\n * why it survives review (R002). The conversion, per axis:\n *\n * ```\n * w_vis = C / I // visible window, as a fraction of the image\n * L = clamp(f − w_vis / 2, 0, 1 − w_vis) // the window's leading edge\n * P = L / (1 − w_vis) // the object-position fraction\n * ```\n *\n * `P` depends on the CONTAINER's ratio through `w_vis`, which is why it is\n * computed at render and never stored (D002 rules out storing it).\n *\n * ## Absent and centre are the same thing\n *\n * {@link readFocalPoint} turns every absent, malformed or out-of-range value into\n * `{0.5, 0.5}`, and the centre produces `object-position: 50% 50%`, which is\n * CSS's initial value. So a legacy row with no focal point and a row whose focal\n * point is the centre render identically, and nothing downstream branches on\n * the difference (D010).\n */\n\n/**\n * A point in the image, as fractions of its width and height. Both in `[0, 1]`.\n *\n * A `type`, not an `interface`, on purpose: it is stored in a `field.json()`\n * column, and only a type alias is assignable to the entity's `JsonValue`\n * (an interface has no implicit index signature).\n */\nexport type FocalPoint = {\n readonly x: number\n readonly y: number\n}\n\n/** A rectangle in the image, as fractions of its width and height. */\nexport type FractionalRect = {\n readonly x: number\n readonly y: number\n readonly w: number\n readonly h: number\n}\n\n/** The default — and what every absent or unusable focal point means. */\nexport const CENTRE_FOCAL_POINT: FocalPoint = Object.freeze({ x: 0.5, y: 0.5 })\n\nfunction isUnitFraction(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1\n}\n\n/**\n * Is `value` a storable focal point — exactly `{x, y}`, both finite fractions?\n *\n * STRICT, because it guards a WRITE (the media row's `focalPoint` column). An\n * extra key is refused rather than stripped: a JSON column that stores what it\n * was sent would otherwise accept `{x, y, notes: '<a megabyte>'}` and hand it to\n * every reader of every page that draws the photograph.\n */\nexport function isFocalPoint(value: unknown): value is FocalPoint {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n const keys = Object.keys(value)\n if (keys.length !== 2 || !keys.includes('x') || !keys.includes('y')) return false\n const { x, y } = value as { x: unknown; y: unknown }\n return isUnitFraction(x) && isUnitFraction(y)\n}\n\n/**\n * The focal point to RENDER with, from whatever a projection carried.\n *\n * LENIENT, because it guards a READ: an absent, null or malformed value is the\n * centre, never a throw. A render that threw on a bad stored value would take\n * the whole page down over one photograph's crop.\n */\nexport function readFocalPoint(value: unknown): FocalPoint {\n return isFocalPoint(value) ? value : CENTRE_FOCAL_POINT\n}\n\n/** Is this the centre — the point that needs no `object-position` at all? */\nexport function isCentreFocalPoint(focal: FocalPoint): boolean {\n return focal.x === 0.5 && focal.y === 0.5\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\n/**\n * The window a `cover` crop leaves visible, as a fraction of the image on each\n * axis.\n *\n * Under `object-fit: cover` the image is scaled until it covers the box, so at\n * most ONE axis overflows and the other is exactly `1`. `imageRatio` and\n * `boxRatio` are width / height. A ratio that is not a positive finite number\n * is a caller bug; it is answered with \"nothing overflows\" (`{1, 1}`), which\n * renders as today's centre crop rather than as `NaN%`.\n */\nexport function visibleWindow(\n imageRatio: number,\n boxRatio: number,\n): { readonly w: number; readonly h: number } {\n if (!(imageRatio > 0 && Number.isFinite(imageRatio) && boxRatio > 0 && Number.isFinite(boxRatio)))\n return { w: 1, h: 1 }\n // Image wider than the box → the horizontal axis overflows.\n if (imageRatio > boxRatio) return { w: boxRatio / imageRatio, h: 1 }\n return { w: 1, h: imageRatio / boxRatio }\n}\n\n/**\n * One axis of the conversion: the window's leading edge `L`, clamped so the\n * window never leaves the image.\n *\n * Clamping `L` to `[0, 1 − w_vis]` is exactly equivalent to clamping the\n * resulting `object-position` to `[0, 1]`, so \"never reveal background\" has one\n * spelling here rather than two (R002).\n */\nfunction windowStart(focal: number, visible: number): number {\n return clamp(focal - visible / 2, 0, 1 - visible)\n}\n\n/**\n * The crop a focal point implies for a box of `boxRatio`, as a fractional rect\n * of the image — the region sharp will `extract()` (images PR04) and the region\n * the browser shows through {@link objectPositionFor}.\n */\nexport function focalCropRect(\n focal: FocalPoint,\n imageRatio: number,\n boxRatio: number,\n): FractionalRect {\n const visible = visibleWindow(imageRatio, boxRatio)\n return {\n x: windowStart(focal.x, visible.w),\n y: windowStart(focal.y, visible.h),\n w: visible.w,\n h: visible.h,\n }\n}\n\n/**\n * The `object-position` fractions that make a `cover`-fitted image show\n * {@link focalCropRect}'s window.\n *\n * An axis that does not overflow (`w_vis == 1`) has no position to choose —\n * `object-position` is inert on it — so it answers `0.5`, CSS's own default.\n * That keeps the centre case literally `{0.5, 0.5}` and the emitted CSS\n * byte-comparable with a slot that has no focal point.\n */\nexport function objectPositionFor(\n focal: FocalPoint,\n imageRatio: number,\n boxRatio: number,\n): FocalPoint {\n const rect = focalCropRect(focal, imageRatio, boxRatio)\n return {\n x: rect.w < 1 ? rect.x / (1 - rect.w) : 0.5,\n y: rect.h < 1 ? rect.y / (1 - rect.h) : 0.5,\n }\n}\n\n/**\n * Format a position as a CSS `object-position` value: two percentages, rounded\n * to two decimals so the emitted markup is stable across floating-point noise\n * (`33.33% 50%`, not `33.333333333333336% 50%`).\n */\nexport function formatObjectPosition(position: FocalPoint): string {\n return `${percent(position.x)} ${percent(position.y)}`\n}\n\nfunction percent(fraction: number): string {\n return `${Math.round(fraction * 10000) / 100}%`\n}\n"],"mappings":"AAoEA,MAAa,EAAiC,OAAO,OAAO,CAAE,EAAG,GAAK,EAAG,EAAI,CAAC,EAE9E,SAAS,EAAe,EAAiC,CACvD,OAAO,OAAO,GAAU,UAAY,OAAO,SAAS,CAAK,GAAK,GAAS,GAAK,GAAS,CACvF,CAUA,SAAgB,EAAa,EAAqC,CAChE,GAAI,OAAO,GAAU,WAAY,GAAkB,MAAM,QAAQ,CAAK,EAAG,MAAO,GAChF,IAAM,EAAO,OAAO,KAAK,CAAK,EAC9B,GAAI,EAAK,SAAW,GAAK,CAAC,EAAK,SAAS,GAAG,GAAK,CAAC,EAAK,SAAS,GAAG,EAAG,MAAO,GAC5E,GAAM,CAAE,IAAG,KAAM,EACjB,OAAO,EAAe,CAAC,GAAK,EAAe,CAAC,CAC9C,CASA,SAAgB,EAAe,EAA4B,CACzD,OAAO,EAAa,CAAK,EAAI,EAAQ,CACvC,CAGA,SAAgB,EAAmB,EAA4B,CAC7D,OAAO,EAAM,IAAM,IAAO,EAAM,IAAM,EACxC,CAEA,SAAS,EAAM,EAAe,EAAa,EAAqB,CAC9D,OAAO,KAAK,IAAI,KAAK,IAAI,EAAO,CAAG,EAAG,CAAG,CAC3C,CAYA,SAAgB,EACd,EACA,EAC4C,CAK5C,OAJM,EAAa,GAAK,OAAO,SAAS,CAAU,GAAK,EAAW,GAAK,OAAO,SAAS,CAAQ,EAG3F,EAAa,EAAiB,CAAE,EAAG,EAAW,EAAY,EAAG,CAAE,EAC5D,CAAE,EAAG,EAAG,EAAG,EAAa,CAAS,EAH/B,CAAE,EAAG,EAAG,EAAG,CAAE,CAIxB,CAUA,SAAS,EAAY,EAAe,EAAyB,CAC3D,OAAO,EAAM,EAAQ,EAAU,EAAG,EAAG,EAAI,CAAO,CAClD,CAOA,SAAgB,EACd,EACA,EACA,EACgB,CAChB,IAAM,EAAU,EAAc,EAAY,CAAQ,EAClD,MAAO,CACL,EAAG,EAAY,EAAM,EAAG,EAAQ,CAAC,EACjC,EAAG,EAAY,EAAM,EAAG,EAAQ,CAAC,EACjC,EAAG,EAAQ,EACX,EAAG,EAAQ,CACb,CACF,CAWA,SAAgB,EACd,EACA,EACA,EACY,CACZ,IAAM,EAAO,EAAc,EAAO,EAAY,CAAQ,EACtD,MAAO,CACL,EAAG,EAAK,EAAI,EAAI,EAAK,GAAK,EAAI,EAAK,GAAK,GACxC,EAAG,EAAK,EAAI,EAAI,EAAK,GAAK,EAAI,EAAK,GAAK,EAC1C,CACF,CAOA,SAAgB,EAAqB,EAA8B,CACjE,MAAO,GAAG,EAAQ,EAAS,CAAC,EAAE,GAAG,EAAQ,EAAS,CAAC,GACrD,CAEA,SAAS,EAAQ,EAA0B,CACzC,MAAO,GAAG,KAAK,MAAM,EAAW,GAAK,EAAI,IAAI,EAC/C"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//#region src/crop/focal-point.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The focal point, and every number derived from it — ONE pure module.
|
|
4
|
+
*
|
|
5
|
+
* A photograph's focal point is stored once, on the media row, as a fraction of
|
|
6
|
+
* the image (`{x, y}` in `[0, 1]`, images D002). Two consumers turn it into a
|
|
7
|
+
* crop: the browser, through `object-position` on a shaped slot (this PR), and
|
|
8
|
+
* sharp, through `extract()` before a shaped variant is resized (images PR04).
|
|
9
|
+
* Both import this file, so the generated crop and the CSS crop cannot
|
|
10
|
+
* disagree — they are the same arithmetic, not two copies of it.
|
|
11
|
+
*
|
|
12
|
+
* No DOM, no sharp, no database. It is imported by `@murumets-ee/blocks`' render
|
|
13
|
+
* spine, which ships to every visitor's browser, so it must stay small and must
|
|
14
|
+
* never import anything.
|
|
15
|
+
*
|
|
16
|
+
* ## Fractions, never pixels
|
|
17
|
+
*
|
|
18
|
+
* A pixel rect breaks the moment the source file is replaced at a different
|
|
19
|
+
* resolution, and this toolkit's stored `width`/`height` are client-measured and
|
|
20
|
+
* can be wrong (images R003). A fraction survives both.
|
|
21
|
+
*
|
|
22
|
+
* ## Why `object-position` is NOT `focal × 100%`
|
|
23
|
+
*
|
|
24
|
+
* `object-position` is an ALIGNMENT percentage: `75%` puts the image's 75% point
|
|
25
|
+
* at the BOX's 75% point, not at its centre (css-backgrounds-3, "the point 75%
|
|
26
|
+
* across … is to be placed at the point 75% across the area"). The naive
|
|
27
|
+
* `object-position: {f·100}%` is therefore wrong by `|f − 0.5| × box_width` —
|
|
28
|
+
* exactly zero for a centred subject and 50% of the box at the edges, which is
|
|
29
|
+
* why it survives review (R002). The conversion, per axis:
|
|
30
|
+
*
|
|
31
|
+
* ```
|
|
32
|
+
* w_vis = C / I // visible window, as a fraction of the image
|
|
33
|
+
* L = clamp(f − w_vis / 2, 0, 1 − w_vis) // the window's leading edge
|
|
34
|
+
* P = L / (1 − w_vis) // the object-position fraction
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* `P` depends on the CONTAINER's ratio through `w_vis`, which is why it is
|
|
38
|
+
* computed at render and never stored (D002 rules out storing it).
|
|
39
|
+
*
|
|
40
|
+
* ## Absent and centre are the same thing
|
|
41
|
+
*
|
|
42
|
+
* {@link readFocalPoint} turns every absent, malformed or out-of-range value into
|
|
43
|
+
* `{0.5, 0.5}`, and the centre produces `object-position: 50% 50%`, which is
|
|
44
|
+
* CSS's initial value. So a legacy row with no focal point and a row whose focal
|
|
45
|
+
* point is the centre render identically, and nothing downstream branches on
|
|
46
|
+
* the difference (D010).
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* A point in the image, as fractions of its width and height. Both in `[0, 1]`.
|
|
50
|
+
*
|
|
51
|
+
* A `type`, not an `interface`, on purpose: it is stored in a `field.json()`
|
|
52
|
+
* column, and only a type alias is assignable to the entity's `JsonValue`
|
|
53
|
+
* (an interface has no implicit index signature).
|
|
54
|
+
*/
|
|
55
|
+
type FocalPoint = {
|
|
56
|
+
readonly x: number;
|
|
57
|
+
readonly y: number;
|
|
58
|
+
};
|
|
59
|
+
/** A rectangle in the image, as fractions of its width and height. */
|
|
60
|
+
type FractionalRect = {
|
|
61
|
+
readonly x: number;
|
|
62
|
+
readonly y: number;
|
|
63
|
+
readonly w: number;
|
|
64
|
+
readonly h: number;
|
|
65
|
+
};
|
|
66
|
+
/** The default — and what every absent or unusable focal point means. */
|
|
67
|
+
declare const CENTRE_FOCAL_POINT: FocalPoint;
|
|
68
|
+
/**
|
|
69
|
+
* Is `value` a storable focal point — exactly `{x, y}`, both finite fractions?
|
|
70
|
+
*
|
|
71
|
+
* STRICT, because it guards a WRITE (the media row's `focalPoint` column). An
|
|
72
|
+
* extra key is refused rather than stripped: a JSON column that stores what it
|
|
73
|
+
* was sent would otherwise accept `{x, y, notes: '<a megabyte>'}` and hand it to
|
|
74
|
+
* every reader of every page that draws the photograph.
|
|
75
|
+
*/
|
|
76
|
+
declare function isFocalPoint(value: unknown): value is FocalPoint;
|
|
77
|
+
/**
|
|
78
|
+
* The focal point to RENDER with, from whatever a projection carried.
|
|
79
|
+
*
|
|
80
|
+
* LENIENT, because it guards a READ: an absent, null or malformed value is the
|
|
81
|
+
* centre, never a throw. A render that threw on a bad stored value would take
|
|
82
|
+
* the whole page down over one photograph's crop.
|
|
83
|
+
*/
|
|
84
|
+
declare function readFocalPoint(value: unknown): FocalPoint;
|
|
85
|
+
/** Is this the centre — the point that needs no `object-position` at all? */
|
|
86
|
+
declare function isCentreFocalPoint(focal: FocalPoint): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* The window a `cover` crop leaves visible, as a fraction of the image on each
|
|
89
|
+
* axis.
|
|
90
|
+
*
|
|
91
|
+
* Under `object-fit: cover` the image is scaled until it covers the box, so at
|
|
92
|
+
* most ONE axis overflows and the other is exactly `1`. `imageRatio` and
|
|
93
|
+
* `boxRatio` are width / height. A ratio that is not a positive finite number
|
|
94
|
+
* is a caller bug; it is answered with "nothing overflows" (`{1, 1}`), which
|
|
95
|
+
* renders as today's centre crop rather than as `NaN%`.
|
|
96
|
+
*/
|
|
97
|
+
declare function visibleWindow(imageRatio: number, boxRatio: number): {
|
|
98
|
+
readonly w: number;
|
|
99
|
+
readonly h: number;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The crop a focal point implies for a box of `boxRatio`, as a fractional rect
|
|
103
|
+
* of the image — the region sharp will `extract()` (images PR04) and the region
|
|
104
|
+
* the browser shows through {@link objectPositionFor}.
|
|
105
|
+
*/
|
|
106
|
+
declare function focalCropRect(focal: FocalPoint, imageRatio: number, boxRatio: number): FractionalRect;
|
|
107
|
+
/**
|
|
108
|
+
* The `object-position` fractions that make a `cover`-fitted image show
|
|
109
|
+
* {@link focalCropRect}'s window.
|
|
110
|
+
*
|
|
111
|
+
* An axis that does not overflow (`w_vis == 1`) has no position to choose —
|
|
112
|
+
* `object-position` is inert on it — so it answers `0.5`, CSS's own default.
|
|
113
|
+
* That keeps the centre case literally `{0.5, 0.5}` and the emitted CSS
|
|
114
|
+
* byte-comparable with a slot that has no focal point.
|
|
115
|
+
*/
|
|
116
|
+
declare function objectPositionFor(focal: FocalPoint, imageRatio: number, boxRatio: number): FocalPoint;
|
|
117
|
+
/**
|
|
118
|
+
* Format a position as a CSS `object-position` value: two percentages, rounded
|
|
119
|
+
* to two decimals so the emitted markup is stable across floating-point noise
|
|
120
|
+
* (`33.33% 50%`, not `33.333333333333336% 50%`).
|
|
121
|
+
*/
|
|
122
|
+
declare function formatObjectPosition(position: FocalPoint): string;
|
|
123
|
+
//#endregion
|
|
124
|
+
export { formatObjectPosition as a, objectPositionFor as c, focalCropRect as i, readFocalPoint as l, FocalPoint as n, isCentreFocalPoint as o, FractionalRect as r, isFocalPoint as s, CENTRE_FOCAL_POINT as t, visibleWindow as u };
|
|
125
|
+
//# sourceMappingURL=focal-point-C_0drV03.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focal-point-C_0drV03.d.mts","names":[],"sources":["../src/crop/focal-point.ts"],"mappings":";;AAsDA;;;;AAEY;AAIZ;;;;;;;;;AAIY;AAIZ;;;;AAA+E;AAc/E;;;;;;;;AAAiE;AAejE;;;;AAA0D;AAK1D;;;;AAAoD;AAkBpD;;;;;;;;;AAGmC;AAyBnC;;;KA9FY,UAAA;EAAA,SACD,CAAA;EAAA,SACA,CAAC;AAAA;;KAIA,cAAA;EAAA,SACD,CAAA;EAAA,SACA,CAAA;EAAA,SACA,CAAA;EAAA,SACA,CAAA;AAAA;;cAIE,kBAAA,EAAoB,UAA8C;;;;;;AA2GlE;AAab;;iBA1GgB,YAAA,CAAa,KAAA,YAAiB,KAAA,IAAS,UAAU;;AA0GR;;;;;;iBA3FzC,cAAA,CAAe,KAAA,YAAiB,UAAU;;iBAK1C,kBAAA,CAAmB,KAAiB,EAAV,UAAU;;;;;;;;;;;iBAkBpC,aAAA,CACd,UAAA,UACA,QAAA;EAAA,SACY,CAAA;EAAA,SAAoB,CAAA;AAAA;;;;;;iBAyBlB,aAAA,CACd,KAAA,EAAO,UAAA,EACP,UAAA,UACA,QAAA,WACC,cAAc;;;;;;;;;;iBAmBD,iBAAA,CACd,KAAA,EAAO,UAAA,EACP,UAAA,UACA,QAAA,WACC,UAAU;;;;;;iBAaG,oBAAA,CAAqB,QAAoB,EAAV,UAAU"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{n as e,t}from"./variant-key-Cki4n-42.mjs";import{n}from"./variant-plan-DV8hsMdz.mjs";import{t as r}from"./owned-delete-Cn_KFFjS.mjs";import{a as i,c as a,h as o,i as ee,m as s,p as c,r as te,s as ne,t as l}from"./process-image-Deedtkh4.mjs";var u=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 d(e,t,n={}){return{mediaId:e,status:t,uploaded:0,reused:0,deleted:0,dropped:0,dimensionsCorrected:!1,focalPointSeeded:!1,...n}}async function re(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 f(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 p(e,t){return JSON.stringify(e??null)===JSON.stringify(t??null)}function m(e,n){return t(e,{shape:n.shape,width:n.width,fingerprint:n.fingerprint,format:n.format})}function ie(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 h(t,h){let{media:y,storage:b,config:x,styles:ae,logger:S}=h,C=await y.findById(t);if(!C)return d(t,`missing`);let w=C;if(w.mediaType!==`image`||!ee(w.mimeType))return d(t,`skipped`);let T=await b.getMetadata(w.fileKey);if(!T)return S?.warn({mediaId:t},`media variants: original file record is missing; nothing to do`),d(t,`skipped`);let E=await re((await b.download(w.fileKey)).body),D=!1,O=await te(E),k={};if(O.width>0&&(w.width!==O.width||w.height!==O.height)&&(k.width=O.width,k.height=O.height),w.focalPoint===null||w.focalPoint===void 0){let e=await l(E).catch(e=>(S?.warn({mediaId:t,err:e},`media variants: focal-point detection failed; left unset`),null));e?k.focalPoint=e:D=!0}if(`focalPoint`in k){let e=await y.findById(t);if(!e)return d(t,`missing`);e.focalPoint!==null&&e.focalPoint!==void 0&&(delete k.focalPoint,w={...w,focalPoint:e.focalPoint,crops:e.crops})}Object.keys(k).length>0&&(await y.updateInternal(t,k),w={...w,...k});let A=n(O,x,{focalPoint:w.focalPoint,crops:w.crops});A.dropped>0&&S?.warn({mediaId:t,dropped:A.dropped},`media variants: the per-image cap cut the largest variants off this photograph`);let j=s(T.metadata),oe=new Map(j.shaped.map(e=>[e.key,e])),M=[],N=[];for(let e of A.variants){let t=m(w.fileKey,e),n=oe.get(t);if(n){M.push({...n,shape:e.shape,rung:e.width});continue}let r=await b.getMetadata(t),i=r?ie(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 ne(E,N.map(e=>e.planned));await f(N.map((e,t)=>({...e,variant:ce[t]})),4,async({planned:e,key:n,replace:r,variant:i})=>{if(i)try{r&&await b.delete(n),await b.upload(i.buffer,{key:n,filename:`${e.shape}_${e.width}w_${w.filename}`,mimeType:i.mimeType,size:i.buffer.byteLength,visibility:T.visibility,metadata:{variantOf:w.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++,S?.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(ae)){let r=e(w.fileKey,t,n.format??`webp`),i=c(n);j.legacy.get(t)===r&&j.legacyRecipes.get(t)===i?(R.set(t,r),z.set(t,i)):B[t]=n}Object.keys(B).length>0&&await f([...(await i(E,B)).variants],4,async([n,r])=>{let i=B[n];if(!i)return;let a=e(w.fileKey,n,r.format),o=!1;try{await b.getMetadata(a)&&(await b.delete(a),o=!0),await b.upload(r.buffer,{key:a,filename:`${n}_${w.filename}`,mimeType:r.mimeType,size:r.buffer.byteLength,visibility:T.visibility,metadata:{variantOf:w.fileKey,style:n}}),R.set(n,a),z.set(n,c(i)),L.push(a),P++}catch(e){F++,o&&V.add(n),S?.warn({mediaId:t,key:a,err:e},`media variants: legacy style upload failed`)}});let H=await y.findById(t);if(!H)return await r(b,w.fileKey,[...I,...L],S),d(t,`missing`,{uploaded:P});let U=!p(H.focalPoint,w.focalPoint)||!p(H.crops,w.crops),W=await b.getMetadata(w.fileKey)??T,G=s(W.metadata),K=_(g(G.legacy,R),V),q=_(g(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(O,x,{focalPoint:H.focalPoint,crops:H.crops}).variants.map(e=>m(w.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=v([...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:v(M),unconfirmed:[]};let Z=o(W.metadata,X);await b.updateMetadata(w.fileKey,{metadata:D?{...Z,[a]:!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(b,w.fileKey,Q,S),$=d(t,U?`stale`:`generated`,{uploaded:P,reused:se,deleted:ue,dropped:A.dropped,dimensionsCorrected:`width`in k,focalPointSeeded:`focalPoint`in k});if(F>0&&!U)throw new u(t,F);return S?.debug($,`media variants: pass complete`),$}function g(e,t){return new Map([...e,...t])}function _(e,t){for(let n of t)e.delete(n);return e}function v(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{h as generateMediaVariants};
|
|
2
|
+
//# sourceMappingURL=generate-variants-zyPjaGcy.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-variants-zyPjaGcy.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-CXvu8U31.mjs`)).default;case`ru`:return(await import(`./ru-Dj-ax8s_.mjs`)).default;default:return(await import(`./en-A8YzaZ51.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-CgkJF5dc.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/image-styles-settings.d.ts
|
|
4
4
|
/**
|
|
@@ -16,6 +16,13 @@ import { t as ImageStyle } from "./types-1idCpe1k.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":";;;;;;;;;;;;;;;;cA2Ia,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-BlKW-0C1.mjs";import"./variant-plan-DV8hsMdz.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_-]*$/,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_-]*$/\n\nconst imageStylesSchema = z
|
|
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\nconst STYLE_NAME_RE = /^[a-z][a-z0-9_-]*$/\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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"}
|