@peteai/presentation-editor 0.0.7 → 0.0.9

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.
Files changed (80) hide show
  1. package/dist/components/editor/active-layers.svelte +24 -9
  2. package/dist/components/editor/active-layers.svelte.d.ts +6 -1
  3. package/dist/components/editor/editor.svelte +15 -17
  4. package/dist/components/editor/editor.svelte.js +10 -8
  5. package/dist/components/editor/header.svelte +25 -21
  6. package/dist/components/editor/hotkeys.svelte +7 -0
  7. package/dist/components/editor/layers/active-layer-border.svelte +1 -1
  8. package/dist/components/editor/layers/buttons/border-button/border-button-colors.svelte +1 -1
  9. package/dist/components/editor/layers/buttons/opacity-button/opacity-button.svelte +1 -6
  10. package/dist/components/editor/layers/controls/corner-scale-control/corner-scale-control.svelte +1 -1
  11. package/dist/components/editor/layers/controls/group-resize-control/group-resize-control.svelte +11 -11
  12. package/dist/components/editor/layers/controls/rotate-control/rotate-control.svelte +12 -6
  13. package/dist/components/editor/layers/controls/rotate-control/rotate-control.svelte.d.ts +4 -1
  14. package/dist/components/editor/layers/controls/side-resize-control/side-resize-control.svelte +13 -13
  15. package/dist/components/editor/layers/controls/side-scale-control/side-scale-control.svelte +1 -1
  16. package/dist/components/editor/layers/index.d.ts +2 -2
  17. package/dist/components/editor/layers/index.js +2 -2
  18. package/dist/components/editor/layers/layer-button.svelte +1 -1
  19. package/dist/components/editor/layers/types/background/background-content-image.svelte +15 -20
  20. package/dist/components/editor/layers/types/background/background-layer-buttons.svelte +1 -1
  21. package/dist/components/editor/layers/types/background/background-layer.svelte +2 -2
  22. package/dist/components/editor/layers/types/image/controls/image-rotate-control/image-rotate-control.svelte +120 -0
  23. package/dist/components/editor/layers/types/image/controls/image-rotate-control/image-rotate-control.svelte.d.ts +8 -0
  24. package/dist/components/editor/layers/types/image/controls/image-rotate-control/index.d.ts +2 -0
  25. package/dist/components/editor/layers/types/image/controls/image-rotate-control/index.js +4 -0
  26. package/dist/components/editor/layers/types/image/controls/image-scale-control/image-scale-control.svelte +154 -0
  27. package/dist/components/editor/layers/types/image/controls/image-scale-control/image-scale-control.svelte.d.ts +91 -0
  28. package/dist/components/editor/layers/types/image/controls/image-scale-control/index.d.ts +2 -0
  29. package/dist/components/editor/layers/types/image/controls/image-scale-control/index.js +4 -0
  30. package/dist/components/editor/layers/types/image/image-layer-content.svelte +3 -3
  31. package/dist/components/editor/layers/types/image/image-layer-crop.svelte +182 -0
  32. package/dist/components/editor/layers/types/image/image-layer-crop.svelte.d.ts +10 -0
  33. package/dist/components/editor/layers/types/image/image-layer.svelte +16 -0
  34. package/dist/components/editor/layers/types/image/index.d.ts +2 -1
  35. package/dist/components/editor/layers/types/image/index.js +2 -1
  36. package/dist/components/editor/layers/types/text/extensions/list-item/list-item.js +0 -2
  37. package/dist/components/editor/layers/utils.d.ts +24 -9
  38. package/dist/components/editor/layers/utils.js +107 -54
  39. package/dist/components/editor/page-editor.svelte +6 -2
  40. package/dist/components/editor/sidebar/color-sidebar/color-sidebar-color.svelte +2 -2
  41. package/dist/components/editor/sidebar/color-sidebar/color-sidebar-gradient-picker.svelte +5 -5
  42. package/dist/components/editor/sidebar/color-sidebar/color-sidebar.svelte +4 -4
  43. package/dist/components/editor/sidebar/font-sidebar/font-sidebar.svelte +1 -1
  44. package/dist/components/editor/sidebar/image-crop-sidebar.svelte +112 -0
  45. package/dist/components/editor/sidebar/image-crop-sidebar.svelte.d.ts +7 -0
  46. package/dist/components/editor/sidebar/position-sidebar.svelte +0 -2
  47. package/dist/components/editor/sidebar/sidebar-uploads-tab.svelte +3 -3
  48. package/dist/components/editor/sidebar/sidebar.svelte +7 -4
  49. package/dist/components/editor/snapping-guides.svelte +3 -3
  50. package/dist/components/editor/types.d.ts +2 -1
  51. package/dist/components/editor/utils.js +1 -0
  52. package/dist/components/ui/color-picker/color-picker-alpha-grid.svelte +2 -1
  53. package/dist/components/ui/color-picker/color-picker.svelte +6 -6
  54. package/dist/components/ui/context-menu/context-menu-checkbox-item.svelte +1 -1
  55. package/dist/components/ui/context-menu/context-menu-content.svelte +3 -1
  56. package/dist/components/ui/context-menu/context-menu-group-heading.svelte +1 -1
  57. package/dist/components/ui/context-menu/context-menu-item.svelte +1 -1
  58. package/dist/components/ui/context-menu/context-menu-radio-item.svelte +1 -1
  59. package/dist/components/ui/context-menu/context-menu-separator.svelte +1 -1
  60. package/dist/components/ui/context-menu/context-menu-shortcut.svelte +1 -1
  61. package/dist/components/ui/context-menu/context-menu-sub-content.svelte +1 -1
  62. package/dist/components/ui/context-menu/context-menu-sub-trigger.svelte +1 -1
  63. package/dist/components/ui/dialog/dialog-content.svelte +4 -2
  64. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +1 -1
  65. package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +2 -0
  66. package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte +1 -1
  67. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
  68. package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
  69. package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  70. package/dist/components/ui/input/input.svelte +1 -1
  71. package/dist/components/ui/slider/slider.svelte +3 -3
  72. package/dist/components/ui/tabs/index.d.ts +4 -4
  73. package/dist/components/ui/tabs/index.js +4 -4
  74. package/dist/components/ui/tabs/tabs-content.svelte +4 -4
  75. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +1 -1
  76. package/dist/components/ui/tabs/tabs-list.svelte +5 -9
  77. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +1 -1
  78. package/dist/components/ui/tabs/tabs-trigger.svelte +4 -4
  79. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +1 -1
  80. package/package.json +19 -19
@@ -12,13 +12,6 @@ export type Transform = {
12
12
  rotate: number;
13
13
  scale: number;
14
14
  };
15
- export declare const rotatePointOld: (point: {
16
- x: number;
17
- y: number;
18
- }, angleRad: number) => {
19
- x: number;
20
- y: number;
21
- };
22
15
  export declare const calculateLayerTransform: (layer: Layer, groupScale?: number) => Transform;
23
16
  export declare const calculateNewPosition: (origin: Origin, transform: Transform, newWidth: number, newHeight: number) => {
24
17
  newX: number;
@@ -31,7 +24,7 @@ export declare function calculateBoundingBox(transform: Transform): {
31
24
  height: number;
32
25
  };
33
26
  export declare function degToRad(deg: number): number;
34
- export declare function rotatePoint(p: Point, origin: Point, angleRad: number): Point;
27
+ export declare function rotatePoint(rad: number, p: Point, center?: Point): Point;
35
28
  export declare function getRotatedCorners(rect: Transform): Point[];
36
29
  export declare function isRotatedVertically(rotate: number): boolean;
37
30
  interface ScaleResult {
@@ -51,9 +44,9 @@ export declare function calculateRelativeRects(bbox: Transform, absoluteRects: T
51
44
  x: number;
52
45
  y: number;
53
46
  rotate: number;
47
+ scale: number;
54
48
  width: number;
55
49
  height: number;
56
- scale: number;
57
50
  }[];
58
51
  export declare function calculateAbsoluteRects(bbox: Transform, rects: Transform[]): Transform[];
59
52
  export declare const checkPolygonsIntersect: (a: {
@@ -68,6 +61,7 @@ export declare function calculateImageCover(image: {
68
61
  width: number;
69
62
  height: number;
70
63
  rotate?: number | null;
64
+ imageRotate?: number | null;
71
65
  }, bbox: {
72
66
  width: number;
73
67
  height: number;
@@ -81,4 +75,25 @@ export declare function calculateImageCover(image: {
81
75
  export declare const defaultColor = "#000000";
82
76
  export declare function extractBorderColors(layers: ImageLayer[]): string[] | undefined;
83
77
  export declare function buildBackgroundCircleStyle(colors: string[]): string;
78
+ export declare function getImageLayerBboxRelativeToImageCenter(layer: ImageLayer): {
79
+ minX: number;
80
+ minY: number;
81
+ maxX: number;
82
+ maxY: number;
83
+ };
84
+ export declare function calculateImageLayerPropsForImageRotate(layer: ImageLayer, imageRotate: number): {
85
+ imageRotate: number;
86
+ scale?: undefined;
87
+ width?: undefined;
88
+ height?: undefined;
89
+ offsetX?: undefined;
90
+ offsetY?: undefined;
91
+ } | {
92
+ imageRotate: number;
93
+ scale: number;
94
+ width: number;
95
+ height: number;
96
+ offsetX: number;
97
+ offsetY: number;
98
+ };
84
99
  export {};
@@ -1,12 +1,4 @@
1
- export const rotatePointOld = (point, angleRad) => {
2
- const cos = Math.cos(angleRad);
3
- const sin = Math.sin(angleRad);
4
- return {
5
- x: point.x * cos - point.y * sin,
6
- y: point.x * sin + point.y * cos,
7
- };
8
- };
9
- const getPoints = (origin, width, height) => {
1
+ const getOriginRelativePoint = (origin, width, height) => {
10
2
  const halfWidth = width / 2;
11
3
  const halfHeight = height / 2;
12
4
  switch (origin) {
@@ -41,36 +33,42 @@ export const calculateLayerTransform = (layer, groupScale = 1) => ({
41
33
  scale: layer.scale,
42
34
  });
43
35
  export const calculateNewPosition = (origin, transform, newWidth, newHeight) => {
44
- const { x, y, width, height, rotate, scale } = transform;
45
- const oldPoint = getPoints(origin, width * scale, height * scale);
46
- const newPoint = getPoints(origin, newWidth, newHeight);
47
- let deltaX = newPoint.x - oldPoint.x;
48
- let deltaY = newPoint.y - oldPoint.y;
49
- if (rotate !== 0) {
50
- const rad = (rotate * Math.PI) / 180;
51
- const rotatedOldCenter = rotatePointOld(oldPoint, rad);
52
- const rotatedNewCenter = rotatePointOld(newPoint, rad);
53
- deltaX = rotatedNewCenter.x - rotatedOldCenter.x;
54
- deltaY = rotatedNewCenter.y - rotatedOldCenter.y;
55
- }
36
+ const { x, y, width: rawWidth, height: rawHeight, rotate, scale } = transform;
37
+ const width = rawWidth * scale;
38
+ const height = rawHeight * scale;
39
+ // Get relative points based on origin for old and new dimensions
40
+ const oldPoint = getOriginRelativePoint(origin, width, height);
41
+ const newPoint = getOriginRelativePoint(origin, newWidth, newHeight);
42
+ // Calculate position delta
43
+ const delta = {
44
+ x: newPoint.x - oldPoint.x,
45
+ y: newPoint.y - oldPoint.y,
46
+ };
47
+ // Apply rotation if needed
48
+ const rotatedDelta = rotate !== 0 ? rotatePoint(degToRad(rotate), delta) : delta;
49
+ // Calculate new position accounting for size difference and delta
56
50
  return {
57
- newX: x - deltaX - (newWidth - width * scale) / 2,
58
- newY: y - deltaY - (newHeight - height * scale) / 2,
51
+ newX: x - rotatedDelta.x - (newWidth - width) / 2,
52
+ newY: y - rotatedDelta.y - (newHeight - height) / 2,
59
53
  };
60
54
  };
61
55
  export function calculateBoundingBox(transform) {
62
- const { x, y, width, height, rotate, scale } = transform;
56
+ const { x, y, width: rawWidth, height: rawHeight, rotate, scale } = transform;
57
+ const width = rawWidth * scale;
58
+ const height = rawHeight * scale;
63
59
  if (rotate === 0) {
64
- return { x, y, width: width * scale, height: height * scale };
60
+ return { x, y, width, height };
65
61
  }
66
- const radians = (rotate * Math.PI) / 180;
67
- const origins = ['top-left', 'top-right'];
68
- const corners = origins.map((position) => rotatePointOld(getPoints(position, width * scale, height * scale), radians));
69
- const maxX = Math.max(...corners.map((corner) => Math.abs(corner.x)));
70
- const maxY = Math.max(...corners.map((corner) => Math.abs(corner.y)));
62
+ const rad = degToRad(rotate);
63
+ const cos = Math.cos(rad);
64
+ const sin = Math.sin(rad);
65
+ const hw = width / 2;
66
+ const hh = height / 2;
67
+ const maxX = Math.abs(hw * cos) + Math.abs(hh * sin);
68
+ const maxY = Math.abs(hw * sin) + Math.abs(hh * cos);
71
69
  return {
72
- x: x + (width * scale) / 2 - maxX,
73
- y: y + (height * scale) / 2 - maxY,
70
+ x: x + width / 2 - maxX,
71
+ y: y + height / 2 - maxY,
74
72
  width: maxX * 2,
75
73
  height: maxY * 2,
76
74
  };
@@ -78,20 +76,20 @@ export function calculateBoundingBox(transform) {
78
76
  export function degToRad(deg) {
79
77
  return (deg * Math.PI) / 180;
80
78
  }
81
- export function rotatePoint(p, origin, angleRad) {
82
- const dx = p.x - origin.x;
83
- const dy = p.y - origin.y;
84
- const cos = Math.cos(angleRad);
85
- const sin = Math.sin(angleRad);
79
+ export function rotatePoint(rad, p, center = { x: 0, y: 0 }) {
80
+ const cos = Math.cos(rad);
81
+ const sin = Math.sin(rad);
82
+ const dx = p.x - center.x;
83
+ const dy = p.y - center.y;
86
84
  return {
87
- x: origin.x + dx * cos - dy * sin,
88
- y: origin.y + dx * sin + dy * cos,
85
+ x: center.x + dx * cos - dy * sin,
86
+ y: center.y + dx * sin + dy * cos,
89
87
  };
90
88
  }
91
89
  export function getRotatedCorners(rect) {
90
+ const rad = degToRad(rect.rotate);
92
91
  const cx = rect.x + (rect.width * rect.scale) / 2;
93
92
  const cy = rect.y + (rect.height * rect.scale) / 2;
94
- const angle = degToRad(rect.rotate);
95
93
  const hw = (rect.width * rect.scale) / 2;
96
94
  const hh = (rect.height * rect.scale) / 2;
97
95
  const corners = [
@@ -100,7 +98,7 @@ export function getRotatedCorners(rect) {
100
98
  { x: cx + hw, y: cy + hh },
101
99
  { x: cx - hw, y: cy + hh },
102
100
  ];
103
- return corners.map((p) => rotatePoint(p, { x: cx, y: cy }, angle));
101
+ return corners.map((p) => rotatePoint(rad, p, { x: cx, y: cy }));
104
102
  }
105
103
  export function isRotatedVertically(rotate) {
106
104
  const rotationNormalized = ((rotate % 360) + 360) % 360;
@@ -166,9 +164,9 @@ export function calculateGroupRotatedBoundingBox(transforms, rotate = 0) {
166
164
  if (!rotate)
167
165
  return calculateGroupBoundingBox(transforms);
168
166
  const allCorners = transforms.flatMap((t) => getRotatedCorners(t));
169
- const theta = degToRad(-rotate); // inverse to align bbox with axes
167
+ const rad = degToRad(rotate);
170
168
  // Rotate all points to align with desired angle
171
- const rotatedPoints = allCorners.map((p) => rotatePoint(p, { x: 0, y: 0 }, theta));
169
+ const rotatedPoints = allCorners.map((p) => rotatePoint(-rad, p));
172
170
  // Compute AABB of rotated points
173
171
  const xs = rotatedPoints.map((p) => p.x);
174
172
  const ys = rotatedPoints.map((p) => p.y);
@@ -183,7 +181,7 @@ export function calculateGroupRotatedBoundingBox(transforms, rotate = 0) {
183
181
  y: (minY + maxY) / 2,
184
182
  };
185
183
  // Rotate center back to original space
186
- const center = rotatePoint(centerRotated, { x: 0, y: 0 }, degToRad(rotate));
184
+ const center = rotatePoint(rad, centerRotated);
187
185
  return {
188
186
  x: center.x - width / 2,
189
187
  y: center.y - height / 2,
@@ -194,7 +192,7 @@ export function calculateGroupRotatedBoundingBox(transforms, rotate = 0) {
194
192
  };
195
193
  }
196
194
  export function calculateRelativeRects(bbox, absoluteRects) {
197
- const theta = degToRad(-bbox.rotate); // inverse to align bbox with axes
195
+ const rad = degToRad(bbox.rotate); // inverse to align bbox with axes
198
196
  const bboxCenter = {
199
197
  x: bbox.x + (bbox.width * bbox.scale) / 2,
200
198
  y: bbox.y + (bbox.height * bbox.scale) / 2,
@@ -206,17 +204,18 @@ export function calculateRelativeRects(bbox, absoluteRects) {
206
204
  y: rect.y + (rect.height * rect.scale) / 2,
207
205
  };
208
206
  // Rotate center ralatively to the bbox center
209
- const relativeCenter = rotatePoint(center, bboxCenter, theta);
207
+ const relativeCenter = rotatePoint(-rad, center, bboxCenter);
210
208
  return {
211
209
  ...rect,
212
- x: (relativeCenter.x - bbox.x) / bbox.scale - (rect.width * rect.scale) / 2,
213
- y: (relativeCenter.y - bbox.y) / bbox.scale - (rect.height * rect.scale) / 2,
210
+ x: ((relativeCenter.x - bbox.x) / bbox.scale - (rect.width * rect.scale) / 2) / bbox.scale,
211
+ y: ((relativeCenter.y - bbox.y) / bbox.scale - (rect.height * rect.scale) / 2) / bbox.scale,
214
212
  rotate: rect.rotate - bbox.rotate,
213
+ scale: rect.scale / bbox.scale,
215
214
  };
216
215
  });
217
216
  }
218
217
  export function calculateAbsoluteRects(bbox, rects) {
219
- const theta = degToRad(bbox.rotate);
218
+ const rad = degToRad(bbox.rotate);
220
219
  const bboxCenter = {
221
220
  x: bbox.x + (bbox.width * bbox.scale) / 2,
222
221
  y: bbox.y + (bbox.height * bbox.scale) / 2,
@@ -226,7 +225,7 @@ export function calculateAbsoluteRects(bbox, rects) {
226
225
  x: bbox.x + (rect.x + (rect.width * rect.scale) / 2) * bbox.scale,
227
226
  y: bbox.y + (rect.y + (rect.height * rect.scale) / 2) * bbox.scale,
228
227
  };
229
- const center = rotatePoint(relativeCenter, bboxCenter, theta);
228
+ const center = rotatePoint(rad, relativeCenter, bboxCenter);
230
229
  return {
231
230
  ...rect,
232
231
  x: center.x - (rect.width * rect.scale * bbox.scale) / 2,
@@ -293,12 +292,14 @@ const minScaleToContain = (rotatedRect, rect) => {
293
292
  };
294
293
  export function calculateImageCover(image, bbox) {
295
294
  const scale = minScaleToContain(image, bbox);
295
+ const width = bbox.width / scale;
296
+ const height = bbox.height / scale;
296
297
  return {
297
- width: bbox.width / scale,
298
- height: bbox.height / scale,
298
+ width,
299
+ height,
299
300
  scale,
300
- offsetX: (image.width - bbox.width / scale) / 2,
301
- offsetY: (image.height - bbox.height / scale) / 2,
301
+ offsetX: (width - image.width) / 2,
302
+ offsetY: (height - image.height) / 2,
302
303
  };
303
304
  }
304
305
  export const defaultColor = '#000000';
@@ -322,3 +323,55 @@ export function buildBackgroundCircleStyle(colors) {
322
323
  .join(', ');
323
324
  return `background-image: conic-gradient(${gradientColors})`;
324
325
  }
326
+ export function getImageLayerBboxRelativeToImageCenter(layer) {
327
+ const { width, height, image, offsetX, offsetY, imageRotate } = layer;
328
+ // current image center in layer coords (rotation doesn't change it)
329
+ const imageCenter = { x: image.width / 2 + offsetX, y: image.height / 2 + offsetY };
330
+ const imageRad = degToRad(imageRotate);
331
+ // For each corner, compute v = corner - C, rotate by -theta, accumulate max abs X/Y
332
+ const corners = [
333
+ { x: 0, y: 0 },
334
+ { x: width, y: 0 },
335
+ { x: width, y: height },
336
+ { x: 0, y: height },
337
+ ].map((p) => {
338
+ const v = { x: p.x - imageCenter.x, y: p.y - imageCenter.y }; // layer->center
339
+ return rotatePoint(-imageRad, v); // bring into image unrotated axes
340
+ });
341
+ // Compute AABB of rotated points
342
+ const xs = corners.map((p) => p.x);
343
+ const ys = corners.map((p) => p.y);
344
+ const minX = Math.min(...xs);
345
+ const maxX = Math.max(...xs);
346
+ const minY = Math.min(...ys);
347
+ const maxY = Math.max(...ys);
348
+ return { minX, minY, maxX, maxY };
349
+ }
350
+ export function calculateImageLayerPropsForImageRotate(layer, imageRotate) {
351
+ const { width, height, scale, image, offsetX, offsetY } = layer;
352
+ // image half-width and half-height
353
+ const ihw = image.width / 2;
354
+ const ihh = image.height / 2;
355
+ // current image center in layer coords (rotation doesn't change it)
356
+ const imageCenter = { x: ihw + offsetX, y: ihh + offsetY };
357
+ const bbox = getImageLayerBboxRelativeToImageCenter({ ...layer, imageRotate });
358
+ const maxAbsX = Math.max(Math.abs(bbox.minX), Math.abs(bbox.maxX));
359
+ const maxAbsY = Math.max(Math.abs(bbox.minY), Math.abs(bbox.maxY));
360
+ // Required scale so half-extents of (Iw*s, Ih*s) cover maxAbsX/Y:
361
+ // Iw*s/2 >= maxAbsX => s >= 2*maxAbsX / Iw
362
+ // Ih*s/2 >= maxAbsY => s >= 2*maxAbsY / Ih
363
+ const sReqX = (2 * maxAbsX) / image.width;
364
+ const sReqY = (2 * maxAbsY) / image.height;
365
+ const minScaleFactor = Math.max(sReqX, sReqY);
366
+ if (minScaleFactor < 1) {
367
+ return { imageRotate };
368
+ }
369
+ return {
370
+ imageRotate,
371
+ scale: scale * minScaleFactor,
372
+ width: width / minScaleFactor,
373
+ height: height / minScaleFactor,
374
+ offsetX: imageCenter.x / minScaleFactor - ihw,
375
+ offsetY: imageCenter.y / minScaleFactor - ihh,
376
+ };
377
+ }
@@ -25,6 +25,8 @@
25
25
 
26
26
  const editor = getEditorContext();
27
27
 
28
+ let pageRef: HTMLDivElement | null = $state(null);
29
+
28
30
  let activeLayerGuides: ActiveLayerGuide[] = $state([]);
29
31
 
30
32
  let hoveredLayerId: string | null = $state(null);
@@ -38,7 +40,7 @@
38
40
  <div class="flex min-h-full flex-1">
39
41
  <div class="box-border flex w-full shrink-0 items-center justify-center p-0">
40
42
  <div bind:this={wrapperRef} class="translate-x-0 translate-y-0 p-4 transition-transform">
41
- <div class="relative flex flex-col items-center" data-page-container>
43
+ <div bind:this={pageRef} class="relative flex flex-col items-center">
42
44
  <div
43
45
  class="m-0 overflow-hidden shadow-xl"
44
46
  style:width="{scaledWidth}px"
@@ -89,7 +91,9 @@
89
91
  {/if}
90
92
  {/if}
91
93
 
92
- <ActiveLayers />
94
+ {#if viewportRef && pageRef}
95
+ <ActiveLayers {viewportRef} {wrapperRef} {pageRef} />
96
+ {/if}
93
97
 
94
98
  <SnappingGuides zoom={editor.zoom} guides={activeLayerGuides} />
95
99
  </div>
@@ -30,7 +30,7 @@
30
30
  )}
31
31
  {...restProps}
32
32
  >
33
- <div class="wrapper after:shadow-inner-1 relative h-full w-full after:absolute after:inset-0">
33
+ <div class="wrapper relative h-full w-full after:absolute after:inset-0 after:shadow-inner-1">
34
34
  <ColorPickerAlphaGrid size={12}>
35
35
  <ColorIndicator {color} />
36
36
  </ColorPickerAlphaGrid>
@@ -40,7 +40,7 @@
40
40
  <div
41
41
  class="pointer-events-none absolute inset-0 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100"
42
42
  >
43
- <div class="bg-background text-foreground rounded-full p-1">
43
+ <div class="rounded-full bg-background p-1 text-foreground">
44
44
  <SettingsIcon class="h-4 w-4" />
45
45
  </div>
46
46
  </div>
@@ -66,11 +66,11 @@
66
66
  <Popover.Trigger
67
67
  class={cn(
68
68
  buttonVariants({ variant: 'ghost', size: 'icon' }),
69
- 'hover:after:shadow-hover data-[state=open]:after:shadow-active relative h-full w-full shrink-0 overflow-hidden rounded-full after:absolute after:inset-0 after:rounded-full after:transition-shadow',
69
+ 'relative h-full w-full shrink-0 overflow-hidden rounded-full after:absolute after:inset-0 after:rounded-full after:transition-shadow hover:after:shadow-hover data-[state=open]:after:shadow-active',
70
70
  )}
71
71
  >
72
72
  <div
73
- class="wrapper after:shadow-inner-1 relative h-full w-full after:absolute after:inset-0 after:rounded-full"
73
+ class="wrapper relative h-full w-full after:absolute after:inset-0 after:rounded-full after:shadow-inner-1"
74
74
  >
75
75
  <ColorPickerAlphaGrid size={12}>
76
76
  <ColorIndicator {color} />
@@ -105,11 +105,11 @@
105
105
  <Button
106
106
  variant="ghost"
107
107
  size="icon"
108
- class="hover:after:shadow-hover relative shrink-0 p-0 after:absolute after:inset-0 after:rounded-full after:transition-shadow"
108
+ class="relative shrink-0 p-0 after:absolute after:inset-0 after:rounded-full after:transition-shadow hover:after:shadow-hover"
109
109
  onclick={addColor}
110
110
  >
111
111
  <div
112
- class="after:shadow-inner-1 relative h-full w-full after:absolute after:inset-0 after:rounded-full"
112
+ class="relative h-full w-full after:absolute after:inset-0 after:rounded-full after:shadow-inner-1"
113
113
  >
114
114
  <div
115
115
  class="h-full w-full rounded-full"
@@ -117,7 +117,7 @@
117
117
  ></div>
118
118
  <span class="absolute inset-0 flex h-full w-full items-center justify-center">
119
119
  <span
120
- class="bg-background text-foreground flex h-6 w-6 items-center justify-center rounded-full"
120
+ class="flex h-6 w-6 items-center justify-center rounded-full bg-background text-foreground"
121
121
  >
122
122
  <PlusIcon />
123
123
  </span>
@@ -325,7 +325,7 @@
325
325
  )}
326
326
  >
327
327
  <div
328
- class="after:shadow-inner-1 relative h-full w-full after:absolute after:inset-0 after:rounded-full"
328
+ class="relative h-full w-full after:absolute after:inset-0 after:rounded-full after:shadow-inner-1"
329
329
  >
330
330
  <div
331
331
  class="h-full w-full rounded-full"
@@ -333,7 +333,7 @@
333
333
  ></div>
334
334
  <span class="absolute inset-0 flex h-full w-full items-center justify-center">
335
335
  <span
336
- class="bg-background text-foreground flex h-6 w-6 items-center justify-center rounded-full"
336
+ class="flex h-6 w-6 items-center justify-center rounded-full bg-background text-foreground"
337
337
  >
338
338
  <PlusIcon />
339
339
  </span>
@@ -381,7 +381,7 @@
381
381
  <!-- <PaletteIcon class="h-5 w-5" /> -->
382
382
  <h5 class="text-left text-sm font-bold">Default colors</h5>
383
383
  </div>
384
- <div class="text-muted-foreground text-xs">Solid colors</div>
384
+ <div class="text-xs text-muted-foreground">Solid colors</div>
385
385
  <div class="grid grid-cols-6 items-stretch gap-1">
386
386
  {#each Object.keys(defaultSolidColors) as color}
387
387
  <div class="relative h-0 w-full" style:padding-top="100%">
@@ -393,7 +393,7 @@
393
393
  </div>
394
394
 
395
395
  {#if gradientsAllowed}
396
- <div class="text-muted-foreground text-xs">Gradients</div>
396
+ <div class="text-xs text-muted-foreground">Gradients</div>
397
397
  <div class="grid grid-cols-6 items-stretch gap-1">
398
398
  {#each defaultGradients as color}
399
399
  <div class="relative h-0 w-full" style:padding-top="100%">
@@ -202,7 +202,7 @@
202
202
  </InfiniteLoader>
203
203
  </div>
204
204
  {:else}
205
- <p class="text-muted-foreground text-sm">
205
+ <p class="text-sm text-muted-foreground">
206
206
  We couldn't find any results for “{search}”. Try searching for something else.
207
207
  </p>
208
208
  {/if}
@@ -0,0 +1,112 @@
1
+ <script lang="ts">
2
+ import XIcon from '@lucide/svelte/icons/x';
3
+ import { Button } from '../../ui/button/index.js';
4
+ import { Slider } from '../../ui/slider/index.js';
5
+ import { Input } from '../../ui/input/index.js';
6
+ import { getEditorContext } from '../editor.svelte.js';
7
+ import {
8
+ calculateImageLayerPropsForImageRotate,
9
+ calculateRelativeRects,
10
+ } from '../layers/utils.js';
11
+ import type { ImageLayer } from '../types.js';
12
+
13
+ interface Props {
14
+ layer: ImageLayer;
15
+ }
16
+
17
+ let { layer }: Props = $props();
18
+
19
+ const editor = $derived(getEditorContext());
20
+
21
+ let value = $derived(Math.round(layer.imageRotate * 10) / 10);
22
+ let originalLayer = $derived(
23
+ editor.selectedLayersNotLocked.find((l) => l.id === layer.id),
24
+ ) as ImageLayer;
25
+
26
+ const min = -180;
27
+ const max = 180;
28
+ const step = 0.1;
29
+
30
+ let layerCache: ImageLayer | null = $state.snapshot(null);
31
+
32
+ const setImageRotate = (value: string | number) => {
33
+ value = Number(value);
34
+ if (!layerCache) {
35
+ layerCache = $state.snapshot(layer);
36
+ }
37
+ const props = calculateImageLayerPropsForImageRotate(layerCache, value);
38
+ Object.assign(layer, props);
39
+ };
40
+
41
+ const applyChanges = () => {
42
+ if (!originalLayer) return;
43
+ const relativeLayer = editor.activeGroupAndChild
44
+ ? (calculateRelativeRects(editor.activeGroupAndChild.groupLayer, [layer])[0] as ImageLayer)
45
+ : layer;
46
+ // Define keys to check for changes in image layer properties
47
+ const imageLayerKeys: (keyof ImageLayer)[] = [
48
+ 'imageRotate', // Rotation angle of the image
49
+ 'scale', // Scale/zoom level
50
+ 'width', // Width dimension
51
+ 'height', // Height dimension
52
+ 'offsetX', // X offset position
53
+ 'offsetY', // Y offset position
54
+ ];
55
+
56
+ // Filter keys that have different values between original and relative layers
57
+ const changedKeys = imageLayerKeys.filter((key) => originalLayer[key] !== relativeLayer[key]);
58
+ if (changedKeys.length > 0) {
59
+ editor.historyPush({
60
+ type: 'layerUpdate',
61
+ pageId: editor.activePage.id,
62
+ layer: { id: layer.id, type: layer.type },
63
+ undo: Object.fromEntries(changedKeys.map((key) => [key, originalLayer[key]])),
64
+ redo: Object.fromEntries(changedKeys.map((key) => [key, relativeLayer[key]])),
65
+ });
66
+ }
67
+ editor.imageCropLayer = null;
68
+ };
69
+
70
+ $effect(() => {
71
+ if (!originalLayer) {
72
+ editor.imageCropLayer = null;
73
+ }
74
+ });
75
+ </script>
76
+
77
+ <div class="flex h-full flex-col gap-2">
78
+ <div class="flex items-center justify-between">
79
+ <div class="text-sm font-bold">Crop</div>
80
+ <Button variant="ghost" size="icon" onclick={() => (editor.imageCropLayer = null)}>
81
+ <XIcon />
82
+ </Button>
83
+ </div>
84
+ <div class="flex-1 resize-none overflow-y-auto">
85
+ <div class="flex items-center gap-2">
86
+ <div class="grow">
87
+ <Slider
88
+ type="single"
89
+ {value}
90
+ {min}
91
+ {max}
92
+ {step}
93
+ onValueChange={(value) => setImageRotate(value)}
94
+ onValueCommit={() => (layerCache = null)}
95
+ />
96
+ </div>
97
+ <Input
98
+ class="w-12 bg-transparent p-0 text-center font-semibold leading-none [&::-webkit-inner-spin-button]:appearance-none"
99
+ inputmode="decimal"
100
+ placeholder="--"
101
+ {value}
102
+ onchange={(e) => setImageRotate(e.currentTarget.value)}
103
+ />
104
+ </div>
105
+ <div class="flex justify-end gap-2 p-2">
106
+ <Button class="flex-1" variant="outline" onclick={() => (editor.imageCropLayer = null)}>
107
+ Cancel
108
+ </Button>
109
+ <Button class="flex-1" onclick={applyChanges}>Done</Button>
110
+ </div>
111
+ </div>
112
+ </div>
@@ -0,0 +1,7 @@
1
+ import type { ImageLayer } from '../types.js';
2
+ interface Props {
3
+ layer: ImageLayer;
4
+ }
5
+ declare const ImageCropSidebar: import("svelte").Component<Props, {}, "">;
6
+ type ImageCropSidebar = ReturnType<typeof ImageCropSidebar>;
7
+ export default ImageCropSidebar;
@@ -19,8 +19,6 @@
19
19
 
20
20
  const editor = $derived(getEditorContext());
21
21
 
22
- $inspect(editor.sortedPages);
23
-
24
22
  const flipDurationMs = 300;
25
23
  const otherOptions = { flipDurationMs, type: 'layers', dropTargetStyle: {} };
26
24
 
@@ -47,7 +47,7 @@
47
47
  <div class="relative flex h-full shrink-0 flex-col gap-2">
48
48
  {#if editor.fileDragged}
49
49
  <div
50
- class="bg-background absolute inset-0 z-10 flex h-full w-full flex-col items-center justify-center"
50
+ class="absolute inset-0 z-10 flex h-full w-full flex-col items-center justify-center bg-background"
51
51
  >
52
52
  <div class="grid gap-6">
53
53
  <div class="grid justify-center">
@@ -60,7 +60,7 @@
60
60
  <ImageIcon class="h-4 w-4" />
61
61
  <p class="text-sm font-semibold">Images</p>
62
62
  </div>
63
- <p class="text-muted-foreground text-xs">GIF, JPG, PNG</p>
63
+ <p class="text-xs text-muted-foreground">GIF, JPG, PNG</p>
64
64
  </div>
65
65
  </div>
66
66
  </div>
@@ -91,7 +91,7 @@
91
91
  class="col-span-3"
92
92
  />
93
93
  {#if error}
94
- <div class="text-destructive col-span-3 col-start-2 text-sm font-medium">
94
+ <div class="col-span-3 col-start-2 text-sm font-medium text-destructive">
95
95
  Invalid
96
96
  </div>
97
97
  {/if}
@@ -10,6 +10,7 @@
10
10
  import PositionSidebar from './position-sidebar.svelte';
11
11
  import { ColorSidebar } from './color-sidebar/index.js';
12
12
  import { FontSidebar } from './font-sidebar/index.js';
13
+ import ImageCropSidebar from './image-crop-sidebar.svelte';
13
14
 
14
15
  interface Props {
15
16
  editor: Editor;
@@ -62,13 +63,15 @@
62
63
  {/each}
63
64
  </div>
64
65
 
65
- {#if editor.activeSidebarPopup || editor.activeSidebarTab}
66
- <div class="bg-background h-full w-80 shrink-0 overflow-y-auto border-r border-gray-200 p-2">
67
- {#if editor.activeSidebarPopup === 'position'}
66
+ {#if editor.activeSidebarPopup || editor.activeSidebarTab || editor.imageCropLayer}
67
+ <div class="h-full w-80 shrink-0 overflow-y-auto border-r border-gray-200 bg-background p-2">
68
+ {#if editor.imageCropLayer}
69
+ <ImageCropSidebar layer={editor.imageCropLayer} />
70
+ {:else if editor.activeSidebarPopup === 'position'}
68
71
  <PositionSidebar />
69
72
  {:else if editor.activeSidebarPopup === 'font'}
70
73
  <FontSidebar />
71
- {:else if editor.activeSidebarPopup}
74
+ {:else if editor.activeSidebarPopup?.indexOf('Color')}
72
75
  <ColorSidebar />
73
76
  {:else if editor.activeSidebarTab === 'text'}
74
77
  <SidebarTextTab />
@@ -13,11 +13,11 @@
13
13
  <div class="absolute h-full w-full">
14
14
  {#each guides as guide (guide.id)}
15
15
  {#if guide.type === 'box'}
16
- <div class="border-primary absolute border" style:inset="{guide.inset * zoom}px"></div>
16
+ <div class="absolute border border-primary" style:inset="{guide.inset * zoom}px"></div>
17
17
  {:else if guide.type === 'vertical'}
18
18
  <div
19
19
  class={cn(
20
- 'border-primary absolute h-full border-l',
20
+ 'absolute h-full border-l border-primary',
21
21
  guide.style === 'dashed' && 'border-dashed',
22
22
  guide.style === 'dotted' && 'border-dotted',
23
23
  )}
@@ -28,7 +28,7 @@
28
28
  {:else if guide.type === 'horizontal'}
29
29
  <div
30
30
  class={cn(
31
- 'border-primary absolute w-full border-t',
31
+ 'absolute w-full border-t border-primary',
32
32
  guide.style === 'dashed' && 'border-dashed',
33
33
  guide.style === 'dotted' && 'border-dotted',
34
34
  )}