@murumets-ee/media 0.71.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 +2 -1
- package/dist/crop.mjs +1 -1
- package/dist/definitions-LJpgrdxd.mjs +2 -0
- package/dist/definitions-LJpgrdxd.mjs.map +1 -0
- package/dist/deps-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/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 +1 -94
- package/dist/picker.d.mts.map +1 -1
- package/dist/picker.mjs +1 -2
- package/dist/picker.mjs.map +1 -1
- package/dist/plugin.d.mts +2 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/process-image-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 +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-CnORVkLZ.d.mts → types-CgkJF5dc.d.mts} +85 -5
- 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 +21 -8
- package/dist/client-CSOH7Zun.mjs +0 -2
- package/dist/client-CSOH7Zun.mjs.map +0 -1
- package/dist/en-0YnQ_Qz-.mjs +0 -2
- package/dist/en-0YnQ_Qz-.mjs.map +0 -1
- package/dist/entity-CNxAERYG.mjs +0 -2
- package/dist/entity-CNxAERYG.mjs.map +0 -1
- package/dist/entity-DKFCdQJT.mjs +0 -2
- package/dist/entity-DKFCdQJT.mjs.map +0 -1
- package/dist/et-BIxGYWVK.mjs +0 -2
- package/dist/et-BIxGYWVK.mjs.map +0 -1
- package/dist/image-styles-settings-DEnu-V2E.mjs +0 -2
- package/dist/image-styles-settings-DEnu-V2E.mjs.map +0 -1
- package/dist/plugin-B_ZtfiAc.mjs +0 -2
- package/dist/plugin-B_ZtfiAc.mjs.map +0 -1
- package/dist/process-image-WByEHTXQ.mjs +0 -2
- package/dist/process-image-WByEHTXQ.mjs.map +0 -1
- package/dist/regenerate-variants-2Lmo5kf8.mjs +0 -2
- package/dist/regenerate-variants-2Lmo5kf8.mjs.map +0 -1
- package/dist/regenerate-variants-B9AXXN6N.mjs +0 -2
- package/dist/regenerate-variants-B9AXXN6N.mjs.map +0 -1
- package/dist/resolve-image-styles-Cakg1Qpp.mjs +0 -2
- package/dist/resolve-image-styles-Cakg1Qpp.mjs.map +0 -1
- package/dist/resolve-image-styles-iN9JbZYf.mjs +0 -2
- package/dist/resolve-image-styles-iN9JbZYf.mjs.map +0 -1
- package/dist/routes-C7Fx-055.mjs +0 -2
- package/dist/routes-C7Fx-055.mjs.map +0 -1
- package/dist/ru-UUTBfsMF.mjs +0 -2
- package/dist/ru-UUTBfsMF.mjs.map +0 -1
- package/dist/types-CnORVkLZ.d.mts.map +0 -1
- package/dist/usage-CVAqkS6h.mjs +0 -2
- package/dist/usage-CVAqkS6h.mjs.map +0 -1
- package/dist/variant-key-JBTJXPL1.mjs +0 -2
- package/dist/variant-key-JBTJXPL1.mjs.map +0 -1
- package/dist/variant-key-RKke2RfS.mjs +0 -2
- package/dist/variant-key-RKke2RfS.mjs.map +0 -1
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e={PluginNav:{media:`Медиа`},Media:{imageVocabulary:{description:`Каждая фотография создаётся в каждой форме ниже и в каждой ширине. Слот, указывающий форму, использует эти файлы; слот со своим соотношением сторон обрезается в браузере.`,usingDefaults:`Здесь пока ничего не сохранено. Показаны значения по умолчанию; пока вы не сохраните, сайт использует их — или формы, заданные в его коде, если они есть.`,widthsLabel:`Ширины (px)`,widthsHelp:`По возрастанию, через запятую. Фотография никогда не увеличивается — ширина, которую она не может обеспечить, пропускается.`,widthsInvalid:`Укажите до {count} возрастающих целых чисел от {min} до {max}.`,shapesLabel:`Формы`,naturalNote:`А также «natural» — каждая фотография в своей форме, в тех же ширинах.`,columnName:`Название`,columnRatio:`Соотношение (Ш:В)`,columnWidths:`Свои ширины`,ownWidthsPlaceholder:`Все ширины`,ratioInvalid:`Запишите соотношение как Ш:В, например 3:2.`,nameInvalid:`Строчные буквы, цифры, - и _, начиная с буквы — и не «natural».`,nameTaken:`Форма с названием «{name}» уже существует.`,tooMany:`Не более {count} форм.`,add:`Добавить форму`,remove:`Удалить {name}`,tooManyFiles:`Это даёт {count} файлов на каждую фотографию; максимум — {max}. Сначала удалите форму или ширину.`},variantBacklog:{waiting:`Изображений, ожидающих создания размеров: {count}`,workerRunning:`Обработчик очереди сейчас их создаёт. Пока он не закончит, эти изображения показываются в полном размере.`,workerMissing:`Ни один обработчик очереди не запущен, поэтому эти изображения показываются в полном размере — страницы грузятся медленнее, особенно на телефонах. Размеры изображений создаёт обработчик очереди — отдельный процесс, который должен работать рядом с сайтом (pnpm worker). Попросите того, кто обслуживает сайт, запустить его.`,waitingSince:`Самое старое поставлено в очередь {since}.`,failed:`Создание размеров не удалось {count} раз(а), попытки прекращены — причина на странице очереди.`,catchUp:`Обновление всей медиатеки поставлено в очередь: каждое изображение сверяется с текущими формами и стилями.`,catchUpWorkerMissing:`Ни один обработчик очереди не запущен, поэтому оно не началось. Размеры изображений создаёт обработчик очереди — отдельный процесс, который должен работать рядом с сайтом (pnpm worker). Попросите того, кто обслуживает сайт, запустить его.`,openQueue:`Открыть очередь`},variantGeneration:{workerRequired:`Размеры изображений создаются в фоне обработчиком очереди — отдельным процессом, который должен работать рядом с сайтом (pnpm worker). Без него сайт работает, но новые и изменённые изображения показываются в полном размере.`,regenerate:`Обновить все изображения`,regenerating:`Запуск…`,queued:`Запущено. Обработчик очереди обновляет медиатеку в фоне; сайт при этом продолжает работать. Следите за ходом на странице очереди.`,alreadyRunning:`Обновление уже идёт — следите за ним на странице очереди.`,inlineDone:`Обработано {processed} из {total}; пропущено {skipped}; ошибок {errors}.`,failed:`Не удалось запустить обновление: {reason}`}},MediaCropEditor:{tabListLabel:`Формы обрезки`,description:`Перетащите, чтобы расположить фотографию, прокрутите или сведите пальцы для масштабирования. Каждое место с этой формой покажет эту обрезку.`,xLabel:`Слева (%)`,yLabel:`Сверху (%)`,widthLabel:`Ширина (%)`,heightLabel:`Высота (%)`,clearShape:`Очистить эту обрезку`,noShapes:`Ни один настроенный стиль изображения пока не имеет фиксированного соотношения сторон, поэтому обрезать нечего.`,hasCropSuffix:`— сохранена собственная обрезка`}};export{e as default};
|
|
2
|
+
//# sourceMappingURL=ru-Dj-ax8s_.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ru-Dj-ax8s_.mjs","names":[],"sources":["../src/messages/ru.json"],"sourcesContent":[""],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{r as e}from"./definitions-LJpgrdxd.mjs";import{t}from"./slot-CY4isqmN.mjs";import{createVariantDeps as n}from"./deps-CzHEhyJK.mjs";let r=!1;async function i(i,a,o){try{let s=t();if(s){let{QueueClient:t}=await import(`@murumets-ee/queue/client`);return{mode:`queued`,jobId:await new t({app:i,logger:i.logger}).enqueue(s.generateVariants,{mediaId:a.id,epoch:e})}}r||(r=!0,i.logger.warn(`media: no queue plugin in this app — image variants are generated inline, so every image upload waits for them. Add queue() and run a worker to make uploads return at once.`));let{generateMediaVariants:c}=await import(`./generate-variants-zyPjaGcy.mjs`);return await c(a.id,await n(i,o)),{mode:`inline`}}catch(e){return i.logger.error({err:e,mediaId:a.id},`media: variants could not be requested; the original is served until a backfill runs`),{mode:`failed`}}}export{i as requestVariants};
|
|
2
|
+
//# sourceMappingURL=schedule-DREjg2ji.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule-DREjg2ji.mjs","names":[],"sources":["../src/jobs/schedule.ts"],"sourcesContent":["/**\n * Asking for a photograph's variants — queued when the app has a queue, inline\n * when it does not.\n *\n * The queued path is the design (images D004): the upload returns as soon as\n * the row exists, and the worker generates. The inline path is what an app\n * without the queue plugin (`apps/web-astro`) always had, kept rather than\n * removed so that such an app does not silently lose variants — it pays the\n * old upload stall instead, and says so once in its log.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport { UPLOAD_EPOCH } from './definitions.js'\nimport { createVariantDeps, type VariantMediaAccess } from './deps.js'\nimport type { VariantGenerationDeps, VariantSourceRow } from './generate-variants.js'\nimport { getMediaJobs } from './slot.js'\n\nexport type VariantRequestOutcome =\n /** A `media:generate-variants` job is waiting for the worker. */\n | { readonly mode: 'queued'; readonly jobId: string }\n /** No queue in this app — generated before returning. */\n | { readonly mode: 'inline' }\n /** Neither happened; logged. The original is served until a backfill runs. */\n | { readonly mode: 'failed' }\n\nlet warnedInline = false\n\n/**\n * Queue (or, with no queue, run) a variant pass for `row`. Never throws: the\n * row already exists and serves its original, which is the correct fallback\n * for anything that goes wrong here.\n */\nexport async function requestVariants(\n app: ToolkitApp,\n row: Pick<VariantSourceRow, 'id'>,\n clients: { media: VariantMediaAccess; storage: VariantGenerationDeps['storage'] },\n): Promise<VariantRequestOutcome> {\n try {\n const jobs = getMediaJobs()\n if (jobs) {\n const { QueueClient } = await import('@murumets-ee/queue/client')\n const queue = new QueueClient({ app, logger: app.logger })\n const jobId = await queue.enqueue(jobs.generateVariants, {\n mediaId: row.id,\n epoch: UPLOAD_EPOCH,\n })\n return { mode: 'queued', jobId }\n }\n if (!warnedInline) {\n warnedInline = true\n app.logger.warn(\n 'media: no queue plugin in this app — image variants are generated inline, so every ' +\n 'image upload waits for them. Add queue() and run a worker to make uploads return at once.',\n )\n }\n const { generateMediaVariants } = await import('./generate-variants.js')\n await generateMediaVariants(row.id, await createVariantDeps(app, clients))\n return { mode: 'inline' }\n } catch (err) {\n app.logger.error(\n { err, mediaId: row.id },\n 'media: variants could not be requested; the original is served until a backfill runs',\n )\n return { mode: 'failed' }\n }\n}\n"],"mappings":"0IAyBA,IAAI,EAAe,GAOnB,eAAsB,EACpB,EACA,EACA,EACgC,CAChC,GAAI,CACF,IAAM,EAAO,EAAa,EAC1B,GAAI,EAAM,CACR,GAAM,CAAE,eAAgB,MAAM,OAAO,6BAMrC,MAAO,CAAE,KAAM,SAAU,MAAA,MAJL,IADF,EAAY,CAAE,MAAK,OAAQ,EAAI,MAAO,CAChC,CAAC,CAAC,QAAQ,EAAK,iBAAkB,CACvD,QAAS,EAAI,GACb,MAAO,CACT,CAAC,CAC8B,CACjC,CACK,IACH,EAAe,GACf,EAAI,OAAO,KACT,8KAEF,GAEF,GAAM,CAAE,yBAA0B,MAAM,OAAO,oCAE/C,OADA,MAAM,EAAsB,EAAI,GAAI,MAAM,EAAkB,EAAK,CAAO,CAAC,EAClE,CAAE,KAAM,QAAS,CAC1B,OAAS,EAAK,CAKZ,OAJA,EAAI,OAAO,MACT,CAAE,MAAK,QAAS,EAAI,EAAG,EACvB,sFACF,EACO,CAAE,KAAM,QAAS,CAC1B,CACF"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { i as ImageStyle, n as ImageFormat, p as ReadonlyImageVocabulary, r as ImageShape } from "./types-CgkJF5dc.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/shapes.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The implicit shape every configuration has: the photograph's own ratio, over
|
|
6
|
+
* the ladder. It is what a slot that declares no shape draws, so it is what
|
|
7
|
+
* that slot's `srcset` is made of. Reserved — no vocabulary shape may use it.
|
|
8
|
+
*/
|
|
9
|
+
declare const NATURAL_SHAPE = "natural";
|
|
10
|
+
/**
|
|
11
|
+
* The default width ladder. Six rungs, because this is a LOCAL sharp on
|
|
12
|
+
* somebody's VPS: Astro ships a shorter list for local services than for
|
|
13
|
+
* remote ones, and says why — every rung is a generated file (R006).
|
|
14
|
+
*/
|
|
15
|
+
declare const DEFAULT_IMAGE_LADDER: readonly number[];
|
|
16
|
+
/**
|
|
17
|
+
* The seeded shape vocabulary.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ Portrait is **3:4, not 4:5**. 4:5 is folk wisdom with no design-system
|
|
20
|
+
* backing anywhere in the survey; 3:4 has eBay's and the UAE design system's
|
|
21
|
+
* (R009). Re-read R009 before changing it.
|
|
22
|
+
*/
|
|
23
|
+
declare const DEFAULT_IMAGE_SHAPES: Readonly<Record<string, Readonly<ImageShape>>>;
|
|
24
|
+
/**
|
|
25
|
+
* The seeded vocabulary + ladder — what a site that configured nothing gets.
|
|
26
|
+
* Frozen: it is shared by every reader, so a caller that wants to edit it
|
|
27
|
+
* copies it first (the settings editor and `resolveMediaConfig` both do).
|
|
28
|
+
*/
|
|
29
|
+
declare const DEFAULT_IMAGE_VOCABULARY: ReadonlyImageVocabulary;
|
|
30
|
+
/** One shape the generator will produce variants for, fully defaulted. */
|
|
31
|
+
interface ResolvedShape {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
/** width / height, or `null` for the photograph's own ratio. */
|
|
34
|
+
readonly ratio: number | null;
|
|
35
|
+
/** Ascending, distinct. The rungs this shape is generated at, before the per-image filter. */
|
|
36
|
+
readonly widths: readonly number[];
|
|
37
|
+
readonly format: ImageFormat;
|
|
38
|
+
readonly quality: number;
|
|
39
|
+
/** Where it came from — the vocabulary, the implicit natural shape, or a legacy fixed style. */
|
|
40
|
+
readonly origin: 'vocabulary' | 'natural' | 'style';
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A legacy style name answered by a (shape, rung) of the vocabulary.
|
|
44
|
+
*
|
|
45
|
+
* The rung is one the shape is CONFIGURED at. A given photograph may still not
|
|
46
|
+
* have it — a source narrower than the rung plans nothing that wide. That is a
|
|
47
|
+
* CONTRACT for the reader, which does not exist yet (images PR06 resolves
|
|
48
|
+
* variants for a render): resolving an alias for one photograph must take the
|
|
49
|
+
* nearest variant that photograph has, as it would for a shape asked by name.
|
|
50
|
+
*/
|
|
51
|
+
interface ShapeAlias {
|
|
52
|
+
readonly shape: string;
|
|
53
|
+
readonly width: number;
|
|
54
|
+
}
|
|
55
|
+
/** A legacy style the shape model cannot express, and why. */
|
|
56
|
+
interface UnmappedStyle {
|
|
57
|
+
readonly name: string;
|
|
58
|
+
readonly reason: string;
|
|
59
|
+
}
|
|
60
|
+
/** The whole variant configuration, resolved. */
|
|
61
|
+
interface VariantConfig {
|
|
62
|
+
/** `natural` first, then sorted by name. */
|
|
63
|
+
readonly shapes: readonly ResolvedShape[];
|
|
64
|
+
/** Legacy style name → the (shape, rung) that now answers it. */
|
|
65
|
+
readonly aliases: ReadonlyMap<string, ShapeAlias>;
|
|
66
|
+
/** Legacy styles and vocabulary entries that have no place in the model. */
|
|
67
|
+
readonly unmapped: readonly UnmappedStyle[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Fold a vocabulary and the stored fixed styles into one {@link VariantConfig}.
|
|
71
|
+
*
|
|
72
|
+
* LENIENT, because it is a READ: the settings route validates what it stores
|
|
73
|
+
* (`image-styles-settings.ts`), but plugin config is code nobody validated, and
|
|
74
|
+
* a bad entry must cost that entry — reported in `unmapped` — never every
|
|
75
|
+
* upload's variants. Deterministic: the same input always yields the same
|
|
76
|
+
* config, in the same order, which the variant keys depend on.
|
|
77
|
+
*/
|
|
78
|
+
declare function buildVariantConfig(input: {
|
|
79
|
+
readonly vocabulary: ReadonlyImageVocabulary;
|
|
80
|
+
readonly styles: Readonly<Record<string, ImageStyle>>;
|
|
81
|
+
}): VariantConfig;
|
|
82
|
+
/**
|
|
83
|
+
* What a legacy style NAME refers to in the shape model: the (shape, rung) of
|
|
84
|
+
* its alias, or — for a style that became a one-rung shape — that shape at its
|
|
85
|
+
* one width. `undefined` for a name the model does not know.
|
|
86
|
+
*/
|
|
87
|
+
declare function resolveStyleName(config: VariantConfig, name: string): ShapeAlias | undefined;
|
|
88
|
+
//#endregion
|
|
89
|
+
export { ResolvedShape as a, VariantConfig as c, NATURAL_SHAPE as i, buildVariantConfig as l, DEFAULT_IMAGE_SHAPES as n, ShapeAlias as o, DEFAULT_IMAGE_VOCABULARY as r, UnmappedStyle as s, DEFAULT_IMAGE_LADDER as t, resolveStyleName as u };
|
|
90
|
+
//# sourceMappingURL=shapes-40dHtTb-.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shapes-40dHtTb-.d.mts","names":[],"sources":["../src/shapes.ts"],"mappings":";;;;AAqMgB;AAIhB;;;cAjJa,aAAA;AAmJI;AAIjB;;;;AAJiB,cA5IJ,oBAAA;;;;;;;;cAWA,oBAAA,EAAsB,QAAA,CAAS,MAAA,SAAe,QAAA,CAAS,UAAA;;;;;AA2IzB;cA/H9B,wBAAA,EAA0B,uBAGrC;;UAqFe,aAAA;EAAA,SACN,IAAA;;WAEA,KAAA;;WAEA,MAAA;EAAA,SACA,MAAA,EAAQ,WAAW;EAAA,SACnB,OAAA;;WAEA,MAAA;AAAA;;;;;;;;;;UAYM,UAAA;EAAA,SACN,KAAA;EAAA,SACA,KAAK;AAAA;;UAIC,aAAA;EAAA,SACN,IAAA;EAAA,SACA,MAAM;AAAA;;UAIA,aAAA;;WAEN,MAAA,WAAiB,aAAA;;WAEjB,OAAA,EAAS,WAAA,SAAoB,UAAA;;WAE7B,QAAA,WAAmB,aAAA;AAAA;;;;;;;;;;iBAwGd,kBAAA,CAAmB,KAAA;EAAA,SACxB,UAAA,EAAY,uBAAA;EAAA,SACZ,MAAA,EAAQ,QAAA,CAAS,MAAA,SAAe,UAAA;AAAA,IACvC,aAAA;;;;;;iBAgIY,gBAAA,CAAiB,MAAA,EAAQ,aAAA,EAAe,IAAA,WAAe,UAAU"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=`natural`,t=Object.freeze([320,640,960,1280,1920,2560]),n=Object.freeze({square:Object.freeze({ratio:`1:1`}),landscape:Object.freeze({ratio:`3:2`}),wide:Object.freeze({ratio:`16:9`}),portrait:Object.freeze({ratio:`3:4`})}),r=Object.freeze({shapes:n,ladder:t}),i=`webp`,a=8192,o=/^[a-z][a-z0-9_-]*$/,s=/^([1-9]\d*):([1-9]\d*)$/;function c(e){if(typeof e!=`string`)return null;let t=s.exec(e);if(t===null)return null;let n=Number(t[1]),r=Number(t[2]);return n>1e4||r>1e4?null:n/r}function l(e){return e.ladder.length+Object.values(e.shapes).reduce((t,n)=>t+(n.widths??e.ladder).length,0)}const u=new Map([[`thumbnail`,{style:{width:200,height:200,fit:`cover`,format:`webp`,quality:80},shape:`square`}],[`card`,{style:{width:600,height:400,fit:`cover`,format:`webp`,quality:85},shape:`landscape`}],[`hero`,{style:{width:1920,height:800,fit:`cover`,format:`webp`,quality:85},shape:`wide`}],[`full`,{style:{width:2400,fit:`inside`,format:`webp`,quality:90},shape:e}]]);function d(e,t){return e.find(e=>e>=t)??e[e.length-1]}function f(e,t){return e<t?-1:+(e>t)}function p(e,t){return e.width===t.width&&e.height===t.height&&(e.fit??`cover`)===(t.fit??`cover`)&&(e.format??`webp`)===(t.format??`webp`)&&(e.quality??80)===(t.quality??80)}function m(e){return typeof e==`number`&&Number.isInteger(e)&&e>=16&&e<=8192}function h(e){return[...new Set(e.filter(m))].sort((e,t)=>e-t)}function g(e){return typeof e==`number`&&Number.isInteger(e)&&e>=1&&e<=100}const _=new Set([`webp`,`jpeg`,`png`,`avif`]);function v(e){return typeof e==`string`&&_.has(e)}function y(n){let r=[],a=new Map,s=h(Array.isArray(n.vocabulary.ladder)?n.vocabulary.ladder:[]).slice(0,12),l=s.length>0?s:[...t],m=Object.entries(n.vocabulary.shapes??{}).sort(([e],[t])=>f(e,t));for(let[e,t]of m){if(!o.test(e)||e===`natural`){r.push({name:e,reason:`not a usable shape name`});continue}if(a.size>=16){r.push({name:e,reason:`more than 16 shapes configured`});continue}let n=c(t?.ratio);if(n===null){r.push({name:e,reason:`ratio is not W:H in positive integers`});continue}let s=Array.isArray(t.widths)?h(t.widths):[];a.set(e,{name:e,ratio:n,widths:s.length>0?s.slice(0,12):l,format:v(t.format)?t.format:i,quality:g(t.quality)?t.quality:80,origin:`vocabulary`})}a.set(e,{name:e,ratio:null,widths:l,format:i,quality:80,origin:`natural`});let _=new Map,y=new Set,x=new Map(a),S=0,C=Object.entries(n.styles??{}).sort(([e],[t])=>f(e,t));for(let[e,t]of C){let n=u.get(e);if(n&&p(t,n.style)&&x.has(n.shape))continue;if(y.add(e),a.has(e)){r.push({name:e,reason:`a vocabulary shape has the same name`});continue}let i=b(e,t);`reason`in i?r.push(i):S>=16?r.push({name:e,reason:`more than 16 fixed styles configured`}):(a.set(e,i),S++)}for(let[e,t]of u){if(y.has(e)||a.has(e))continue;let n=x.get(t.shape),r=n&&t.style.width&&d(n.widths,t.style.width);n&&r&&_.set(e,{shape:n.name,width:r})}return{shapes:[...a.values()].sort((e,t)=>e.origin===`natural`?-1:t.origin===`natural`?1:f(e.name,t.name)),aliases:_,unmapped:r}}function b(e,t){if(!o.test(e))return{name:e,reason:`not a usable shape name`};let n=t.fit??`cover`,r=v(t.format)?t.format:i,a=g(t.quality)?t.quality:80;return m(t.width)?n===`cover`?m(t.height)?{name:e,ratio:t.width/t.height,widths:[t.width],format:r,quality:a,origin:`style`}:{name:e,reason:`a cover style needs a height`}:n===`inside`&&t.height===void 0?{name:e,ratio:null,widths:[t.width],format:r,quality:a,origin:`style`}:{name:e,reason:`fit '${n}' with this box has no single ratio and width`}:{name:e,reason:`no width, or a width outside the ladder bounds`}}function x(e,t){let n=e.aliases.get(t);if(n)return n;let r=e.shapes.find(e=>e.name===t&&e.origin===`style`),i=r?.widths[0];return r&&i!==void 0?{shape:r.name,width:i}:void 0}export{e as a,c,a as i,l,n,o,r,y as s,t,x as u};
|
|
2
|
+
//# sourceMappingURL=shapes-BlKW-0C1.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shapes-BlKW-0C1.mjs","names":[],"sources":["../src/shapes.ts"],"sourcesContent":["/**\n * The shape model — what a variant IS once size and shape are separate axes\n * (images D003).\n *\n * - A **shape** is a name carrying one ratio (`square` 1:1, `portrait` 3:4, …).\n * - The **ladder** is one global, ascending list of widths.\n * - A **variant** is (shape × rung), its height derived from the ratio.\n *\n * `ImageStyle` conflated the two — `card: {600×400}` is one ratio at one\n * width — which is why no `srcset` could ever come out of it: a `srcset` needs\n * several widths at ONE shape.\n *\n * ## Why this lives in the SAME settings namespace\n *\n * The vocabulary is a second key (`vocabulary`) of the existing\n * `media.imageStyles` namespace, not a namespace of its own (C7). So it inherits\n * the settings DB → plugin config → hardcoded waterfall, the auto-derived\n * `settings_media.imageStyles` permission resource, and the admin settings page.\n * The existing `imageStyles` key keeps its meaning — an operator's fixed styles\n * — and keeps generating exactly as before for every caller that reads it.\n *\n * ## How the existing styles map onto the model — read-time, never rewritten\n *\n * {@link buildVariantConfig} folds the stored styles into the model when it is\n * READ, so no stored row is migrated and nothing an operator saved is lost:\n *\n * - The four seeded defaults, UNMODIFIED, become **aliases** onto the vocabulary\n * (`thumbnail` → `square`@320, `card` → `landscape`@640, `hero` → `wide`@1920,\n * `full` → the natural ladder's top rung). A caller that still asks for\n * `'thumbnail'` by name is pointed at a variant the vocabulary generates\n * anyway, instead of commissioning a near-duplicate file.\n * - Any other style — an operator's own, or a default they edited — becomes a\n * **one-rung shape** under its own name. It keeps generating at the width they\n * chose, and now at the ratio they chose as a first-class shape.\n * - A style no shape can express (`contain`/`fill`/`outside` fits, a height-only\n * box, an `inside` box bounded on both axes) is reported as **unmapped**,\n * with the reason. It is not dropped from anything: it still generates through\n * the legacy path. It simply has no place in a ratio-and-width model.\n *\n * ## Bounded by configuration\n *\n * Nothing here takes a ratio from markup. A render site asking for a raw ratio\n * (`'1007/577'`) gets CSS cropping and the natural ladder, and no generated\n * variants (D003, N1) — which is what keeps the variant matrix bounded by what\n * an operator configured rather than by what a page happens to contain.\n *\n * Pure — no DB, no sharp, no settings client. Unit-tested on its own.\n */\n\nimport type { ImageFormat, ImageShape, ImageStyle, ReadonlyImageVocabulary } from './types.js'\n\n/**\n * The implicit shape every configuration has: the photograph's own ratio, over\n * the ladder. It is what a slot that declares no shape draws, so it is what\n * that slot's `srcset` is made of. Reserved — no vocabulary shape may use it.\n */\nexport const NATURAL_SHAPE = 'natural'\n\n/**\n * The default width ladder. Six rungs, because this is a LOCAL sharp on\n * somebody's VPS: Astro ships a shorter list for local services than for\n * remote ones, and says why — every rung is a generated file (R006).\n */\nexport const DEFAULT_IMAGE_LADDER: readonly number[] = Object.freeze([\n 320, 640, 960, 1280, 1920, 2560,\n])\n\n/**\n * The seeded shape vocabulary.\n *\n * ⚠️ Portrait is **3:4, not 4:5**. 4:5 is folk wisdom with no design-system\n * backing anywhere in the survey; 3:4 has eBay's and the UAE design system's\n * (R009). Re-read R009 before changing it.\n */\nexport const DEFAULT_IMAGE_SHAPES: Readonly<Record<string, Readonly<ImageShape>>> = Object.freeze({\n square: Object.freeze({ ratio: '1:1' }),\n landscape: Object.freeze({ ratio: '3:2' }),\n wide: Object.freeze({ ratio: '16:9' }),\n portrait: Object.freeze({ ratio: '3:4' }),\n})\n\n/**\n * The seeded vocabulary + ladder — what a site that configured nothing gets.\n * Frozen: it is shared by every reader, so a caller that wants to edit it\n * copies it first (the settings editor and `resolveMediaConfig` both do).\n */\nexport const DEFAULT_IMAGE_VOCABULARY: ReadonlyImageVocabulary = Object.freeze({\n shapes: DEFAULT_IMAGE_SHAPES,\n ladder: DEFAULT_IMAGE_LADDER,\n})\n\nexport const DEFAULT_VARIANT_FORMAT: ImageFormat = 'webp'\nexport const DEFAULT_VARIANT_QUALITY = 80\n\n/**\n * Bounds on the vocabulary. Every shape and every rung is a file per image, so\n * these are what keep one upload from commissioning an unbounded amount of\n * sharp work (CLAUDE.md \"No unbounded fan-out\"). The planner applies a hard\n * per-image cap on top — `MAX_VARIANTS_PER_IMAGE` in `variant-plan.ts` —\n * because legacy styles, which predate these bounds, also become shapes.\n */\nexport const MAX_SHAPES = 16\n/**\n * How many legacy fixed styles become one-rung shapes; the rest are reported\n * as unmapped. Legacy styles predate every bound here, and the planner deals\n * rungs round-robin, so this is what guarantees EVERY shape its first variant\n * under `MAX_VARIANTS_PER_IMAGE`: `MAX_SHAPES + MAX_STYLE_SHAPES + 1` (natural)\n * is 33, well under 64. A style past it is not resolvable by name either — it\n * is in no shape, so `resolveStyleName` answers `undefined` for it.\n */\nexport const MAX_STYLE_SHAPES = 16\nexport const MAX_LADDER_RUNGS = 12\nexport const MIN_VARIANT_WIDTH = 16\nexport const MAX_VARIANT_WIDTH = 8192\n/** The largest term a `W:H` ratio may carry. `1:10000` is already absurd. */\nexport const MAX_RATIO_TERM = 10000\n\n/** Shape and style names — the same rule the style-name validator has always had. */\nexport const SHAPE_NAME_PATTERN = /^[a-z][a-z0-9_-]*$/\n\nconst RATIO_PATTERN = /^([1-9]\\d*):([1-9]\\d*)$/\n\n/**\n * `'3:2'` → `1.5` (width / height), or `null` when the value is not a ratio\n * this model accepts: two positive integers, neither above\n * {@link MAX_RATIO_TERM}.\n */\nexport function parseShapeRatio(ratio: unknown): number | null {\n if (typeof ratio !== 'string') return null\n const match = RATIO_PATTERN.exec(ratio)\n if (match === null) return null\n const w = Number(match[1])\n const h = Number(match[2])\n if (w > MAX_RATIO_TERM || h > MAX_RATIO_TERM) return null\n return w / h\n}\n\n/**\n * How many files a vocabulary plans per photograph, at most: every shape at its\n * widths (its own, else the ladder) plus `natural` over the ladder. The ONE\n * count both the settings schema and the editor check against the per-image\n * cap, so the editor cannot build what the route will refuse.\n */\nexport function plannedFileCount(vocabulary: {\n readonly shapes: Readonly<Record<string, { readonly widths?: readonly number[] | undefined }>>\n readonly ladder: readonly number[]\n}): number {\n return (\n vocabulary.ladder.length +\n Object.values(vocabulary.shapes).reduce(\n (sum, shape) => sum + (shape.widths ?? vocabulary.ladder).length,\n 0,\n )\n )\n}\n\nfunction gcd(a: number, b: number): number {\n let x = a\n let y = b\n while (y !== 0) {\n const t = y\n y = x % y\n x = t\n }\n return x\n}\n\n/** `(600, 400)` → `'3:2'`. Both arguments positive integers. */\nexport function reduceRatio(width: number, height: number): string {\n const d = gcd(width, height)\n return `${width / d}:${height / d}`\n}\n\n/** One shape the generator will produce variants for, fully defaulted. */\nexport interface ResolvedShape {\n readonly name: string\n /** width / height, or `null` for the photograph's own ratio. */\n readonly ratio: number | null\n /** Ascending, distinct. The rungs this shape is generated at, before the per-image filter. */\n readonly widths: readonly number[]\n readonly format: ImageFormat\n readonly quality: number\n /** Where it came from — the vocabulary, the implicit natural shape, or a legacy fixed style. */\n readonly origin: 'vocabulary' | 'natural' | 'style'\n}\n\n/**\n * A legacy style name answered by a (shape, rung) of the vocabulary.\n *\n * The rung is one the shape is CONFIGURED at. A given photograph may still not\n * have it — a source narrower than the rung plans nothing that wide. That is a\n * CONTRACT for the reader, which does not exist yet (images PR06 resolves\n * variants for a render): resolving an alias for one photograph must take the\n * nearest variant that photograph has, as it would for a shape asked by name.\n */\nexport interface ShapeAlias {\n readonly shape: string\n readonly width: number\n}\n\n/** A legacy style the shape model cannot express, and why. */\nexport interface UnmappedStyle {\n readonly name: string\n readonly reason: string\n}\n\n/** The whole variant configuration, resolved. */\nexport interface VariantConfig {\n /** `natural` first, then sorted by name. */\n readonly shapes: readonly ResolvedShape[]\n /** Legacy style name → the (shape, rung) that now answers it. */\n readonly aliases: ReadonlyMap<string, ShapeAlias>\n /** Legacy styles and vocabulary entries that have no place in the model. */\n readonly unmapped: readonly UnmappedStyle[]\n}\n\n/**\n * The four seeded defaults, exactly as `defaultImageStyles` has always defined\n * them, and the vocabulary shape each one maps to (images PR04 acceptance).\n *\n * The RUNG is not stored: it is the smallest of the shape's widths that is at\n * least the style's own width, else its largest — so on the seeded ladder\n * `thumbnail` (200) → `square`@320, `card` (600) → `landscape`@640, `hero`\n * (1920) → `wide`@1920 and `full` (2400) → `natural`@2560, and on an\n * operator's own ladder it lands on a rung that ladder actually has.\n *\n * A stored style is aliased only when it is STILL this default. An operator who\n * edited `card` to 800×600 meant 800×600; that becomes a one-rung shape.\n */\nexport const LEGACY_STYLE_ALIASES: ReadonlyMap<\n string,\n { readonly style: ImageStyle; readonly shape: string }\n> = new Map([\n [\n 'thumbnail',\n {\n style: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n shape: 'square',\n },\n ],\n [\n 'card',\n {\n style: { width: 600, height: 400, fit: 'cover', format: 'webp', quality: 85 },\n shape: 'landscape',\n },\n ],\n [\n 'hero',\n {\n style: { width: 1920, height: 800, fit: 'cover', format: 'webp', quality: 85 },\n shape: 'wide',\n },\n ],\n [\n 'full',\n { style: { width: 2400, fit: 'inside', format: 'webp', quality: 90 }, shape: NATURAL_SHAPE },\n ],\n])\n\n/** The rung an alias answers with — see {@link LEGACY_STYLE_ALIASES}. */\nfunction aliasRung(widths: readonly number[], wanted: number): number | undefined {\n return widths.find((w) => w >= wanted) ?? widths[widths.length - 1]\n}\n\n/**\n * Code-unit order, not `localeCompare`: the order decides nothing a reader\n * sees, but it must be the same on every process that plans a key, and a\n * collation depends on the ICU data and locale the process happens to have.\n */\nfunction compareNames(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0\n}\n\nfunction sameStyle(a: ImageStyle, b: ImageStyle): boolean {\n return (\n a.width === b.width &&\n a.height === b.height &&\n (a.fit ?? 'cover') === (b.fit ?? 'cover') &&\n (a.format ?? DEFAULT_VARIANT_FORMAT) === (b.format ?? DEFAULT_VARIANT_FORMAT) &&\n (a.quality ?? DEFAULT_VARIANT_QUALITY) === (b.quality ?? DEFAULT_VARIANT_QUALITY)\n )\n}\n\nfunction isWidth(value: unknown): value is number {\n return (\n typeof value === 'number' &&\n Number.isInteger(value) &&\n value >= MIN_VARIANT_WIDTH &&\n value <= MAX_VARIANT_WIDTH\n )\n}\n\n/** Ascending and distinct, invalid entries dropped. */\nfunction normaliseWidths(widths: readonly unknown[]): number[] {\n return [...new Set(widths.filter(isWidth))].sort((a, b) => a - b)\n}\n\nfunction isQuality(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 100\n}\n\nconst FORMATS: ReadonlySet<string> = new Set<ImageFormat>(['webp', 'jpeg', 'png', 'avif'])\n\nfunction isFormat(value: unknown): value is ImageFormat {\n return typeof value === 'string' && FORMATS.has(value)\n}\n\n/**\n * Fold a vocabulary and the stored fixed styles into one {@link VariantConfig}.\n *\n * LENIENT, because it is a READ: the settings route validates what it stores\n * (`image-styles-settings.ts`), but plugin config is code nobody validated, and\n * a bad entry must cost that entry — reported in `unmapped` — never every\n * upload's variants. Deterministic: the same input always yields the same\n * config, in the same order, which the variant keys depend on.\n */\nexport function buildVariantConfig(input: {\n readonly vocabulary: ReadonlyImageVocabulary\n readonly styles: Readonly<Record<string, ImageStyle>>\n}): VariantConfig {\n const unmapped: UnmappedStyle[] = []\n const byName = new Map<string, ResolvedShape>()\n\n const ladder = normaliseWidths(\n Array.isArray(input.vocabulary.ladder) ? input.vocabulary.ladder : [],\n ).slice(0, MAX_LADDER_RUNGS)\n const effectiveLadder = ladder.length > 0 ? ladder : [...DEFAULT_IMAGE_LADDER]\n\n const vocabularyEntries = Object.entries(input.vocabulary.shapes ?? {}).sort(([a], [b]) =>\n compareNames(a, b),\n )\n for (const [name, shape] of vocabularyEntries) {\n if (!SHAPE_NAME_PATTERN.test(name) || name === NATURAL_SHAPE) {\n unmapped.push({ name, reason: 'not a usable shape name' })\n continue\n }\n if (byName.size >= MAX_SHAPES) {\n unmapped.push({ name, reason: `more than ${MAX_SHAPES} shapes configured` })\n continue\n }\n const ratio = parseShapeRatio(shape?.ratio)\n if (ratio === null) {\n unmapped.push({ name, reason: 'ratio is not W:H in positive integers' })\n continue\n }\n const own = Array.isArray(shape.widths) ? normaliseWidths(shape.widths) : []\n byName.set(name, {\n name,\n ratio,\n widths: own.length > 0 ? own.slice(0, MAX_LADDER_RUNGS) : effectiveLadder,\n format: isFormat(shape.format) ? shape.format : DEFAULT_VARIANT_FORMAT,\n quality: isQuality(shape.quality) ? shape.quality : DEFAULT_VARIANT_QUALITY,\n origin: 'vocabulary',\n })\n }\n\n byName.set(NATURAL_SHAPE, {\n name: NATURAL_SHAPE,\n ratio: null,\n widths: effectiveLadder,\n format: DEFAULT_VARIANT_FORMAT,\n quality: DEFAULT_VARIANT_QUALITY,\n origin: 'natural',\n })\n\n // A Map, not an object literal: every lookup below is by an operator-chosen\n // NAME, and `constructor` passes the name rule — on a plain object it would\n // find `Object.prototype.constructor` instead of nothing.\n const aliases = new Map<string, ShapeAlias>()\n const claimed = new Set<string>()\n // What an alias may point at: the vocabulary and `natural` — fixed BEFORE the\n // styles are read, so an operator's own style never becomes an alias target\n // (a 3:1 style called `square` must not answer for `thumbnail`), and the\n // answer does not depend on which style names sort first.\n const aliasTargets = new Map(byName)\n let styleShapes = 0\n\n const styleEntries = Object.entries(input.styles ?? {}).sort(([a], [b]) => compareNames(a, b))\n for (const [name, style] of styleEntries) {\n const legacy = LEGACY_STYLE_ALIASES.get(name)\n // Still the default AND its shape still exists → an alias, below. With the\n // shape gone the style stands on its own as a one-rung shape, rather than\n // being neither a shape, an alias nor unmapped.\n if (legacy && sameStyle(style, legacy.style) && aliasTargets.has(legacy.shape)) continue\n claimed.add(name)\n if (byName.has(name)) {\n unmapped.push({ name, reason: 'a vocabulary shape has the same name' })\n continue\n }\n const converted = styleAsShape(name, style)\n if ('reason' in converted) unmapped.push(converted)\n else if (styleShapes >= MAX_STYLE_SHAPES) {\n unmapped.push({ name, reason: `more than ${MAX_STYLE_SHAPES} fixed styles configured` })\n } else {\n byName.set(name, converted)\n styleShapes++\n }\n }\n\n for (const [name, legacy] of LEGACY_STYLE_ALIASES) {\n // A name an operator reused for their own style, or one the vocabulary\n // itself defines, answers as itself — never as the alias.\n if (claimed.has(name) || byName.has(name)) continue\n const target = aliasTargets.get(legacy.shape)\n const width = target && legacy.style.width && aliasRung(target.widths, legacy.style.width)\n if (target && width) aliases.set(name, { shape: target.name, width })\n }\n\n // `natural` first: it is what every slot that names no shape draws, so when\n // the per-image cap has to cut, it must not be the shape that loses.\n const shapes = [...byName.values()].sort((a, b) =>\n a.origin === 'natural' ? -1 : b.origin === 'natural' ? 1 : compareNames(a.name, b.name),\n )\n return { shapes, aliases, unmapped }\n}\n\n/** A legacy fixed style as a one-rung shape, or the reason it cannot be one. */\nfunction styleAsShape(name: string, style: ImageStyle): ResolvedShape | UnmappedStyle {\n if (!SHAPE_NAME_PATTERN.test(name)) return { name, reason: 'not a usable shape name' }\n const fit = style.fit ?? 'cover'\n const format = isFormat(style.format) ? style.format : DEFAULT_VARIANT_FORMAT\n const quality = isQuality(style.quality) ? style.quality : DEFAULT_VARIANT_QUALITY\n if (!isWidth(style.width)) {\n return { name, reason: 'no width, or a width outside the ladder bounds' }\n }\n if (fit === 'cover') {\n if (!isWidth(style.height)) return { name, reason: 'a cover style needs a height' }\n return {\n name,\n ratio: style.width / style.height,\n widths: [style.width],\n format,\n quality,\n origin: 'style',\n }\n }\n if (fit === 'inside' && style.height === undefined) {\n return { name, ratio: null, widths: [style.width], format, quality, origin: 'style' }\n }\n return { name, reason: `fit '${fit}' with this box has no single ratio and width` }\n}\n\n/**\n * What a legacy style NAME refers to in the shape model: the (shape, rung) of\n * its alias, or — for a style that became a one-rung shape — that shape at its\n * one width. `undefined` for a name the model does not know.\n */\nexport function resolveStyleName(config: VariantConfig, name: string): ShapeAlias | undefined {\n const alias = config.aliases.get(name)\n if (alias) return alias\n const shape = config.shapes.find((s) => s.name === name && s.origin === 'style')\n const width = shape?.widths[0]\n return shape && width !== undefined ? { shape: shape.name, width } : undefined\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";var t=e({getMediaJobs:()=>r,setMediaJobs:()=>i});const n=Symbol.for(`@murumets-ee/media:jobs`);function r(){return globalThis[n]}function i(e){globalThis[n]=e}export{i as n,t as r,r as t};
|
|
2
|
+
//# sourceMappingURL=slot-CY4isqmN.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot-CY4isqmN.mjs","names":[],"sources":["../src/jobs/slot.ts"],"sourcesContent":["/**\n * Where the registered media job handles live, so an enqueuer can find them\n * without importing the registration (which pulls in `@murumets-ee/core`) —\n * the entity's own behavior reads this, and `entity.ts` must stay light.\n *\n * `globalThis` + `Symbol.for`, the queue's own registry shape: it survives HMR\n * and a duplicated module instance, so the upload route in the Next process\n * sees the handles the plugin's `init` stored.\n */\n\nimport type { JobDefinition } from '@murumets-ee/queue/client'\nimport type { GenerateVariantsPayload, VariantsBackfillPayload } from './definitions.js'\n\nexport interface MediaJobs {\n readonly generateVariants: JobDefinition<GenerateVariantsPayload>\n readonly variantsBackfill: JobDefinition<VariantsBackfillPayload>\n}\n\nconst SLOT = Symbol.for('@murumets-ee/media:jobs')\ninterface GlobalWithSlot {\n [SLOT]?: MediaJobs\n}\n\n/** The registered job handles, or `undefined` when the queue plugin is not in this app. */\nexport function getMediaJobs(): MediaJobs | undefined {\n return (globalThis as GlobalWithSlot)[SLOT]\n}\n\n/** Written once, by `registerMediaJobs`. */\nexport function setMediaJobs(jobs: MediaJobs): void {\n ;(globalThis as GlobalWithSlot)[SLOT] = jobs\n}\n\n/** For tests only. */\nexport function clearMediaJobs(): void {\n delete (globalThis as GlobalWithSlot)[SLOT]\n}\n"],"mappings":"qGAkBA,MAAM,EAAO,OAAO,IAAI,yBAAyB,EAMjD,SAAgB,GAAsC,CACpD,OAAQ,WAA8B,EACxC,CAGA,SAAgB,EAAa,EAAuB,CACjD,WAA+B,GAAQ,CAC1C"}
|
|
@@ -72,6 +72,18 @@ declare const Media: import("@murumets-ee/entity").Entity<{
|
|
|
72
72
|
* its own accepts any JSON.
|
|
73
73
|
*/
|
|
74
74
|
focalPoint: import("@murumets-ee/entity").JsonField & Partial<import("@murumets-ee/entity").JsonField>;
|
|
75
|
+
/**
|
|
76
|
+
* Per-shape crop overrides, keyed by shape name — `{x, y, w, h}` fractions
|
|
77
|
+
* of the image, `[0,1]` (images D002). Optional: a shape absent from this
|
|
78
|
+
* map falls back to the rect `focalPoint` implies for that shape's ratio,
|
|
79
|
+
* which is why a missing crop is a fallback and never a hole. Written by
|
|
80
|
+
* the crop editor (`@murumets-ee/media/crop-editor`, images PR03).
|
|
81
|
+
*
|
|
82
|
+
* `null` and `{}` both mean "no shape has an override" — the same reading
|
|
83
|
+
* `readFocalPoint` gives an absent focal point (D010). Shape is enforced
|
|
84
|
+
* by `cropsValidation()` below.
|
|
85
|
+
*/
|
|
86
|
+
crops: import("@murumets-ee/entity").JsonField & Partial<import("@murumets-ee/entity").JsonField>;
|
|
75
87
|
mediaType: import("@murumets-ee/entity").SelectField & {
|
|
76
88
|
options: readonly ["image", "video", "audio", "document", "other"];
|
|
77
89
|
} & {
|
|
@@ -79,7 +91,7 @@ declare const Media: import("@murumets-ee/entity").Entity<{
|
|
|
79
91
|
readonly required: true;
|
|
80
92
|
readonly indexed: true;
|
|
81
93
|
};
|
|
82
|
-
}, "media", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<Record<never, never>, "focalPointValidation">, import("@murumets-ee/entity").SearchableBehavior]>;
|
|
94
|
+
}, "media", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<Record<never, never>, "focalPointValidation">, import("@murumets-ee/entity").Behavior<Record<never, never>, "cropsValidation">, import("@murumets-ee/entity").Behavior<Record<never, never>, "variantRegeneration">, import("@murumets-ee/entity").SearchableBehavior]>;
|
|
83
95
|
//#endregion
|
|
84
96
|
//#region src/types.d.ts
|
|
85
97
|
/** Media type derived from MIME type */
|
|
@@ -146,6 +158,68 @@ interface MediaListResult {
|
|
|
146
158
|
limit: number;
|
|
147
159
|
offset: number;
|
|
148
160
|
}
|
|
161
|
+
/** An output encoding for a generated variant. */
|
|
162
|
+
type ImageFormat = 'webp' | 'jpeg' | 'png' | 'avif';
|
|
163
|
+
/**
|
|
164
|
+
* A named aspect ratio in the operator's shape vocabulary (images D003).
|
|
165
|
+
*
|
|
166
|
+
* A shape is a NAME carrying ONE ratio; its SIZES come from the global
|
|
167
|
+
* {@link ImageVocabulary.ladder}. A variant is (shape × ladder rung), so one
|
|
168
|
+
* shape yields a whole `srcset` — the thing an {@link ImageStyle}, being one
|
|
169
|
+
* ratio at one width, could never produce.
|
|
170
|
+
*
|
|
171
|
+
* A `type`, not an `interface`, because it is stored inside a `setting.json()`
|
|
172
|
+
* value, and only a type alias is assignable to a JSON value type.
|
|
173
|
+
*/
|
|
174
|
+
type ImageShape = {
|
|
175
|
+
/** `W:H` in positive integers — `'3:2'`, `'3:4'`, `'16:9'`. */ratio: string;
|
|
176
|
+
/**
|
|
177
|
+
* Widths for THIS shape instead of the global ladder. Unset for a vocabulary
|
|
178
|
+
* shape; a legacy fixed style survives as a shape with exactly one entry.
|
|
179
|
+
*/
|
|
180
|
+
widths?: number[]; /** Output format (default: `'webp'`). */
|
|
181
|
+
format?: ImageFormat; /** Output quality 1–100 (default: 80). */
|
|
182
|
+
quality?: number;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* The shape vocabulary and the width ladder — the `vocabulary` key of the
|
|
186
|
+
* `media.imageStyles` settings namespace (images D003, C7).
|
|
187
|
+
*/
|
|
188
|
+
type ImageVocabulary = {
|
|
189
|
+
/** Shape name → shape. Names follow the style-name rule and may not be `natural`. */shapes: Record<string, ImageShape>; /** Ascending, distinct pixel widths every shape is generated at. */
|
|
190
|
+
ladder: number[];
|
|
191
|
+
};
|
|
192
|
+
/** A vocabulary as it is READ — what config and the model accept; nothing mutates one. */
|
|
193
|
+
type ReadonlyImageVocabulary = {
|
|
194
|
+
readonly shapes: Readonly<Record<string, ImageShape>>;
|
|
195
|
+
readonly ladder: readonly number[];
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* A FIXED-BOX image processing preset — one ratio at one width.
|
|
199
|
+
*
|
|
200
|
+
* The pre-images-D003 model, still honoured: stored styles keep generating
|
|
201
|
+
* exactly as before, and each one also appears in the shape model as a
|
|
202
|
+
* one-rung shape (or, for the four seeded defaults, as an ALIAS onto the
|
|
203
|
+
* vocabulary — see `shapes.ts`).
|
|
204
|
+
*/
|
|
205
|
+
/**
|
|
206
|
+
* A named shape the crop editor offers a tab for (images PR03) — a
|
|
207
|
+
* configured image style that declares a fixed aspect ratio.
|
|
208
|
+
*
|
|
209
|
+
* Derived from `ImageStyle`, not from {@link ImageShape}: the crop editor's
|
|
210
|
+
* own aspect-lock math (`crop-editor.tsx`, `crop/crop-rect.ts`) works in a
|
|
211
|
+
* NUMERIC width/height ratio throughout, and `ImageShape.ratio` is the
|
|
212
|
+
* vocabulary's `'W:H'` STRING form (images D003, PR04) — converting the
|
|
213
|
+
* editor to consume it is a separate, larger change than this PR's scope.
|
|
214
|
+
* `deriveCropShapes` excludes a style with only ONE dimension set
|
|
215
|
+
* (`full: { width: 2400, fit: 'inside' }`), which has no fixed ratio to crop
|
|
216
|
+
* against (see `crop/shapes.ts`).
|
|
217
|
+
*/
|
|
218
|
+
interface CropShape {
|
|
219
|
+
readonly name: string;
|
|
220
|
+
/** width / height. */
|
|
221
|
+
readonly ratio: number;
|
|
222
|
+
}
|
|
149
223
|
/** Named image processing preset — defines how to resize/convert an image variant */
|
|
150
224
|
interface ImageStyle {
|
|
151
225
|
/** Max width in pixels (omit for height-only constraint) */
|
|
@@ -155,7 +229,7 @@ interface ImageStyle {
|
|
|
155
229
|
/** Resize strategy (default: 'cover') */
|
|
156
230
|
fit?: 'cover' | 'contain' | 'inside' | 'outside' | 'fill';
|
|
157
231
|
/** Output format (default: 'webp') */
|
|
158
|
-
format?:
|
|
232
|
+
format?: ImageFormat;
|
|
159
233
|
/** Output quality 1–100 (default: 80) */
|
|
160
234
|
quality?: number;
|
|
161
235
|
}
|
|
@@ -173,9 +247,15 @@ interface MediaPluginConfig {
|
|
|
173
247
|
maxUploadSize?: number;
|
|
174
248
|
/** Default file visibility (default: 'public') */
|
|
175
249
|
defaultVisibility?: FileVisibility;
|
|
176
|
-
/** Named image processing presets.
|
|
250
|
+
/** Named image processing presets. A file per style is generated for every image, by the queue worker (images D004). */
|
|
177
251
|
imageStyles?: Record<string, ImageStyle>;
|
|
252
|
+
/**
|
|
253
|
+
* The shape vocabulary and width ladder, used when the settings DB holds none
|
|
254
|
+
* (default: `DEFAULT_IMAGE_VOCABULARY` — square 1:1, landscape 3:2, wide 16:9,
|
|
255
|
+
* portrait 3:4 over `[320, 640, 960, 1280, 1920, 2560]`).
|
|
256
|
+
*/
|
|
257
|
+
vocabulary?: ReadonlyImageVocabulary;
|
|
178
258
|
}
|
|
179
259
|
//#endregion
|
|
180
|
-
export {
|
|
181
|
-
//# sourceMappingURL=types-
|
|
260
|
+
export { ImageVocabulary as a, MediaPluginConfig as c, MediaUploadOptions as d, MediaUploadResult as f, ImageStyle as i, MediaRecord as l, Media as m, ImageFormat as n, MediaListOptions as o, ReadonlyImageVocabulary as p, ImageShape as r, MediaListResult as s, CropShape as t, MediaType as u };
|
|
261
|
+
//# sourceMappingURL=types-CgkJF5dc.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-CgkJF5dc.d.mts","names":[],"sources":["../src/entity.ts","../src/types.ts"],"mappings":";;;;;;;;AAsBA;;;;;;;;;;;;cAAa,KAAA,gCAAK,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KCjBN,SAAA;;UAGK,kBAAA;;EAEf,QAAA;;EAEA,QAAA;;EAEA,IAAA;;EAEA,UAAA,GAAa,cAAc;EDMX;ECJhB,KAAA;;EAEA,GAAA;;EAEA,WAAA;;EAEA,KAAA;;EAEA,MAAA;;EAEA,UAAA;AAAA;;UAIe,iBAAA;;EAEf,KAAA,EAAO,WAAW;;EAElB,GAAA;AAAA;;;;;;;;KAUU,WAAA,GAAc,cAAc,QAAQ,KAAA,CAAM,SAAA;;UAGrC,gBAAA;;EAEf,SAAA,GAAY,SAAS;;EAErB,cAAA;;EAEA,MAAA;;EAEA,KAAA;;EAEA,MAAA;;EAEA,OAAA;;EAEA,cAAA;AAAA;;UAIe,eAAA;EACf,KAAA,EAAO,WAAW;EAClB,KAAA;EACA,KAAA;EACA,MAAA;AAAA;;KAIU,WAAA;;;;;;;;;;;;KAaA,UAAA;iEAEV,KAAA;;AArFF;;;EA0FE,MAAA,aA1FmB;EA4FnB,MAAA,GAAS,WAAW,EAzFa;EA2FjC,OAAA;AAAA;;;;;KAOU,eAAA;EAxFV,qFA0FA,MAAA,EAAQ,MAAM,SAAS,UAAA,GAtFvB;EAwFA,MAAA;AAAA;;KAIU,uBAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,MAAA,SAAe,UAAA;EAAA,SAChC,MAAA;AAAA;;;;;;;AAhFN;AAUL;;;;;;;;AAA+D;AAG/D;;;;;UA4FiB,SAAA;EAAA,SACN,IAAA;EAvFT;EAAA,SAyFS,KAAK;AAAA;;UAIC,UAAA;EArFD;EAuFd,KAAA;EAnFe;EAqFf,MAAA;;EAEA,GAAA;EAtFA;EAwFA,MAAA,GAAS,WAAW;EAvFpB;EAyFA,OAAA;AAAA;;UAIe,iBAAA;EAvFL;;;;AAAW;AAavB;;EAkFE,aAAA;EAzEoB;EA2EpB,aAAA;EA7EA;EA+EA,iBAAA,GAAoB,cAAA;EA7EX;EA+ET,WAAA,GAAc,MAAA,SAAe,UAAA;EA7EtB;AAAA;AAOT;;;EA4EE,UAAA,GAAa,uBAAA;AAAA"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/picker/types.d.ts
|
|
4
|
+
/** Lightweight media item for the picker (no server-only deps) */
|
|
5
|
+
interface MediaPickerItem {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string | null;
|
|
8
|
+
alt: string | null;
|
|
9
|
+
filename: string;
|
|
10
|
+
mimeType: string;
|
|
11
|
+
size: number;
|
|
12
|
+
width: number | null;
|
|
13
|
+
height: number | null;
|
|
14
|
+
mediaType: 'image' | 'video' | 'audio' | 'document' | 'other';
|
|
15
|
+
url: string;
|
|
16
|
+
}
|
|
17
|
+
/** Paginated result from fetchMedia callback */
|
|
18
|
+
interface MediaPickerListResult {
|
|
19
|
+
items: MediaPickerItem[];
|
|
20
|
+
total: number;
|
|
21
|
+
/**
|
|
22
|
+
* False when the server reports storage is not configured (missing
|
|
23
|
+
* env vars etc.). UI renders a banner with `reason` instead of the
|
|
24
|
+
* upload zone + grid. Omitted/true when everything is wired.
|
|
25
|
+
*/
|
|
26
|
+
configured?: boolean;
|
|
27
|
+
/** Human-readable explanation when `configured === false`. */
|
|
28
|
+
reason?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Callbacks the consumer provides — framework-agnostic */
|
|
31
|
+
interface MediaPickerCallbacks {
|
|
32
|
+
/** Fetch media items with filtering and pagination */
|
|
33
|
+
fetchMedia: (options: {
|
|
34
|
+
search?: string | undefined;
|
|
35
|
+
mediaType?: string | undefined;
|
|
36
|
+
limit: number;
|
|
37
|
+
offset: number;
|
|
38
|
+
}) => Promise<MediaPickerListResult>;
|
|
39
|
+
/** Upload a file and return the created media item */
|
|
40
|
+
uploadMedia: (file: File) => Promise<MediaPickerItem>;
|
|
41
|
+
}
|
|
42
|
+
/** Selection mode */
|
|
43
|
+
type MediaPickerMode = 'single' | 'multiple';
|
|
44
|
+
/** Per-element class overrides for MediaPicker */
|
|
45
|
+
interface MediaPickerClassNames {
|
|
46
|
+
overlay?: string;
|
|
47
|
+
content?: string;
|
|
48
|
+
header?: string;
|
|
49
|
+
title?: string;
|
|
50
|
+
toolbar?: string;
|
|
51
|
+
searchInput?: string;
|
|
52
|
+
filterTabs?: string;
|
|
53
|
+
grid?: string;
|
|
54
|
+
card?: string;
|
|
55
|
+
cardSelected?: string;
|
|
56
|
+
cardImage?: string;
|
|
57
|
+
cardLabel?: string;
|
|
58
|
+
uploadZone?: string;
|
|
59
|
+
uploadZoneActive?: string;
|
|
60
|
+
footer?: string;
|
|
61
|
+
confirmButton?: string;
|
|
62
|
+
cancelButton?: string;
|
|
63
|
+
loading?: string;
|
|
64
|
+
empty?: string;
|
|
65
|
+
}
|
|
66
|
+
/** Props for the MediaPicker dialog */
|
|
67
|
+
interface MediaPickerProps {
|
|
68
|
+
/** Whether the dialog is open */
|
|
69
|
+
open: boolean;
|
|
70
|
+
/** Called when open state changes */
|
|
71
|
+
onOpenChange: (open: boolean) => void;
|
|
72
|
+
/** Called when user confirms selection */
|
|
73
|
+
onSelect: (items: MediaPickerItem[]) => void;
|
|
74
|
+
/** Selection mode (default: 'single') */
|
|
75
|
+
mode?: MediaPickerMode | undefined;
|
|
76
|
+
/** MIME patterns for upload restriction (e.g., ['image/*', 'video/*']) — passed to file input */
|
|
77
|
+
accept?: string[] | undefined;
|
|
78
|
+
/** Filter browse results to a specific media classification (e.g., 'image', 'video') */
|
|
79
|
+
mediaType?: string | undefined;
|
|
80
|
+
/** Maximum items selectable in multi mode */
|
|
81
|
+
maxSelect?: number | undefined;
|
|
82
|
+
/** Currently selected item IDs (for pre-selection) */
|
|
83
|
+
selectedIds?: string[] | undefined;
|
|
84
|
+
/** Dialog title (default: 'Select Media') */
|
|
85
|
+
title?: string | undefined;
|
|
86
|
+
/** Dialog description for screen readers (optional) */
|
|
87
|
+
description?: string | undefined;
|
|
88
|
+
/** Per-element class overrides */
|
|
89
|
+
classNames?: MediaPickerClassNames | undefined;
|
|
90
|
+
/** Additional className for the dialog content */
|
|
91
|
+
className?: string | undefined;
|
|
92
|
+
/** Children rendered in dialog footer (extra actions) */
|
|
93
|
+
children?: ReactNode | undefined;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { MediaPickerMode as a, MediaPickerListResult as i, MediaPickerClassNames as n, MediaPickerProps as o, MediaPickerItem as r, MediaPickerCallbacks as t };
|
|
97
|
+
//# sourceMappingURL=types-DwzblZfW.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-DwzblZfW.d.mts","names":[],"sources":["../src/picker/types.ts"],"mappings":";;;;UAGiB,eAAA;EACf,EAAA;EACA,KAAA;EACA,GAAA;EACA,QAAA;EACA,QAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,SAAA;EACA,GAAA;AAAA;;UAIe,qBAAA;EACf,KAAA,EAAO,eAAe;EACtB,KAAA;EANG;AAIL;;;;EAQE,UAAA;EAPO;EASP,MAAA;AAAA;;UAIe,oBAAA;EAJT;EAMN,UAAA,GAAa,OAAA;IACX,MAAA;IACA,SAAA;IACA,KAAA;IACA,MAAA;EAAA,MACI,OAAA,CAAQ,qBAAA;EAGuB;EAArC,WAAA,GAAc,IAAA,EAAM,IAAA,KAAS,OAAA,CAAQ,eAAA;AAAA;;KAI3B,eAAA;;UAGK,qBAAA;EACf,OAAA;EACA,OAAA;EACA,MAAA;EACA,KAAA;EACA,OAAA;EACA,WAAA;EACA,UAAA;EACA,IAAA;EACA,IAAA;EACA,YAAA;EACA,SAAA;EACA,SAAA;EACA,UAAA;EACA,gBAAA;EACA,MAAA;EACA,aAAA;EACA,YAAA;EACA,OAAA;EACA,KAAA;AAAA;;UAIe,gBAAA;EArBf;EAuBA,IAAA;EArBA;EAuBA,YAAA,GAAe,IAAA;EArBf;EAuBA,QAAA,GAAW,KAAA,EAAO,eAAA;EArBlB;EAuBA,IAAA,GAAO,eAAA;EArBP;EAuBA,MAAA;EArBA;EAuBA,SAAA;EArBA;EAuBA,SAAA;EArBA;EAuBA,WAAA;EArBA;EAuBA,KAAA;EAtBK;EAwBL,WAAA;EApBe;EAsBf,UAAA,GAAa,qBAAA;;EAEb,SAAA;EAhBO;EAkBP,QAAA,GAAW,SAAA;AAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";var t=e({deriveShapedVariantKey:()=>a,deriveVariantKey:()=>n,fnv1a36:()=>i,variantFingerprint:()=>r});function n(e,t,n=`webp`){let r=e.lastIndexOf(`/`);return`${e.substring(0,r)}/${t}_${e.substring(r+1).replace(/\.[^.]+$/,``)}.${n}`}function r(e){let{extract:t}=e;return i(`${t.left},${t.top},${t.width},${t.height}>${e.width}x${e.height}q${e.quality}`)}function i(e){let t=2166136261;for(let n=0;n<e.length;n++)t^=e.charCodeAt(n),t=Math.imul(t,16777619);return(t>>>0).toString(36).padStart(7,`0`)}function a(e,t){let n=e.lastIndexOf(`/`);return`${n===-1?``:`${e.substring(0,n)}/`}variants/${e.substring(n+1).replace(/\.[^.]+$/,``)}_${t.shape}_${t.width}w_${t.fingerprint}.${t.format}`}export{t as i,n,r,a as t};
|
|
2
|
+
//# sourceMappingURL=variant-key-Cki4n-42.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"variant-key-Cki4n-42.mjs","names":[],"sources":["../src/variant-key.ts"],"sourcesContent":["/**\n * Variant key derivation — where a generated variant lives in storage.\n *\n * Two key shapes coexist (images D005):\n *\n * **Legacy** — one file per image STYLE, keyed by the style name alone:\n *\n * Original: uploads/2026/02/{uuid}/photo.jpg\n * Variant: uploads/2026/02/{uuid}/thumbnail_photo.webp\n *\n * Sound only while nothing about a variant can change. It stays exactly as it\n * is: every row that has not been regenerated under the shape model still has\n * its files there, and ticketing derives its thumbnails with it.\n *\n * **Shaped** — one file per (shape × rung), carrying a FINGERPRINT of the\n * recipe that produced it:\n *\n * Variant: uploads/2026/02/{uuid}/variants/photo_portrait_640w_1k2x9a0.webp\n *\n * The moment an editor can re-crop, a name-only key serves different bytes at\n * the same URL, and every cache in front of it — the CDN, the browser, the\n * content-api response that inlined the URL — keeps the old picture. So the\n * fingerprint is of the EFFECTIVE recipe: the integral pixel rect actually\n * extracted, the output box and the quality (R013). Same recipe → same key → a\n * backfill that re-derives it uploads nothing. Different recipe → different key\n * → every cache misses correctly, because it is a different file. No\n * timestamps, no salt: determinism is what makes the backfill idempotent.\n *\n * ⚠️ A query-string cache-buster is not an alternative. Storage keys are what\n * `getPublicUrls` resolves, so a query string is not part of the identity and\n * the stale file is still the file (D005 (d); Craft's issue #3685 is that\n * failure in a shipping product).\n *\n * The shaped keys live in a `variants/` directory beside the original, which is\n * what makes the two shapes impossible to confuse: a legacy key is always a\n * direct child of the original's directory (a style name cannot contain `/`),\n * and both stay under the original's own prefix — which ticketing's\n * prefix-ownership check relies on.\n */\n\n/**\n * Derive a LEGACY variant storage key from the original key + style name.\n *\n * @param originalKey - The original file's storage key\n * @param styleName - The image style name (e.g., 'thumbnail', 'medium')\n * @param format - The variant output format (default: 'webp')\n * @returns The derived variant key\n */\nexport function deriveVariantKey(originalKey: string, styleName: string, format = 'webp'): string {\n const lastSlash = originalKey.lastIndexOf('/')\n const dir = originalKey.substring(0, lastSlash)\n const filename = originalKey.substring(lastSlash + 1)\n const baseName = filename.replace(/\\.[^.]+$/, '')\n return `${dir}/${styleName}_${baseName}.${format}`\n}\n\n/** Everything that determines a shaped variant's bytes, apart from the source file itself. */\nexport interface VariantRecipe {\n /** The region `extract()` takes, in integral pixels of the image as DRAWN (post-EXIF). */\n readonly extract: {\n readonly left: number\n readonly top: number\n readonly width: number\n readonly height: number\n }\n /** The box the region is resized to. */\n readonly width: number\n readonly height: number\n readonly quality: number\n}\n\n/**\n * A short deterministic fingerprint of a {@link VariantRecipe}: 32-bit FNV-1a\n * over a canonical string, as 7 base-36 characters.\n *\n * Not cryptographic and not meant to be — it only has to differ between two\n * recipes for the SAME file, shape and width, and nothing an attacker controls\n * chooses between colliding recipes. Pure JS rather than `node:crypto` so the\n * key can be derived in any realm that needs to name a variant.\n */\nexport function variantFingerprint(recipe: VariantRecipe): string {\n const { extract: e } = recipe\n return fnv1a36(\n `${e.left},${e.top},${e.width},${e.height}>${recipe.width}x${recipe.height}q${recipe.quality}`,\n )\n}\n\n/** 32-bit FNV-1a of `canonical`, as 7 base-36 characters — see {@link variantFingerprint}. */\nexport function fnv1a36(canonical: string): string {\n let hash = 0x811c9dc5\n for (let i = 0; i < canonical.length; i++) {\n hash ^= canonical.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193)\n }\n return (hash >>> 0).toString(36).padStart(7, '0')\n}\n\n/** The parts a shaped variant key is made of. */\nexport interface ShapedVariantKeyParts {\n /** The shape name (`square`, `natural`, …). */\n readonly shape: string\n /** The rung the variant was PLANNED at — part of the name, not a claim about the file. */\n readonly width: number\n /** {@link variantFingerprint} of the recipe. */\n readonly fingerprint: string\n /** Output format — the extension. */\n readonly format: string\n}\n\n/**\n * Derive a SHAPED variant storage key (images D005).\n *\n * `photo.jpg` + `{portrait, 640, 1k2x9a0, webp}` →\n * `<dir>/variants/photo_portrait_640w_1k2x9a0.webp`.\n */\nexport function deriveShapedVariantKey(originalKey: string, parts: ShapedVariantKeyParts): string {\n const lastSlash = originalKey.lastIndexOf('/')\n const dir = lastSlash === -1 ? '' : `${originalKey.substring(0, lastSlash)}/`\n const filename = originalKey.substring(lastSlash + 1)\n const baseName = filename.replace(/\\.[^.]+$/, '')\n return `${dir}variants/${baseName}_${parts.shape}_${parts.width}w_${parts.fingerprint}.${parts.format}`\n}\n"],"mappings":"0JAgDA,SAAgB,EAAiB,EAAqB,EAAmB,EAAS,OAAgB,CAChG,IAAM,EAAY,EAAY,YAAY,GAAG,EAI7C,MAAO,GAHK,EAAY,UAAU,EAAG,CAGzB,EAAE,GAAG,EAAU,GAFV,EAAY,UAAU,EAAY,CAC3B,CAAC,CAAC,QAAQ,WAAY,EACT,EAAE,GAAG,GAC5C,CA0BA,SAAgB,EAAmB,EAA+B,CAChE,GAAM,CAAE,QAAS,GAAM,EACvB,OAAO,EACL,GAAG,EAAE,KAAK,GAAG,EAAE,IAAI,GAAG,EAAE,MAAM,GAAG,EAAE,OAAO,GAAG,EAAO,MAAM,GAAG,EAAO,OAAO,GAAG,EAAO,SACvF,CACF,CAGA,SAAgB,EAAQ,EAA2B,CACjD,IAAI,EAAO,WACX,IAAK,IAAI,EAAI,EAAG,EAAI,EAAU,OAAQ,IACpC,GAAQ,EAAU,WAAW,CAAC,EAC9B,EAAO,KAAK,KAAK,EAAM,QAAU,EAEnC,OAAQ,IAAS,EAAA,CAAG,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAClD,CAoBA,SAAgB,EAAuB,EAAqB,EAAsC,CAChG,IAAM,EAAY,EAAY,YAAY,GAAG,EAI7C,MAAO,GAHK,IAAc,GAAK,GAAK,GAAG,EAAY,UAAU,EAAG,CAAS,EAAE,GAG7D,WAFG,EAAY,UAAU,EAAY,CAC3B,CAAC,CAAC,QAAQ,WAAY,EACd,EAAE,GAAG,EAAM,MAAM,GAAG,EAAM,MAAM,IAAI,EAAM,YAAY,GAAG,EAAM,QACjG"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{i as e}from"./crop-rect-B-AJ2pWm.mjs";import{n as t,s as n}from"./focal-point-BWhtJzef.mjs";import{r}from"./variant-key-Cki4n-42.mjs";const i=64;function a(e,t,n){return Math.min(Math.max(e,t),n)}function o(e,t,n){let r=a(Math.round(e.x*t),0,t-1),i=a(Math.round(e.y*n),0,n-1);return{left:r,top:i,width:a(Math.round(e.w*t),1,t-r),height:a(Math.round(e.h*n),1,n-i)}}function s(r,i,a){return r.ratio===null?{left:0,top:0,width:i.width,height:i.height}:o(e(a.crops).get(r.name)??t(n(a.focalPoint),i.width/i.height,r.ratio),i.width,i.height)}function c(e,t){let n=e.ratio??t.width/t.height,r=[];for(let i of e.widths){let e=Math.max(1,Math.round(i/n));i<=t.width&&e<=t.height&&r.push([i,e])}if(r.length>0)return r;let i=Math.max(1,Math.round(t.width/n));return i<=t.height?[[t.width,i]]:[[a(Math.round(t.height*n),1,t.width),t.height]]}function l(e,t,n){if(!(e.width>=1&&e.height>=1))return{variants:[],dropped:0};let i=t.shapes.map(t=>{let r=s(t,e,n);return{shape:t,region:r,sizes:c(t,r)}}),a=[],o=0,l=Math.max(0,...i.map(e=>e.sizes.length));for(let e=0;e<l;e++)for(let{shape:t,region:n,sizes:s}of i){let i=s[e];if(!i)continue;if(a.length>=64){o++;continue}let[c,l]=i,u={extract:n,width:c,height:l,quality:t.quality};a.push({shape:t.name,width:c,height:l,extract:n,format:t.format,quality:t.quality,fingerprint:r(u)})}return{variants:a,dropped:o}}export{l as n,i as t};
|
|
2
|
+
//# sourceMappingURL=variant-plan-DV8hsMdz.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"variant-plan-DV8hsMdz.mjs","names":[],"sources":["../src/variant-plan.ts"],"sourcesContent":["/**\n * Planning a photograph's variants — which (shape × rung) files it gets, the\n * exact region each is cut from, and the fingerprint that names it.\n *\n * Pure: no sharp, no storage, no database. Planning is split from generation\n * so the same answer is available to a caller that must NAME a variant without\n * producing it — a queue job's idempotency key (images PR05), a resolver\n * deriving the key of a file that should already exist (images PR06) — and so\n * it can be unit-tested exhaustively without decoding a single image.\n *\n * ## The effective rect\n *\n * For a shape with a ratio, the region is `crops[shape]` when the editor drew\n * one, and otherwise the window the focal point implies for that ratio —\n * `focalCropRect`, the SAME function the browser's `object-position` comes\n * from, so the generated crop and the CSS crop cannot disagree (phase 03).\n * `natural` is the whole image. The rect is then denormalised to integral\n * pixels, because `extract()` takes nothing else (R001, C5).\n *\n * ## Never upscale\n *\n * A rung is planned only when the region can supply it on BOTH axes. When no\n * rung fits — a small source, or a tight crop — the shape still gets exactly\n * one variant, at the largest size the region supplies, so every shape has a\n * file and none of them is enlarged (R006: Astro filters the same way; unpic\n * cannot, because it does not know its originals).\n *\n * The planned width is what the generator asks for. What it RECORDS is what\n * sharp returned (F003) — see `generateShapedVariants`.\n */\n\nimport { readCrops } from './crop/crop-rect.js'\nimport { type FractionalRect, focalCropRect, readFocalPoint } from './crop/focal-point.js'\nimport type { ResolvedShape, VariantConfig } from './shapes.js'\nimport type { ImageFormat } from './types.js'\nimport { type VariantRecipe, variantFingerprint } from './variant-key.js'\n\n/**\n * The hard ceiling on variants per image, whatever the configuration says.\n *\n * The vocabulary is bounded (`MAX_SHAPES` × `MAX_LADDER_RUNGS`), but legacy\n * styles predate those bounds and each becomes a shape, so the product is not.\n * Every variant is a decode-extract-resize-encode on the generating process, so\n * this is the cap that keeps one upload from being an amplification vector. The\n * seeded vocabulary plans at most 30 (four shapes and `natural`, six rungs).\n * What it drops is COUNTED in {@link VariantPlan.dropped}, never silent.\n */\nexport const MAX_VARIANTS_PER_IMAGE = 64\n\n/** The region `extract()` takes, in integral pixels of the image as drawn. */\nexport interface PixelRect {\n readonly left: number\n readonly top: number\n readonly width: number\n readonly height: number\n}\n\n/** One variant to generate. */\nexport interface PlannedVariant {\n readonly shape: string\n /** The width to ask sharp for. Never above what {@link extract} supplies. */\n readonly width: number\n /** The height to ask sharp for — derived from the shape's ratio. */\n readonly height: number\n readonly extract: PixelRect\n readonly format: ImageFormat\n readonly quality: number\n /** {@link variantFingerprint} of this variant's recipe — goes into its key. */\n readonly fingerprint: string\n}\n\nexport interface VariantPlan {\n readonly variants: readonly PlannedVariant[]\n /** How many variants {@link MAX_VARIANTS_PER_IMAGE} cut off. */\n readonly dropped: number\n}\n\n/** What the plan needs from the media row. Untrusted — both are read leniently. */\nexport interface VariantPlanInput {\n readonly focalPoint?: unknown\n readonly crops?: unknown\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\n/** A fractional rect as integral pixels, never empty and never outside the image. */\nexport function toPixelRect(rect: FractionalRect, width: number, height: number): PixelRect {\n const left = clamp(Math.round(rect.x * width), 0, width - 1)\n const top = clamp(Math.round(rect.y * height), 0, height - 1)\n return {\n left,\n top,\n width: clamp(Math.round(rect.w * width), 1, width - left),\n height: clamp(Math.round(rect.h * height), 1, height - top),\n }\n}\n\n/**\n * The region a shape is cut from — its crop, else its focal window, else the\n * whole image.\n *\n * A stored crop is used as drawn even when its ratio is not quite the shape's\n * (`isCropRect` checks bounds, not ratio): the resize is `cover`, so the drift\n * is trimmed evenly from the region's long axis. The crop editor constrains the\n * window to the shape, so in practice the drift is sub-pixel rounding.\n */\nexport function effectiveRect(\n shape: ResolvedShape,\n source: { readonly width: number; readonly height: number },\n input: VariantPlanInput,\n): PixelRect {\n if (shape.ratio === null) return { left: 0, top: 0, width: source.width, height: source.height }\n const crop = readCrops(input.crops).get(shape.name)\n const rect =\n crop ??\n focalCropRect(readFocalPoint(input.focalPoint), source.width / source.height, shape.ratio)\n return toPixelRect(rect, source.width, source.height)\n}\n\n/**\n * The (width, height) boxes a shape is generated at, from one region: every\n * rung the region supplies on both axes, or — when none does — one box at the\n * largest size it supplies.\n */\nfunction sizesFor(shape: ResolvedShape, region: PixelRect): Array<[number, number]> {\n const ratio = shape.ratio ?? region.width / region.height\n const sizes: Array<[number, number]> = []\n for (const rung of shape.widths) {\n const h = Math.max(1, Math.round(rung / ratio))\n if (rung <= region.width && h <= region.height) sizes.push([rung, h])\n }\n if (sizes.length > 0) return sizes\n // The whole region's width, unless the ratio makes that too tall — then the\n // whole region's height. Rounded, so a region that IS the shape (to the\n // pixel) is used whole rather than trimmed by a floor.\n const tall = Math.max(1, Math.round(region.width / ratio))\n if (tall <= region.height) return [[region.width, tall]]\n return [[clamp(Math.round(region.height * ratio), 1, region.width), region.height]]\n}\n\n/**\n * Plan every variant a photograph of `source` dimensions gets under `config`.\n *\n * `source` must be the DRAWN size (post-EXIF orientation) of the actual file —\n * `getImageDimensions`, not the row's client-measured `width`/`height` — since\n * the regions are cut from the file.\n */\nexport function planVariants(\n source: { readonly width: number; readonly height: number },\n config: VariantConfig,\n input: VariantPlanInput,\n): VariantPlan {\n if (!(source.width >= 1 && source.height >= 1)) return { variants: [], dropped: 0 }\n const perShape = config.shapes.map((shape) => {\n const region = effectiveRect(shape, source, input)\n return { shape, region, sizes: sizesFor(shape, region) }\n })\n // Round-robin by rung — every shape's smallest variant before any shape's\n // second — so if the cap cuts, it cuts the LARGEST files of the most shapes\n // rather than every file of whichever shapes sort last. `config.shapes` puts\n // `natural` first, so it is never the one left with nothing.\n const variants: PlannedVariant[] = []\n let dropped = 0\n const deepest = Math.max(0, ...perShape.map((p) => p.sizes.length))\n for (let rung = 0; rung < deepest; rung++) {\n for (const { shape, region, sizes } of perShape) {\n const size = sizes[rung]\n if (!size) continue\n if (variants.length >= MAX_VARIANTS_PER_IMAGE) {\n dropped++\n continue\n }\n const [width, height] = size\n const recipe: VariantRecipe = { extract: region, width, height, quality: shape.quality }\n variants.push({\n shape: shape.name,\n width,\n height,\n extract: region,\n format: shape.format,\n quality: shape.quality,\n fingerprint: variantFingerprint(recipe),\n })\n }\n }\n return { variants, dropped }\n}\n"],"mappings":"6IA+CA,MAAa,EAAyB,GAoCtC,SAAS,EAAM,EAAe,EAAa,EAAqB,CAC9D,OAAO,KAAK,IAAI,KAAK,IAAI,EAAO,CAAG,EAAG,CAAG,CAC3C,CAGA,SAAgB,EAAY,EAAsB,EAAe,EAA2B,CAC1F,IAAM,EAAO,EAAM,KAAK,MAAM,EAAK,EAAI,CAAK,EAAG,EAAG,EAAQ,CAAC,EACrD,EAAM,EAAM,KAAK,MAAM,EAAK,EAAI,CAAM,EAAG,EAAG,EAAS,CAAC,EAC5D,MAAO,CACL,OACA,MACA,MAAO,EAAM,KAAK,MAAM,EAAK,EAAI,CAAK,EAAG,EAAG,EAAQ,CAAI,EACxD,OAAQ,EAAM,KAAK,MAAM,EAAK,EAAI,CAAM,EAAG,EAAG,EAAS,CAAG,CAC5D,CACF,CAWA,SAAgB,EACd,EACA,EACA,EACW,CAMX,OALI,EAAM,QAAU,KAAa,CAAE,KAAM,EAAG,IAAK,EAAG,MAAO,EAAO,MAAO,OAAQ,EAAO,MAAO,EAKxF,EAJM,EAAU,EAAM,KAAK,CAAC,CAAC,IAAI,EAAM,IAEzC,GACH,EAAc,EAAe,EAAM,UAAU,EAAG,EAAO,MAAQ,EAAO,OAAQ,EAAM,KAAK,EAClE,EAAO,MAAO,EAAO,MAAM,CACtD,CAOA,SAAS,EAAS,EAAsB,EAA4C,CAClF,IAAM,EAAQ,EAAM,OAAS,EAAO,MAAQ,EAAO,OAC7C,EAAiC,CAAC,EACxC,IAAK,IAAM,KAAQ,EAAM,OAAQ,CAC/B,IAAM,EAAI,KAAK,IAAI,EAAG,KAAK,MAAM,EAAO,CAAK,CAAC,EAC1C,GAAQ,EAAO,OAAS,GAAK,EAAO,QAAQ,EAAM,KAAK,CAAC,EAAM,CAAC,CAAC,CACtE,CACA,GAAI,EAAM,OAAS,EAAG,OAAO,EAI7B,IAAM,EAAO,KAAK,IAAI,EAAG,KAAK,MAAM,EAAO,MAAQ,CAAK,CAAC,EAEzD,OADI,GAAQ,EAAO,OAAe,CAAC,CAAC,EAAO,MAAO,CAAI,CAAC,EAChD,CAAC,CAAC,EAAM,KAAK,MAAM,EAAO,OAAS,CAAK,EAAG,EAAG,EAAO,KAAK,EAAG,EAAO,MAAM,CAAC,CACpF,CASA,SAAgB,EACd,EACA,EACA,EACa,CACb,GAAI,EAAE,EAAO,OAAS,GAAK,EAAO,QAAU,GAAI,MAAO,CAAE,SAAU,CAAC,EAAG,QAAS,CAAE,EAClF,IAAM,EAAW,EAAO,OAAO,IAAK,GAAU,CAC5C,IAAM,EAAS,EAAc,EAAO,EAAQ,CAAK,EACjD,MAAO,CAAE,QAAO,SAAQ,MAAO,EAAS,EAAO,CAAM,CAAE,CACzD,CAAC,EAKK,EAA6B,CAAC,EAChC,EAAU,EACR,EAAU,KAAK,IAAI,EAAG,GAAG,EAAS,IAAK,GAAM,EAAE,MAAM,MAAM,CAAC,EAClE,IAAK,IAAI,EAAO,EAAG,EAAO,EAAS,IACjC,IAAK,GAAM,CAAE,QAAO,SAAQ,WAAW,EAAU,CAC/C,IAAM,EAAO,EAAM,GACnB,GAAI,CAAC,EAAM,SACX,GAAI,EAAS,QAAA,GAAkC,CAC7C,IACA,QACF,CACA,GAAM,CAAC,EAAO,GAAU,EAClB,EAAwB,CAAE,QAAS,EAAQ,QAAO,SAAQ,QAAS,EAAM,OAAQ,EACvF,EAAS,KAAK,CACZ,MAAO,EAAM,KACb,QACA,SACA,QAAS,EACT,OAAQ,EAAM,OACd,QAAS,EAAM,QACf,YAAa,EAAmB,CAAM,CACxC,CAAC,CACH,CAEF,MAAO,CAAE,WAAU,SAAQ,CAC7B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@murumets-ee/media",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -55,6 +55,10 @@
|
|
|
55
55
|
"./crop": {
|
|
56
56
|
"types": "./dist/crop.d.mts",
|
|
57
57
|
"import": "./dist/crop.mjs"
|
|
58
|
+
},
|
|
59
|
+
"./crop-editor": {
|
|
60
|
+
"types": "./dist/crop-editor.d.mts",
|
|
61
|
+
"import": "./dist/crop-editor.mjs"
|
|
58
62
|
}
|
|
59
63
|
},
|
|
60
64
|
"files": [
|
|
@@ -65,29 +69,38 @@
|
|
|
65
69
|
"@radix-ui/react-visually-hidden": "^1.2.5",
|
|
66
70
|
"clsx": "^2.1.0",
|
|
67
71
|
"drizzle-orm": "^0.45.2",
|
|
72
|
+
"react-easy-crop": "6.2.3",
|
|
68
73
|
"server-only": "^0.0.1",
|
|
69
74
|
"sharp": "^0.34.5",
|
|
70
75
|
"tailwind-merge": "^2.6.0",
|
|
71
76
|
"zod": "^3.24.1",
|
|
72
|
-
"@murumets-ee/core": "0.
|
|
73
|
-
"@murumets-ee/db": "0.
|
|
74
|
-
"@murumets-ee/entity": "0.
|
|
75
|
-
"@murumets-ee/logging": "0.
|
|
76
|
-
"@murumets-ee/
|
|
77
|
-
"@murumets-ee/
|
|
78
|
-
"@murumets-ee/
|
|
77
|
+
"@murumets-ee/core": "0.72.0",
|
|
78
|
+
"@murumets-ee/db": "0.72.0",
|
|
79
|
+
"@murumets-ee/entity": "0.72.0",
|
|
80
|
+
"@murumets-ee/logging": "0.72.0",
|
|
81
|
+
"@murumets-ee/queue": "0.72.0",
|
|
82
|
+
"@murumets-ee/settings": "0.72.0",
|
|
83
|
+
"@murumets-ee/storage": "0.72.0",
|
|
84
|
+
"@murumets-ee/ui": "0.72.0"
|
|
79
85
|
},
|
|
80
86
|
"peerDependencies": {
|
|
81
87
|
"lucide-react": ">=0.400.0 <1",
|
|
88
|
+
"next-intl": ">=4.0.0 <5",
|
|
82
89
|
"react": ">=19.0.0 <20",
|
|
83
90
|
"react-dom": ">=19.0.0 <20"
|
|
84
91
|
},
|
|
92
|
+
"peerDependenciesMeta": {
|
|
93
|
+
"next-intl": {
|
|
94
|
+
"optional": true
|
|
95
|
+
}
|
|
96
|
+
},
|
|
85
97
|
"devDependencies": {
|
|
86
98
|
"@types/node": "^20.19.42",
|
|
87
99
|
"@types/react": "^19",
|
|
88
100
|
"@types/react-dom": "^19",
|
|
89
101
|
"file-type": "^21.3.4",
|
|
90
102
|
"lucide-react": "^0.577.0",
|
|
103
|
+
"next-intl": "^4.13.0",
|
|
91
104
|
"react": "19.2.7",
|
|
92
105
|
"react-dom": "19.2.7",
|
|
93
106
|
"tsdown": "^0.22.2",
|
package/dist/client-CSOH7Zun.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import"./entity-CNxAERYG.mjs";import{i as e,n as t,r as n,t as r}from"./variant-key-RKke2RfS.mjs";import"server-only";function i(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`}var a=class{admin;storage;imageStyles;constructor(e){this.admin=e.admin,this.storage=e.storage,this.imageStyles=e.imageStyles??null}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;let{getApp:e}=await import(`@murumets-ee/core`),{resolveImageStyles:t}=await import(`./resolve-image-styles-Cakg1Qpp.mjs`),n=e();return this.imageStyles=await t(n,n.logger),this.imageStyles}invalidateImageStylesCache(){this.imageStyles=null}async upload(a,c){let l=await this.storage.upload(a,{filename:c.filename,mimeType:c.mimeType,size:c.size,visibility:c.visibility,uploadedBy:c.uploadedBy}),u=c.width??null,d=c.height??null,f=null,p={};if(a instanceof Buffer&&n(c.mimeType))try{let n=await this.resolveImageStyles(),[i,o]=await Promise.all([e(a,n),t(a).catch(async e=>(await s(e,c.filename),null))]);f=o,u=i.width,d=i.height;let m=l.visibility;await Promise.all([...i.variants.entries()].map(async([e,t])=>{let n=r(l.key,e,t.format);try{await this.storage.upload(t.buffer,{key:n,filename:`${e}_${c.filename}`,mimeType:t.mimeType,size:t.buffer.byteLength,visibility:m,metadata:{variantOf:l.key,style:e},uploadedBy:c.uploadedBy}),p[e]=n}catch{}})),Object.keys(p).length>0&&await this.storage.updateMetadata(l.key,{metadata:{...l.metadata??{},variants:p}}).catch(()=>{})}catch{}let m=i(c.mimeType);try{return{media:await this.admin.create({title:c.title??o(c.filename),alt:c.alt??null,description:c.description??null,fileKey:l.key,filename:c.filename,mimeType:c.mimeType,size:c.size,width:u,height:d,focalPoint:f,mediaType:m}),url:await this.storage.getUrl(l.key)}}catch(e){for(let e of Object.values(p))await this.storage.delete(e).catch(()=>{});throw await this.storage.delete(l.key).catch(()=>{}),e}}async findById(e,t){return this.admin.findById(e,t)}async findMany(e){let{schemaRegistry:t}=await import(`@murumets-ee/db`),{and:n,asc:r,desc:i,eq:a,ilike:o,or:s,sql:c}=await import(`drizzle-orm`),l=t.get(`media`);if(!l)throw Error(`Media schema not registered. Is the media() plugin loaded?`);let u=[];if(e?.mediaType&&u.push(a(l.mediaType,e.mediaType)),e?.mimeTypePrefix){let t=e.mimeTypePrefix.replace(/[\\%_]/g,`\\$&`);u.push(o(l.mimeType,`${t}%`))}if(e?.search){let t=`%${e.search.replace(/[\\%_]/g,`\\$&`)}%`;u.push(s(o(l.filename,t),c`${l.fields} ->> 'title' ILIKE ${t}`))}let d=e?.limit??50,f=e?.offset??0,p=u.length>0?n(...u):void 0,m=await this.admin.count({where:p}),h=e?.orderBy===`filename`?l.filename:l.createdAt,g=(e?.orderDirection??`desc`)===`asc`?r:i;return{items:await this.admin.findMany({where:p,limit:d,offset:f,orderBy:g(h)}),total:m,limit:d,offset:f}}async update(e,t){return this.admin.update(e,t)}async delete(e){let t=await this.admin.findById(e);if(!t)throw Error(`Media not found: ${e}`);let n=t.fileKey;await this.admin.delete(e);let r=(await this.storage.getMetadata(n))?.metadata?.variants;if(r)for(let e of Object.values(r))await this.storage.delete(e).catch(()=>{});await this.storage.delete(n).catch(()=>{})}async getUrl(e){let t=await this.admin.findById(e);if(!t)throw Error(`Media not found: ${e}`);return this.storage.getUrl(t.fileKey)}async getUrls(e){if(e.length===0)return new Map;let{schemaRegistry:t}=await import(`@murumets-ee/db`),{inArray:n}=await import(`drizzle-orm`),r=t.get(`media`);if(!r)return new Map;let i=await this.admin.findMany({where:n(r.id,e),limit:e.length}),a=new Map;return await Promise.all(i.map(async e=>{let t=await this.storage.getUrl(e.fileKey);a.set(e.id,t)})),a}async getVariantUrl(e,t){let n=await this.admin.findById(e);if(!n)return null;let i=n.fileKey,a=(await this.resolveImageStyles())[t];if(a){let e=r(i,t,a.format??`webp`);try{return await this.storage.getUrl(e)}catch{}}try{return await this.storage.getUrl(i)}catch{return null}}async getVariantUrls(e,t){if(e.length===0)return new Map;let{schemaRegistry:n}=await import(`@murumets-ee/db`),{inArray:i}=await import(`drizzle-orm`),a=n.get(`media`);if(!a)return new Map;let o=await this.admin.findMany({where:i(a.id,e),limit:e.length}),s=(await this.resolveImageStyles())[t],c=new Map;return await Promise.all(o.map(async e=>{if(s){let n=r(e.fileKey,t,s.format??`webp`);try{let t=await this.storage.getUrl(n);c.set(e.id,t);return}catch{}}try{let t=await this.storage.getUrl(e.fileKey);c.set(e.id,t)}catch{}})),c}};function o(e){return e.replace(/\.[^.]+$/,``).replace(/[-_]/g,` `)}async function s(e,t){try{let{getApp:n}=await import(`@murumets-ee/core`);n().logger.warn({err:e,filename:t},`media: focal-point detection failed; stored as centre`)}catch(n){console.warn(`media: focal-point detection failed; stored as centre`,t,e,n)}}export{a as MediaClient};
|
|
2
|
-
//# sourceMappingURL=client-CSOH7Zun.mjs.map
|