compote-ui 0.5.7 → 0.6.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.
@@ -2,18 +2,7 @@
2
2
  import PhX from '../../icons/PhX.svelte';
3
3
  import { Dialog } from '@ark-ui/svelte/dialog';
4
4
  import { Portal } from '@ark-ui/svelte/portal';
5
-
6
- type Variant = 'default' | 'destructive';
7
-
8
- type Props = {
9
- open: boolean;
10
- title: string;
11
- description?: string | string[];
12
- confirmLabel?: string;
13
- cancelLabel?: string;
14
- onConfirm?: () => void;
15
- variant?: Variant;
16
- };
5
+ import type { AlertDialogProps } from './dialog.types';
17
6
 
18
7
  let {
19
8
  open = $bindable(),
@@ -22,8 +11,12 @@
22
11
  confirmLabel = 'Confirm',
23
12
  cancelLabel = 'Cancel',
24
13
  onConfirm,
25
- variant = 'default'
26
- }: Props = $props();
14
+ onCancel,
15
+ variant = 'default',
16
+ lazyMount = true,
17
+ unmountOnExit = true,
18
+ ...restProps
19
+ }: AlertDialogProps = $props();
27
20
 
28
21
  const confirmClass = $derived(
29
22
  variant === 'destructive'
@@ -32,7 +25,7 @@
32
25
  );
33
26
  </script>
34
27
 
35
- <Dialog.Root role="alertdialog" bind:open>
28
+ <Dialog.Root role="alertdialog" bind:open {lazyMount} {unmountOnExit} {...restProps}>
36
29
  <Portal>
37
30
  <Dialog.Backdrop
38
31
  class="data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 fixed inset-0 z-50 bg-black/50"
@@ -54,6 +47,7 @@
54
47
  <div class="mt-6 flex justify-end gap-3">
55
48
  <Dialog.CloseTrigger
56
49
  class="inline-flex h-9 items-center justify-center rounded-md border bg-surface-1 px-4 text-sm font-medium shadow-sm transition-colors hover:bg-surface-2 hover:text-ink focus-visible:ring-1 focus-visible:ring-primary focus-visible:outline-none"
50
+ onclick={onCancel}
57
51
  >
58
52
  {cancelLabel}
59
53
  </Dialog.CloseTrigger>
@@ -1,13 +1,4 @@
1
- type Variant = 'default' | 'destructive';
2
- type Props = {
3
- open: boolean;
4
- title: string;
5
- description?: string | string[];
6
- confirmLabel?: string;
7
- cancelLabel?: string;
8
- onConfirm?: () => void;
9
- variant?: Variant;
10
- };
11
- declare const AlertDialog: import("svelte").Component<Props, {}, "open">;
1
+ import type { AlertDialogProps } from './dialog.types';
2
+ declare const AlertDialog: import("svelte").Component<AlertDialogProps, {}, "open">;
12
3
  type AlertDialog = ReturnType<typeof AlertDialog>;
13
4
  export default AlertDialog;
@@ -1,19 +1,9 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
2
  import { Dialog } from '@ark-ui/svelte/dialog';
4
3
  import { Portal } from '@ark-ui/svelte/portal';
5
4
  import { cn } from 'tailwind-variants';
6
5
  import PhX from '../../icons/PhX.svelte';
7
-
8
- interface Props {
9
- open: boolean;
10
- title: string;
11
- description?: string;
12
- children: Snippet;
13
- footer?: Snippet;
14
- onClose?: () => void;
15
- contentClass?: string;
16
- }
6
+ import type { DialogProps } from './dialog.types';
17
7
 
18
8
  let {
19
9
  open = $bindable(),
@@ -21,12 +11,14 @@
21
11
  description,
22
12
  children,
23
13
  footer,
24
- onClose,
25
- contentClass
26
- }: Props = $props();
14
+ contentClass,
15
+ lazyMount = true,
16
+ unmountOnExit = true,
17
+ ...restProps
18
+ }: DialogProps = $props();
27
19
  </script>
28
20
 
29
- <Dialog.Root bind:open lazyMount unmountOnExit>
21
+ <Dialog.Root bind:open {lazyMount} {unmountOnExit} {...restProps}>
30
22
  <Portal>
31
23
  <Dialog.Backdrop
32
24
  class="data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 fixed inset-0 z-50 bg-black/50"
@@ -57,7 +49,6 @@
57
49
 
58
50
  <Dialog.CloseTrigger
59
51
  class="absolute top-3 right-3 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:outline-none active:opacity-50"
60
- onclick={onClose}
61
52
  >
62
53
  <PhX class="size-4" />
63
54
  <span class="sr-only">Close</span>
@@ -1,14 +1,5 @@
1
- import type { Snippet } from 'svelte';
2
1
  import { Dialog } from '@ark-ui/svelte/dialog';
3
- interface Props {
4
- open: boolean;
5
- title: string;
6
- description?: string;
7
- children: Snippet;
8
- footer?: Snippet;
9
- onClose?: () => void;
10
- contentClass?: string;
11
- }
12
- declare const Dialog: import("svelte").Component<Props, {}, "open">;
2
+ import type { DialogProps } from './dialog.types';
3
+ declare const Dialog: import("svelte").Component<DialogProps, {}, "open">;
13
4
  type Dialog = ReturnType<typeof Dialog>;
14
5
  export default Dialog;
@@ -0,0 +1,24 @@
1
+ import type { DialogRootBaseProps } from '@ark-ui/svelte/dialog';
2
+ import type { Snippet } from 'svelte';
3
+ export type { DialogRootBaseProps };
4
+ type DialogSharedProps = Pick<DialogRootBaseProps, 'closeOnEscape' | 'closeOnInteractOutside' | 'onOpenChange' | 'lazyMount' | 'unmountOnExit'>;
5
+ export interface DialogProps extends DialogSharedProps {
6
+ open: boolean;
7
+ title: string;
8
+ description?: string;
9
+ children: Snippet;
10
+ footer?: Snippet;
11
+ contentClass?: string;
12
+ initialFocusEl?: DialogRootBaseProps['initialFocusEl'];
13
+ }
14
+ export type AlertDialogVariant = 'default' | 'destructive';
15
+ export interface AlertDialogProps extends DialogSharedProps {
16
+ open: boolean;
17
+ title: string;
18
+ description?: string | string[];
19
+ confirmLabel?: string;
20
+ cancelLabel?: string;
21
+ onConfirm?: () => void;
22
+ onCancel?: () => void;
23
+ variant?: AlertDialogVariant;
24
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ import Dialog from '../dialog/dialog.svelte';
3
+ import ImageCropper from '../image-cropper/image-cropper.svelte';
4
+ import { Button } from '../..';
5
+ import { cropImage, processImage } from '../../utils/image-processing';
6
+ import type { ImageCropperCropData } from '../image-cropper/types';
7
+ import type { ImageCropDialogProps } from './types';
8
+
9
+ let {
10
+ open = $bindable(),
11
+ imageSrc,
12
+ onConfirm,
13
+ onCancel,
14
+ title = 'Crop Image',
15
+ description = 'Adjust the crop area or skip to use the full image.',
16
+ aspectRatio = undefined,
17
+ processOptions = undefined,
18
+ showSkipCrop = true,
19
+ confirmLabel = 'Crop & Save',
20
+ skipLabel = 'Skip Crop'
21
+ }: ImageCropDialogProps = $props();
22
+
23
+ let getCropData = $state<(() => ImageCropperCropData) | undefined>(undefined);
24
+ let processing = $state(false);
25
+
26
+ async function handleCrop() {
27
+ processing = true;
28
+ try {
29
+ const cropData = getCropData?.();
30
+ if (cropData) {
31
+ const processed = await cropImage(imageSrc, cropData, processOptions);
32
+ onConfirm(processed);
33
+ }
34
+ } finally {
35
+ processing = false;
36
+ }
37
+ }
38
+
39
+ async function handleSkipCrop() {
40
+ processing = true;
41
+ try {
42
+ const processed = await processImage(imageSrc, processOptions);
43
+ onConfirm(processed);
44
+ } finally {
45
+ processing = false;
46
+ }
47
+ }
48
+ </script>
49
+
50
+ <Dialog bind:open {title} {description} onOpenChange={(details) => { if (!details.open) onCancel(); }}>
51
+ <ImageCropper bind:getCropData src={imageSrc} alt="Crop preview" {aspectRatio} />
52
+ {#snippet footer()}
53
+ <Button variant="outline" onclick={onCancel} disabled={processing}>Cancel</Button>
54
+ {#if showSkipCrop}
55
+ <Button variant="outline" onclick={handleSkipCrop} disabled={processing}>
56
+ {processing ? 'Processing...' : skipLabel}
57
+ </Button>
58
+ {/if}
59
+ <Button onclick={handleCrop} disabled={processing}>
60
+ {processing ? 'Processing...' : confirmLabel}
61
+ </Button>
62
+ {/snippet}
63
+ </Dialog>
@@ -0,0 +1,4 @@
1
+ import type { ImageCropDialogProps } from './types';
2
+ declare const ImageCropDialog: import("svelte").Component<ImageCropDialogProps, {}, "open">;
3
+ type ImageCropDialog = ReturnType<typeof ImageCropDialog>;
4
+ export default ImageCropDialog;
@@ -0,0 +1,25 @@
1
+ import type { ProcessImageOptions } from '../../utils/image-processing';
2
+ export interface ImageCropDialogProps {
3
+ /** Controls dialog open state */
4
+ open: boolean;
5
+ /** Source image (data URL, blob URL, or regular URL) */
6
+ imageSrc: string;
7
+ /** Called with the processed data URL on crop or skip */
8
+ onConfirm: (processedDataUrl: string) => void;
9
+ /** Called when the user cancels (Cancel button or clicking outside/Escape) */
10
+ onCancel: () => void;
11
+ /** Dialog title */
12
+ title?: string;
13
+ /** Dialog description */
14
+ description?: string;
15
+ /** Default aspect ratio for the cropper (undefined = free) */
16
+ aspectRatio?: number;
17
+ /** Processing options applied to both crop and skip paths */
18
+ processOptions?: ProcessImageOptions;
19
+ /** Whether to show the "Skip Crop" button (default: true) */
20
+ showSkipCrop?: boolean;
21
+ /** Label for the confirm button */
22
+ confirmLabel?: string;
23
+ /** Label for the skip button */
24
+ skipLabel?: string;
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -2,6 +2,8 @@
2
2
  import { ImageCropper, useImageCropper } from '@ark-ui/svelte/image-cropper';
3
3
  import type { ImageCropperProps } from './types';
4
4
  import { Button } from '../..';
5
+ import { cropImage } from '../../utils/image-processing';
6
+ import type { ProcessImageOptions } from '../../utils/image-processing';
5
7
 
6
8
  let {
7
9
  src,
@@ -11,6 +13,8 @@
11
13
  getCroppedImage = $bindable(),
12
14
  // eslint-disable-next-line no-useless-assignment
13
15
  getCropData = $bindable(),
16
+ // eslint-disable-next-line no-useless-assignment
17
+ getProcessedImage = $bindable(),
14
18
  ...cropperProps
15
19
  }: ImageCropperProps = $props();
16
20
 
@@ -43,6 +47,8 @@
43
47
  getCroppedImage = (options) => imageCropper().getCroppedImage(options);
44
48
  // eslint-disable-next-line no-useless-assignment
45
49
  getCropData = () => imageCropper().getCropData();
50
+ // eslint-disable-next-line no-useless-assignment
51
+ getProcessedImage = (opts?: ProcessImageOptions) => cropImage(src, imageCropper().getCropData(), opts);
46
52
 
47
53
  let cropData = $derived(imageCropper().getCropData());
48
54
  </script>
@@ -1,5 +1,5 @@
1
1
  import { ImageCropper } from '@ark-ui/svelte/image-cropper';
2
2
  import type { ImageCropperProps } from './types';
3
- declare const ImageCropper: import("svelte").Component<ImageCropperProps, {}, "getCropData" | "aspectRatio" | "getCroppedImage">;
3
+ declare const ImageCropper: import("svelte").Component<ImageCropperProps, {}, "getCropData" | "aspectRatio" | "getCroppedImage" | "getProcessedImage">;
4
4
  type ImageCropper = ReturnType<typeof ImageCropper>;
5
5
  export default ImageCropper;
@@ -1,8 +1,11 @@
1
1
  import type { UseImageCropperProps, UseImageCropperReturn } from '@ark-ui/svelte/image-cropper';
2
+ import type { ProcessImageOptions } from '../../utils/image-processing';
2
3
  export type ImageCropperCropData = ReturnType<ReturnType<UseImageCropperReturn>['getCropData']>;
3
4
  export interface ImageCropperProps extends UseImageCropperProps {
4
5
  src: string;
5
6
  alt?: string;
6
7
  getCroppedImage?: ReturnType<UseImageCropperReturn>['getCroppedImage'];
7
8
  getCropData?: ReturnType<UseImageCropperReturn>['getCropData'];
9
+ /** Bindable: returns a processed (cropped + resized + converted) data URL in one call */
10
+ getProcessedImage?: (opts?: ProcessImageOptions) => Promise<string>;
8
11
  }
package/dist/index.d.ts CHANGED
@@ -1,12 +1,20 @@
1
1
  export { default as Button } from './components/button/button.svelte';
2
+ export { loadImage, fileToDataUrl, cropImage, processImage } from './utils/image-processing';
3
+ export type { ProcessImageOptions, CropRegion } from './utils/image-processing';
2
4
  export { default as Carousel } from './components/carousel/carousel.svelte';
3
5
  export { default as Checkbox } from './components/checkbox/checkbox.svelte';
4
6
  export { default as CheckboxGroup } from './components/checkbox/checkbox-group.svelte';
5
7
  export { default as Combobox } from './components/combobox/combobox.svelte';
6
8
  export { default as Dialog } from './components/dialog/dialog.svelte';
7
9
  export { default as AlertDialog } from './components/dialog/alert-dialog.svelte';
10
+ export type { DialogProps, AlertDialogProps } from './components/dialog/dialog.types';
11
+ export { default as FileUploadDropzone } from './components/file-upload/dropzone.svelte';
12
+ export { default as FileUploadBasicDocument } from './components/file-upload/basic-document.svelte';
13
+ export type { FileType } from './components/file-upload/utils';
8
14
  export { default as ImageCropper } from './components/image-cropper/image-cropper.svelte';
9
15
  export type { ImageCropperProps, ImageCropperCropData } from './components/image-cropper/types';
16
+ export { default as ImageCropDialog } from './components/image-crop-dialog/image-crop-dialog.svelte';
17
+ export type { ImageCropDialogProps } from './components/image-crop-dialog/types';
10
18
  export { default as Listbox } from './components/listbox/listbox.svelte';
11
19
  export { default as NumberInput } from './components/number-input/number-input.svelte';
12
20
  export type { NumberInputProps } from './components/number-input/types';
package/dist/index.js CHANGED
@@ -1,12 +1,16 @@
1
1
  // Reexport your entry components here
2
2
  export { default as Button } from './components/button/button.svelte';
3
+ export { loadImage, fileToDataUrl, cropImage, processImage } from './utils/image-processing';
3
4
  export { default as Carousel } from './components/carousel/carousel.svelte';
4
5
  export { default as Checkbox } from './components/checkbox/checkbox.svelte';
5
6
  export { default as CheckboxGroup } from './components/checkbox/checkbox-group.svelte';
6
7
  export { default as Combobox } from './components/combobox/combobox.svelte';
7
8
  export { default as Dialog } from './components/dialog/dialog.svelte';
8
9
  export { default as AlertDialog } from './components/dialog/alert-dialog.svelte';
10
+ export { default as FileUploadDropzone } from './components/file-upload/dropzone.svelte';
11
+ export { default as FileUploadBasicDocument } from './components/file-upload/basic-document.svelte';
9
12
  export { default as ImageCropper } from './components/image-cropper/image-cropper.svelte';
13
+ export { default as ImageCropDialog } from './components/image-crop-dialog/image-crop-dialog.svelte';
10
14
  export { default as Listbox } from './components/listbox/listbox.svelte';
11
15
  export { default as NumberInput } from './components/number-input/number-input.svelte';
12
16
  export { default as Select } from './components/select/select.svelte';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Client-side image processing utilities using the Canvas API.
3
+ * These are browser-only utilities — they will throw if called during SSR.
4
+ */
5
+ export interface ProcessImageOptions {
6
+ maxWidth?: number;
7
+ maxHeight?: number;
8
+ quality?: number;
9
+ format?: 'image/webp' | 'image/jpeg' | 'image/png';
10
+ }
11
+ export interface CropRegion {
12
+ x: number;
13
+ y: number;
14
+ width: number;
15
+ height: number;
16
+ }
17
+ /** Load an image element from a src URL (data URL, blob URL, or regular URL) */
18
+ export declare function loadImage(src: string): Promise<HTMLImageElement>;
19
+ /** Convert a File to a base64 data URL */
20
+ export declare function fileToDataUrl(file: File): Promise<string>;
21
+ /**
22
+ * Crop a full-resolution source image using natural pixel coordinates, then resize and convert.
23
+ * Use this instead of getCroppedImage() from Ark UI which outputs at CSS/display resolution.
24
+ */
25
+ export declare function cropImage(src: string, crop: CropRegion, opts?: ProcessImageOptions): Promise<string>;
26
+ /** Resize and convert an image without cropping, returns a base64 data URL */
27
+ export declare function processImage(src: string, opts?: ProcessImageOptions): Promise<string>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Client-side image processing utilities using the Canvas API.
3
+ * These are browser-only utilities — they will throw if called during SSR.
4
+ */
5
+ const defaults = {
6
+ maxWidth: 1000,
7
+ maxHeight: 1000,
8
+ quality: 0.85,
9
+ format: 'image/webp'
10
+ };
11
+ /** Load an image element from a src URL (data URL, blob URL, or regular URL) */
12
+ export function loadImage(src) {
13
+ return new Promise((resolve, reject) => {
14
+ const img = new Image();
15
+ img.crossOrigin = 'anonymous';
16
+ img.onload = () => resolve(img);
17
+ img.onerror = () => reject(new Error('Failed to load image'));
18
+ img.src = src;
19
+ });
20
+ }
21
+ /** Convert a File to a base64 data URL */
22
+ export function fileToDataUrl(file) {
23
+ return new Promise((resolve, reject) => {
24
+ const reader = new FileReader();
25
+ reader.onload = () => resolve(reader.result);
26
+ reader.onerror = () => reject(new Error('Failed to read file'));
27
+ reader.readAsDataURL(file);
28
+ });
29
+ }
30
+ /**
31
+ * Crop a full-resolution source image using natural pixel coordinates, then resize and convert.
32
+ * Use this instead of getCroppedImage() from Ark UI which outputs at CSS/display resolution.
33
+ */
34
+ export async function cropImage(src, crop, opts) {
35
+ const { maxWidth, maxHeight, quality, format } = { ...defaults, ...opts };
36
+ const img = await loadImage(src);
37
+ let { width, height } = crop;
38
+ if (width > maxWidth || height > maxHeight) {
39
+ const ratio = Math.min(maxWidth / width, maxHeight / height);
40
+ width = Math.round(width * ratio);
41
+ height = Math.round(height * ratio);
42
+ }
43
+ const canvas = document.createElement('canvas');
44
+ canvas.width = width;
45
+ canvas.height = height;
46
+ const ctx = canvas.getContext('2d');
47
+ ctx.drawImage(img, crop.x, crop.y, crop.width, crop.height, 0, 0, width, height);
48
+ return canvas.toDataURL(format, quality);
49
+ }
50
+ /** Resize and convert an image without cropping, returns a base64 data URL */
51
+ export async function processImage(src, opts) {
52
+ const { maxWidth, maxHeight, quality, format } = { ...defaults, ...opts };
53
+ const img = await loadImage(src);
54
+ let { width, height } = img;
55
+ if (width > maxWidth || height > maxHeight) {
56
+ const ratio = Math.min(maxWidth / width, maxHeight / height);
57
+ width = Math.round(width * ratio);
58
+ height = Math.round(height * ratio);
59
+ }
60
+ const canvas = document.createElement('canvas');
61
+ canvas.width = width;
62
+ canvas.height = height;
63
+ const ctx = canvas.getContext('2d');
64
+ ctx.drawImage(img, 0, 0, width, height);
65
+ return canvas.toDataURL(format, quality);
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compote-ui",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",