mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
State now reads through shape, weight and tone rather than hue: the active editor step and selected file carry a filled mark and heavier label, recipients are told apart by six neutral tones, and status icons lean on the clock/check/file glyphs they already draw. Red stays as the one accent, reserved for destructive and rejected. Drops the unreferenced --new-* ramps and the dawn/water scales, points the stock Tailwind ring at --ring, and desaturates the sign scale so the PDF certificate, audit log and email templates match the app.
207 lines
4.6 KiB
TypeScript
207 lines
4.6 KiB
TypeScript
import Konva from 'konva';
|
|
|
|
import {
|
|
DEFAULT_RECT_BACKGROUND,
|
|
RECIPIENT_COLOR_STYLES,
|
|
} from '@hanzo/esign-ui/lib/recipient-colors';
|
|
|
|
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
|
import { calculateFieldPosition } from './field-renderer';
|
|
|
|
export const konvaTextFontFamily =
|
|
'"Noto Sans", "Noto Sans Japanese", "Noto Sans Chinese", "Noto Sans Korean", sans-serif';
|
|
export const konvaTextFill = 'black';
|
|
|
|
export const upsertFieldGroup = (
|
|
field: FieldToRender,
|
|
options: RenderFieldElementOptions,
|
|
): Konva.Group => {
|
|
const { pageWidth, pageHeight, pageLayer, editable, scale } = options;
|
|
|
|
const { fieldX, fieldY, fieldWidth, fieldHeight } = calculateFieldPosition(
|
|
field,
|
|
pageWidth,
|
|
pageHeight,
|
|
);
|
|
|
|
const fieldGroup: Konva.Group =
|
|
pageLayer.findOne(`#${field.renderId}`) ||
|
|
new Konva.Group({
|
|
id: field.renderId,
|
|
name: 'field-group',
|
|
});
|
|
|
|
const maxXPosition = (pageWidth - fieldWidth) * scale;
|
|
const maxYPosition = (pageHeight - fieldHeight) * scale;
|
|
|
|
fieldGroup.setAttrs({
|
|
scaleX: 1,
|
|
scaleY: 1,
|
|
x: fieldX,
|
|
y: fieldY,
|
|
draggable: editable,
|
|
dragBoundFunc: (pos) => {
|
|
const newX = Math.max(0, Math.min(maxXPosition, pos.x));
|
|
const newY = Math.max(0, Math.min(maxYPosition, pos.y));
|
|
|
|
return { x: newX, y: newY };
|
|
},
|
|
} satisfies Partial<Konva.GroupConfig>);
|
|
|
|
return fieldGroup;
|
|
};
|
|
|
|
export const upsertFieldRect = (
|
|
field: FieldToRender,
|
|
options: RenderFieldElementOptions,
|
|
): Konva.Rect => {
|
|
const { pageWidth, pageHeight, mode, pageLayer, color } = options;
|
|
|
|
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
|
|
|
const fieldRect: Konva.Rect =
|
|
pageLayer.findOne(`#${field.renderId}-rect`) ||
|
|
new Konva.Rect({
|
|
id: `${field.renderId}-rect`,
|
|
name: 'field-rect',
|
|
});
|
|
|
|
fieldRect.setAttrs({
|
|
width: fieldWidth,
|
|
height: fieldHeight,
|
|
fill: DEFAULT_RECT_BACKGROUND,
|
|
stroke: color ? RECIPIENT_COLOR_STYLES[color].baseRing : '#e5e7eb',
|
|
strokeWidth: 2,
|
|
cornerRadius: 2,
|
|
strokeScaleEnabled: false,
|
|
visible: mode !== 'export',
|
|
} satisfies Partial<Konva.RectConfig>);
|
|
|
|
return fieldRect;
|
|
};
|
|
|
|
export const createSpinner = ({
|
|
fieldWidth,
|
|
fieldHeight,
|
|
}: {
|
|
fieldWidth: number;
|
|
fieldHeight: number;
|
|
}) => {
|
|
const loadingGroup = new Konva.Group({
|
|
name: 'loading-spinner-group',
|
|
});
|
|
|
|
const rect = new Konva.Rect({
|
|
x: 4,
|
|
y: 4,
|
|
width: fieldWidth - 8,
|
|
height: fieldHeight - 8,
|
|
fill: 'white',
|
|
opacity: 0.8,
|
|
});
|
|
|
|
const maxSpinnerSize = 10;
|
|
const smallerDimension = Math.min(fieldWidth, fieldHeight);
|
|
const spinnerSize = Math.min(smallerDimension, maxSpinnerSize);
|
|
|
|
const spinner = new Konva.Arc({
|
|
x: fieldWidth / 2,
|
|
y: fieldHeight / 2,
|
|
innerRadius: spinnerSize,
|
|
outerRadius: spinnerSize / 2,
|
|
angle: 270,
|
|
rotation: 0,
|
|
fill: 'rgba(92, 92, 92, 1)',
|
|
lineCap: 'round',
|
|
});
|
|
|
|
rect.moveToTop();
|
|
spinner.moveToTop();
|
|
|
|
loadingGroup.add(rect);
|
|
loadingGroup.add(spinner);
|
|
|
|
const anim = new Konva.Animation((frame) => {
|
|
spinner.rotate(180 * (frame.timeDiff / 500));
|
|
});
|
|
|
|
anim.start();
|
|
|
|
return loadingGroup;
|
|
};
|
|
|
|
type CreateFieldHoverInteractionOptions = {
|
|
options: RenderFieldElementOptions;
|
|
fieldGroup: Konva.Group;
|
|
fieldRect: Konva.Rect;
|
|
};
|
|
|
|
/**
|
|
* Adds smooth transition-like behavior for hover effects to the field group and rectangle.
|
|
*/
|
|
export const createFieldHoverInteraction = ({
|
|
options,
|
|
fieldGroup,
|
|
fieldRect,
|
|
}: CreateFieldHoverInteractionOptions) => {
|
|
const { mode } = options;
|
|
|
|
if (mode === 'export' || !options.color) {
|
|
return;
|
|
}
|
|
|
|
const hoverColor = RECIPIENT_COLOR_STYLES[options.color].baseRingHover;
|
|
|
|
fieldGroup.on('mouseover', () => {
|
|
const layer = fieldRect.getLayer();
|
|
if (!layer) {
|
|
return;
|
|
}
|
|
|
|
new Konva.Tween({
|
|
node: fieldRect,
|
|
duration: 0.3,
|
|
fill: hoverColor,
|
|
}).play();
|
|
});
|
|
|
|
fieldGroup.on('mouseout', () => {
|
|
const layer = fieldRect.getLayer();
|
|
if (!layer) {
|
|
return;
|
|
}
|
|
|
|
new Konva.Tween({
|
|
node: fieldRect,
|
|
duration: 0.3,
|
|
fill: DEFAULT_RECT_BACKGROUND,
|
|
}).play();
|
|
});
|
|
|
|
fieldGroup.on('transformstart', () => {
|
|
const layer = fieldRect.getLayer();
|
|
if (!layer) {
|
|
return;
|
|
}
|
|
|
|
new Konva.Tween({
|
|
node: fieldRect,
|
|
duration: 0.3,
|
|
fill: hoverColor,
|
|
}).play();
|
|
});
|
|
|
|
fieldGroup.on('transformend', () => {
|
|
const layer = fieldRect.getLayer();
|
|
if (!layer) {
|
|
return;
|
|
}
|
|
|
|
new Konva.Tween({
|
|
node: fieldRect,
|
|
duration: 0.3,
|
|
fill: DEFAULT_RECT_BACKGROUND,
|
|
}).play();
|
|
});
|
|
};
|