@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,261 @@
|
|
|
1
|
+
import { InferEntityDTO } from "@murumets-ee/entity";
|
|
2
|
+
import { FileVisibility } from "@murumets-ee/storage";
|
|
3
|
+
|
|
4
|
+
//#region src/entity.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Pre-defined Media entity.
|
|
7
|
+
*
|
|
8
|
+
* Auto-registered by the media() plugin — users do NOT add this to their entities array.
|
|
9
|
+
* Connected to toolkit_files via the fileKey field (stores the StorageClient file key).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { media } from '@murumets-ee/media/plugin'
|
|
14
|
+
*
|
|
15
|
+
* export default defineConfig({
|
|
16
|
+
* plugins: [storage(), media()],
|
|
17
|
+
* // Media entity is auto-added — no need to list it here
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare const Media: import("@murumets-ee/entity").Entity<{
|
|
22
|
+
id: import("@murumets-ee/entity").IdField;
|
|
23
|
+
} & import("@murumets-ee/entity").AuditableFields & Record<never, never> & {
|
|
24
|
+
title: import("@murumets-ee/entity").TextField & {
|
|
25
|
+
readonly translatable: true;
|
|
26
|
+
};
|
|
27
|
+
alt: import("@murumets-ee/entity").TextField & {
|
|
28
|
+
readonly translatable: true;
|
|
29
|
+
};
|
|
30
|
+
description: import("@murumets-ee/entity").TextField & {
|
|
31
|
+
readonly translatable: true;
|
|
32
|
+
}; /** Key into toolkit_files table (e.g., 'uploads/2026/02/uuid/photo.jpg') */
|
|
33
|
+
fileKey: import("@murumets-ee/entity").TextField & {
|
|
34
|
+
readonly required: true;
|
|
35
|
+
readonly indexed: true;
|
|
36
|
+
};
|
|
37
|
+
filename: import("@murumets-ee/entity").TextField & {
|
|
38
|
+
readonly required: true;
|
|
39
|
+
};
|
|
40
|
+
mimeType: import("@murumets-ee/entity").TextField & {
|
|
41
|
+
readonly required: true;
|
|
42
|
+
readonly indexed: true;
|
|
43
|
+
readonly admin: {
|
|
44
|
+
readonly display: {
|
|
45
|
+
readonly type: "mimeType";
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
size: import("@murumets-ee/entity").NumberField & {
|
|
50
|
+
readonly required: true;
|
|
51
|
+
readonly integer: true;
|
|
52
|
+
readonly admin: {
|
|
53
|
+
readonly display: {
|
|
54
|
+
readonly type: "filesize";
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
width: import("@murumets-ee/entity").NumberField & {
|
|
59
|
+
readonly integer: true;
|
|
60
|
+
};
|
|
61
|
+
height: import("@murumets-ee/entity").NumberField & {
|
|
62
|
+
readonly integer: true;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Where the subject is, as `{ x, y }` fractions of the image — never pixels,
|
|
66
|
+
* so it survives the file being replaced at another resolution (images
|
|
67
|
+
* D002). A shaped slot crops around it (`@murumets-ee/media/crop`).
|
|
68
|
+
*
|
|
69
|
+
* Nullable, and `null` means the centre: an absent point and `{0.5, 0.5}`
|
|
70
|
+
* render identically, so no reader branches on a legacy row (D010). The
|
|
71
|
+
* shape is enforced by `focalPointValidation()` below — `field.json()` on
|
|
72
|
+
* its own accepts any JSON.
|
|
73
|
+
*/
|
|
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>;
|
|
87
|
+
mediaType: import("@murumets-ee/entity").SelectField & {
|
|
88
|
+
options: readonly ["image", "video", "audio", "document", "other"];
|
|
89
|
+
} & {
|
|
90
|
+
readonly options: readonly ["image", "video", "audio", "document", "other"];
|
|
91
|
+
readonly required: true;
|
|
92
|
+
readonly indexed: true;
|
|
93
|
+
};
|
|
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]>;
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/types.d.ts
|
|
97
|
+
/** Media type derived from MIME type */
|
|
98
|
+
type MediaType = 'image' | 'video' | 'audio' | 'document' | 'other';
|
|
99
|
+
/** Options for uploading media through MediaClient */
|
|
100
|
+
interface MediaUploadOptions {
|
|
101
|
+
/** Original filename */
|
|
102
|
+
filename: string;
|
|
103
|
+
/** MIME type */
|
|
104
|
+
mimeType: string;
|
|
105
|
+
/** File size in bytes */
|
|
106
|
+
size: number;
|
|
107
|
+
/** File visibility (default: from plugin config) */
|
|
108
|
+
visibility?: FileVisibility;
|
|
109
|
+
/** Display title (defaults to filename without extension) */
|
|
110
|
+
title?: string;
|
|
111
|
+
/** Alt text for images */
|
|
112
|
+
alt?: string;
|
|
113
|
+
/** Description */
|
|
114
|
+
description?: string;
|
|
115
|
+
/** Image width in pixels (client-measured) */
|
|
116
|
+
width?: number;
|
|
117
|
+
/** Image height in pixels (client-measured) */
|
|
118
|
+
height?: number;
|
|
119
|
+
/** UUID of the uploading user */
|
|
120
|
+
uploadedBy?: string;
|
|
121
|
+
}
|
|
122
|
+
/** Result of a media upload: entity record + storage URL */
|
|
123
|
+
interface MediaUploadResult {
|
|
124
|
+
/** The created media entity record */
|
|
125
|
+
media: MediaRecord;
|
|
126
|
+
/** The file's public/signed URL */
|
|
127
|
+
url: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Lightweight view of a media entity for API consumers.
|
|
131
|
+
*
|
|
132
|
+
* Structural alias of `InferEntityDTO<typeof Media.allFields>` — the Media entity
|
|
133
|
+
* is the source of truth. Use this alias instead of re-declaring the shape so
|
|
134
|
+
* schema changes propagate automatically.
|
|
135
|
+
*/
|
|
136
|
+
type MediaRecord = InferEntityDTO<typeof Media.allFields>;
|
|
137
|
+
/** Options for listing media */
|
|
138
|
+
interface MediaListOptions {
|
|
139
|
+
/** Filter by media type */
|
|
140
|
+
mediaType?: MediaType;
|
|
141
|
+
/** Filter by MIME type prefix (e.g., 'image/') */
|
|
142
|
+
mimeTypePrefix?: string;
|
|
143
|
+
/** Search in title and filename */
|
|
144
|
+
search?: string;
|
|
145
|
+
/** Pagination limit (default: 50) */
|
|
146
|
+
limit?: number;
|
|
147
|
+
/** Pagination offset (default: 0) */
|
|
148
|
+
offset?: number;
|
|
149
|
+
/** Order by field (default: 'createdAt') */
|
|
150
|
+
orderBy?: 'createdAt' | 'filename' | 'size';
|
|
151
|
+
/** Order direction (default: 'desc') */
|
|
152
|
+
orderDirection?: 'asc' | 'desc';
|
|
153
|
+
}
|
|
154
|
+
/** Paginated media list result */
|
|
155
|
+
interface MediaListResult {
|
|
156
|
+
items: MediaRecord[];
|
|
157
|
+
total: number;
|
|
158
|
+
limit: number;
|
|
159
|
+
offset: number;
|
|
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
|
+
}
|
|
223
|
+
/** Named image processing preset — defines how to resize/convert an image variant */
|
|
224
|
+
interface ImageStyle {
|
|
225
|
+
/** Max width in pixels (omit for height-only constraint) */
|
|
226
|
+
width?: number;
|
|
227
|
+
/** Max height in pixels (omit for height-only constraint) */
|
|
228
|
+
height?: number;
|
|
229
|
+
/** Resize strategy (default: 'cover') */
|
|
230
|
+
fit?: 'cover' | 'contain' | 'inside' | 'outside' | 'fill';
|
|
231
|
+
/** Output format (default: 'webp') */
|
|
232
|
+
format?: ImageFormat;
|
|
233
|
+
/** Output quality 1–100 (default: 80) */
|
|
234
|
+
quality?: number;
|
|
235
|
+
}
|
|
236
|
+
/** Plugin configuration for @murumets-ee/media */
|
|
237
|
+
interface MediaPluginConfig {
|
|
238
|
+
/**
|
|
239
|
+
* Accepted MIME type patterns, matched against the DETECTED type: exact, or a
|
|
240
|
+
* trailing `*` prefix (`image/*`). Default: images, video, audio, PDF,
|
|
241
|
+
* modern Office (docx/xlsx/pptx), OpenDocument, plain text and CSV
|
|
242
|
+
* (`DEFAULT_MEDIA_ACCEPTED_TYPES` — see it for what is refused and why).
|
|
243
|
+
* Enforced by the upload route (415).
|
|
244
|
+
*/
|
|
245
|
+
acceptedTypes?: string[];
|
|
246
|
+
/** Max upload size in bytes (default: 50MB). Enforced before the body is read (411/413). */
|
|
247
|
+
maxUploadSize?: number;
|
|
248
|
+
/** Default file visibility (default: 'public') */
|
|
249
|
+
defaultVisibility?: FileVisibility;
|
|
250
|
+
/** Named image processing presets. A file per style is generated for every image, by the queue worker (images D004). */
|
|
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;
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
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": {
|
|
@@ -51,6 +51,14 @@
|
|
|
51
51
|
"./processing": {
|
|
52
52
|
"types": "./dist/processing.d.mts",
|
|
53
53
|
"import": "./dist/processing.mjs"
|
|
54
|
+
},
|
|
55
|
+
"./crop": {
|
|
56
|
+
"types": "./dist/crop.d.mts",
|
|
57
|
+
"import": "./dist/crop.mjs"
|
|
58
|
+
},
|
|
59
|
+
"./crop-editor": {
|
|
60
|
+
"types": "./dist/crop-editor.d.mts",
|
|
61
|
+
"import": "./dist/crop-editor.mjs"
|
|
54
62
|
}
|
|
55
63
|
},
|
|
56
64
|
"files": [
|
|
@@ -61,29 +69,38 @@
|
|
|
61
69
|
"@radix-ui/react-visually-hidden": "^1.2.5",
|
|
62
70
|
"clsx": "^2.1.0",
|
|
63
71
|
"drizzle-orm": "^0.45.2",
|
|
72
|
+
"react-easy-crop": "6.2.3",
|
|
64
73
|
"server-only": "^0.0.1",
|
|
65
74
|
"sharp": "^0.34.5",
|
|
66
75
|
"tailwind-merge": "^2.6.0",
|
|
67
76
|
"zod": "^3.24.1",
|
|
68
|
-
"@murumets-ee/core": "0.
|
|
69
|
-
"@murumets-ee/db": "0.
|
|
70
|
-
"@murumets-ee/entity": "0.
|
|
71
|
-
"@murumets-ee/logging": "0.
|
|
72
|
-
"@murumets-ee/
|
|
73
|
-
"@murumets-ee/
|
|
74
|
-
"@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"
|
|
75
85
|
},
|
|
76
86
|
"peerDependencies": {
|
|
77
87
|
"lucide-react": ">=0.400.0 <1",
|
|
88
|
+
"next-intl": ">=4.0.0 <5",
|
|
78
89
|
"react": ">=19.0.0 <20",
|
|
79
90
|
"react-dom": ">=19.0.0 <20"
|
|
80
91
|
},
|
|
92
|
+
"peerDependenciesMeta": {
|
|
93
|
+
"next-intl": {
|
|
94
|
+
"optional": true
|
|
95
|
+
}
|
|
96
|
+
},
|
|
81
97
|
"devDependencies": {
|
|
82
98
|
"@types/node": "^20.19.42",
|
|
83
99
|
"@types/react": "^19",
|
|
84
100
|
"@types/react-dom": "^19",
|
|
85
101
|
"file-type": "^21.3.4",
|
|
86
102
|
"lucide-react": "^0.577.0",
|
|
103
|
+
"next-intl": "^4.13.0",
|
|
87
104
|
"react": "19.2.7",
|
|
88
105
|
"react-dom": "19.2.7",
|
|
89
106
|
"tsdown": "^0.22.2",
|
package/dist/client-YlSoGADl.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import"./entity-Caba_NFw.mjs";import{n as e,r as t,t as n}from"./variant-key-CLlac3_1.mjs";import"server-only";var r=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-BzSRuXUs.mjs`),n=e();return this.imageStyles=await t(n,n.logger),this.imageStyles}invalidateImageStylesCache(){this.imageStyles=null}async upload(r,o){let s=await this.storage.upload(r,{filename:o.filename,mimeType:o.mimeType,size:o.size,visibility:o.visibility,uploadedBy:o.uploadedBy}),c=o.width??null,l=o.height??null,u={};if(r instanceof Buffer&&e(o.mimeType))try{let e=await t(r,await this.resolveImageStyles());c=e.width,l=e.height;let i=s.visibility;await Promise.all([...e.variants.entries()].map(async([e,t])=>{let r=n(s.key,e,t.format);try{await this.storage.upload(t.buffer,{key:r,filename:`${e}_${o.filename}`,mimeType:t.mimeType,size:t.buffer.byteLength,visibility:i,metadata:{variantOf:s.key,style:e},uploadedBy:o.uploadedBy}),u[e]=r}catch{}})),Object.keys(u).length>0&&await this.storage.updateMetadata(s.key,{metadata:{...s.metadata??{},variants:u}}).catch(()=>{})}catch{}let d=i(o.mimeType);try{return{media:await this.admin.create({title:o.title??a(o.filename),alt:o.alt??null,description:o.description??null,fileKey:s.key,filename:o.filename,mimeType:o.mimeType,size:o.size,width:c,height:l,mediaType:d}),url:await this.storage.getUrl(s.key)}}catch(e){for(let e of Object.values(u))await this.storage.delete(e).catch(()=>{});throw await this.storage.delete(s.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 r=await this.admin.findById(e);if(!r)return null;let i=r.fileKey,a=(await this.resolveImageStyles())[t];if(a){let e=n(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:r}=await import(`@murumets-ee/db`),{inArray:i}=await import(`drizzle-orm`),a=r.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 r=n(e.fileKey,t,s.format??`webp`);try{let t=await this.storage.getUrl(r);c.set(e.id,t);return}catch{}}try{let t=await this.storage.getUrl(e.fileKey);c.set(e.id,t)}catch{}})),c}};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`}function a(e){return e.replace(/\.[^.]+$/,``).replace(/[-_]/g,` `)}export{r as MediaClient};
|
|
2
|
-
//# sourceMappingURL=client-YlSoGADl.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client-YlSoGADl.mjs","names":[],"sources":["../src/async-cache.ts","../src/client.ts"],"sourcesContent":["/**\n * Small async caching primitives shared by the media read paths.\n *\n * A LEAF module on purpose: `client.ts` needs\n * {@link cacheOnceUnlessRejected} and `public-resolver.ts` dynamically imports\n * `client.ts`, so keeping the helper in either would put a static edge across a\n * dynamic one. Both import this instead.\n *\n * Both helpers share one rule — **a rejection is never pinned**. A cached\n * failure turns one transient blip into a permanent outage that only a restart\n * clears, which is tolerable behind an admin action and is not behind the\n * anonymous request path these now serve.\n */\n\n/**\n * Cache a one-shot async initialisation — but NEVER a rejection.\n *\n * A bare `promise ??= load()` singleton pins a rejected promise for the life of\n * the process: one transient failure during initialisation and every later\n * caller replays that same rejection until restart. That is tolerable when the\n * singleton serves an admin operation someone will retry by hand; it is not\n * when it serves the anonymous request path, where the symptom is every image\n * on the site failing permanently with no way to recover short of a redeploy.\n *\n * Same rule as {@link memoiseWithTtl}, without the TTL — a success here is\n * process-lifetime by design (the value is configuration, not data).\n */\nexport function cacheOnceUnlessRejected<T>(load: () => Promise<T>): () => Promise<T> {\n let cached: Promise<T> | null = null\n return () => {\n if (cached) return cached\n const pending = load()\n cached = pending\n pending.catch(() => {\n // Only clear OUR entry — a later call may already have replaced it.\n if (cached === pending) cached = null\n })\n return pending\n }\n}\n","/**\n * MediaClient — wraps AdminClient + StorageClient for media management.\n *\n * Usage:\n * import { createMediaClient } from '@murumets-ee/media/client'\n * const media = await createMediaClient(storageClient)\n * const result = await media.upload(buffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', size: 12345 })\n */\n\nimport 'server-only'\n\nimport type { AdminClient } from '@murumets-ee/entity/admin'\nimport type { StorageClient } from '@murumets-ee/storage'\nimport { cacheOnceUnlessRejected } from './async-cache.js'\nimport { Media } from './entity.js'\nimport { isProcessableImage, processImage } from './process-image.js'\nimport type {\n ImageStyle,\n MediaListOptions,\n MediaListResult,\n MediaRecord,\n MediaType,\n MediaUploadOptions,\n MediaUploadResult,\n} from './types.js'\nimport { deriveVariantKey } from './variant-key.js'\n\ntype MediaFields = typeof Media.allFields\n\nexport interface MediaClientConfig {\n admin: AdminClient<MediaFields>\n storage: StorageClient\n /** Image styles to generate on upload. Loaded from plugin config if not provided. */\n imageStyles?: Record<string, ImageStyle>\n}\n\nexport class MediaClient {\n private admin: AdminClient<MediaFields>\n private storage: StorageClient\n private imageStyles: Record<string, ImageStyle> | null\n\n constructor(config: MediaClientConfig) {\n this.admin = config.admin\n this.storage = config.storage\n this.imageStyles = config.imageStyles ?? null\n }\n\n /**\n * Resolve image styles via the shared waterfall (settings DB → plugin\n * config → hardcoded defaults) and cache the result on this instance.\n * Call `invalidateImageStylesCache()` after a settings update.\n */\n private async resolveImageStyles(): Promise<Record<string, ImageStyle>> {\n if (this.imageStyles) return this.imageStyles\n const { getApp } = await import('@murumets-ee/core')\n const { resolveImageStyles } = await import('./resolve-image-styles.js')\n const app = getApp()\n this.imageStyles = await resolveImageStyles(app, app.logger)\n return this.imageStyles\n }\n\n /** Clear cached styles so next access re-reads from settings DB. */\n invalidateImageStylesCache(): void {\n this.imageStyles = null\n }\n\n // ---------------------------------------------------------------\n // Upload — the key convenience method\n // ---------------------------------------------------------------\n\n /**\n * Upload a file and create a media entity record in one step.\n * 1. Uploads original to storage (StorageClient)\n * 2. If image: extracts dimensions via Sharp + generates variants\n * 3. Creates media entity record (AdminClient)\n * 4. Returns the media record + URL\n *\n * Rolls back storage upload if entity creation fails.\n * Variant generation failures are logged but don't fail the upload.\n */\n async upload(\n data: Buffer | ReadableStream<Uint8Array>,\n options: MediaUploadOptions,\n ): Promise<MediaUploadResult> {\n // 1. Upload original file to storage\n const fileRecord = await this.storage.upload(data, {\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n visibility: options.visibility,\n uploadedBy: options.uploadedBy,\n })\n\n // 2. Image processing — extract dimensions + generate variants\n let width = options.width ?? null\n let height = options.height ?? null\n const variantKeys: Record<string, string> = {}\n\n if (data instanceof Buffer && isProcessableImage(options.mimeType)) {\n try {\n const styles = await this.resolveImageStyles()\n const processed = await processImage(data, styles)\n\n // Set dimensions from Sharp metadata\n width = processed.width\n height = processed.height\n\n // Upload each variant to storage\n const visibility = fileRecord.visibility\n await Promise.all(\n [...processed.variants.entries()].map(async ([styleName, variant]) => {\n const vKey = deriveVariantKey(fileRecord.key, styleName, variant.format)\n try {\n await this.storage.upload(variant.buffer, {\n key: vKey,\n filename: `${styleName}_${options.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility,\n metadata: { variantOf: fileRecord.key, style: styleName },\n uploadedBy: options.uploadedBy,\n })\n variantKeys[styleName] = vKey\n } catch {\n // Variant upload failure is non-fatal — original is saved\n }\n }),\n )\n\n // Store variant keys in original file's metadata\n if (Object.keys(variantKeys).length > 0) {\n await this.storage\n .updateMetadata(fileRecord.key, {\n metadata: {\n ...(fileRecord.metadata ?? {}),\n variants: variantKeys,\n },\n })\n .catch(() => {\n // Metadata update failure is non-fatal\n })\n }\n } catch {\n // Image processing failure is non-fatal — original is saved\n }\n }\n\n // 3. Create media entity record\n const mediaType = deriveMediaType(options.mimeType)\n\n try {\n const entityRecord = await this.admin.create({\n title: options.title ?? deriveTitle(options.filename),\n alt: options.alt ?? null,\n description: options.description ?? null,\n fileKey: fileRecord.key,\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n width,\n height,\n mediaType,\n })\n\n // 4. Get URL\n const url = await this.storage.getUrl(fileRecord.key)\n\n return {\n media: entityRecord,\n url,\n }\n } catch (error) {\n // Rollback: delete variants + original if entity creation fails\n\n // Delete variants first (best-effort)\n for (const vKey of Object.values(variantKeys)) {\n await this.storage.delete(vKey).catch(() => {})\n }\n\n // Delete original\n await this.storage.delete(fileRecord.key).catch(() => {\n // Storage rollback failure is already being handled — propagate original error\n })\n throw error\n }\n }\n\n // ---------------------------------------------------------------\n // CRUD delegation\n // ---------------------------------------------------------------\n\n async findById(id: string, options?: { locale?: string }): Promise<MediaRecord | null> {\n return this.admin.findById(id, options)\n }\n\n async findMany(options?: MediaListOptions): Promise<MediaListResult> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { and, asc, desc, eq, ilike, or, sql } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) throw new Error('Media schema not registered. Is the media() plugin loaded?')\n\n // Build where conditions\n const conditions = []\n\n if (options?.mediaType) {\n conditions.push(eq(table.mediaType, options.mediaType))\n }\n if (options?.mimeTypePrefix) {\n // Escape ILIKE wildcards (%, _) in user input to prevent pattern injection\n const escaped = options.mimeTypePrefix.replace(/[\\\\%_]/g, '\\\\$&')\n conditions.push(ilike(table.mimeType, `${escaped}%`))\n }\n if (options?.search) {\n // Escape ILIKE wildcards in user input, then wrap with %...%\n const escaped = options.search.replace(/[\\\\%_]/g, '\\\\$&')\n const pattern = `%${escaped}%`\n conditions.push(\n or(\n ilike(table.filename, pattern),\n sql`${table.fields} ->> 'title' ILIKE ${pattern}`,\n )!,\n )\n }\n\n const limit = options?.limit ?? 50\n const offset = options?.offset ?? 0\n const whereClause = conditions.length > 0 ? and(...conditions) : undefined\n\n // Count total via AdminClient\n const total = await this.admin.count({ where: whereClause })\n\n // Fetch items via AdminClient for proper DTO shaping\n const orderField = options?.orderBy === 'filename' ? table.filename : table.createdAt\n const orderFn = (options?.orderDirection ?? 'desc') === 'asc' ? asc : desc\n\n const items = await this.admin.findMany({\n where: whereClause,\n limit,\n offset,\n orderBy: orderFn(orderField),\n })\n\n return {\n items,\n total,\n limit,\n offset,\n }\n }\n\n async update(\n id: string,\n data: { title?: string; alt?: string; description?: string },\n ): Promise<MediaRecord> {\n return this.admin.update(id, data)\n }\n\n /**\n * Delete a media entity, its variants, and its original file in storage.\n */\n async delete(id: string): Promise<void> {\n const record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n const fileKey = record.fileKey\n\n // 1. Delete entity first (ref checking via entity_refs happens here)\n await this.admin.delete(id)\n\n // 2. Look up original file record for variant metadata\n const fileRecord = await this.storage.getMetadata(fileKey)\n const variants = (fileRecord?.metadata as Record<string, unknown> | null)?.variants as\n | Record<string, string>\n | undefined\n\n // 3. Delete variants (best-effort)\n if (variants) {\n for (const vKey of Object.values(variants)) {\n await this.storage.delete(vKey).catch(() => {\n // Variant deletion failure is non-fatal\n })\n }\n }\n\n // 4. Delete original file (best-effort)\n await this.storage.delete(fileKey).catch(() => {\n // Storage deletion failure is non-fatal — entity is already deleted\n })\n }\n\n // ---------------------------------------------------------------\n // URL resolution\n // ---------------------------------------------------------------\n\n /**\n * Get URL for a media entity by its ID.\n * Resolves entity -> fileKey -> storage URL.\n */\n async getUrl(id: string): Promise<string> {\n const record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n return this.storage.getUrl(record.fileKey)\n }\n\n /**\n * Get URLs for multiple media entities (batch).\n * Returns a Map of mediaId -> url.\n */\n async getUrls(ids: string[]): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await this.admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n records.map(async (record) => {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n }),\n )\n\n return urlMap\n }\n\n /**\n * Get variant URL for a specific image style.\n * Falls back to original URL if the variant doesn't exist.\n *\n * @param id - Media entity ID\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns The variant URL, or original URL as fallback, or null if media not found\n */\n async getVariantUrl(id: string, styleName: string): Promise<string | null> {\n const record = await this.admin.findById(id)\n if (!record) return null\n\n const fileKey = record.fileKey\n\n // Try variant key first\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n if (style) {\n const vKey = deriveVariantKey(fileKey, styleName, style.format ?? 'webp')\n try {\n return await this.storage.getUrl(vKey)\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n return await this.storage.getUrl(fileKey)\n } catch {\n return null\n }\n }\n\n /**\n * Get variant URLs for multiple media entities (batch).\n * Falls back to original URL per item if the variant doesn't exist.\n *\n * @param ids - Media entity IDs\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns Map of mediaId -> variant URL (or original URL as fallback)\n */\n async getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await this.admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n records.map(async (record) => {\n // Try variant URL\n if (style) {\n const vKey = deriveVariantKey(record.fileKey, styleName, style.format ?? 'webp')\n try {\n const url = await this.storage.getUrl(vKey)\n urlMap.set(record.id, url)\n return\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n } catch {\n // Skip — no URL available\n }\n }),\n )\n\n return urlMap\n }\n}\n\n// ---------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------\n\nfunction 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\nfunction deriveTitle(filename: string): string {\n const withoutExt = filename.replace(/\\.[^.]+$/, '')\n return withoutExt.replace(/[-_]/g, ' ')\n}\n\n/**\n * Factory — creates a MediaClient with an explicit StorageClient.\n * Must be called after createApp().\n */\nexport async function createMediaClient(storage: StorageClient): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n\n// ---------------------------------------------------------------------------\n// Per-request MediaClient factory\n// ---------------------------------------------------------------------------\n//\n// NOTE: Storage config is process-global and safe to cache.\n// MediaClient / AdminClient must be built per-request — they carry a\n// context resolver tied to the calling request's user + permissions.\n// A singleton would leak one request's security context across others.\n\n/**\n * The storage singleton, which does NOT pin a failed initialisation.\n *\n * `_storagePromise ??= (...)()` used to cache the rejected promise too, so a\n * transient failure during storage init (config not yet loaded, a DB blip)\n * broke every subsequent caller until the process restarted. That mattered\n * little while this served admin operations; `plan/api` PR 5 puts it on the\n * ANONYMOUS request path via `getSharedStorageClient`, where the symptom\n * becomes every image on the site failing permanently with no recovery short of\n * a redeploy. See `cacheOnceUnlessRejected`, which is unit-tested.\n */\nconst _storageOnce = cacheOnceUnlessRejected<StorageClient>(async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n const app = getApp()\n return createStorageClient(getStorageConfig(), { app })\n})\n\n/**\n * The process-wide `StorageClient`, built once from the storage plugin config.\n *\n * Exported so the PUBLIC media resolver (`./public-resolver.ts`) reaches the\n * same instance rather than constructing a second one from a copy of these two\n * lines. Safe to share, and unlike `MediaClient`/`AdminClient` it MUST NOT be\n * rebuilt per request: it carries no security context — the read-side gate\n * lives in whichever client resolves the media rows, not here.\n */\nexport async function getSharedStorageClient(): Promise<StorageClient> {\n return getStorageSingleton()\n}\n\nasync function getStorageSingleton(): Promise<StorageClient> {\n return _storageOnce()\n}\n\n/**\n * Returns a fresh MediaClient wired to the current request's context.\n * Must be called after createApp(), inside a request context\n * (withAdminContext, runAsCli, etc.).\n *\n * Despite the name, this is NOT cached — the storage config is cached\n * internally but the MediaClient and its AdminClient are rebuilt per call\n * so the security context resolver attaches to the correct request.\n */\nexport async function getMediaClient(): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const storage = await getStorageSingleton()\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n"],"mappings":"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"}
|
package/dist/en-0YnQ_Qz-.mjs
DELETED
package/dist/en-0YnQ_Qz-.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"en-0YnQ_Qz-.mjs","names":[],"sources":["../src/messages/en.json"],"sourcesContent":[""],"mappings":""}
|
package/dist/entity-Caba_NFw.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{behavior as e,defineEntity as t,field as n,searchable as r}from"@murumets-ee/entity";const i=t({name:`media`,fields:{title:n.text({translatable:!0}),alt:n.text({translatable:!0}),description:n.text({translatable:!0}),fileKey:n.text({required:!0,indexed:!0}),filename:n.text({required:!0}),mimeType:n.text({required:!0,indexed:!0}),size:n.number({required:!0,integer:!0}),width:n.number({integer:!0}),height:n.number({integer:!0}),mediaType:n.select({options:[`image`,`video`,`audio`,`document`,`other`],required:!0,indexed:!0})},behaviors:[e.auditable(),r({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})})],scope:`global`,access:{view:`public`,create:`group.editor`,update:`group.editor`,delete:`group.admin`}});export{i as Media};
|
|
2
|
-
//# sourceMappingURL=entity-Caba_NFw.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"entity-Caba_NFw.mjs","names":[],"sources":["../src/entity.ts"],"sourcesContent":["/**\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'\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 mimeType: field.text({ required: true, indexed: true }),\n size: field.number({ required: true, integer: true }),\n\n // --- Image-specific metadata (client-measured on upload) ---\n width: field.number({ integer: true }),\n height: field.number({ integer: true }),\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 // 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 scope: 'global',\n access: {\n view: 'public',\n create: 'group.editor',\n update: 'group.editor',\n delete: 'group.admin',\n },\n})\n"],"mappings":"4FAmBA,MAAa,EAAQ,EAAa,CAChC,KAAM,QACN,OAAQ,CAEN,MAAO,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EACxC,IAAK,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EACtC,YAAa,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EAI9C,QAAS,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EAGrD,SAAU,EAAM,KAAK,CAAE,SAAU,EAAK,CAAC,EACvC,SAAU,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EACtD,KAAM,EAAM,OAAO,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EAGpD,MAAO,EAAM,OAAO,CAAE,QAAS,EAAK,CAAC,EACrC,OAAQ,EAAM,OAAO,CAAE,QAAS,EAAK,CAAC,EAGtC,UAAW,EAAM,OAAO,CACtB,QAAS,CAAC,QAAS,QAAS,QAAS,WAAY,OAAO,EACxD,SAAU,GACV,QAAS,EACX,CAAC,CACH,EACA,UAAW,CACT,EAAS,UAAU,EAInB,EAAW,CACT,OAAQ,CAAC,WAAY,UAAW,WAAY,QAAS,MAAO,aAAa,EACzE,IAAK,CACH,OAAQ,CAAC,WAAY,QAAS,MAAO,aAAa,EAClD,SAAU,QACZ,EACA,WAAa,IAAS,CACpB,GAAI,EAAI,GACR,MAAQ,EAAI,OAAiC,EAAI,SACjD,YAAa,EAAI,QACnB,EACF,CAAC,CACH,EACA,MAAO,SACP,OAAQ,CACN,KAAM,SACN,OAAQ,eACR,OAAQ,eACR,OAAQ,aACV,CACF,CAAC"}
|
package/dist/entity-fxw-Qywj.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";import{behavior as t,defineEntity as n,field as r,searchable as i}from"@murumets-ee/entity";var a=e({Media:()=>o});const o=n({name:`media`,fields:{title:r.text({translatable:!0}),alt:r.text({translatable:!0}),description:r.text({translatable:!0}),fileKey:r.text({required:!0,indexed:!0}),filename:r.text({required:!0}),mimeType:r.text({required:!0,indexed:!0}),size:r.number({required:!0,integer:!0}),width:r.number({integer:!0}),height:r.number({integer:!0}),mediaType:r.select({options:[`image`,`video`,`audio`,`document`,`other`],required:!0,indexed:!0})},behaviors:[t.auditable(),i({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})})],scope:`global`,access:{view:`public`,create:`group.editor`,update:`group.editor`,delete:`group.admin`}});export{a as n,o as t};
|
|
2
|
-
//# sourceMappingURL=entity-fxw-Qywj.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"entity-fxw-Qywj.mjs","names":[],"sources":["../src/entity.ts"],"sourcesContent":["/**\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'\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 mimeType: field.text({ required: true, indexed: true }),\n size: field.number({ required: true, integer: true }),\n\n // --- Image-specific metadata (client-measured on upload) ---\n width: field.number({ integer: true }),\n height: field.number({ integer: true }),\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 // 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 scope: 'global',\n access: {\n view: 'public',\n create: 'group.editor',\n update: 'group.editor',\n delete: 'group.admin',\n },\n})\n"],"mappings":"uKAmBA,MAAa,EAAQ,EAAa,CAChC,KAAM,QACN,OAAQ,CAEN,MAAO,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EACxC,IAAK,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EACtC,YAAa,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EAI9C,QAAS,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EAGrD,SAAU,EAAM,KAAK,CAAE,SAAU,EAAK,CAAC,EACvC,SAAU,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EACtD,KAAM,EAAM,OAAO,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EAGpD,MAAO,EAAM,OAAO,CAAE,QAAS,EAAK,CAAC,EACrC,OAAQ,EAAM,OAAO,CAAE,QAAS,EAAK,CAAC,EAGtC,UAAW,EAAM,OAAO,CACtB,QAAS,CAAC,QAAS,QAAS,QAAS,WAAY,OAAO,EACxD,SAAU,GACV,QAAS,EACX,CAAC,CACH,EACA,UAAW,CACT,EAAS,UAAU,EAInB,EAAW,CACT,OAAQ,CAAC,WAAY,UAAW,WAAY,QAAS,MAAO,aAAa,EACzE,IAAK,CACH,OAAQ,CAAC,WAAY,QAAS,MAAO,aAAa,EAClD,SAAU,QACZ,EACA,WAAa,IAAS,CACpB,GAAI,EAAI,GACR,MAAQ,EAAI,OAAiC,EAAI,SACjD,YAAa,EAAI,QACnB,EACF,CAAC,CACH,EACA,MAAO,SACP,OAAQ,CACN,KAAM,SACN,OAAQ,eACR,OAAQ,eACR,OAAQ,aACV,CACF,CAAC"}
|
package/dist/et-BIxGYWVK.mjs
DELETED
package/dist/et-BIxGYWVK.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"et-BIxGYWVK.mjs","names":[],"sources":["../src/messages/et.json"],"sourcesContent":[""],"mappings":""}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{defineSettings as e,setting as t}from"@murumets-ee/settings/define";import{z as n}from"zod";const r=n.object({width:n.number().int().positive().optional(),height:n.number().int().positive().optional(),fit:n.enum([`cover`,`contain`,`inside`,`outside`,`fill`]).optional(),format:n.enum([`webp`,`jpeg`,`png`,`avif`]).optional(),quality:n.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`}),i=/^[a-z][a-z0-9_-]*$/,a=n.record(n.string(),r).superRefine((e,t)=>{for(let r of Object.keys(e))i.test(r)||t.addIssue({code:n.ZodIssueCode.custom,path:[r],message:`Invalid style name "${r}": must be lowercase alphanumeric (a-z, 0-9, -, _)`})}),o=e({namespace:`media.imageStyles`,scope:`global`,label:`Image Styles`,iconName:`image-down`,schema:{imageStyles:t.json({default:{thumbnail:{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}}.thumbnail},label:`Image processing presets`,schema:a,renderer:`media.imageStyles`})}});export{o as imageStylesSettings};
|
|
2
|
-
//# sourceMappingURL=image-styles-settings-ClIA2MJI.mjs.map
|