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.
@@ -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-C4AVPLLP.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-BoiS81Ei.css">
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mjpic",
3
3
  "description": "敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。",
4
- "version": "1.0.8",
4
+ "version": "1.0.17",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mjpic": "./dist/cli/cli.js"
@@ -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(85);
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(85);
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 avgLuminance = totalLuminance / sampleCount;
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
- let brightnessAdjust = 0;
271
- if (avgLuminance < 100) brightnessAdjust = (100 - avgLuminance) * 0.15;
272
- else if (avgLuminance > 160) brightnessAdjust = -(avgLuminance - 160) * 0.1;
290
+ // 1. 智能亮度调整(基于图像亮度分布)
291
+ // 使用曲线映射而非固定增量,保护高光
292
+ let shadowBoost = 0; // 暗部提升
293
+ let highlightCompress = 0; // 高光压缩
273
294
 
274
- const contrastAdjust = (maxVal - minVal) < 150 ? 15 : 8;
275
- const contrastFactor = (259 * (contrastAdjust + 255)) / (255 * (259 - contrastAdjust));
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
- const applyStep = 4; // 逐像素处理
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
- data[i] = Math.min(255, data[i] * wbR);
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
- const lum = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
286
- const newLum = lum + brightnessAdjust;
287
- const contrastLum = contrastFactor * (newLum - 128) + 128;
288
- const ratio = contrastLum / (lum || 1);
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
- data[i] = Math.min(255, data[i] * ratio);
291
- data[i+1] = Math.min(255, data[i+1] * ratio);
292
- data[i+2] = Math.min(255, data[i+2] * ratio);
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
- const gray = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
296
- const sat = 1.15;
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 sharpenAmount = 0.3;
305
- const center = 1 + 4 * sharpenAmount;
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
- enabledAnchors={isFreeCrop
822
- ? ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'middle-left', 'middle-right', 'top-center', 'bottom-center']
823
- : ['top-left', 'top-right', 'bottom-left', 'bottom-right']
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 { previewImage, config, originalWidth, originalHeight } = useImageStore.getState();
43
+ const { config, originalWidth, originalHeight } = useImageStore.getState();
44
44
 
45
- if (!previewImage) {
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 || 800;
52
- let finalHeight = originalHeight || 600;
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
- // Create a temporary canvas with the correct dimensions
60
- const canvas = document.createElement('canvas');
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
- if (!ctx) {
66
- console.error('Failed to create canvas context');
67
- return;
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
- // Set high quality rendering
71
- ctx.imageSmoothingEnabled = true;
72
- ctx.imageSmoothingQuality = 'high';
73
+ if (transformer) transformer.visible(false);
74
+ if (cropGroup) cropGroup.visible(false);
75
+ if (rotationGrid) rotationGrid.visible(false);
73
76
 
74
- // Load the preview image as the source
75
- const img = new Image();
76
- img.crossOrigin = 'anonymous';
77
+ // Force a redraw
78
+ stageRef.current.batchDraw();
77
79
 
78
- await new Promise<void>((resolve, reject) => {
79
- img.onload = () => {
80
- // Draw the image with the correct dimensions
81
- ctx.drawImage(img, 0, 0, finalWidth, finalHeight);
82
- resolve();
83
- };
84
- img.onerror = reject;
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
- // Convert canvas to data URL with correct quality
89
- const dataUrl = canvas.toDataURL(format, quality / 100);
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}