@minutemailer/kit 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/engagement-chart.js +1 -1
- package/components/image-slot.d.ts +5 -2
- package/components/image-slot.js +23 -7
- package/components/minutemailer-kit.d.ts +12 -3
- package/components/minutemailer-kit.js +10 -3
- package/components/more.d.ts +2 -0
- package/components/more.js +1 -1
- package/components/page-tabs.d.ts +12 -0
- package/components/page-tabs.js +8 -0
- package/components/s3-image.d.ts +16 -0
- package/components/s3-image.js +60 -0
- package/components/ui/accordion.js +1 -1
- package/components/ui/alert.js +2 -2
- package/components/ui/checkbox.js +1 -1
- package/components/ui/dialog.js +2 -2
- package/components/ui/sidebar.js +4 -4
- package/components/ui/tabs.d.ts +7 -0
- package/components/ui/tabs.js +13 -0
- package/components/view-title.d.ts +7 -0
- package/components/view-title.js +7 -0
- package/hooks/use-mobile.js +1 -1
- package/hooks/use-s3-image.d.ts +2 -0
- package/hooks/use-s3-image.js +7 -0
- package/icons/MinutemailerLogo.d.ts +3 -0
- package/icons/MinutemailerLogo.js +3 -0
- package/icons/index.d.ts +1 -0
- package/icons/index.js +1 -0
- package/package.json +3 -1
- package/utils/s3Image/index.d.ts +2 -0
- package/utils/s3Image/index.js +37 -0
- package/utils/s3Image/s3ImageDimensions.d.ts +2 -0
- package/utils/s3Image/s3ImageDimensions.js +7 -0
- package/utils/s3Image/s3ImageSrc.d.ts +2 -0
- package/utils/s3Image/s3ImageSrc.js +51 -0
- package/utils/s3Image/types.d.ts +22 -0
- package/utils/s3Image/types.js +66 -0
|
@@ -27,7 +27,7 @@ export function EngagementChart({ data = defaultData }) {
|
|
|
27
27
|
left: 0,
|
|
28
28
|
right: 0,
|
|
29
29
|
top: 12,
|
|
30
|
-
}, children: [_jsxs("defs", { children: [_jsxs("linearGradient", { id: "opensGradient", x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: "var(--color-opens)", stopOpacity: 0.3 }), _jsx("stop", { offset: "100%", stopColor: "var(--color-opens)", stopOpacity: 0 })] }), _jsxs("linearGradient", { id: "clicksGradient", x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: "var(--color-clicks)", stopOpacity: 0.3 }), _jsx("stop", { offset: "100%", stopColor: "var(--color-clicks)", stopOpacity: 0 })] })] }), _jsx(CartesianGrid, { vertical: false }), _jsx(XAxis, { dataKey: "hour", tickLine: false, axisLine: false, tickMargin: 8 }), _jsx(YAxis, { tickLine: false, axisLine: false, tickMargin: 8 }), _jsx(Area, { dataKey: "opens", type: "linear", fill: "url(#opensGradient)", stroke: "var(--color-opens)", strokeWidth: 2, dot: false, activeDot: {
|
|
30
|
+
}, children: [_jsxs("defs", { children: [_jsxs("linearGradient", { id: "opensGradient", x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: "var(--color-opens)", stopOpacity: 0.3 }), _jsx("stop", { offset: "100%", stopColor: "var(--color-opens)", stopOpacity: 0 })] }), _jsxs("linearGradient", { id: "clicksGradient", x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: "var(--color-clicks)", stopOpacity: 0.3 }), _jsx("stop", { offset: "100%", stopColor: "var(--color-clicks)", stopOpacity: 0 })] })] }), _jsx(CartesianGrid, { vertical: false, strokeOpacity: 0.2 }), _jsx(XAxis, { dataKey: "hour", tickLine: false, axisLine: false, tickMargin: 8 }), _jsx(YAxis, { tickLine: false, axisLine: false, tickMargin: 8 }), _jsx(Area, { dataKey: "opens", type: "linear", fill: "url(#opensGradient)", stroke: "var(--color-opens)", strokeWidth: 2, dot: false, activeDot: {
|
|
31
31
|
r: 4,
|
|
32
32
|
fill: 'var(--color-opens)',
|
|
33
33
|
stroke: 'var(--color-opens)',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
1
|
+
import { type CSSProperties, type ReactNode } from 'react';
|
|
2
|
+
import { type S3ImageProps } from '../components/s3-image.js';
|
|
2
3
|
export interface ImageSlotAction {
|
|
3
4
|
name: string;
|
|
4
5
|
value: string;
|
|
@@ -7,6 +8,7 @@ export interface ImageSlotAction {
|
|
|
7
8
|
}
|
|
8
9
|
export interface ImageSlotProps {
|
|
9
10
|
src?: string;
|
|
11
|
+
s3?: Omit<S3ImageProps, 'width' | 'height'>;
|
|
10
12
|
alt?: string;
|
|
11
13
|
width: number;
|
|
12
14
|
height: number;
|
|
@@ -17,6 +19,7 @@ export interface ImageSlotProps {
|
|
|
17
19
|
loading?: boolean;
|
|
18
20
|
accept?: string[];
|
|
19
21
|
className?: string;
|
|
22
|
+
style?: CSSProperties;
|
|
20
23
|
}
|
|
21
|
-
declare function ImageSlot({ src, alt, width, height, actions, onAction, onRemove, onFile, loading, accept, className, }: ImageSlotProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
declare function ImageSlot({ src, s3, alt, width, height, actions, onAction, onRemove, onFile, loading, accept, className, style, }: ImageSlotProps): import("react/jsx-runtime").JSX.Element;
|
|
22
25
|
export { ImageSlot };
|
package/components/image-slot.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
3
|
-
import { useState } from 'react';
|
|
2
|
+
import { useRef, useState, } from 'react';
|
|
4
3
|
import { cn } from '../utils/utils.js';
|
|
5
4
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../components/ui/dropdown-menu.js';
|
|
6
5
|
import { Button } from '../components/ui/button.js';
|
|
7
6
|
import { Spinner } from '../components/ui/spinner.js';
|
|
8
7
|
import { toast } from 'sonner';
|
|
9
8
|
import { ChevronDownIcon, ImageIcon, Trash2Icon } from 'lucide-react';
|
|
10
|
-
|
|
9
|
+
import { S3Image } from '../components/s3-image.js';
|
|
10
|
+
function ImageSlot({ src, s3, alt = '', width, height, actions = [], onAction, onRemove, onFile, loading = false, accept = ['image/gif', 'image/png', 'image/jpeg'], className, style, }) {
|
|
11
11
|
const aspectRatio = width / height;
|
|
12
12
|
const [dragState, setDragState] = useState('idle');
|
|
13
|
-
const dragCounterRef =
|
|
14
|
-
const fileInputRef =
|
|
13
|
+
const dragCounterRef = useRef(0);
|
|
14
|
+
const fileInputRef = useRef(null);
|
|
15
|
+
const [s3ImageData, setS3ImageData] = useState(null);
|
|
15
16
|
const handleAction = (action) => {
|
|
16
17
|
onAction?.(action);
|
|
17
18
|
};
|
|
@@ -78,8 +79,22 @@ function ImageSlot({ src, alt = '', width, height, actions = [], onAction, onRem
|
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
};
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
const onS3ImageLoaded = (result) => {
|
|
83
|
+
s3?.onLoaded?.(result);
|
|
84
|
+
setS3ImageData(result);
|
|
85
|
+
};
|
|
86
|
+
const handleRemove = () => {
|
|
87
|
+
onRemove?.();
|
|
88
|
+
setS3ImageData(null);
|
|
89
|
+
};
|
|
90
|
+
const containerWidth = s3ImageData ? s3ImageData.width : width;
|
|
91
|
+
const containerHeight = s3ImageData ? s3ImageData.height : height;
|
|
92
|
+
if (src || s3) {
|
|
93
|
+
return (_jsxs("div", { className: cn('relative group', className), style: {
|
|
94
|
+
width: containerWidth,
|
|
95
|
+
height: containerHeight,
|
|
96
|
+
...style,
|
|
97
|
+
}, children: [s3 ? (_jsx(S3Image, { ...s3, width: width, height: height, onLoaded: onS3ImageLoaded, alt: alt, className: "block w-full h-full object-cover" })) : (_jsx("img", { src: src, alt: alt, width: width, height: height, className: "block w-full h-full object-cover" })), onRemove && (_jsx("div", { className: "absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center", children: _jsx(Button, { variant: "outline", className: "rounded-full", size: "icon", onClick: handleRemove, children: _jsx(Trash2Icon, {}) }) }))] }));
|
|
83
98
|
}
|
|
84
99
|
// Placeholder view
|
|
85
100
|
return (_jsx("div", { "data-slot": "image-slot", className: cn('bg-muted flex items-center justify-center @container transition-colors', dragState === 'over' && 'bg-green-100 dark:bg-green-950', className), style: {
|
|
@@ -87,6 +102,7 @@ function ImageSlot({ src, alt = '', width, height, actions = [], onAction, onRem
|
|
|
87
102
|
maxHeight: height,
|
|
88
103
|
maxWidth: '100%',
|
|
89
104
|
height: width / aspectRatio,
|
|
105
|
+
...style,
|
|
90
106
|
}, onDragEnter: handleDragEnter, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: loading ? (_jsx(Spinner, { className: "size-8" })) : dragState === 'over' ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Drop image to upload" })) : (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileInputRef, type: "file", accept: accept.join(','), onChange: handleFileInputChange, className: "hidden" }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(ImageIcon, { className: "@xs:hidden" }), _jsx("span", { className: "hidden @xs:inline", children: "Add image" }), _jsx(ChevronDownIcon, { className: "hidden @xs:inline" })] }) }), _jsx(DropdownMenuContent, { children: actions.map((action) => (_jsxs(DropdownMenuItem, { onClick: () => handleActionClick(action), children: [action.icon, action.name] }, action.value))) })] })] })) }));
|
|
91
107
|
}
|
|
92
108
|
export { ImageSlot };
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
|
|
2
|
+
type S3Config = {
|
|
3
|
+
bucket: string;
|
|
4
|
+
cloudFrontUrl: string;
|
|
5
|
+
resizeCloudUrl: string;
|
|
6
|
+
};
|
|
7
|
+
type ContextValue = {
|
|
3
8
|
t: (key: string) => string;
|
|
4
|
-
|
|
9
|
+
s3?: S3Config;
|
|
10
|
+
};
|
|
11
|
+
export declare const Context: import("react").Context<ContextValue>;
|
|
5
12
|
type MinutemailerKitProps = {
|
|
6
13
|
children: ReactNode;
|
|
7
14
|
t: (key: string) => string;
|
|
15
|
+
s3?: S3Config;
|
|
8
16
|
};
|
|
9
|
-
export declare function MinutemailerKit({ children, t }: MinutemailerKitProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export declare function MinutemailerKit({ children, t, s3 }: MinutemailerKitProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export declare function useS3Config(): S3Config;
|
|
10
19
|
export {};
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext } from 'react';
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
3
|
export const Context = createContext({
|
|
4
4
|
t: (key) => key,
|
|
5
5
|
});
|
|
6
|
-
export function MinutemailerKit({ children, t }) {
|
|
7
|
-
return _jsx(Context.Provider, { value: { t }, children: children });
|
|
6
|
+
export function MinutemailerKit({ children, t, s3 }) {
|
|
7
|
+
return _jsx(Context.Provider, { value: { t, s3 }, children: children });
|
|
8
|
+
}
|
|
9
|
+
export function useS3Config() {
|
|
10
|
+
const context = useContext(Context);
|
|
11
|
+
if (!context.s3) {
|
|
12
|
+
throw new Error('S3 configuration not provided in MinutemailerKit');
|
|
13
|
+
}
|
|
14
|
+
return context.s3;
|
|
8
15
|
}
|
package/components/more.d.ts
CHANGED
package/components/more.js
CHANGED
|
@@ -10,5 +10,5 @@ export function More({ options, loading = false, onChange, disabled = false, })
|
|
|
10
10
|
onChange(value);
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", disabled: disabled || loading, "aria-label": "More options", children: loading ? _jsx(Spinner, {}) : _jsx(MoreIcon, {}) }) }), _jsx(DropdownMenuContent, { align: "end", children: options.map((option, index) => (_jsxs(React.Fragment, { children: [option.separator && _jsx(DropdownMenuSeparator, {}),
|
|
13
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", disabled: disabled || loading, "aria-label": "More options", children: loading ? _jsx(Spinner, {}) : _jsx(MoreIcon, {}) }) }), _jsx(DropdownMenuContent, { align: "end", children: options.map((option, index) => (_jsxs(React.Fragment, { children: [option.separator && _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "py-2.5", variant: option.variant, disabled: option.disabled, onSelect: () => handleSelect(option.value), children: [option.icon && _jsx(option.icon, {}), option.name] })] }, option.value))) })] }));
|
|
14
14
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface PageTab {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PageTabsProps {
|
|
7
|
+
tabs: PageTab[];
|
|
8
|
+
activeTab: string;
|
|
9
|
+
onTabChange?: (tabId: string) => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function PageTabs({ tabs, activeTab, onTabChange, className, }: PageTabsProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from '../components/ui/button.js';
|
|
3
|
+
import { cn } from '../utils/utils.js';
|
|
4
|
+
export function PageTabs({ tabs, activeTab, onTabChange, className, }) {
|
|
5
|
+
return (_jsx("div", { className: cn('flex items-center border-b bg-background px-6', className), children: _jsx("div", { className: "flex h-12 items-center gap-6", children: tabs.map((tab) => (_jsx(Button, { variant: "ghost", className: cn('h-12 rounded-none border-b-2 px-2 font-medium hover:bg-transparent hover:text-foreground text-base', activeTab === tab.id
|
|
6
|
+
? 'border-black text-foreground'
|
|
7
|
+
: 'border-transparent text-muted-foreground'), onClick: () => onTabChange?.(tab.id), children: tab.label }, tab.id))) }) }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ImgHTMLAttributes } from 'react';
|
|
2
|
+
import type { FitOptions, ImageEdits, OverlayOptions, S3ImageResult } from '../utils/s3Image/types.js';
|
|
3
|
+
export interface S3ImageProps extends ImgHTMLAttributes<HTMLImageElement> {
|
|
4
|
+
imageKey: string;
|
|
5
|
+
width: number | string | 'auto';
|
|
6
|
+
height?: number | string | 'auto';
|
|
7
|
+
fit?: FitOptions;
|
|
8
|
+
retina?: boolean;
|
|
9
|
+
overlayWith?: OverlayOptions;
|
|
10
|
+
edits?: ImageEdits;
|
|
11
|
+
containerClassName?: string;
|
|
12
|
+
onLoaded?: (result: S3ImageResult) => void;
|
|
13
|
+
onError?: () => void;
|
|
14
|
+
}
|
|
15
|
+
declare function S3Image({ imageKey, width, height, fit, retina, overlayWith, edits, containerClassName, className, alt, onLoaded, onError, ...imgProps }: S3ImageProps): import("react/jsx-runtime").JSX.Element | null;
|
|
16
|
+
export { S3Image };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { cn } from '../utils/utils.js';
|
|
4
|
+
import { Spinner } from '../components/ui/spinner.js';
|
|
5
|
+
import { useS3Image } from '../hooks/use-s3-image.js';
|
|
6
|
+
function S3Image({ imageKey, width, height = 0, fit = 'inside', retina = true, overlayWith, edits = {}, containerClassName, className, alt = '', onLoaded, onError, ...imgProps }) {
|
|
7
|
+
const s3Image = useS3Image();
|
|
8
|
+
const [imageData, setImageData] = useState(null);
|
|
9
|
+
const [loading, setLoading] = useState(true);
|
|
10
|
+
const [error, setError] = useState(false);
|
|
11
|
+
// Calculate container dimensions
|
|
12
|
+
const containerWidth = width === 'auto'
|
|
13
|
+
? undefined
|
|
14
|
+
: typeof width === 'string'
|
|
15
|
+
? parseInt(width, 10)
|
|
16
|
+
: width;
|
|
17
|
+
const containerHeight = height === 'auto'
|
|
18
|
+
? undefined
|
|
19
|
+
: typeof height === 'string'
|
|
20
|
+
? parseInt(height, 10)
|
|
21
|
+
: height;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!imageKey) {
|
|
24
|
+
setLoading(false);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
setLoading(true);
|
|
28
|
+
setError(false);
|
|
29
|
+
const imageLoader = s3Image(imageKey, width, height, fit, retina, overlayWith, edits);
|
|
30
|
+
imageLoader()
|
|
31
|
+
.then((result) => {
|
|
32
|
+
setImageData(result);
|
|
33
|
+
setLoading(false);
|
|
34
|
+
onLoaded?.(result);
|
|
35
|
+
})
|
|
36
|
+
.catch(() => {
|
|
37
|
+
setError(true);
|
|
38
|
+
setLoading(false);
|
|
39
|
+
onError?.();
|
|
40
|
+
});
|
|
41
|
+
}, [
|
|
42
|
+
imageKey,
|
|
43
|
+
width,
|
|
44
|
+
height,
|
|
45
|
+
fit,
|
|
46
|
+
retina,
|
|
47
|
+
JSON.stringify(overlayWith),
|
|
48
|
+
JSON.stringify(edits),
|
|
49
|
+
s3Image,
|
|
50
|
+
]);
|
|
51
|
+
const containerStyle = {
|
|
52
|
+
width: imageData ? imageData.width : containerWidth || 'auto',
|
|
53
|
+
height: imageData ? imageData.height : containerHeight || 'auto',
|
|
54
|
+
};
|
|
55
|
+
if (!imageKey) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return (_jsxs("div", { className: cn('relative inline-block', containerClassName), style: containerStyle, children: [loading && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-muted", children: _jsx(Spinner, { className: "size-6" }) })), error && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-muted text-muted-foreground text-sm", children: "Failed to load image" })), imageData && !loading && (_jsx("img", { src: imageData.src, alt: alt, width: imageData.width, height: imageData.height, loading: "lazy", className: cn(className), ...imgProps }))] }));
|
|
59
|
+
}
|
|
60
|
+
export { S3Image };
|
|
@@ -12,6 +12,6 @@ function AccordionTrigger({ className, children, ...props }) {
|
|
|
12
12
|
return (_jsx(AccordionPrimitive.Header, { className: "flex", "data-slot": "accordion-header", children: _jsxs(AccordionPrimitive.Trigger, { "data-slot": "accordion-trigger", className: cn('flex flex-1 cursor-pointer items-center justify-between rounded-md px-4 py-5 font-medium transition-all hover:bg-accent/50 [&[data-state=open]>svg]:rotate-180', className), ...props, children: [children, _jsx(ChevronDown, { className: "h-4 w-4 shrink-0 transition-transform duration-200" })] }) }));
|
|
13
13
|
}
|
|
14
14
|
function AccordionContent({ className, children, ...props }) {
|
|
15
|
-
return (_jsx(AccordionPrimitive.Content, { "data-slot": "accordion-content", className: "data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm transition-all", ...props, children: _jsx("div", { className: cn('pb-4 pt-0', className), children: children }) }));
|
|
15
|
+
return (_jsx(AccordionPrimitive.Content, { "data-slot": "accordion-content", className: "data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden px-4 py-4 text-sm transition-all", ...props, children: _jsx("div", { className: cn('pb-4 pt-0', className), children: children }) }));
|
|
16
16
|
}
|
|
17
17
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
package/components/ui/alert.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cva } from 'class-variance-authority';
|
|
3
3
|
import { cn } from '../../utils/utils.js';
|
|
4
|
-
const alertVariants = cva('relative w-full rounded-lg border px-4 py-
|
|
4
|
+
const alertVariants = cva('relative w-full rounded-lg border px-4 py-4 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', {
|
|
5
5
|
variants: {
|
|
6
6
|
variant: {
|
|
7
7
|
default: 'bg-card text-card-foreground',
|
|
8
|
-
destructive: 'text-destructive bg-
|
|
8
|
+
destructive: 'text-destructive bg-destructive/5 border-destructive/20 [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
|
|
9
9
|
},
|
|
10
10
|
},
|
|
11
11
|
defaultVariants: {
|
|
@@ -4,6 +4,6 @@ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
|
4
4
|
import { CheckIcon } from 'lucide-react';
|
|
5
5
|
import { cn } from '../../utils/utils.js';
|
|
6
6
|
function Checkbox({ className, ...props }) {
|
|
7
|
-
return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn('peer border-
|
|
7
|
+
return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn('peer border-black/20 cursor-pointer hover:border-foreground/40 dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border-2 shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "flex items-center justify-center transition-none", children: _jsx(CheckIcon, { className: "size-3.5 !text-white" }) }) }));
|
|
8
8
|
}
|
|
9
9
|
export { Checkbox };
|
package/components/ui/dialog.js
CHANGED
|
@@ -18,7 +18,7 @@ function DialogOverlay({ className, ...props }) {
|
|
|
18
18
|
return (_jsx(DialogPrimitive.Overlay, { "data-slot": "dialog-overlay", className: cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50', className), ...props }));
|
|
19
19
|
}
|
|
20
20
|
function DialogContent({ className, children, showCloseButton = true, ...props }) {
|
|
21
|
-
return (_jsxs(DialogPortal, { "data-slot": "dialog-portal", children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", className: cn('bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-
|
|
21
|
+
return (_jsxs(DialogPortal, { "data-slot": "dialog-portal", children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", className: cn('bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-8 shadow-lg duration-200 sm:max-w-lg', className), ...props, children: [children, showCloseButton && (_jsxs(DialogPrimitive.Close, { "data-slot": "dialog-close", className: "ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }))] })] }));
|
|
22
22
|
}
|
|
23
23
|
function DialogHeader({ className, ...props }) {
|
|
24
24
|
return (_jsx("div", { "data-slot": "dialog-header", className: cn('flex flex-col gap-2 text-center sm:text-left', className), ...props }));
|
|
@@ -27,7 +27,7 @@ function DialogFooter({ className, ...props }) {
|
|
|
27
27
|
return (_jsx("div", { "data-slot": "dialog-footer", className: cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className), ...props }));
|
|
28
28
|
}
|
|
29
29
|
function DialogTitle({ className, ...props }) {
|
|
30
|
-
return (_jsx(DialogPrimitive.Title, { "data-slot": "dialog-title", className: cn('text-lg leading-none font-semibold', className), ...props }));
|
|
30
|
+
return (_jsx(DialogPrimitive.Title, { "data-slot": "dialog-title", className: cn('text-lg leading-none font-semibold mb-4', className), ...props }));
|
|
31
31
|
}
|
|
32
32
|
function DialogDescription({ className, ...props }) {
|
|
33
33
|
return (_jsx(DialogPrimitive.Description, { "data-slot": "dialog-description", className: cn('text-muted-foreground text-sm', className), ...props }));
|
package/components/ui/sidebar.js
CHANGED
|
@@ -16,7 +16,7 @@ const SIDEBAR_COOKIE_NAME = 'sidebar_state';
|
|
|
16
16
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|
17
17
|
const SIDEBAR_WIDTH = '16rem';
|
|
18
18
|
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
|
19
|
-
const SIDEBAR_WIDTH_ICON = '
|
|
19
|
+
const SIDEBAR_WIDTH_ICON = '4.5rem';
|
|
20
20
|
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
|
21
21
|
const SidebarContext = React.createContext(null);
|
|
22
22
|
function useSidebar() {
|
|
@@ -98,9 +98,9 @@ function Sidebar({ side = 'left', variant = 'sidebar', collapsible = 'offcanvas'
|
|
|
98
98
|
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
|
99
99
|
}, side: side, children: [_jsxs(SheetHeader, { className: "sr-only", children: [_jsx(SheetTitle, { children: "Sidebar" }), _jsx(SheetDescription, { children: "Displays the mobile sidebar." })] }), _jsx("div", { className: "flex h-full w-full flex-col", children: children })] }) }));
|
|
100
100
|
}
|
|
101
|
-
return (_jsxs("div", { className: "group peer text-sidebar-foreground hidden
|
|
101
|
+
return (_jsxs("div", { className: "group peer text-sidebar-foreground hidden sm:block", "data-state": state, "data-collapsible": state === 'collapsed' ? collapsible : '', "data-variant": variant, "data-side": side, "data-slot": "sidebar", children: [_jsx("div", { "data-slot": "sidebar-gap", className: cn('relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear', 'group-data-[collapsible=offcanvas]:w-0', 'group-data-[side=right]:rotate-180', variant === 'floating' || variant === 'inset'
|
|
102
102
|
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
|
103
|
-
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)') }), _jsx("div", { "data-slot": "sidebar-container", className: cn('fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear
|
|
103
|
+
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)') }), _jsx("div", { "data-slot": "sidebar-container", className: cn('fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear sm:flex', side === 'left'
|
|
104
104
|
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
|
105
105
|
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
|
106
106
|
// Adjust the padding for floating and inset variants.
|
|
@@ -159,7 +159,7 @@ function SidebarMenu({ className, ...props }) {
|
|
|
159
159
|
function SidebarMenuItem({ className, ...props }) {
|
|
160
160
|
return (_jsx("li", { "data-slot": "sidebar-menu-item", "data-sidebar": "menu-item", className: cn('group/menu-item relative', className), ...props }));
|
|
161
161
|
}
|
|
162
|
-
const sidebarMenuButtonVariants = cva('peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', {
|
|
162
|
+
const sidebarMenuButtonVariants = cva('peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:mx-auto! group-data-[collapsible=icon]:p-2! group-data-[collapsible=icon]:justify-center [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', {
|
|
163
163
|
variants: {
|
|
164
164
|
variant: {
|
|
165
165
|
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
3
|
+
declare const Tabs: React.ForwardRefExoticComponent<TabsPrimitive.TabsProps & React.RefAttributes<HTMLDivElement>>;
|
|
4
|
+
declare const TabsList: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsListProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
5
|
+
declare const TabsTrigger: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
|
|
6
|
+
declare const TabsContent: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
7
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
5
|
+
import { cn } from '../../utils/utils.js';
|
|
6
|
+
const Tabs = TabsPrimitive.Root;
|
|
7
|
+
const TabsList = React.forwardRef(({ className, ...props }, ref) => (_jsx(TabsPrimitive.List, { ref: ref, className: cn('inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground', className), ...props })));
|
|
8
|
+
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
9
|
+
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (_jsx(TabsPrimitive.Trigger, { ref: ref, className: cn('inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow', className), ...props })));
|
|
10
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
11
|
+
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (_jsx(TabsPrimitive.Content, { ref: ref, className: cn('mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', className), ...props })));
|
|
12
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
13
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ViewTitleProps {
|
|
2
|
+
title: string;
|
|
3
|
+
showBackButton?: boolean;
|
|
4
|
+
onBackClick?: () => void;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function ViewTitle({ title, showBackButton, onBackClick, className, }: ViewTitleProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ArrowLeft } from 'lucide-react';
|
|
3
|
+
import { Button } from '../components/ui/button.js';
|
|
4
|
+
import { cn } from '../utils/utils.js';
|
|
5
|
+
export function ViewTitle({ title, showBackButton = false, onBackClick, className, }) {
|
|
6
|
+
return (_jsxs("div", { className: cn('flex items-center gap-3', className), children: [showBackButton && (_jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Go back", onClick: onBackClick, children: _jsx(ArrowLeft, {}) })), _jsx("h1", { className: "text-lg font-semibold lg:text-xl", children: title })] }));
|
|
7
|
+
}
|
package/hooks/use-mobile.js
CHANGED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { FitOptions, ImageEdits, OverlayOptions } from '../utils/s3Image/types.js';
|
|
2
|
+
export declare function useS3Image(): (key: string, width: number | string | "auto", height?: number | string | "auto", fit?: FitOptions, retina?: boolean, overlayWith?: OverlayOptions | undefined, edits?: ImageEdits) => () => Promise<import("../utils/s3Image/types.js").S3ImageResult>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { useS3Config } from '../components/minutemailer-kit.js';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { s3Image } from '../utils/s3Image/index.js';
|
|
4
|
+
export function useS3Image() {
|
|
5
|
+
const { bucket, resizeCloudUrl, cloudFrontUrl } = useS3Config();
|
|
6
|
+
return useCallback((key, width, height = 0, fit = 'inside', retina = true, overlayWith = undefined, edits = {}) => s3Image(key, width, height, fit, retina, overlayWith, edits, bucket, resizeCloudUrl, cloudFrontUrl), [bucket, resizeCloudUrl, cloudFrontUrl]);
|
|
7
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const SvgMinutemailerLogo = (props) => (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", fill: "none", viewBox: "0 0 120 14", ...props, children: [_jsx("path", { fill: "currentColor", fillRule: "evenodd", d: "M15.958 13.938 22.283 0 0 7.373 8.539 9.27 8.579 14l2.561-2.529-1.993-2.006z", clipRule: "evenodd" }), _jsx("path", { fill: "currentColor", d: "M39.66 2.872c0-.65.52-1.138 1.146-1.138.677 0 1.145.487 1.145 1.083 0 .65-.468 1.192-1.145 1.192a1.133 1.133 0 0 1-1.146-1.137m.26 2.492h1.823v6.935H39.92zM99.273 2.872c0-.65.521-1.138 1.146-1.138.677 0 1.145.487 1.145 1.083 0 .65-.468 1.192-1.145 1.192a1.133 1.133 0 0 1-1.146-1.137m.26 2.492h1.823v6.935h-1.822zM51.22 7.64v4.66h-1.822V7.91c0-1.462-.677-2.112-1.875-2.112-1.25-.054-2.134.758-2.134 2.384v4.117h-1.823V4.172h1.719v1.03c.572-.759 1.562-1.138 2.707-1.138 1.822.054 3.228 1.138 3.228 3.576M60.483 4.172V12.3h-1.718v-1.03c-.573.76-1.51 1.139-2.5 1.139-1.978 0-3.331-1.138-3.331-3.576v-4.66h1.822v4.389c0 1.463.677 2.167 1.822 2.167 1.25 0 2.135-.813 2.135-2.438V4.172zM67.095 11.865c-.417.38-1.094.542-1.77.542-1.719 0-2.708-.92-2.708-2.709v-3.9h-1.25V4.28h1.302V1.734h1.822v2.492h2.083v1.517H64.49V9.59c0 .813.365 1.192 1.094 1.192.364 0 .729-.109 1.04-.325zM75.117 8.832h-6.092c.209 1.192 1.146 1.95 2.5 1.95.885 0 1.562-.27 2.134-.867l.99 1.192c-.677.867-1.77 1.3-3.124 1.3-2.604 0-4.322-1.733-4.322-4.171s1.718-4.118 4.01-4.118c2.29 0 3.956 1.68 3.956 4.172 0 .162-.052.38-.052.542m-6.092-1.246h4.426c-.156-1.192-.99-1.95-2.187-1.95-1.25 0-2.082.758-2.239 1.95M89.436 7.64v4.66h-1.822V7.91c0-1.463-.625-2.113-1.77-2.113-1.198 0-2.031.813-2.031 2.438v4.118H81.99V7.965c0-1.462-.624-2.113-1.77-2.113-1.197 0-2.03.813-2.03 2.438v4.118h-1.823V4.281h1.718v1.03c.573-.705 1.51-1.138 2.552-1.138 1.145 0 2.082.433 2.603 1.354.625-.812 1.666-1.354 2.916-1.354 1.978-.054 3.28 1.03 3.28 3.467M37.942 7.64v4.66H36.12V7.91c0-1.462-.625-2.112-1.77-2.112-1.198 0-2.03.812-2.03 2.438v4.117h-1.823V7.965c0-1.463-.625-2.113-1.77-2.113-1.146-.108-1.979.704-1.979 2.33v4.117h-1.822V4.172h1.718v1.03c.573-.705 1.51-1.138 2.551-1.138 1.146 0 2.083.433 2.604 1.354.624-.812 1.666-1.354 2.915-1.354 1.927.054 3.228 1.138 3.228 3.576M97.765 7.532V12.3h-1.719v-.976c-.416.705-1.301 1.084-2.447 1.084-1.77 0-2.915-1.03-2.915-2.438 0-1.354.885-2.438 3.228-2.438h2.03v-.108c0-1.084-.624-1.734-1.926-1.734-.885 0-1.77.325-2.343.813l-.73-1.355c.834-.65 2.032-.975 3.281-.975 2.239-.054 3.54 1.03 3.54 3.359m-1.823 2.221v-.92h-1.874c-1.25 0-1.562.487-1.562 1.083 0 .704.573 1.138 1.51 1.138.885-.055 1.614-.434 1.926-1.3M103.18 1.68h1.822V12.3h-1.822zM114.164 8.832h-6.092c.209 1.192 1.146 1.95 2.5 1.95.885 0 1.562-.27 2.134-.867l.99 1.192c-.677.867-1.771 1.3-3.124 1.3-2.604 0-4.322-1.733-4.322-4.171s1.718-4.118 4.009-4.118 3.957 1.68 3.957 4.172c0 .162 0 .38-.052.542m-6.092-1.246h4.426c-.156-1.192-.989-1.95-2.187-1.95s-2.082.758-2.239 1.95M120.001 4.118v1.788c-.156-.054-.312-.054-.416-.054-1.406 0-2.239.867-2.239 2.492V12.3h-1.823V4.172h1.719v1.192c.52-.812 1.458-1.246 2.759-1.246" })] }));
|
|
3
|
+
export default SvgMinutemailerLogo;
|
package/icons/index.d.ts
CHANGED
package/icons/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minutemailer/kit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Minutemailer UI Kit",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Minutemailer",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@storybook/addon-docs": "^10.0.8",
|
|
26
26
|
"@storybook/react-vite": "^10.0.8",
|
|
27
27
|
"@svgr/cli": "^8.1.0",
|
|
28
|
+
"@types/node": "^24.10.1",
|
|
28
29
|
"@types/react": "^19.2.0",
|
|
29
30
|
"@types/react-dom": "^19.2.0",
|
|
30
31
|
"@vitest/coverage-v8": "^3.2.4",
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
"@radix-ui/react-popover": "^1.1.15",
|
|
53
54
|
"@radix-ui/react-separator": "^1.1.7",
|
|
54
55
|
"@radix-ui/react-slot": "^1.2.3",
|
|
56
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
55
57
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
56
58
|
"@tailwindcss/vite": "^4.1.14",
|
|
57
59
|
"class-variance-authority": "^0.7.1",
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { FitOptions, ImageEdits, OverlayOptions, S3ImageResult } from './types.js';
|
|
2
|
+
export declare function s3Image(key: string, width: number | string | 'auto', height: number | string | "auto" | undefined, fit: FitOptions | undefined, retina: boolean | undefined, overlayWith: OverlayOptions | undefined, edits: ImageEdits | undefined, bucket: string, resizeCloudUrl: string, cloudFrontUrl: string): () => Promise<S3ImageResult>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { s3ImageDimensions } from './s3ImageDimensions.js';
|
|
2
|
+
import { s3ImageSrc } from './s3ImageSrc.js';
|
|
3
|
+
export function s3Image(key, width, height = 0, fit = 'inside', retina = true, overlayWith = undefined, edits = {}, bucket, resizeCloudUrl, cloudFrontUrl) {
|
|
4
|
+
if (!key) {
|
|
5
|
+
return () => new Promise((resolve) => {
|
|
6
|
+
resolve({ src: '', width: 0, height: 0 });
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
return () => new Promise((resolve, reject) => {
|
|
10
|
+
const imageSrc = s3ImageSrc(key, width, height, fit, retina, overlayWith, edits, bucket, resizeCloudUrl, cloudFrontUrl);
|
|
11
|
+
const extension = key.split('.').pop();
|
|
12
|
+
const resizeWidth = width === 'auto' ? 0 : parseInt(width.toString(), 10);
|
|
13
|
+
const resizeHeight = height === 'auto' ? 0 : parseInt(height.toString(), 10);
|
|
14
|
+
const image = new Image();
|
|
15
|
+
image.onload = (ev) => {
|
|
16
|
+
const img = ev.currentTarget;
|
|
17
|
+
const [realWidth, realHeight] = s3ImageDimensions(img.width, img.height, resizeWidth, resizeHeight);
|
|
18
|
+
if (extension === 'gif') {
|
|
19
|
+
const dimensions = s3ImageDimensions(realWidth, realHeight, resizeWidth, resizeHeight, fit);
|
|
20
|
+
resolve({
|
|
21
|
+
src: img.src,
|
|
22
|
+
width: dimensions[0],
|
|
23
|
+
height: dimensions[1],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
resolve({
|
|
28
|
+
src: img.src,
|
|
29
|
+
width: realWidth,
|
|
30
|
+
height: realHeight,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
image.onerror = reject;
|
|
35
|
+
image.src = imageSrc;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function s3ImageDimensions(width, height, resizeWidth, resizeHeight, fit = 'inside') {
|
|
2
|
+
if (['fill', 'contain', 'cover'].indexOf(fit) > -1) {
|
|
3
|
+
return [resizeWidth, resizeHeight];
|
|
4
|
+
}
|
|
5
|
+
const ratio = Math.min(Math.min(resizeWidth === 0 ? 9999 : resizeWidth, width) / width, Math.min(resizeHeight === 0 ? 9999 : resizeHeight, height) / height);
|
|
6
|
+
return [width * ratio, height * ratio];
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { FitOptions, ImageEdits, OverlayOptions } from './types.js';
|
|
2
|
+
export declare function s3ImageSrc(key: string | null | undefined, width: number | string | 'auto', height: number | string | "auto" | undefined, fit: FitOptions | undefined, retina: boolean | undefined, overlayWith: OverlayOptions | undefined, edits: ImageEdits | undefined, bucket: string, resizeCloudUrl: string, cloudFrontUrl: string): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function s3ImageSrc(key, width, height = 0, fit = 'inside', retina = true, overlayWith = undefined, edits = {}, bucket, resizeCloudUrl, cloudFrontUrl) {
|
|
2
|
+
if (!key) {
|
|
3
|
+
return '';
|
|
4
|
+
}
|
|
5
|
+
const url = resizeCloudUrl;
|
|
6
|
+
const extension = key.split('.').pop();
|
|
7
|
+
const path = key.replace(/^\/|\/$/g, '');
|
|
8
|
+
const resizeWidth = width === 'auto' ? 0 : parseInt(width.toString(), 10);
|
|
9
|
+
const resizeHeight = height === 'auto' ? 0 : parseInt(height.toString(), 10);
|
|
10
|
+
let imageSrc;
|
|
11
|
+
if (extension === 'gif') {
|
|
12
|
+
imageSrc = `${cloudFrontUrl.replace('%s', bucket)}/${path}`;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
const req = {
|
|
16
|
+
bucket,
|
|
17
|
+
key: path,
|
|
18
|
+
edits: {
|
|
19
|
+
resize: {
|
|
20
|
+
width: retina ? resizeWidth * 2 : resizeWidth,
|
|
21
|
+
height: retina ? resizeHeight * 2 : resizeHeight,
|
|
22
|
+
fit,
|
|
23
|
+
background: {
|
|
24
|
+
r: 255,
|
|
25
|
+
g: 255,
|
|
26
|
+
b: 255,
|
|
27
|
+
alpha: 1,
|
|
28
|
+
},
|
|
29
|
+
withoutEnlargement: true,
|
|
30
|
+
},
|
|
31
|
+
jpeg: {
|
|
32
|
+
quality: 70,
|
|
33
|
+
force: false,
|
|
34
|
+
},
|
|
35
|
+
png: {
|
|
36
|
+
quality: 70,
|
|
37
|
+
force: edits.roundCrop,
|
|
38
|
+
},
|
|
39
|
+
overlayWith: overlayWith
|
|
40
|
+
? {
|
|
41
|
+
...overlayWith,
|
|
42
|
+
bucket,
|
|
43
|
+
}
|
|
44
|
+
: undefined,
|
|
45
|
+
...edits,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
imageSrc = `${url}/${btoa(JSON.stringify(req))}`;
|
|
49
|
+
}
|
|
50
|
+
return imageSrc;
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const ALTERNATE_EDIT_ALLOWLIST_ARRAY: readonly ["overlayWith", "smartCrop", "roundCrop", "contentModeration", "crop", "animated"];
|
|
2
|
+
export declare const SHARP_EDIT_ALLOWLIST_ARRAY: string[];
|
|
3
|
+
type AllowlistedEdit = (typeof SHARP_EDIT_ALLOWLIST_ARRAY)[number] | (typeof ALTERNATE_EDIT_ALLOWLIST_ARRAY)[number];
|
|
4
|
+
export type ImageEdits = Partial<Record<AllowlistedEdit, any>>;
|
|
5
|
+
export type OverlayOptions = {
|
|
6
|
+
key: string;
|
|
7
|
+
bucket: string;
|
|
8
|
+
wRatio: number;
|
|
9
|
+
hRatio: number;
|
|
10
|
+
alpha: number;
|
|
11
|
+
options?: {
|
|
12
|
+
top: number;
|
|
13
|
+
left: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type FitOptions = 'inside' | 'contain' | 'cover' | 'fill' | 'outside';
|
|
17
|
+
export interface S3ImageResult {
|
|
18
|
+
src: string;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const ALTERNATE_EDIT_ALLOWLIST_ARRAY = [
|
|
2
|
+
'overlayWith',
|
|
3
|
+
'smartCrop',
|
|
4
|
+
'roundCrop',
|
|
5
|
+
'contentModeration',
|
|
6
|
+
'crop',
|
|
7
|
+
'animated',
|
|
8
|
+
];
|
|
9
|
+
const SHARP_IMAGE_OPERATIONS = {
|
|
10
|
+
CHANNEL_FUNCTIONS: [
|
|
11
|
+
'removeAlpha',
|
|
12
|
+
'ensureAlpha',
|
|
13
|
+
'extractChannel',
|
|
14
|
+
'joinChannel',
|
|
15
|
+
'bandbool',
|
|
16
|
+
],
|
|
17
|
+
COLOR_FUNCTIONS: [
|
|
18
|
+
'tint',
|
|
19
|
+
'greyscale',
|
|
20
|
+
'grayscale',
|
|
21
|
+
'pipelineColourspace',
|
|
22
|
+
'pipelineColorspace',
|
|
23
|
+
'toColourspace',
|
|
24
|
+
'toColorspace',
|
|
25
|
+
],
|
|
26
|
+
OPERATION_FUNCTIONS: [
|
|
27
|
+
'rotate',
|
|
28
|
+
'flip',
|
|
29
|
+
'flop',
|
|
30
|
+
'affine',
|
|
31
|
+
'sharpen',
|
|
32
|
+
'median',
|
|
33
|
+
'blur',
|
|
34
|
+
'flatten',
|
|
35
|
+
'unflatten',
|
|
36
|
+
'gamma',
|
|
37
|
+
'negate',
|
|
38
|
+
'normalise',
|
|
39
|
+
'normalize',
|
|
40
|
+
'clahe',
|
|
41
|
+
'convolve',
|
|
42
|
+
'threshold',
|
|
43
|
+
'boolean',
|
|
44
|
+
'linear',
|
|
45
|
+
'recomb',
|
|
46
|
+
'modulate',
|
|
47
|
+
],
|
|
48
|
+
FORMAT_OPERATIONS: [
|
|
49
|
+
'jpeg',
|
|
50
|
+
'png',
|
|
51
|
+
'webp',
|
|
52
|
+
'gif',
|
|
53
|
+
'avif',
|
|
54
|
+
'tiff',
|
|
55
|
+
'heif',
|
|
56
|
+
'toFormat',
|
|
57
|
+
],
|
|
58
|
+
RESIZE_OPERATIONS: ['resize', 'extend', 'extract', 'trim'],
|
|
59
|
+
};
|
|
60
|
+
export const SHARP_EDIT_ALLOWLIST_ARRAY = [
|
|
61
|
+
...SHARP_IMAGE_OPERATIONS.CHANNEL_FUNCTIONS,
|
|
62
|
+
...SHARP_IMAGE_OPERATIONS.COLOR_FUNCTIONS,
|
|
63
|
+
...SHARP_IMAGE_OPERATIONS.OPERATION_FUNCTIONS,
|
|
64
|
+
...SHARP_IMAGE_OPERATIONS.FORMAT_OPERATIONS,
|
|
65
|
+
...SHARP_IMAGE_OPERATIONS.RESIZE_OPERATIONS,
|
|
66
|
+
];
|