compote-ui 0.5.7 → 0.7.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.
Files changed (30) hide show
  1. package/dist/components/dialog/alert-dialog.svelte +9 -15
  2. package/dist/components/dialog/alert-dialog.svelte.d.ts +2 -11
  3. package/dist/components/dialog/dialog.svelte +7 -16
  4. package/dist/components/dialog/dialog.svelte.d.ts +2 -11
  5. package/dist/components/dialog/dialog.types.d.ts +24 -0
  6. package/dist/components/dialog/dialog.types.js +1 -0
  7. package/dist/components/file-upload/{dropzone.svelte → file-upload-dropzone.svelte} +2 -2
  8. package/dist/components/file-upload/file-upload-dropzone.svelte.d.ts +4 -0
  9. package/dist/components/file-upload/file-upload.svelte +90 -0
  10. package/dist/components/file-upload/file-upload.svelte.d.ts +5 -0
  11. package/dist/components/file-upload/types.d.ts +7 -1
  12. package/dist/components/image-crop-dialog/image-crop-dialog.svelte +70 -0
  13. package/dist/components/image-crop-dialog/image-crop-dialog.svelte.d.ts +4 -0
  14. package/dist/components/image-crop-dialog/types.d.ts +25 -0
  15. package/dist/components/image-crop-dialog/types.js +1 -0
  16. package/dist/components/image-cropper/image-cropper.svelte +7 -0
  17. package/dist/components/image-cropper/image-cropper.svelte.d.ts +1 -1
  18. package/dist/components/image-cropper/types.d.ts +3 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.js +4 -0
  21. package/dist/utils/image-processing.d.ts +27 -0
  22. package/dist/utils/image-processing.js +66 -0
  23. package/package.json +1 -1
  24. package/dist/components/file-upload/basic-document.svelte +0 -89
  25. package/dist/components/file-upload/basic-document.svelte.d.ts +0 -4
  26. package/dist/components/file-upload/basic.svelte +0 -50
  27. package/dist/components/file-upload/basic.svelte.d.ts +0 -18
  28. package/dist/components/file-upload/dropzone.svelte.d.ts +0 -4
  29. package/dist/components/file-upload/files-list.svelte +0 -97
  30. package/dist/components/file-upload/files-list.svelte.d.ts +0 -18
@@ -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 {};
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { FileUpload } from '@ark-ui/svelte/file-upload';
3
3
  import { getAcceptAttribute } from './utils';
4
- import type { Props } from './types';
4
+ import type { FileUploadDropzoneProps } from './types';
5
5
  import PhUploadSimple from '../../icons/PhUploadSimple.svelte';
6
6
 
7
7
  let {
@@ -10,7 +10,7 @@
10
10
  maxFiles = 1,
11
11
  onFileAccept,
12
12
  ...restProps
13
- }: Props = $props();
13
+ }: FileUploadDropzoneProps = $props();
14
14
 
15
15
  const accept = $derived(getAcceptAttribute(fileType));
16
16
  </script>
@@ -0,0 +1,4 @@
1
+ import type { FileUploadDropzoneProps } from './types';
2
+ declare const FileUploadDropzone: import("svelte").Component<FileUploadDropzoneProps, {}, "">;
3
+ type FileUploadDropzone = ReturnType<typeof FileUploadDropzone>;
4
+ export default FileUploadDropzone;
@@ -0,0 +1,90 @@
1
+ <script lang="ts">
2
+ import { FileUpload, type FileUploadFileError } from '@ark-ui/svelte/file-upload';
3
+ import PhX from '../../icons/PhX.svelte';
4
+ import type { FileUploadProps } from './types';
5
+ import { getAcceptAttribute } from './utils';
6
+ import { getFileIcon } from './icons';
7
+
8
+ const errorMessages: Record<FileUploadFileError, string> = {
9
+ TOO_MANY_FILES: 'Too many files',
10
+ FILE_INVALID_TYPE: 'Invalid file type',
11
+ FILE_TOO_LARGE: 'File too large',
12
+ FILE_TOO_SMALL: 'File too small',
13
+ FILE_INVALID: 'Invalid file',
14
+ FILE_EXISTS: 'File already exists'
15
+ };
16
+
17
+ let {
18
+ fileType,
19
+ label,
20
+ triggerLabel = 'Choose file',
21
+ maxFiles = 1,
22
+ ...restProps
23
+ }: FileUploadProps = $props();
24
+
25
+ const accept = $derived(getAcceptAttribute(fileType));
26
+ </script>
27
+
28
+ <FileUpload.Root {maxFiles} {accept} class="flex flex-col gap-1.5" {...restProps}>
29
+ {#if label}
30
+ <FileUpload.Label class="text-sm font-medium">{label}</FileUpload.Label>
31
+ {/if}
32
+
33
+ <FileUpload.Context>
34
+ {#snippet render(context)}
35
+ {@const ctx = context()}
36
+ {@const accepted = ctx.acceptedFiles}
37
+ {@const rejected = ctx.rejectedFiles}
38
+
39
+ {#if maxFiles === 1}
40
+ {#if accepted.length === 0}
41
+ <FileUpload.Trigger
42
+ class="inline-flex h-9 cursor-pointer items-center gap-2 rounded border px-3 text-sm text-ink transition-colors hover:bg-surface-2 disabled:pointer-events-none disabled:opacity-50"
43
+ >
44
+ {triggerLabel}
45
+ </FileUpload.Trigger>
46
+ {/if}
47
+ {:else}
48
+ <FileUpload.Trigger
49
+ disabled={accepted.length >= maxFiles}
50
+ class="inline-flex h-9 cursor-pointer items-center gap-2 rounded border px-3 text-sm text-ink transition-colors hover:bg-surface-2 disabled:pointer-events-none disabled:opacity-50"
51
+ >
52
+ {triggerLabel}
53
+ </FileUpload.Trigger>
54
+ {/if}
55
+
56
+ <FileUpload.ItemGroup class="flex flex-col gap-1">
57
+ {#each accepted as file (file.name)}
58
+ <FileUpload.Item {file} class="flex items-center gap-2 rounded border px-3 py-2 text-sm">
59
+ <FileUpload.ItemPreview type="image/*">
60
+ <FileUpload.ItemPreviewImage class="size-8 shrink-0 rounded object-cover" />
61
+ </FileUpload.ItemPreview>
62
+ {#if !file.type.startsWith('image/')}
63
+ {@const Icon = getFileIcon(file)}
64
+ <Icon class="size-4 shrink-0" />
65
+ {/if}
66
+ <FileUpload.ItemName class="flex-1 truncate" />
67
+ <FileUpload.ItemSizeText class="shrink-0 text-xs text-ink-dim" />
68
+ <FileUpload.ItemDeleteTrigger
69
+ class="ml-auto shrink-0 cursor-pointer text-ink-dim transition-colors hover:text-ink"
70
+ >
71
+ <PhX class="size-4" />
72
+ </FileUpload.ItemDeleteTrigger>
73
+ </FileUpload.Item>
74
+ {/each}
75
+ </FileUpload.ItemGroup>
76
+
77
+ {#if rejected.length > 0}
78
+ <div class="mt-1 flex flex-col gap-0.5">
79
+ {#each rejected as rejection (rejection.file.name)}
80
+ <p class="text-xs text-red-600">
81
+ {rejection.file.name}: {rejection.errors.map((e) => errorMessages[e]).join(', ')}
82
+ </p>
83
+ {/each}
84
+ </div>
85
+ {/if}
86
+ {/snippet}
87
+ </FileUpload.Context>
88
+
89
+ <FileUpload.HiddenInput />
90
+ </FileUpload.Root>
@@ -0,0 +1,5 @@
1
+ import { FileUpload } from '@ark-ui/svelte/file-upload';
2
+ import type { FileUploadProps } from './types';
3
+ declare const FileUpload: import("svelte").Component<FileUploadProps, {}, "">;
4
+ type FileUpload = ReturnType<typeof FileUpload>;
5
+ export default FileUpload;
@@ -1,6 +1,12 @@
1
1
  import type { FileUploadRootProps } from '@ark-ui/svelte/file-upload';
2
2
  import type { FileType } from './utils';
3
- export interface Props extends FileUploadRootProps {
3
+ export interface FileUploadDropzoneProps extends FileUploadRootProps {
4
4
  fileType: FileType;
5
5
  label: string;
6
6
  }
7
+ export interface FileUploadProps extends Omit<FileUploadRootProps, 'accept'> {
8
+ fileType?: FileType;
9
+ label?: string;
10
+ triggerLabel?: string;
11
+ maxFiles?: number;
12
+ }
@@ -0,0 +1,70 @@
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
51
+ bind:open
52
+ {title}
53
+ {description}
54
+ onOpenChange={(details) => {
55
+ if (!details.open) onCancel();
56
+ }}
57
+ >
58
+ <ImageCropper bind:getCropData src={imageSrc} alt="Crop preview" {aspectRatio} />
59
+ {#snippet footer()}
60
+ <Button variant="outline" onclick={onCancel} disabled={processing}>Cancel</Button>
61
+ {#if showSkipCrop}
62
+ <Button variant="outline" onclick={handleSkipCrop} disabled={processing}>
63
+ {processing ? 'Processing...' : skipLabel}
64
+ </Button>
65
+ {/if}
66
+ <Button onclick={handleCrop} disabled={processing}>
67
+ {processing ? 'Processing...' : confirmLabel}
68
+ </Button>
69
+ {/snippet}
70
+ </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,9 @@
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) =>
52
+ cropImage(src, imageCropper().getCropData(), opts);
46
53
 
47
54
  let cropData = $derived(imageCropper().getCropData());
48
55
  </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,21 @@
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/file-upload-dropzone.svelte';
12
+ export { default as FileUpload } from './components/file-upload/file-upload.svelte';
13
+ export type { FileUploadProps } from './components/file-upload/types';
14
+ export type { FileType } from './components/file-upload/utils';
8
15
  export { default as ImageCropper } from './components/image-cropper/image-cropper.svelte';
9
16
  export type { ImageCropperProps, ImageCropperCropData } from './components/image-cropper/types';
17
+ export { default as ImageCropDialog } from './components/image-crop-dialog/image-crop-dialog.svelte';
18
+ export type { ImageCropDialogProps } from './components/image-crop-dialog/types';
10
19
  export { default as Listbox } from './components/listbox/listbox.svelte';
11
20
  export { default as NumberInput } from './components/number-input/number-input.svelte';
12
21
  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/file-upload-dropzone.svelte';
11
+ export { default as FileUpload } from './components/file-upload/file-upload.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.7.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -1,89 +0,0 @@
1
- <script lang="ts">
2
- import { FileUpload } from '@ark-ui/svelte/file-upload';
3
- import { Field } from '@ark-ui/svelte/field';
4
-
5
- import { getFileIcon } from './icons';
6
-
7
- import { getAcceptAttribute } from './utils';
8
- import type { Props } from './types';
9
- import PhFileText from '../../icons/PhFileText.svelte';
10
-
11
- let {
12
- fileType,
13
- acceptedFiles,
14
- onFileChange = $bindable(),
15
- label,
16
- ...restProps
17
- }: Props = $props();
18
- const accept = $derived(getAcceptAttribute(fileType));
19
- // const id = $props.id();
20
- </script>
21
-
22
- <!-- {id} -->
23
-
24
- <Field.Root class="w-full max-w-sm">
25
- <FileUpload.Root
26
- {...restProps}
27
- maxFiles={1}
28
- {accept}
29
- {acceptedFiles}
30
- {onFileChange}
31
- class="flex flex-col items-start gap-3"
32
- >
33
- {#if label}
34
- <Field.Label>
35
- {label}
36
- </Field.Label>
37
- {/if}
38
- <FileUpload.Context>
39
- {#snippet render(context)}
40
- <div class="flex items-center gap-3">
41
- <!-- Image Preview / Placeholder -->
42
- <div
43
- class="flex h-10 w-10 items-center justify-center overflow-hidden rounded-xl border bg-surface-2"
44
- >
45
- {#if context().acceptedFiles.length > 0}
46
- <FileUpload.ItemGroup>
47
- {#each context().acceptedFiles as file (file.name)}
48
- <FileUpload.Item file={context().acceptedFiles[0]}>
49
- {#if file.type.startsWith('image/')}
50
- <FileUpload.ItemPreview type="image/*">
51
- <FileUpload.ItemPreviewImage class="h-full w-full object-cover" />
52
- </FileUpload.ItemPreview>
53
- {:else}
54
- {@const IconComponent = getFileIcon(file)}
55
- <IconComponent />
56
- {/if}
57
- </FileUpload.Item>
58
- {/each}
59
- </FileUpload.ItemGroup>
60
- {:else}
61
- <PhFileText class="h-5 w-5 text-ink-dim" />
62
- {/if}
63
- </div>
64
-
65
- <!-- Upload/Change Button -->
66
- <FileUpload.Trigger
67
- class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:outline-hidden"
68
- >
69
- {context().acceptedFiles.length > 0 ? 'Change image' : 'Upload image'}
70
- </FileUpload.Trigger>
71
- </div>
72
-
73
- <!-- Filename and Remove -->
74
- {#if context().acceptedFiles.length > 0}
75
- <FileUpload.ItemGroup>
76
- <FileUpload.Item file={context().acceptedFiles[0]} class="flex items-center gap-2">
77
- <FileUpload.ItemName class="text-sm text-ink-dim" />
78
- <FileUpload.ItemDeleteTrigger class="text-sm text-red-500 hover:text-red-600">
79
- Remove
80
- </FileUpload.ItemDeleteTrigger>
81
- </FileUpload.Item>
82
- </FileUpload.ItemGroup>
83
- {/if}
84
- {/snippet}
85
- </FileUpload.Context>
86
-
87
- <FileUpload.HiddenInput />
88
- </FileUpload.Root>
89
- </Field.Root>
@@ -1,4 +0,0 @@
1
- import type { Props } from './types';
2
- declare const BasicDocument: import("svelte").Component<Props, {}, "onFileChange">;
3
- type BasicDocument = ReturnType<typeof BasicDocument>;
4
- export default BasicDocument;
@@ -1,50 +0,0 @@
1
- <script lang="ts">
2
- import PhUser from '../../icons/PhUser.svelte';
3
- import { FileUpload } from '@ark-ui/svelte/file-upload';
4
- </script>
5
-
6
- <FileUpload.Root maxFiles={1} accept="image/*" class="flex flex-col items-start gap-3">
7
- <FileUpload.Context>
8
- {#snippet render(context)}
9
- <div class="flex items-center gap-3">
10
- <!-- Image Preview / Placeholder -->
11
- <div
12
- class="flex h-10 w-10 items-center justify-center overflow-hidden rounded-xl border bg-surface-2"
13
- >
14
- {#if context().acceptedFiles.length > 0}
15
- <FileUpload.ItemGroup>
16
- <FileUpload.Item file={context().acceptedFiles[0]}>
17
- <FileUpload.ItemPreview type="image/*">
18
- <FileUpload.ItemPreviewImage class="h-full w-full object-cover" />
19
- </FileUpload.ItemPreview>
20
- </FileUpload.Item>
21
- </FileUpload.ItemGroup>
22
- {:else}
23
- <PhUser class="h-5 w-5 text-ink-dim" />
24
- {/if}
25
- </div>
26
-
27
- <!-- Upload/Change Button -->
28
- <FileUpload.Trigger
29
- class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:outline-hidden"
30
- >
31
- {context().acceptedFiles.length > 0 ? 'Change image' : 'Upload image'}
32
- </FileUpload.Trigger>
33
- </div>
34
-
35
- <!-- Filename and Remove -->
36
- {#if context().acceptedFiles.length > 0}
37
- <FileUpload.ItemGroup>
38
- <FileUpload.Item file={context().acceptedFiles[0]} class="flex items-center gap-2">
39
- <FileUpload.ItemName class="text-sm text-ink-dim" />
40
- <FileUpload.ItemDeleteTrigger class="text-sm text-red-500 hover:text-red-600">
41
- Remove
42
- </FileUpload.ItemDeleteTrigger>
43
- </FileUpload.Item>
44
- </FileUpload.ItemGroup>
45
- {/if}
46
- {/snippet}
47
- </FileUpload.Context>
48
-
49
- <FileUpload.HiddenInput />
50
- </FileUpload.Root>
@@ -1,18 +0,0 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const Basic: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type Basic = InstanceType<typeof Basic>;
18
- export default Basic;
@@ -1,4 +0,0 @@
1
- import type { Props } from './types';
2
- declare const Dropzone: import("svelte").Component<Props, {}, "">;
3
- type Dropzone = ReturnType<typeof Dropzone>;
4
- export default Dropzone;
@@ -1,97 +0,0 @@
1
- <script lang="ts">
2
- import { FileUpload } from '@ark-ui/svelte/file-upload';
3
- import { getFileIcon } from './icons';
4
- import PhFileText from '../../icons/PhFileText.svelte';
5
- import PhX from '../../icons/PhX.svelte';
6
-
7
- const defaultFiles = [
8
- new File(['Welcome to Ark UI Svelte`'], 'document.pdf', {
9
- type: 'text/plain'
10
- }),
11
- new File(['Welcome to Ark UI Svelte, this is a zip file`'], 'showcase.zip', {
12
- type: 'application/zip'
13
- }),
14
- new File(['Welcome to Ark UI Svelte, this is an excel file`'], 'data.xlsx', {
15
- type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
16
- })
17
- ];
18
- </script>
19
-
20
- <FileUpload.Root
21
- maxFiles={10}
22
- maxFileSize={100 * 1024 * 1024}
23
- class="w-full max-w-2xl space-y-4"
24
- defaultAcceptedFiles={defaultFiles}
25
- >
26
- <FileUpload.Context>
27
- {#snippet render(context)}
28
- <!-- Dropzone -->
29
- <FileUpload.Dropzone
30
- class="flex w-full cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-surface-3 bg-surface-2 px-6 py-12 transition-colors hover:bg-surface-3"
31
- >
32
- <!-- File Icon -->
33
- <div
34
- class="mb-4 flex h-12 w-12 items-center justify-center rounded-full border bg-surface-1"
35
- >
36
- <PhFileText class="h-5 w-5 text-ink-dim" />
37
- </div>
38
-
39
- <!-- Text -->
40
- <div class="space-y-2 text-center">
41
- <h3 class="text-sm font-medium text-ink">Upload files</h3>
42
- <p class="text-sm text-ink-dim">Drag & drop or click to browse</p>
43
- <p class="text-xs text-ink-dim">All files • Max 10 files • Up to 100MB</p>
44
- </div>
45
- </FileUpload.Dropzone>
46
-
47
- <!-- Files List -->
48
- {#if context().acceptedFiles.length > 0}
49
- <div class="space-y-3">
50
- <FileUpload.ItemGroup>
51
- {#each context().acceptedFiles as file (file.name)}
52
- <FileUpload.Item {file}>
53
- <div class="flex items-center gap-3 rounded-lg border bg-surface-1 p-3">
54
- <!-- File Icon/Preview -->
55
- <div
56
- class="flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg border bg-surface-2"
57
- >
58
- {#if file.type.startsWith('image/')}
59
- <FileUpload.ItemPreview type="image/*">
60
- <FileUpload.ItemPreviewImage class="h-full w-full object-cover" />
61
- </FileUpload.ItemPreview>
62
- {:else}
63
- {@const IconComponent = getFileIcon(file)}
64
- <IconComponent class="h-4 w-4 opacity-60" />
65
- {/if}
66
- </div>
67
-
68
- <!-- File Info -->
69
- <div class="min-w-0 flex-1">
70
- <FileUpload.ItemName class="truncate text-sm font-medium text-ink" />
71
- <FileUpload.ItemSizeText class="text-xs text-ink-dim" />
72
- </div>
73
-
74
- <!-- Delete Button -->
75
- <FileUpload.ItemDeleteTrigger
76
- class="flex h-6 w-6 shrink-0 items-center justify-center text-ink-dim hover:text-ink"
77
- >
78
- <PhX class="h-4 w-4" />
79
- </FileUpload.ItemDeleteTrigger>
80
- </div>
81
- </FileUpload.Item>
82
- {/each}
83
- </FileUpload.ItemGroup>
84
-
85
- <!-- Remove All Button -->
86
- <FileUpload.ClearTrigger
87
- class="inline-flex items-center rounded-md border bg-surface-1 px-3 py-1.5 text-xs font-medium text-ink hover:bg-surface-2 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:outline-hidden"
88
- >
89
- Remove all files
90
- </FileUpload.ClearTrigger>
91
- </div>
92
- {/if}
93
- {/snippet}
94
- </FileUpload.Context>
95
-
96
- <FileUpload.HiddenInput />
97
- </FileUpload.Root>
@@ -1,18 +0,0 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const FilesList: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type FilesList = InstanceType<typeof FilesList>;
18
- export default FilesList;