mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
feat: enhance document signing page with field canvas style integration (#2876)
This commit is contained in:
@@ -7,7 +7,7 @@ import type React from 'react';
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { TEnvelope } from '../../types/envelope';
|
||||
import type { FieldRenderMode } from '../../universal/field-renderer/render-field';
|
||||
import type { FieldRenderMode } from '../../universal/field-renderer/field-renderer';
|
||||
|
||||
/**
|
||||
* The signature data for an inserted signature field.
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"@vvo/tzdb": "^6.196.0",
|
||||
"ai": "^5.0.104",
|
||||
"bullmq": "^5.71.1",
|
||||
"colord": "^2.9.3",
|
||||
"csv-parse": "^6.1.0",
|
||||
"inngest": "^3.54.0",
|
||||
"ioredis": "^5.10.1",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
getFieldCanvasStyleCacheKey,
|
||||
getOpacityValue,
|
||||
getPixelValue,
|
||||
getRenderableColor,
|
||||
TRANSPARENT_COLOR,
|
||||
} from './field-canvas-style';
|
||||
import type { FieldToRender } from './field-renderer';
|
||||
|
||||
const createField = (overrides: Partial<FieldToRender> = {}) =>
|
||||
({
|
||||
type: 'SIGNATURE',
|
||||
inserted: false,
|
||||
fieldMeta: null,
|
||||
...overrides,
|
||||
}) as FieldToRender;
|
||||
|
||||
describe('getPixelValue', () => {
|
||||
it('parses pixel values', () => {
|
||||
expect(getPixelValue('12px')).toBe(12);
|
||||
expect(getPixelValue('0px')).toBe(0);
|
||||
expect(getPixelValue('1.5px')).toBe(1.5);
|
||||
});
|
||||
|
||||
it('parses unitless numeric strings', () => {
|
||||
expect(getPixelValue('42')).toBe(42);
|
||||
});
|
||||
|
||||
it('returns undefined for non-finite or unparseable values', () => {
|
||||
expect(getPixelValue('')).toBeUndefined();
|
||||
expect(getPixelValue('auto')).toBeUndefined();
|
||||
expect(getPixelValue('inherit')).toBeUndefined();
|
||||
expect(getPixelValue('NaN')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOpacityValue', () => {
|
||||
it('returns undefined for fully opaque (the default) so callers fall back', () => {
|
||||
expect(getOpacityValue('1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps values inside the [0, 1) range as-is', () => {
|
||||
expect(getOpacityValue('0')).toBe(0);
|
||||
expect(getOpacityValue('0.5')).toBe(0.5);
|
||||
expect(getOpacityValue('0.999')).toBe(0.999);
|
||||
});
|
||||
|
||||
it('clamps out-of-range values into [0, 1]', () => {
|
||||
expect(getOpacityValue('-0.5')).toBe(0);
|
||||
expect(getOpacityValue('2')).toBe(1);
|
||||
});
|
||||
|
||||
it('returns undefined for non-finite values', () => {
|
||||
expect(getOpacityValue('')).toBeUndefined();
|
||||
expect(getOpacityValue('inherit')).toBeUndefined();
|
||||
expect(getOpacityValue('NaN')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRenderableColor', () => {
|
||||
it('passes non-transparent colors through unchanged', () => {
|
||||
expect(getRenderableColor('rgb(255, 0, 0)')).toBe('rgb(255, 0, 0)');
|
||||
expect(getRenderableColor('rgba(0, 128, 0, 0.5)')).toBe('rgba(0, 128, 0, 0.5)');
|
||||
expect(getRenderableColor('rgba(255, 255, 255, 0.9)')).toBe('rgba(255, 255, 255, 0.9)');
|
||||
expect(getRenderableColor('#abcdef')).toBe('#abcdef');
|
||||
});
|
||||
|
||||
it('returns undefined for falsy input', () => {
|
||||
expect(getRenderableColor(undefined)).toBeUndefined();
|
||||
expect(getRenderableColor('')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('normalizes the `transparent` keyword to a renderable transparent color, regardless of case or whitespace', () => {
|
||||
expect(getRenderableColor('transparent')).toBe(TRANSPARENT_COLOR);
|
||||
expect(getRenderableColor('TRANSPARENT')).toBe(TRANSPARENT_COLOR);
|
||||
expect(getRenderableColor(' Transparent ')).toBe(TRANSPARENT_COLOR);
|
||||
});
|
||||
|
||||
it('normalizes fully transparent rgba() colors to a renderable transparent color', () => {
|
||||
expect(getRenderableColor('rgba(0, 0, 0, 0)')).toBe(TRANSPARENT_COLOR);
|
||||
expect(getRenderableColor('rgba(255, 0, 0, 0)')).toBe(TRANSPARENT_COLOR);
|
||||
expect(getRenderableColor('rgba(255, 0, 0, 0.0)')).toBe(TRANSPARENT_COLOR);
|
||||
expect(getRenderableColor('rgba(255, 0, 0, 0.00)')).toBe(TRANSPARENT_COLOR);
|
||||
});
|
||||
|
||||
it('normalizes space-separated (CSS Color 4) fully transparent colors to a renderable transparent color', () => {
|
||||
expect(getRenderableColor('rgb(0 128 0 / 0)')).toBe(TRANSPARENT_COLOR);
|
||||
expect(getRenderableColor('rgba(255 0 0 / 0)')).toBe(TRANSPARENT_COLOR);
|
||||
});
|
||||
|
||||
it('passes space-separated (CSS Color 4) colors through unchanged', () => {
|
||||
expect(getRenderableColor('rgb(0 128 0 / 0.5)')).toBe('rgb(0 128 0 / 0.5)');
|
||||
});
|
||||
|
||||
it('returns undefined for unparseable color values', () => {
|
||||
expect(getRenderableColor('none')).toBeUndefined();
|
||||
expect(getRenderableColor('not-a-color')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not strip non-zero alpha colors', () => {
|
||||
expect(getRenderableColor('rgba(255, 0, 0, 0.5)')).toBe('rgba(255, 0, 0, 0.5)');
|
||||
expect(getRenderableColor('rgba(255, 0, 0, 1)')).toBe('rgba(255, 0, 0, 1)');
|
||||
expect(getRenderableColor('rgba(255, 0, 0, 0.01)')).toBe('rgba(255, 0, 0, 0.01)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldCanvasStyleCacheKey', () => {
|
||||
it('includes validation state', () => {
|
||||
expect(getFieldCanvasStyleCacheKey(createField({ isValidating: false }))).toBe('SIGNATURE:false:false:false');
|
||||
expect(getFieldCanvasStyleCacheKey(createField({ isValidating: true }))).toBe('SIGNATURE:false:false:true');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
FIELD_PROBE_ANCHOR_SELECTOR,
|
||||
FIELD_ROOT_CONTAINER_PROBE_CLASS_NAME,
|
||||
} from '@documenso/ui/lib/field-root-container-classes';
|
||||
import { colord } from 'colord';
|
||||
|
||||
import type { FieldCanvasStyle, FieldRenderMode, FieldToRender } from './field-renderer';
|
||||
|
||||
export type FieldCanvasStyleCache = Map<string, FieldCanvasStyle | undefined>;
|
||||
|
||||
export const createFieldCanvasStyleCache = (): FieldCanvasStyleCache => new Map();
|
||||
|
||||
export const getFieldCanvasStyleCacheKey = (field: FieldToRender) =>
|
||||
`${field.type}:${field.inserted}:${field.fieldMeta?.readOnly ?? false}:${field.isValidating ?? false}`;
|
||||
|
||||
export const getPixelValue = (value: string) => {
|
||||
const parsedValue = Number.parseFloat(value);
|
||||
|
||||
if (!Number.isFinite(parsedValue)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parsedValue;
|
||||
};
|
||||
|
||||
export const getOpacityValue = (value: string) => {
|
||||
const parsedValue = Number.parseFloat(value);
|
||||
|
||||
if (!Number.isFinite(parsedValue) || parsedValue === 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(parsedValue, 1));
|
||||
};
|
||||
|
||||
// Canonical value Konva paints as fully transparent. We normalize transparent
|
||||
// inputs to this so the renderer can tell "customer asked for transparent"
|
||||
// (honored — paint nothing) apart from "no custom style" (undefined — fall back
|
||||
// to the renderer default).
|
||||
export const TRANSPARENT_COLOR = 'rgba(0, 0, 0, 0)';
|
||||
|
||||
export const getRenderableColor = (value: string | undefined) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const color = colord(value);
|
||||
|
||||
// Unparseable input (e.g. `none`) has no canvas meaning, so fall back to the
|
||||
// renderer defaults. Inputs come from `getComputedStyle`, which normalizes to
|
||||
// `rgb()`/`rgba()`, so the base colord parser (no named-colors plugin) is
|
||||
// sufficient here. The `transparent` keyword is the one exception colord
|
||||
// reports as invalid; we treat it as an explicit transparent request.
|
||||
if (!color.isValid()) {
|
||||
return value.trim().toLowerCase() === 'transparent' ? TRANSPARENT_COLOR : undefined;
|
||||
}
|
||||
|
||||
// A fully transparent color is a deliberate choice — honor it by painting
|
||||
// nothing rather than falling back to the default background/border.
|
||||
if (color.alpha() === 0) {
|
||||
return TRANSPARENT_COLOR;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a throwaway field container that mirrors the real `FieldRootContainer`
|
||||
* (same classes + data attributes) so we can read whatever the active embed CSS
|
||||
* resolves for this field's state.
|
||||
*
|
||||
* `transition` is disabled because we read the computed style synchronously right
|
||||
* after attaching — leaving transitions on would surface mid-animation values.
|
||||
* `visibility: hidden` + zero size keep it invisible and out of layout flow
|
||||
* without using `display: none`, which would prevent border/background resolution.
|
||||
*/
|
||||
const createFieldProbeElement = (field: FieldToRender): HTMLElement => {
|
||||
const $probe = document.createElement('div');
|
||||
|
||||
$probe.className = FIELD_ROOT_CONTAINER_PROBE_CLASS_NAME;
|
||||
$probe.setAttribute('aria-hidden', 'true');
|
||||
|
||||
$probe.dataset.fieldType = field.type;
|
||||
$probe.dataset.inserted = field.inserted ? 'true' : 'false';
|
||||
$probe.dataset.validate = field.isValidating ? 'true' : 'false';
|
||||
$probe.dataset.readonly = field.fieldMeta?.readOnly ? 'true' : 'false';
|
||||
|
||||
Object.assign($probe.style, {
|
||||
position: 'absolute',
|
||||
width: '0',
|
||||
height: '0',
|
||||
overflow: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
visibility: 'hidden',
|
||||
transition: 'none',
|
||||
} satisfies Partial<CSSStyleDeclaration>);
|
||||
|
||||
return $probe;
|
||||
};
|
||||
|
||||
const computeFieldCanvasStyleFromProbe = (field: FieldToRender): FieldCanvasStyle | undefined => {
|
||||
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// The probe must be appended inside the same subtree as the real fields so it
|
||||
// inherits the identical CSS cascade. Custom embed CSS is typically scoped
|
||||
// under `.embed--DocumentContainer`; appending to `document.body` would resolve
|
||||
// a different (wrong) cascade. If the anchor is absent (non-embed contexts),
|
||||
// there is no custom field CSS to read, so we skip the probe entirely.
|
||||
const $anchor = document.querySelector(FIELD_PROBE_ANCHOR_SELECTOR);
|
||||
|
||||
if (!$anchor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const $probe = createFieldProbeElement(field);
|
||||
|
||||
$anchor.appendChild($probe);
|
||||
|
||||
try {
|
||||
const computedStyle = window.getComputedStyle($probe);
|
||||
const borderWidth = getPixelValue(computedStyle.borderTopWidth);
|
||||
const borderColor = getRenderableColor(computedStyle.borderTopColor);
|
||||
const hasBorderStyle = computedStyle.borderTopStyle !== 'none' && Boolean(borderWidth);
|
||||
const borderRadius = getPixelValue(computedStyle.borderTopLeftRadius);
|
||||
|
||||
return {
|
||||
backgroundColor: getRenderableColor(computedStyle.backgroundColor),
|
||||
borderColor: hasBorderStyle ? borderColor : undefined,
|
||||
borderRadius,
|
||||
borderWidth: hasBorderStyle ? borderWidth : undefined,
|
||||
opacity: getOpacityValue(computedStyle.opacity),
|
||||
};
|
||||
} finally {
|
||||
$probe.remove();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the canvas style for a field by reading a throwaway probe element's
|
||||
* computed CSS.
|
||||
*
|
||||
* Sign-mode only — the editor and export views intentionally use the renderer
|
||||
* defaults. Reads are cache-gated, so the probe is created/removed at most once
|
||||
* per unique field state per render pass.
|
||||
*/
|
||||
export const resolveFieldCanvasStyle = (
|
||||
field: FieldToRender,
|
||||
mode: FieldRenderMode,
|
||||
cache?: FieldCanvasStyleCache,
|
||||
): FieldCanvasStyle | undefined => {
|
||||
if (mode !== 'sign') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cacheKey = getFieldCanvasStyleCacheKey(field);
|
||||
|
||||
if (cache?.has(cacheKey)) {
|
||||
return cache.get(cacheKey);
|
||||
}
|
||||
|
||||
const style = computeFieldCanvasStyleFromProbe(field);
|
||||
|
||||
cache?.set(cacheKey, style);
|
||||
|
||||
return style;
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export const upsertFieldGroup = (field: FieldToRender, options: RenderFieldEleme
|
||||
x: fieldX,
|
||||
y: fieldY,
|
||||
draggable: editable,
|
||||
opacity: options.fieldCanvasStyle?.opacity ?? 1,
|
||||
dragBoundFunc: (pos) => {
|
||||
const newX = Math.max(0, Math.min(maxXPosition, pos.x));
|
||||
const newY = Math.max(0, Math.min(maxYPosition, pos.y));
|
||||
@@ -42,6 +43,7 @@ export const upsertFieldGroup = (field: FieldToRender, options: RenderFieldEleme
|
||||
|
||||
export const upsertFieldRect = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Rect => {
|
||||
const { pageWidth, pageHeight, mode, pageLayer, color } = options;
|
||||
const { fieldCanvasStyle } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
@@ -55,10 +57,10 @@ export const upsertFieldRect = (field: FieldToRender, options: RenderFieldElemen
|
||||
fieldRect.setAttrs({
|
||||
width: fieldWidth,
|
||||
height: fieldHeight,
|
||||
fill: DEFAULT_RECT_BACKGROUND,
|
||||
stroke: color ? getRecipientColorStyles(color).baseRing : '#e5e7eb',
|
||||
strokeWidth: 2,
|
||||
cornerRadius: 2,
|
||||
fill: fieldCanvasStyle?.backgroundColor ?? DEFAULT_RECT_BACKGROUND,
|
||||
stroke: fieldCanvasStyle?.borderColor ?? (color ? getRecipientColorStyles(color).baseRing : '#e5e7eb'),
|
||||
strokeWidth: fieldCanvasStyle?.borderWidth ?? 2,
|
||||
cornerRadius: fieldCanvasStyle?.borderRadius ?? 2,
|
||||
strokeScaleEnabled: false,
|
||||
visible: mode !== 'export',
|
||||
} satisfies Partial<Konva.RectConfig>);
|
||||
@@ -127,6 +129,10 @@ export const createFieldHoverInteraction = ({ options, fieldGroup, fieldRect }:
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.fieldCanvasStyle?.backgroundColor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hoverColor = getRecipientColorStyles(options.color).baseRingHover;
|
||||
|
||||
fieldGroup.on('mouseover', () => {
|
||||
|
||||
@@ -16,21 +16,42 @@ export type FieldToRender = Pick<
|
||||
height: number;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
isValidating?: boolean;
|
||||
fieldMeta?: TFieldMetaSchema | null;
|
||||
signature?: Pick<Signature, 'signatureImageAsBase64' | 'typedSignature'> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The render type.
|
||||
*
|
||||
* @default 'edit'
|
||||
*
|
||||
* - `edit` - The field is rendered in editor page.
|
||||
* - `sign` - The field is rendered for the signing page.
|
||||
* - `export` - The field is rendered for exporting and sealing into the PDF. No backgrounds, interactive elements, etc.
|
||||
*/
|
||||
export type FieldRenderMode = 'edit' | 'sign' | 'export';
|
||||
|
||||
export type RenderFieldElementOptions = {
|
||||
pageLayer: Konva.Layer;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
mode: 'edit' | 'sign' | 'export';
|
||||
mode: FieldRenderMode;
|
||||
editable?: boolean;
|
||||
scale: number;
|
||||
color?: TRecipientColor;
|
||||
fieldCanvasStyle?: FieldCanvasStyle;
|
||||
translations: Record<FieldType, string> | null;
|
||||
};
|
||||
|
||||
export type FieldCanvasStyle = {
|
||||
backgroundColor?: string;
|
||||
borderColor?: string;
|
||||
borderRadius?: number;
|
||||
borderWidth?: number;
|
||||
opacity?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a fields percentage based values to pixel based values.
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
||||
import type { Signature } from '@prisma/client';
|
||||
import { type Field, FieldType } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import type Konva from 'konva';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import type { TFieldMetaSchema } from '../../types/field-meta';
|
||||
import type { FieldCanvasStyleCache } from './field-canvas-style';
|
||||
import { resolveFieldCanvasStyle } from './field-canvas-style';
|
||||
import type { FieldRenderMode, FieldToRender } from './field-renderer';
|
||||
import { renderCheckboxFieldElement } from './render-checkbox-field';
|
||||
import { renderDropdownFieldElement } from './render-dropdown-field';
|
||||
import { renderGenericTextFieldElement } from './render-generic-text-field';
|
||||
@@ -14,30 +15,6 @@ import { renderSignatureFieldElement } from './render-signature-field';
|
||||
export const MIN_FIELD_HEIGHT_PX = 12;
|
||||
export const MIN_FIELD_WIDTH_PX = 36;
|
||||
|
||||
/**
|
||||
* The render type.
|
||||
*
|
||||
* @default 'edit'
|
||||
*
|
||||
* - `edit` - The field is rendered in editor page.
|
||||
* - `sign` - The field is rendered for the signing page.
|
||||
* - `export` - The field is rendered for exporting and sealing into the PDF. No backgrounds, interactive elements, etc.
|
||||
*/
|
||||
export type FieldRenderMode = 'edit' | 'sign' | 'export';
|
||||
|
||||
export type FieldToRender = Pick<
|
||||
Field,
|
||||
'envelopeItemId' | 'recipientId' | 'type' | 'page' | 'customText' | 'inserted' | 'recipientId'
|
||||
> & {
|
||||
renderId: string; // A unique ID for the field in the render.
|
||||
width: number;
|
||||
height: number;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
fieldMeta?: TFieldMetaSchema | null;
|
||||
signature?: Pick<Signature, 'signatureImageAsBase64' | 'typedSignature'> | null;
|
||||
};
|
||||
|
||||
type RenderFieldOptions = {
|
||||
field: FieldToRender;
|
||||
pageLayer: Konva.Layer;
|
||||
@@ -52,6 +29,7 @@ type RenderFieldOptions = {
|
||||
|
||||
scale: number;
|
||||
editable?: boolean;
|
||||
fieldCanvasStyleCache?: FieldCanvasStyleCache;
|
||||
};
|
||||
|
||||
export const renderField = ({
|
||||
@@ -64,6 +42,7 @@ export const renderField = ({
|
||||
scale,
|
||||
editable,
|
||||
color,
|
||||
fieldCanvasStyleCache,
|
||||
}: RenderFieldOptions) => {
|
||||
const options = {
|
||||
pageLayer,
|
||||
@@ -74,6 +53,7 @@ export const renderField = ({
|
||||
color,
|
||||
editable,
|
||||
scale,
|
||||
fieldCanvasStyle: resolveFieldCanvasStyle(field, mode, fieldCanvasStyleCache),
|
||||
};
|
||||
|
||||
// If the generic text field element array changes, update the `GenericTextFieldTypeMetas` type
|
||||
|
||||
@@ -7,6 +7,7 @@ import { type Field, FieldType } from '@prisma/client';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { FIELD_ROOT_CONTAINER_CLASS_NAME } from '../../lib/field-root-container-classes';
|
||||
import type { RecipientColorStyles } from '../../lib/recipient-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
@@ -117,7 +118,7 @@ export function FieldRootContainer({ field, children, color, className, readonly
|
||||
data-inserted={field.inserted ? 'true' : 'false'}
|
||||
data-readonly={readonly ? 'true' : 'false'}
|
||||
className={cn(
|
||||
'field--FieldRootContainer field-card-container dark-mode-disabled group relative z-20 flex h-full w-full items-center rounded-[2px] bg-white/90 ring-2 ring-gray-200 transition-all',
|
||||
FIELD_ROOT_CONTAINER_CLASS_NAME,
|
||||
color?.base,
|
||||
{
|
||||
'px-2': field.type !== FieldType.SIGNATURE && field.type !== FieldType.FREE_SIGNATURE,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
const FIELD_ROOT_CONTAINER_SHARED_CLASS_NAME =
|
||||
'field--FieldRootContainer field-card-container dark-mode-disabled group rounded-[2px] bg-white/90 ring-2 ring-gray-200 transition-all';
|
||||
|
||||
export const FIELD_ROOT_CONTAINER_CLASS_NAME = `${FIELD_ROOT_CONTAINER_SHARED_CLASS_NAME} relative z-20 flex h-full w-full items-center`;
|
||||
|
||||
export const FIELD_ROOT_CONTAINER_PROBE_CLASS_NAME = `field--FieldRootContainerProbe ${FIELD_ROOT_CONTAINER_SHARED_CLASS_NAME}`;
|
||||
|
||||
/**
|
||||
* Selector for the element the probe is appended into when reading computed
|
||||
* field styles. It must be an ancestor of where real fields render so the probe
|
||||
* inherits the same CSS cascade (custom embed CSS is commonly scoped under
|
||||
* `.embed--Root` / `.embed--DocumentContainer`).
|
||||
*/
|
||||
export const FIELD_PROBE_ANCHOR_SELECTOR = '.embed--DocumentContainer';
|
||||
@@ -61,6 +61,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk": "^0.2.1",
|
||||
"colord": "^2.9.3",
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-react": "^0.554.0",
|
||||
"luxon": "^3.7.2",
|
||||
|
||||
Reference in New Issue
Block a user