mjpic 1.0.8 → 1.0.17
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/client/assets/index-C6nMBMvY.css +1 -0
- package/dist/client/assets/index-Diek5Vcg.js +197 -0
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/components/dialogs/SaveDialog.tsx +2 -2
- package/src/components/layout/CanvasArea.tsx +132 -40
- package/src/components/layout/Header.tsx +38 -29
- package/dist/client/assets/index-BoiS81Ei.css +0 -1
- package/dist/client/assets/index-C4AVPLLP.js +0 -197
package/dist/client/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<meta name="description" content="敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。" />
|
|
8
8
|
<title>敏捷图片 (mjpic) - 轻量级网页版图片处理工具</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-Diek5Vcg.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C6nMBMvY.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ interface SaveDialogProps {
|
|
|
13
13
|
export const SaveDialog = ({ isOpen, onClose, onConfirm, defaultPath, defaultFileName }: SaveDialogProps) => {
|
|
14
14
|
const { t } = useTranslation();
|
|
15
15
|
const [format, setFormat] = useState('image/jpeg');
|
|
16
|
-
const [quality, setQuality] = useState(
|
|
16
|
+
const [quality, setQuality] = useState(95);
|
|
17
17
|
const [savePath, setSavePath] = useState('');
|
|
18
18
|
const [fileName, setFileName] = useState('');
|
|
19
19
|
|
|
@@ -21,7 +21,7 @@ export const SaveDialog = ({ isOpen, onClose, onConfirm, defaultPath, defaultFil
|
|
|
21
21
|
useEffect(() => {
|
|
22
22
|
if (isOpen) {
|
|
23
23
|
setFormat('image/jpeg');
|
|
24
|
-
setQuality(
|
|
24
|
+
setQuality(95);
|
|
25
25
|
|
|
26
26
|
const extMap: Record<string, string> = {
|
|
27
27
|
'image/jpeg': '.jpg',
|
|
@@ -231,7 +231,13 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
|
231
231
|
filters.push(sharpnessFilter);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
// Auto Enhance -
|
|
234
|
+
// Auto Enhance - 智能自适应图片美化
|
|
235
|
+
// 算法特点:
|
|
236
|
+
// 1. 基于图像直方图分析,动态生成优化参数
|
|
237
|
+
// 2. 自适应亮度控制:保护高光,提升暗部
|
|
238
|
+
// 3. 智能对比度:根据动态范围自适应
|
|
239
|
+
// 4. 自然饱和度:避免过度饱和
|
|
240
|
+
// 5. 保持色温不变
|
|
235
241
|
if (config.enhancements?.autoEnhance) {
|
|
236
242
|
// @ts-ignore
|
|
237
243
|
const autoEnhanceFilter = function(imageData) {
|
|
@@ -240,71 +246,138 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
|
240
246
|
const height = imageData.height;
|
|
241
247
|
const pixelCount = width * height;
|
|
242
248
|
|
|
249
|
+
// === 第一阶段:图像统计分析 ===
|
|
243
250
|
// 使用采样来加速统计(每8个像素采样一次)
|
|
244
251
|
const sampleStep = 8;
|
|
245
|
-
let sumR = 0, sumG = 0, sumB = 0;
|
|
246
252
|
let minVal = 255, maxVal = 0;
|
|
247
253
|
let totalLuminance = 0;
|
|
254
|
+
let darkPixels = 0, brightPixels = 0; // 暗部和亮部像素统计
|
|
248
255
|
let sampleCount = 0;
|
|
249
256
|
|
|
257
|
+
// 用于计算色彩分布
|
|
258
|
+
let sumR = 0, sumG = 0, sumB = 0;
|
|
259
|
+
|
|
250
260
|
for (let i = 0; i < data.length; i += 4 * sampleStep) {
|
|
251
261
|
const r = data[i], g = data[i+1], b = data[i+2];
|
|
252
|
-
sumR += r; sumG += g; sumB += b;
|
|
253
262
|
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
263
|
+
|
|
254
264
|
if (lum < minVal) minVal = lum;
|
|
255
265
|
if (lum > maxVal) maxVal = lum;
|
|
256
266
|
totalLuminance += lum;
|
|
267
|
+
|
|
268
|
+
// 统计暗部(<50)和亮部(>200)像素
|
|
269
|
+
if (lum < 50) darkPixels++;
|
|
270
|
+
if (lum > 200) brightPixels++;
|
|
271
|
+
|
|
272
|
+
sumR += r; sumG += g; sumB += b;
|
|
257
273
|
sampleCount++;
|
|
258
274
|
}
|
|
259
275
|
|
|
276
|
+
const avgLuminance = totalLuminance / sampleCount;
|
|
277
|
+
const dynamicRange = maxVal - minVal;
|
|
278
|
+
const darkRatio = darkPixels / sampleCount;
|
|
279
|
+
const brightRatio = brightPixels / sampleCount;
|
|
280
|
+
|
|
281
|
+
// 计算平均色彩(用于饱和度判断)
|
|
260
282
|
const avgR = sumR / sampleCount;
|
|
261
283
|
const avgG = sumG / sampleCount;
|
|
262
284
|
const avgB = sumB / sampleCount;
|
|
263
|
-
const
|
|
285
|
+
const avgColorLum = 0.299 * avgR + 0.587 * avgG + 0.114 * avgB;
|
|
286
|
+
const colorSaturation = (Math.abs(avgR - avgColorLum) + Math.abs(avgG - avgColorLum) + Math.abs(avgB - avgColorLum)) / 3 / 255;
|
|
264
287
|
|
|
265
|
-
//
|
|
266
|
-
const wbR = avgG / (avgR || 1);
|
|
267
|
-
const wbB = avgG / (avgB || 1);
|
|
288
|
+
// === 第二阶段:动态参数生成 ===
|
|
268
289
|
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
290
|
+
// 1. 智能亮度调整(基于图像亮度分布)
|
|
291
|
+
// 使用曲线映射而非固定增量,保护高光
|
|
292
|
+
let shadowBoost = 0; // 暗部提升
|
|
293
|
+
let highlightCompress = 0; // 高光压缩
|
|
273
294
|
|
|
274
|
-
|
|
275
|
-
|
|
295
|
+
if (avgLuminance < 100) {
|
|
296
|
+
// 较暗图片:适度提升暗部,保护高光
|
|
297
|
+
shadowBoost = 25 + (100 - avgLuminance) * 0.15;
|
|
298
|
+
highlightCompress = 0;
|
|
299
|
+
} else if (avgLuminance > 180) {
|
|
300
|
+
// 较亮图片:减少提升,避免过曝
|
|
301
|
+
shadowBoost = 10;
|
|
302
|
+
highlightCompress = (avgLuminance - 180) * 0.3;
|
|
303
|
+
} else {
|
|
304
|
+
// 正常亮度:温和提升
|
|
305
|
+
shadowBoost = 20;
|
|
306
|
+
highlightCompress = 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 2. 智能对比度(基于动态范围)
|
|
310
|
+
// 动态范围小的图片需要更多对比度
|
|
311
|
+
let contrastAdjust = 0;
|
|
312
|
+
if (dynamicRange < 100) {
|
|
313
|
+
contrastAdjust = 8;
|
|
314
|
+
} else if (dynamicRange < 150) {
|
|
315
|
+
contrastAdjust = 5;
|
|
316
|
+
} else if (dynamicRange > 200) {
|
|
317
|
+
contrastAdjust = -3; // 高动态范围图片,轻微降低对比度
|
|
318
|
+
}
|
|
319
|
+
const contrastFactor = contrastAdjust !== 0
|
|
320
|
+
? (259 * (contrastAdjust + 255)) / (255 * (259 - contrastAdjust))
|
|
321
|
+
: 1;
|
|
276
322
|
|
|
277
|
-
//
|
|
278
|
-
|
|
323
|
+
// 3. 智能饱和度(基于当前饱和度)
|
|
324
|
+
// 低饱和度图片需要更多提升,高饱和度图片减少提升
|
|
325
|
+
let saturationBoost = 1.08; // 基础提升8%
|
|
326
|
+
if (colorSaturation < 0.15) {
|
|
327
|
+
saturationBoost = 1.12; // 低饱和度图片提升12%
|
|
328
|
+
} else if (colorSaturation > 0.4) {
|
|
329
|
+
saturationBoost = 1.04; // 高饱和度图片仅提升4%
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// === 第三阶段:应用处理 ===
|
|
279
333
|
for (let i = 0; i < data.length; i += 4) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
data[i+2] = Math.min(255, data[i+2] * wbB);
|
|
334
|
+
let r = data[i], g = data[i+1], b = data[i+2];
|
|
335
|
+
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
283
336
|
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
337
|
+
// 步骤1:自适应亮度调整(使用曲线映射)
|
|
338
|
+
// 暗部提升多,亮部提升少,保护高光
|
|
339
|
+
let brightnessCurve = 0;
|
|
340
|
+
if (lum < 80) {
|
|
341
|
+
// 暗部:线性提升
|
|
342
|
+
brightnessCurve = shadowBoost * (1 - lum / 80 * 0.3);
|
|
343
|
+
} else if (lum < 180) {
|
|
344
|
+
// 中间调:渐变减少提升
|
|
345
|
+
brightnessCurve = shadowBoost * 0.7 * (1 - (lum - 80) / 100 * 0.5);
|
|
346
|
+
} else {
|
|
347
|
+
// 高光:轻微提升或压缩
|
|
348
|
+
brightnessCurve = shadowBoost * 0.35 - highlightCompress * ((lum - 180) / 75);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
r = Math.min(255, Math.max(0, r + brightnessCurve));
|
|
352
|
+
g = Math.min(255, Math.max(0, g + brightnessCurve));
|
|
353
|
+
b = Math.min(255, Math.max(0, b + brightnessCurve));
|
|
289
354
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
355
|
+
// 步骤2:对比度调整(以128为中心)
|
|
356
|
+
if (contrastFactor !== 1) {
|
|
357
|
+
r = Math.min(255, Math.max(0, contrastFactor * (r - 128) + 128));
|
|
358
|
+
g = Math.min(255, Math.max(0, contrastFactor * (g - 128) + 128));
|
|
359
|
+
b = Math.min(255, Math.max(0, contrastFactor * (b - 128) + 128));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 步骤3:饱和度提升(保持色温)
|
|
363
|
+
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
364
|
+
r = Math.min(255, Math.max(0, gray + (r - gray) * saturationBoost));
|
|
365
|
+
g = Math.min(255, Math.max(0, gray + (g - gray) * saturationBoost));
|
|
366
|
+
b = Math.min(255, Math.max(0, gray + (b - gray) * saturationBoost));
|
|
293
367
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
data[i] = Math.min(255, gray + (data[i] - gray) * sat);
|
|
298
|
-
data[i+1] = Math.min(255, gray + (data[i+1] - gray) * sat);
|
|
299
|
-
data[i+2] = Math.min(255, gray + (data[i+2] - gray) * sat);
|
|
368
|
+
data[i] = r;
|
|
369
|
+
data[i+1] = g;
|
|
370
|
+
data[i+2] = b;
|
|
300
371
|
}
|
|
301
372
|
|
|
302
|
-
//
|
|
373
|
+
// === 第四阶段:智能锐化 ===
|
|
374
|
+
// 根据图像内容自适应锐化强度
|
|
375
|
+
const sharpnessAmount = dynamicRange < 100 ? 0.25 : 0.2;
|
|
303
376
|
const oldData = new Uint8ClampedArray(data);
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
const neighbor = -sharpenAmount;
|
|
377
|
+
const center = 1 + 4 * sharpnessAmount;
|
|
378
|
+
const neighbor = -sharpnessAmount;
|
|
307
379
|
|
|
380
|
+
// 隔行隔列采样处理,提升性能
|
|
308
381
|
for (let y = 1; y < height - 1; y += 2) {
|
|
309
382
|
for (let x = 1; x < width - 1; x += 2) {
|
|
310
383
|
const idx = (y * width + x) * 4;
|
|
@@ -818,10 +891,11 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
|
818
891
|
anchorStroke="#3b82f6"
|
|
819
892
|
anchorFill="#3b82f6"
|
|
820
893
|
anchorSize={8}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
894
|
+
// 所有裁剪模式都支持 8 个方向的手柄,方便对称裁剪
|
|
895
|
+
enabledAnchors={[
|
|
896
|
+
'top-left', 'top-right', 'bottom-left', 'bottom-right',
|
|
897
|
+
'top-center', 'bottom-center', 'middle-left', 'middle-right'
|
|
898
|
+
]}
|
|
825
899
|
boundBoxFunc={(oldBox, newBox) => {
|
|
826
900
|
if (newBox.width < 20 || newBox.height < 20) {
|
|
827
901
|
return oldBox;
|
|
@@ -840,6 +914,24 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
|
840
914
|
}
|
|
841
915
|
}
|
|
842
916
|
|
|
917
|
+
// 检测正在使用的手柄,保持中心位置不变
|
|
918
|
+
const transformerInstance = transformerRef.current;
|
|
919
|
+
if (transformerInstance) {
|
|
920
|
+
const activeAnchor = transformerInstance.getActiveAnchor();
|
|
921
|
+
|
|
922
|
+
if (activeAnchor === 'top-center' || activeAnchor === 'bottom-center') {
|
|
923
|
+
// 使用上/下手柄时,保持水平中心位置不变
|
|
924
|
+
const oldCenterX = oldBox.x + oldBox.width / 2;
|
|
925
|
+
const newCenterX = newBox.x + newBox.width / 2;
|
|
926
|
+
newBox.x += oldCenterX - newCenterX;
|
|
927
|
+
} else if (activeAnchor === 'middle-left' || activeAnchor === 'middle-right') {
|
|
928
|
+
// 使用左/右手柄时,保持垂直中心位置不变
|
|
929
|
+
const oldCenterY = oldBox.y + oldBox.height / 2;
|
|
930
|
+
const newCenterY = newBox.y + newBox.height / 2;
|
|
931
|
+
newBox.y += oldCenterY - newCenterY;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
843
935
|
const relX = newBox.x - contentX;
|
|
844
936
|
const relY = newBox.y - contentY;
|
|
845
937
|
if (relX < -5 || relY < -5 ||
|
|
@@ -40,53 +40,62 @@ export const Header = ({ stageRef }: HeaderProps) => {
|
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
42
|
// Get the current state from the image store
|
|
43
|
-
const {
|
|
43
|
+
const { config, originalWidth, originalHeight } = useImageStore.getState();
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
// Find the image node to get actual dimensions
|
|
46
|
+
const imageNode = stageRef.current.findOne('Image') as Konva.Image;
|
|
47
|
+
if (!imageNode) {
|
|
46
48
|
console.error('No image found to save');
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
// Determine the final dimensions based on resize config
|
|
51
|
-
let finalWidth = originalWidth ||
|
|
52
|
-
let finalHeight = originalHeight ||
|
|
53
|
+
let finalWidth = originalWidth || imageNode.width();
|
|
54
|
+
let finalHeight = originalHeight || imageNode.height();
|
|
53
55
|
|
|
54
56
|
if (config.resize && config.resize.width > 0 && config.resize.height > 0) {
|
|
55
57
|
finalWidth = config.resize.width;
|
|
56
58
|
finalHeight = config.resize.height;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
canvas.width = finalWidth;
|
|
62
|
-
canvas.height = finalHeight;
|
|
63
|
-
const ctx = canvas.getContext('2d');
|
|
61
|
+
// Calculate the pixel ratio needed to get the correct output size
|
|
62
|
+
const pixelRatio = finalWidth / imageNode.width();
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
// Hide other elements temporarily to only capture the image
|
|
65
|
+
const transformer = stageRef.current.findOne('Transformer');
|
|
66
|
+
const cropGroup = stageRef.current.findOne('#crop-group');
|
|
67
|
+
const rotationGrid = stageRef.current.findOne('#rotation-grid');
|
|
68
|
+
|
|
69
|
+
const transformerVisible = transformer?.visible();
|
|
70
|
+
const cropGroupVisible = cropGroup?.visible();
|
|
71
|
+
const rotationGridVisible = rotationGrid?.visible();
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
if (transformer) transformer.visible(false);
|
|
74
|
+
if (cropGroup) cropGroup.visible(false);
|
|
75
|
+
if (rotationGrid) rotationGrid.visible(false);
|
|
73
76
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
img.crossOrigin = 'anonymous';
|
|
77
|
+
// Force a redraw
|
|
78
|
+
stageRef.current.batchDraw();
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
img.src = previewImage;
|
|
80
|
+
// Wait a bit for the render to complete
|
|
81
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
82
|
+
|
|
83
|
+
// Use Konva's toCanvas method to get the rendered canvas with filters
|
|
84
|
+
// This ensures all filters are applied correctly
|
|
85
|
+
const renderedCanvas = imageNode.toCanvas({
|
|
86
|
+
pixelRatio: pixelRatio
|
|
86
87
|
});
|
|
87
88
|
|
|
88
|
-
//
|
|
89
|
-
const dataUrl =
|
|
89
|
+
// Get the data URL from the rendered canvas
|
|
90
|
+
const dataUrl = renderedCanvas.toDataURL(format, quality / 100);
|
|
91
|
+
|
|
92
|
+
// Restore visibility
|
|
93
|
+
if (transformer) transformer.visible(transformerVisible);
|
|
94
|
+
if (cropGroup) cropGroup.visible(cropGroupVisible);
|
|
95
|
+
if (rotationGrid) rotationGrid.visible(rotationGridVisible);
|
|
96
|
+
|
|
97
|
+
// Force another redraw
|
|
98
|
+
stageRef.current.batchDraw();
|
|
90
99
|
|
|
91
100
|
// Check if we are in CLI mode with an opened file
|
|
92
101
|
// If savePath is provided (from dialog), use it
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.right-3{right:.75rem}.top-1\/2{top:50%}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-auto{margin-left:auto}.mr-2{margin-right:.5rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.grid{display:grid}.h-1{height:.25rem}.h-12{height:3rem}.h-16{height:4rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[80vh\]{max-height:80vh}.w-16{width:4rem}.w-32{width:8rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-80{width:20rem}.w-96{width:24rem}.w-\[280px\]{width:280px}.w-\[80px\]{width:80px}.w-full{width:100%}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.resize{resize:both}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.self-end{align-self:flex-end}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-0{border-width:0px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-blue-500\/50{border-color:#3b82f680}.border-transparent{border-color:transparent}.border-zinc-600{--tw-border-opacity: 1;border-color:rgb(82 82 91 / var(--tw-border-opacity, 1))}.border-zinc-700{--tw-border-opacity: 1;border-color:rgb(63 63 70 / var(--tw-border-opacity, 1))}.border-zinc-800{--tw-border-opacity: 1;border-color:rgb(39 39 42 / var(--tw-border-opacity, 1))}.bg-black\/50{background-color:#00000080}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-zinc-700{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity, 1))}.bg-zinc-800{--tw-bg-opacity: 1;background-color:rgb(39 39 42 / var(--tw-bg-opacity, 1))}.bg-zinc-800\/50{background-color:#27272a80}.bg-zinc-900{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity, 1))}.bg-zinc-950{--tw-bg-opacity: 1;background-color:rgb(9 9 11 / var(--tw-bg-opacity, 1))}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pb-1{padding-bottom:.25rem}.pr-1{padding-right:.25rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-none{line-height:1}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-zinc-100{--tw-text-opacity: 1;color:rgb(244 244 245 / var(--tw-text-opacity, 1))}.text-zinc-200{--tw-text-opacity: 1;color:rgb(228 228 231 / var(--tw-text-opacity, 1))}.text-zinc-300{--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity, 1))}.text-zinc-400{--tw-text-opacity: 1;color:rgb(161 161 170 / var(--tw-text-opacity, 1))}.text-zinc-500{--tw-text-opacity: 1;color:rgb(113 113 122 / var(--tw-text-opacity, 1))}.text-zinc-600{--tw-text-opacity: 1;color:rgb(82 82 91 / var(--tw-text-opacity, 1))}.accent-blue-500{accent-color:#3b82f6}.opacity-50{opacity:.5}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-blue-500{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Noto Sans,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";line-height:1.5;font-weight:400;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.hover\:bg-blue-500:hover{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-zinc-600:hover{--tw-bg-opacity: 1;background-color:rgb(82 82 91 / var(--tw-bg-opacity, 1))}.hover\:bg-zinc-700:hover{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity, 1))}.hover\:bg-zinc-700\/50:hover{background-color:#3f3f4680}.hover\:bg-zinc-800:hover{--tw-bg-opacity: 1;background-color:rgb(39 39 42 / var(--tw-bg-opacity, 1))}.hover\:text-blue-400:hover{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:text-zinc-100:hover{--tw-text-opacity: 1;color:rgb(244 244 245 / var(--tw-text-opacity, 1))}.hover\:text-zinc-200:hover{--tw-text-opacity: 1;color:rgb(228 228 231 / var(--tw-text-opacity, 1))}.hover\:text-zinc-300:hover{--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity, 1))}.hover\:text-zinc-400:hover{--tw-text-opacity: 1;color:rgb(161 161 170 / var(--tw-text-opacity, 1))}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.active\:cursor-grabbing:active{cursor:grabbing}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}
|