payload-plugin-aspect-preview 0.1.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.
@@ -0,0 +1,482 @@
1
+ @import '~@payloadcms/ui/scss';
2
+
3
+ // ─────────────────────────────────────────────
4
+ // Aspect Ratio Preview Grid (right column)
5
+ // ─────────────────────────────────────────────
6
+ .focal-preview {
7
+ &__grid {
8
+ // True masonry via CSS columns — works in all modern browsers
9
+ // Items flow top-to-bottom in each column, tightly packed with no
10
+ // empty spaces regardless of individual card heights.
11
+ columns: 3;
12
+ column-gap: base(0.5);
13
+
14
+ @media (max-width: 1400px) {
15
+ columns: 2;
16
+ }
17
+
18
+ @media (max-width: 768px) {
19
+ columns: 1;
20
+ }
21
+ }
22
+
23
+ &__card {
24
+ // Prevent card from splitting across columns
25
+ break-inside: avoid;
26
+ margin-bottom: base(0.5);
27
+
28
+ background: var(--theme-elevation-0);
29
+ border: 1px solid var(--theme-elevation-100);
30
+ border-radius: var(--border-radius-s);
31
+ overflow: hidden;
32
+ transition:
33
+ transform 0.15s ease,
34
+ box-shadow 0.15s ease;
35
+
36
+ &:hover {
37
+ transform: translateY(-2px);
38
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
39
+ }
40
+ }
41
+
42
+ &__card-header {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: space-between;
46
+ gap: base(0.25);
47
+ padding: base(0.375) base(0.5);
48
+ background: var(--theme-elevation-50);
49
+ border-bottom: 1px solid var(--theme-elevation-100);
50
+ }
51
+
52
+ &__card-name {
53
+ font-size: base(0.65);
54
+ font-weight: 600;
55
+ color: var(--theme-text);
56
+ }
57
+
58
+ &__card-ratio {
59
+ font-size: base(0.6);
60
+ color: var(--theme-elevation-500);
61
+ background: var(--theme-elevation-100);
62
+ padding: 1px 5px;
63
+ border-radius: var(--border-radius-s);
64
+ font-weight: 500;
65
+ }
66
+
67
+ &__card-frame {
68
+ position: relative;
69
+ width: 100%;
70
+ overflow: hidden;
71
+ background: var(--theme-elevation-100);
72
+ line-height: 0;
73
+ }
74
+
75
+ &__card-image {
76
+ display: block;
77
+ transition: opacity 0.2s ease;
78
+ }
79
+
80
+ &__card-overlay {
81
+ position: absolute;
82
+ inset: 0;
83
+ pointer-events: none;
84
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);
85
+ z-index: 1;
86
+ }
87
+
88
+ &__card-crosshair {
89
+ position: absolute;
90
+ left: 50%;
91
+ top: 50%;
92
+ transform: translate(-50%, -50%);
93
+ pointer-events: none;
94
+ z-index: 2;
95
+ width: 16px;
96
+ height: 16px;
97
+
98
+ &::before,
99
+ &::after {
100
+ content: '';
101
+ position: absolute;
102
+ background: rgba(255, 255, 255, 0.95);
103
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
104
+ }
105
+
106
+ &::before {
107
+ width: 100%;
108
+ height: 2px;
109
+ top: 50%;
110
+ left: 0;
111
+ transform: translateY(-50%);
112
+ }
113
+
114
+ &::after {
115
+ width: 2px;
116
+ height: 100%;
117
+ left: 50%;
118
+ top: 0;
119
+ transform: translateX(-50%);
120
+ }
121
+ }
122
+
123
+ &__card-footer {
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: space-between;
127
+ gap: base(0.25);
128
+ padding: base(0.25) base(0.5);
129
+ background: var(--theme-elevation-50);
130
+ border-top: 1px solid var(--theme-elevation-100);
131
+ }
132
+
133
+ &__card-usage {
134
+ font-size: base(0.55);
135
+ color: var(--theme-elevation-500);
136
+ line-height: 1.3;
137
+ }
138
+
139
+ &__card-source {
140
+ font-size: base(0.55);
141
+ font-weight: 600;
142
+ padding: 1px 4px;
143
+ border-radius: var(--border-radius-s);
144
+ text-transform: uppercase;
145
+ letter-spacing: 0.03em;
146
+ flex-shrink: 0;
147
+
148
+ &--css {
149
+ color: #0f766e;
150
+ background: #ccfbf1;
151
+ }
152
+
153
+ &--crop {
154
+ color: #9a3412;
155
+ background: #ffedd5;
156
+ }
157
+ }
158
+ }
159
+
160
+ // ─────────────────────────────────────────────
161
+ // Focal Point Editor (two-column)
162
+ // ─────────────────────────────────────────────
163
+ .focal-editor {
164
+ margin-top: base(1);
165
+ padding: base(1);
166
+ background: var(--theme-elevation-0);
167
+ border-radius: var(--border-radius-m);
168
+ border: 1px solid var(--theme-elevation-100);
169
+
170
+ // Two-column layout
171
+ &__layout {
172
+ display: flex;
173
+ gap: base(1.5);
174
+ align-items: flex-start;
175
+
176
+ @media (max-width: 900px) {
177
+ flex-direction: column;
178
+ }
179
+ }
180
+
181
+ &__left {
182
+ position: sticky;
183
+ top: 75px;
184
+ align-self: flex-start;
185
+ width: 50%;
186
+ flex-shrink: 0;
187
+ display: flex;
188
+ flex-direction: column;
189
+ gap: base(0.75);
190
+
191
+ @media (max-width: 900px) {
192
+ position: relative;
193
+ top: auto;
194
+ width: 100%;
195
+ }
196
+ }
197
+
198
+ &__right {
199
+ flex: 1;
200
+ min-width: 0;
201
+ }
202
+
203
+ // Toolbar: inputs (2 rows) on the left, button column on the right
204
+ &__toolbar {
205
+ display: flex;
206
+ align-items: stretch;
207
+ gap: base(2);
208
+ margin-bottom: base(0.5);
209
+ flex-wrap: wrap;
210
+ }
211
+
212
+ &__toolbar-inputs {
213
+ flex: 1 1 auto;
214
+ min-width: 0;
215
+ display: flex;
216
+ gap: base(2);
217
+ flex-wrap: wrap;
218
+ }
219
+
220
+ &__toolbar-row {
221
+ display: flex;
222
+ align-items: center;
223
+ gap: base(1);
224
+ flex-wrap: wrap;
225
+ }
226
+
227
+ &__toolbar-col {
228
+ display: flex;
229
+ flex-direction: column;
230
+ gap: base(0.5);
231
+ }
232
+
233
+ &__row-label {
234
+ font-size: base(0.7);
235
+ font-weight: 600;
236
+ color: var(--theme-elevation-800);
237
+ text-transform: uppercase;
238
+ letter-spacing: 0.04em;
239
+ width: 52px;
240
+ flex-shrink: 0;
241
+ text-align: left;
242
+ align-self: flex-start;
243
+ padding-right: base(0.25);
244
+ }
245
+
246
+ &__toolbar-actions {
247
+ display: flex;
248
+ flex-direction: column;
249
+ gap: base(0.35);
250
+ align-items: stretch;
251
+ justify-content: space-between;
252
+ flex: 1 1 auto;
253
+ }
254
+
255
+ &__mode-toggle {
256
+ display: flex;
257
+ gap: 0;
258
+ border-radius: var(--border-radius-s);
259
+ overflow: hidden;
260
+ border: 1px solid var(--theme-elevation-150);
261
+ }
262
+
263
+ &__mode-btn {
264
+ padding: base(0.35) base(0.75);
265
+ border: none;
266
+ background: var(--theme-elevation-0);
267
+ color: var(--theme-text);
268
+ font-size: base(0.7);
269
+ font-weight: 500;
270
+ cursor: pointer;
271
+ transition: all 0.15s ease;
272
+ border-right: 1px solid var(--theme-elevation-150);
273
+ flex: 1 1 100%;
274
+
275
+ &:last-child {
276
+ border-right: none;
277
+ }
278
+
279
+ &--active {
280
+ background: var(--theme-elevation-900);
281
+ color: var(--theme-elevation-0);
282
+ }
283
+
284
+ &:hover:not(&--active) {
285
+ background: var(--theme-elevation-100);
286
+ }
287
+ }
288
+
289
+ &__inputs {
290
+ display: flex;
291
+ align-items: center;
292
+ gap: base(0.5);
293
+ }
294
+
295
+ &__subgroup {
296
+ display: flex;
297
+ flex-direction: column;
298
+ gap: base(0.5);
299
+ }
300
+
301
+ &__subgroup-label {
302
+ font-size: base(0.6);
303
+ font-weight: 600;
304
+ color: var(--theme-elevation-500);
305
+ text-transform: uppercase;
306
+ letter-spacing: 0.05em;
307
+ line-height: 1;
308
+ width: 100%;
309
+ text-align: left;
310
+ }
311
+
312
+ &__subgroup-inputs {
313
+ display: flex;
314
+ align-items: center;
315
+ gap: base(0.4);
316
+ }
317
+
318
+ &__input-group {
319
+ display: flex;
320
+ align-items: center;
321
+ gap: base(0.25);
322
+
323
+ label {
324
+ font-size: base(0.7);
325
+ color: var(--theme-elevation-600);
326
+ font-weight: 600;
327
+ line-height: 1;
328
+ min-width: 10px;
329
+ text-align: center;
330
+ }
331
+
332
+ input {
333
+ width: 48px;
334
+ padding: base(0.25) base(0.35);
335
+ border: 1px solid var(--theme-elevation-150);
336
+ border-radius: var(--border-radius-s);
337
+ background: var(--theme-elevation-0);
338
+ color: var(--theme-text);
339
+ font-size: base(0.7);
340
+ text-align: center;
341
+ font-weight: 500;
342
+
343
+ &:focus {
344
+ outline: none;
345
+ border-color: var(--theme-elevation-400);
346
+ box-shadow: 0 0 0 2px var(--theme-elevation-100);
347
+ }
348
+
349
+ &:disabled {
350
+ background: var(--theme-elevation-50);
351
+ color: var(--theme-elevation-400);
352
+ cursor: not-allowed;
353
+ }
354
+ }
355
+
356
+ span {
357
+ font-size: base(0.65);
358
+ color: var(--theme-elevation-500);
359
+ }
360
+ }
361
+
362
+ &__btn {
363
+ padding: base(0.35) base(0.625);
364
+ border-radius: var(--border-radius-s);
365
+ border: 1px solid var(--theme-elevation-150);
366
+ background: var(--theme-elevation-0);
367
+ color: var(--theme-text);
368
+ font-size: base(0.7);
369
+ font-weight: 500;
370
+ cursor: pointer;
371
+ transition: all 0.15s ease;
372
+
373
+ &:hover {
374
+ background: var(--theme-elevation-100);
375
+ border-color: var(--theme-elevation-200);
376
+ }
377
+ }
378
+
379
+ // Image
380
+ &__image-wrapper {
381
+ width: 100%;
382
+ border-radius: var(--border-radius-m);
383
+ overflow: hidden;
384
+ border: 1px solid var(--theme-elevation-100);
385
+ background: var(--theme-elevation-50);
386
+ display: flex;
387
+ justify-content: center;
388
+ align-items: center;
389
+ }
390
+
391
+ &__react-crop {
392
+ width: 100%;
393
+ display: block;
394
+ line-height: 0;
395
+
396
+ &.is-focal-mode {
397
+ cursor: crosshair;
398
+ }
399
+
400
+ &.is-crop-mode {
401
+ cursor: crosshair;
402
+ }
403
+
404
+ img {
405
+ display: block;
406
+ width: 100%;
407
+ height: auto;
408
+ object-fit: contain;
409
+ }
410
+ }
411
+
412
+ &__crop-overlay {
413
+ position: absolute;
414
+ z-index: 5;
415
+ pointer-events: none;
416
+ border: 2px dashed rgba(255, 255, 255, 0.9);
417
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45);
418
+ transition: all 0.15s ease;
419
+ }
420
+
421
+ &__focal-overlay {
422
+ position: absolute;
423
+ inset: 0;
424
+ z-index: 10;
425
+ pointer-events: none;
426
+
427
+ &.is-active {
428
+ pointer-events: auto;
429
+ cursor: crosshair;
430
+ }
431
+ }
432
+
433
+ &__crosshair {
434
+ position: absolute;
435
+ transform: translate(-50%, -50%);
436
+ pointer-events: none;
437
+ z-index: 2;
438
+ width: 24px;
439
+ height: 24px;
440
+
441
+ &-h,
442
+ &-v {
443
+ position: absolute;
444
+ background: rgba(255, 255, 255, 0.95);
445
+ box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.4);
446
+ }
447
+
448
+ &-h {
449
+ width: 24px;
450
+ height: 2px;
451
+ left: 50%;
452
+ top: 50%;
453
+ transform: translate(-50%, -50%);
454
+ }
455
+
456
+ &-v {
457
+ width: 2px;
458
+ height: 24px;
459
+ left: 50%;
460
+ top: 50%;
461
+ transform: translate(-50%, -50%);
462
+ }
463
+ }
464
+
465
+ &__empty {
466
+ padding: base(2);
467
+ text-align: center;
468
+ color: var(--theme-elevation-600);
469
+ font-size: base(0.75);
470
+ border: 2px dashed var(--theme-elevation-100);
471
+ border-radius: var(--border-radius-m);
472
+ background: var(--theme-elevation-50);
473
+ }
474
+ }
475
+
476
+ // React Crop overrides
477
+ .ReactCrop {
478
+ &__crop-selection {
479
+ border: 2px dashed rgba(255, 255, 255, 0.9);
480
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
481
+ }
482
+ }
@@ -0,0 +1,39 @@
1
+ export interface AspectRatioConfig {
2
+ /** Display label, e.g. "Social Share". */
3
+ name: string;
4
+ /** Display token, e.g. "16:9". */
5
+ ratio: string;
6
+ /** Ratio numerator (or absolute px width for exact targets). */
7
+ width: number;
8
+ /** Ratio denominator (or absolute px height for exact targets). */
9
+ height: number;
10
+ /** How the frontend renders this ratio; drives a display badge only. Defaults to 'css'. */
11
+ source?: 'css' | 'crop';
12
+ /** Optional description shown in the preview card footer. */
13
+ usage?: string;
14
+ /** Optional free-form grouping label. */
15
+ category?: string;
16
+ }
17
+ export interface AspectPreviewPluginOptions {
18
+ /** Upload collections (by slug) to enhance with the inline editor. */
19
+ collections: string[];
20
+ /** Aspect ratios shown in the live preview grid. Defaults to DEFAULT_ASPECT_RATIOS. */
21
+ aspectRatios?: AspectRatioConfig[];
22
+ /** Disable the plugin while keeping the schema field for DB consistency. */
23
+ disabled?: boolean;
24
+ }
25
+ export interface CropConfig {
26
+ unit: 'px' | '%';
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ }
32
+ export interface FocalPointState {
33
+ x: number;
34
+ y: number;
35
+ }
36
+ export interface UploadEditsState {
37
+ crop?: CropConfig;
38
+ focalPoint?: FocalPointState;
39
+ }
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ export { };
2
+
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["export interface AspectRatioConfig {\n /** Display label, e.g. \"Social Share\". */\n name: string\n /** Display token, e.g. \"16:9\". */\n ratio: string\n /** Ratio numerator (or absolute px width for exact targets). */\n width: number\n /** Ratio denominator (or absolute px height for exact targets). */\n height: number\n /** How the frontend renders this ratio; drives a display badge only. Defaults to 'css'. */\n source?: 'css' | 'crop'\n /** Optional description shown in the preview card footer. */\n usage?: string\n /** Optional free-form grouping label. */\n category?: string\n}\n\nexport interface AspectPreviewPluginOptions {\n /** Upload collections (by slug) to enhance with the inline editor. */\n collections: string[]\n /** Aspect ratios shown in the live preview grid. Defaults to DEFAULT_ASPECT_RATIOS. */\n aspectRatios?: AspectRatioConfig[]\n /** Disable the plugin while keeping the schema field for DB consistency. */\n disabled?: boolean\n}\n\nexport interface CropConfig {\n unit: 'px' | '%'\n x: number\n y: number\n width: number\n height: number\n}\n\nexport interface FocalPointState {\n x: number\n y: number\n}\n\nexport interface UploadEditsState {\n crop?: CropConfig\n focalPoint?: FocalPointState\n}\n"],"names":[],"mappings":"AAuCA,WAGC"}
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "payload-plugin-aspect-preview",
3
+ "version": "0.1.0",
4
+ "description": "Single-screen crop, focal point, and live multi-aspect-ratio preview editor for Payload upload collections.",
5
+ "keywords": ["payload", "payload-plugin", "payloadcms", "focal-point", "crop", "aspect-ratio", "media"],
6
+ "license": "MIT",
7
+ "author": "Sebastian Kolbusz <s.kolbusz@outlook.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/s-kolbusz/payload-plugin-aspect-preview.git"
11
+ },
12
+ "homepage": "https://github.com/s-kolbusz/payload-plugin-aspect-preview#readme",
13
+ "bugs": "https://github.com/s-kolbusz/payload-plugin-aspect-preview/issues",
14
+ "engines": {
15
+ "node": ">=20.9.0"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "type": "module",
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "import": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./client": {
30
+ "import": "./dist/exports/client.js",
31
+ "types": "./dist/exports/client.d.ts",
32
+ "default": "./dist/exports/client.js"
33
+ },
34
+ "./types": {
35
+ "import": "./dist/exports/types.js",
36
+ "types": "./dist/exports/types.d.ts",
37
+ "default": "./dist/exports/types.js"
38
+ }
39
+ },
40
+ "files": ["dist"],
41
+ "scripts": {
42
+ "build": "pnpm clean && pnpm copyfiles && pnpm build:types && pnpm build:swc",
43
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths --ignore \"**/*.spec.ts\"",
44
+ "build:types": "tsc --emitDeclarationOnly --outDir dist",
45
+ "clean": "rimraf dist *.tsbuildinfo",
46
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{scss,css}\" dist/",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "dev": "next dev dev --turbo",
50
+ "prepublishOnly": "pnpm build"
51
+ },
52
+ "dependencies": {
53
+ "react-image-crop": "^11.1.2"
54
+ },
55
+ "peerDependencies": {
56
+ "@payloadcms/ui": "^3.85.0",
57
+ "payload": "^3.85.0",
58
+ "react": "^19.0.0",
59
+ "react-dom": "^19.0.0"
60
+ },
61
+ "devDependencies": {
62
+ "@payloadcms/db-mongodb": "^3.85.0",
63
+ "@payloadcms/next": "^3.85.0",
64
+ "@payloadcms/richtext-lexical": "^3.85.0",
65
+ "@payloadcms/ui": "^3.85.0",
66
+ "@swc/cli": "^0.5.0",
67
+ "@swc/core": "^1.10.0",
68
+ "@types/node": "^22.0.0",
69
+ "@types/react": "^19.0.0",
70
+ "@types/react-dom": "^19.0.0",
71
+ "copyfiles": "^2.4.1",
72
+ "cross-env": "^7.0.3",
73
+ "next": "^16.0.0",
74
+ "payload": "^3.85.0",
75
+ "react": "^19.0.0",
76
+ "react-dom": "^19.0.0",
77
+ "rimraf": "^6.0.0",
78
+ "sharp": "0.34.2",
79
+ "typescript": "^5.7.0",
80
+ "vitest": "^3.0.0"
81
+ }
82
+ }