ywana-core8 0.2.14 → 0.2.16
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/dist/index.css +59 -11
- package/dist/index.js +102 -21
- package/dist/index.js.map +1 -1
- package/dist/index.modern.js +103 -22
- package/dist/index.modern.js.map +1 -1
- package/dist/index.umd.js +102 -21
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/html/index.js +1 -1
- package/src/widgets/image/ImageViewer.css +14 -1
- package/src/widgets/image/ImageViewer.js +72 -23
- package/src/widgets/viewer/Viewer.css +45 -10
- package/src/widgets/viewer/Viewer.js +71 -19
package/package.json
CHANGED
package/src/html/index.js
CHANGED
@@ -9,7 +9,7 @@ export { Header2 } from './header2'
|
|
9
9
|
export { Icon } from './icon'
|
10
10
|
export { List } from './list'
|
11
11
|
export { MenuIcon, Menu, MenuItem, MenuSeparator } from './menu'
|
12
|
-
export { CircularProgress, LinearProgress } from './progress'
|
12
|
+
export { CircularProgress, LinearProgress, StepProgress, RadialProgress, MultiProgress } from './progress'
|
13
13
|
export { Property } from './property'
|
14
14
|
export { RadioButton, RadioGroup } from './radio'
|
15
15
|
export { Section } from './section'
|
@@ -1,5 +1,18 @@
|
|
1
|
-
.image-viewer
|
1
|
+
.image-viewer {
|
2
2
|
display: flex;
|
3
3
|
justify-content: center;
|
4
4
|
align-items: center;
|
5
|
+
width: 100%;
|
6
|
+
height: 100%;
|
7
|
+
overflow: hidden;
|
8
|
+
}
|
9
|
+
|
10
|
+
.image-viewer canvas {
|
11
|
+
cursor: grab;
|
12
|
+
max-width: 100%;
|
13
|
+
max-height: 100%;
|
14
|
+
}
|
15
|
+
|
16
|
+
.image-viewer canvas:active {
|
17
|
+
cursor: grabbing;
|
5
18
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, { useRef, useMemo, useEffect, useState } from "react";
|
1
|
+
import React, { useRef, useMemo, useEffect, useState, forwardRef, useImperativeHandle } from "react";
|
2
2
|
import './ImageViewer.css'
|
3
3
|
|
4
4
|
const ImageViewerTest = (props) => {
|
@@ -14,10 +14,10 @@ const SCROLL_SENSITIVITY = 0.0005;
|
|
14
14
|
const MAX_ZOOM = 5;
|
15
15
|
const MIN_ZOOM = 0.1;
|
16
16
|
|
17
|
-
export const ImageViewer = ({ image }) => {
|
17
|
+
export const ImageViewer = forwardRef(({ image }, ref) => {
|
18
18
|
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
19
19
|
const [zoom, setZoom] = useState(1);
|
20
|
-
const [
|
20
|
+
const [dragging, setDragging] = useState(false);
|
21
21
|
|
22
22
|
const touch = useRef({ x: 0, y: 0 });
|
23
23
|
const canvasRef = useRef(null);
|
@@ -27,9 +27,23 @@ export const ImageViewer = ({ image }) => {
|
|
27
27
|
|
28
28
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
29
29
|
|
30
|
+
// Expose zoom functions to parent component
|
31
|
+
useImperativeHandle(ref, () => ({
|
32
|
+
zoomIn: () => {
|
33
|
+
setZoom(prevZoom => clamp(prevZoom + 0.2, MIN_ZOOM, MAX_ZOOM));
|
34
|
+
},
|
35
|
+
zoomOut: () => {
|
36
|
+
setZoom(prevZoom => clamp(prevZoom - 0.2, MIN_ZOOM, MAX_ZOOM));
|
37
|
+
},
|
38
|
+
resetZoom: () => {
|
39
|
+
setZoom(1);
|
40
|
+
setOffset({ x: 0, y: 0 });
|
41
|
+
}
|
42
|
+
}), []);
|
43
|
+
|
30
44
|
const handleWheel = (event) => {
|
31
45
|
const { deltaY } = event;
|
32
|
-
if (!
|
46
|
+
if (!dragging) {
|
33
47
|
setZoom((zoom) =>
|
34
48
|
clamp(zoom + deltaY * SCROLL_SENSITIVITY * -1, MIN_ZOOM, MAX_ZOOM)
|
35
49
|
);
|
@@ -37,7 +51,7 @@ export const ImageViewer = ({ image }) => {
|
|
37
51
|
};
|
38
52
|
|
39
53
|
const handleMouseMove = (event) => {
|
40
|
-
if (
|
54
|
+
if (dragging) {
|
41
55
|
const { x, y } = touch.current;
|
42
56
|
const { clientX, clientY } = event;
|
43
57
|
setOffset({
|
@@ -57,34 +71,50 @@ export const ImageViewer = ({ image }) => {
|
|
57
71
|
const handleMouseUp = () => setDragging(false);
|
58
72
|
|
59
73
|
const draw = () => {
|
60
|
-
if (canvasRef.current) {
|
61
|
-
|
62
|
-
|
74
|
+
if (!canvasRef.current || !background.complete || !background.naturalWidth) {
|
75
|
+
return;
|
76
|
+
}
|
77
|
+
|
78
|
+
try {
|
79
|
+
const canvas = canvasRef.current;
|
80
|
+
const { width, height } = canvas;
|
81
|
+
const context = canvas.getContext("2d");
|
63
82
|
|
64
83
|
// Set canvas dimensions
|
65
|
-
|
66
|
-
|
84
|
+
canvas.width = width;
|
85
|
+
canvas.height = height;
|
67
86
|
|
68
|
-
//
|
87
|
+
// Save context state
|
88
|
+
context.save();
|
89
|
+
|
90
|
+
// Clear canvas and apply transformations
|
69
91
|
context.translate(-offset.x, -offset.y);
|
70
92
|
context.scale(zoom, zoom);
|
71
93
|
context.clearRect(0, 0, width, height);
|
72
94
|
|
73
95
|
// Make sure we're zooming to the center
|
74
|
-
const x = (
|
75
|
-
const y = (
|
96
|
+
const x = (canvas.width / zoom - background.width) / 2;
|
97
|
+
const y = (canvas.height / zoom - background.height) / 2;
|
76
98
|
|
77
99
|
// Draw image
|
78
100
|
context.drawImage(background, x, y);
|
101
|
+
|
102
|
+
// Restore context state
|
103
|
+
context.restore();
|
104
|
+
} catch (error) {
|
105
|
+
console.warn('Error drawing image:', error);
|
79
106
|
}
|
80
107
|
};
|
81
108
|
|
82
109
|
useEffect(() => {
|
110
|
+
const container = containerRef.current;
|
111
|
+
if (!container) return;
|
112
|
+
|
83
113
|
observer.current = new ResizeObserver((entries) => {
|
84
114
|
entries.forEach(({ target }) => {
|
85
115
|
const { width, height } = background;
|
86
116
|
// If width of the container is smaller than image, scale image down
|
87
|
-
if (target.clientWidth < width) {
|
117
|
+
if (target.clientWidth < width && canvasRef.current) {
|
88
118
|
// Calculate scale
|
89
119
|
const scale = target.clientWidth / width;
|
90
120
|
|
@@ -97,16 +127,23 @@ export const ImageViewer = ({ image }) => {
|
|
97
127
|
}
|
98
128
|
});
|
99
129
|
});
|
100
|
-
observer.current.observe(containerRef.current);
|
101
130
|
|
102
|
-
|
131
|
+
observer.current.observe(container);
|
132
|
+
|
133
|
+
return () => {
|
134
|
+
if (observer.current && container) {
|
135
|
+
observer.current.unobserve(container);
|
136
|
+
}
|
137
|
+
};
|
103
138
|
}, []);
|
104
139
|
|
105
140
|
useEffect(() => {
|
141
|
+
if (!image) return;
|
142
|
+
|
106
143
|
background.src = image;
|
107
144
|
|
108
|
-
|
109
|
-
|
145
|
+
const handleImageLoad = () => {
|
146
|
+
if (canvasRef.current) {
|
110
147
|
// Get the image dimensions
|
111
148
|
const { width, height } = background;
|
112
149
|
canvasRef.current.width = width;
|
@@ -114,13 +151,25 @@ export const ImageViewer = ({ image }) => {
|
|
114
151
|
|
115
152
|
// Set image as background
|
116
153
|
canvasRef.current.getContext("2d").drawImage(background, 0, 0);
|
117
|
-
}
|
118
|
-
}
|
119
|
-
|
154
|
+
}
|
155
|
+
};
|
156
|
+
|
157
|
+
const handleImageError = () => {
|
158
|
+
console.warn('Failed to load image:', image);
|
159
|
+
};
|
160
|
+
|
161
|
+
background.onload = handleImageLoad;
|
162
|
+
background.onerror = handleImageError;
|
163
|
+
|
164
|
+
return () => {
|
165
|
+
background.onload = null;
|
166
|
+
background.onerror = null;
|
167
|
+
};
|
168
|
+
}, [image, background]);
|
120
169
|
|
121
170
|
useEffect(() => {
|
122
171
|
draw();
|
123
|
-
}, [zoom, offset]);
|
172
|
+
}, [zoom, offset, background.src]);
|
124
173
|
|
125
174
|
return (
|
126
175
|
<div className="image-viewer" ref={containerRef}>
|
@@ -133,7 +182,7 @@ export const ImageViewer = ({ image }) => {
|
|
133
182
|
/>
|
134
183
|
</div>
|
135
184
|
);
|
136
|
-
};
|
185
|
+
});
|
137
186
|
|
138
187
|
|
139
188
|
export default ImageViewer;
|
@@ -1,12 +1,16 @@
|
|
1
1
|
.viewer {
|
2
|
+
position: fixed;
|
3
|
+
top: 0;
|
4
|
+
left: 0;
|
2
5
|
width: 100vw;
|
3
6
|
height: 100vh;
|
4
|
-
background-color: rgba(0, 0, 0, .
|
7
|
+
background-color: rgba(0, 0, 0, .85);
|
5
8
|
display: grid;
|
6
9
|
grid-template-columns: 1fr auto;
|
7
10
|
grid-template-rows: 4rem 1fr;
|
8
11
|
grid-template-areas: "header aside" "main aside";
|
9
12
|
color: #FFF;
|
13
|
+
z-index: 1000;
|
10
14
|
}
|
11
15
|
|
12
16
|
.viewer>header {
|
@@ -34,6 +38,7 @@
|
|
34
38
|
|
35
39
|
.viewer>main>.resizer {
|
36
40
|
width: 100%;
|
41
|
+
height: 100%;
|
37
42
|
overflow: hidden;
|
38
43
|
position: relative;
|
39
44
|
display: flex;
|
@@ -42,9 +47,18 @@
|
|
42
47
|
padding: 1rem;
|
43
48
|
}
|
44
49
|
|
45
|
-
.viewer>main>.resizer
|
50
|
+
.viewer>main>.resizer picture {
|
46
51
|
width: 100%;
|
47
|
-
|
52
|
+
height: 100%;
|
53
|
+
display: flex;
|
54
|
+
justify-content: center;
|
55
|
+
align-items: center;
|
56
|
+
}
|
57
|
+
|
58
|
+
.viewer>main>.resizer img {
|
59
|
+
max-width: 100%;
|
60
|
+
max-height: 100%;
|
61
|
+
object-fit: contain;
|
48
62
|
}
|
49
63
|
|
50
64
|
@media (min-width: 800px) {
|
@@ -56,9 +70,12 @@
|
|
56
70
|
|
57
71
|
.viewer>aside {
|
58
72
|
min-width: 22rem;
|
73
|
+
max-width: 25rem;
|
59
74
|
grid-area: aside;
|
60
75
|
display: none;
|
61
|
-
background-color:
|
76
|
+
background-color: rgba(40, 40, 40, 0.95);
|
77
|
+
backdrop-filter: blur(10px);
|
78
|
+
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
62
79
|
}
|
63
80
|
|
64
81
|
.viewer>aside.open {
|
@@ -67,20 +84,38 @@
|
|
67
84
|
flex-direction: column;
|
68
85
|
}
|
69
86
|
|
87
|
+
.viewer>aside main {
|
88
|
+
flex: 1;
|
89
|
+
overflow-y: auto;
|
90
|
+
padding: 0;
|
91
|
+
}
|
92
|
+
|
70
93
|
.viewer>aside .property>label {
|
71
94
|
color: #AAA;
|
72
95
|
}
|
73
96
|
|
74
97
|
.viewer>footer {
|
75
|
-
width:
|
98
|
+
width: 12rem;
|
76
99
|
position: absolute;
|
77
|
-
bottom:
|
78
|
-
left: calc(50vw -
|
79
|
-
border-radius:
|
100
|
+
bottom: 2rem;
|
101
|
+
left: calc(50vw - 6rem);
|
102
|
+
border-radius: 8px;
|
80
103
|
z-index: 100;
|
81
104
|
display: flex;
|
82
105
|
justify-content: space-between;
|
83
106
|
align-items: center;
|
84
|
-
padding: .
|
85
|
-
background-color: rgba(0, 0, 0, .
|
107
|
+
padding: 0.75rem 1rem;
|
108
|
+
background-color: rgba(0, 0, 0, 0.8);
|
109
|
+
backdrop-filter: blur(10px);
|
110
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
111
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
112
|
+
}
|
113
|
+
|
114
|
+
.viewer>footer .icon {
|
115
|
+
transition: all 0.2s ease;
|
116
|
+
}
|
117
|
+
|
118
|
+
.viewer>footer .icon:hover {
|
119
|
+
transform: scale(1.1);
|
120
|
+
color: #fff;
|
86
121
|
}
|
@@ -3,35 +3,87 @@ import { Icon, Header, Text } from '../../html'
|
|
3
3
|
import { ImageViewer} from '../image/ImageViewer'
|
4
4
|
import './Viewer.css'
|
5
5
|
|
6
|
-
const ViewerTest = (
|
6
|
+
export const ViewerTest = () => {
|
7
|
+
const [showViewer, setShowViewer] = useState(false);
|
8
|
+
|
9
|
+
const sampleInfo = (
|
10
|
+
<div style={{ padding: '1rem' }}>
|
11
|
+
<h3>Sample Image</h3>
|
12
|
+
<p><strong>Size:</strong> 1920x1080</p>
|
13
|
+
<p><strong>Format:</strong> PNG</p>
|
14
|
+
<p><strong>Created:</strong> 2024-01-15</p>
|
15
|
+
</div>
|
16
|
+
);
|
17
|
+
|
18
|
+
const sampleActions = [
|
19
|
+
<Icon key="download" icon="download" clickable title="Download" />,
|
20
|
+
<Icon key="share" icon="share" clickable title="Share" />
|
21
|
+
];
|
22
|
+
|
7
23
|
return (
|
8
|
-
<
|
9
|
-
|
10
|
-
|
24
|
+
<div>
|
25
|
+
<button onClick={() => setShowViewer(true)}>
|
26
|
+
Open Viewer Test
|
27
|
+
</button>
|
28
|
+
{showViewer && (
|
29
|
+
<Viewer
|
30
|
+
title="Sample Image"
|
31
|
+
src="https://concepto.de/wp-content/uploads/2015/03/paisaje-800x409.jpg"
|
32
|
+
info={sampleInfo}
|
33
|
+
actions={sampleActions}
|
34
|
+
tools={true}
|
35
|
+
onClose={() => setShowViewer(false)}
|
36
|
+
/>
|
37
|
+
)}
|
38
|
+
</div>
|
39
|
+
);
|
40
|
+
};
|
11
41
|
|
12
42
|
/**
|
13
43
|
* Viewer
|
14
44
|
*/
|
15
45
|
export const Viewer = ({ title, src, info, actions = [], tools = false, onClose }) => {
|
16
|
-
|
17
|
-
const [
|
46
|
+
const [showDetails, setShowDetails] = useState(false);
|
47
|
+
const [imageViewerRef, setImageViewerRef] = useState(null);
|
18
48
|
|
19
49
|
function toggleDetails() {
|
20
|
-
setShowDetails(!showDetails)
|
50
|
+
setShowDetails(!showDetails);
|
21
51
|
}
|
22
52
|
|
23
|
-
|
53
|
+
function handleZoomIn() {
|
54
|
+
if (imageViewerRef && imageViewerRef.zoomIn) {
|
55
|
+
imageViewerRef.zoomIn();
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
function handleZoomOut() {
|
60
|
+
if (imageViewerRef && imageViewerRef.zoomOut) {
|
61
|
+
imageViewerRef.zoomOut();
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
function handleZoomReset() {
|
66
|
+
if (imageViewerRef && imageViewerRef.resetZoom) {
|
67
|
+
imageViewerRef.resetZoom();
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
const headerTitle = <Text use="headline6">{title}</Text>;
|
72
|
+
|
24
73
|
return (
|
25
74
|
<div className="viewer">
|
26
|
-
<Header icon="view" title={headerTitle}
|
27
|
-
{onClose ? <Icon icon="close" clickable action={onClose} /> : null}
|
28
|
-
{showDetails ? '' : <Icon icon="info" clickable action={toggleDetails} />}
|
75
|
+
<Header icon="view" title={headerTitle}>
|
29
76
|
{actions}
|
77
|
+
{!showDetails && <Icon icon="info" clickable action={toggleDetails} />}
|
78
|
+
{onClose && <Icon icon="close" clickable action={onClose} />}
|
30
79
|
</Header>
|
31
80
|
<main>
|
32
81
|
<div className="resizer">
|
33
82
|
<picture>
|
34
|
-
<ImageViewer
|
83
|
+
<ImageViewer
|
84
|
+
image={src}
|
85
|
+
ref={setImageViewerRef}
|
86
|
+
/>
|
35
87
|
</picture>
|
36
88
|
</div>
|
37
89
|
</main>
|
@@ -43,13 +95,13 @@ export const Viewer = ({ title, src, info, actions = [], tools = false, onClose
|
|
43
95
|
{info}
|
44
96
|
</main>
|
45
97
|
</aside>
|
46
|
-
{tools
|
98
|
+
{tools && (
|
47
99
|
<footer>
|
48
|
-
<Icon clickable icon="zoom_out" />
|
49
|
-
<Icon clickable icon="zoom_out_map" />
|
50
|
-
<Icon clickable icon="zoom_in" />
|
100
|
+
<Icon clickable icon="zoom_out" action={handleZoomOut} />
|
101
|
+
<Icon clickable icon="zoom_out_map" action={handleZoomReset} />
|
102
|
+
<Icon clickable icon="zoom_in" action={handleZoomIn} />
|
51
103
|
</footer>
|
52
|
-
)
|
104
|
+
)}
|
53
105
|
</div>
|
54
|
-
)
|
55
|
-
}
|
106
|
+
);
|
107
|
+
};
|