Initial commit: Korrith website, assessment API, PocketBase schema, Docker/Coolify deployment
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { getEditing } from "../../state/editing-state.js";
|
||||
import { stopFontSizeEdit, updateTextFormatToolbar, scheduleRepositionEditOverlays } from "../text-format/toolbar/toolbar.js";
|
||||
import { hideAllPanels } from "../../state/panel-state.js";
|
||||
import { resolveInlineStylesAtRange } from "../../utils/inline-style-state.js";
|
||||
import { EDITOR_UI_SELECTOR } from "../../constants/selectors.js";
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
* @param {HTMLElement} container
|
||||
* @returns {{ value: string, isGradient: boolean }}
|
||||
*/
|
||||
function resolveTextColor(element, container) {
|
||||
const color = getComputedStyle(element).color;
|
||||
|
||||
if (color !== 'rgba(0, 0, 0, 0)' && color !== 'transparent') {
|
||||
return { value: color, isGradient: false };
|
||||
}
|
||||
|
||||
let current = element;
|
||||
while (current && current !== container.parentElement) {
|
||||
const computedStyle = getComputedStyle(current);
|
||||
const backgroundImage = computedStyle.backgroundImage;
|
||||
const backgroundClip = computedStyle.backgroundClip || computedStyle['webkitBackgroundClip'];
|
||||
|
||||
if (backgroundImage && backgroundImage !== 'none' && backgroundImage.includes('gradient') && backgroundClip === 'text') {
|
||||
return { value: backgroundImage, isGradient: true };
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
return { value: color, isGradient: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves caret/selection presentation state for the formatting toolbar.
|
||||
* Bold, italic, and underline are detected independently via ancestor walks
|
||||
* so nested markup (`<em><u><strong>…</strong></u></em>`) reports all active styles.
|
||||
* @returns {object|null}
|
||||
*/
|
||||
export function getStylesAtCursor() {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return null;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
let node = range.startContainer;
|
||||
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const child = node.childNodes[range.startOffset];
|
||||
if (child) node = child;
|
||||
}
|
||||
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
node = node.parentElement;
|
||||
}
|
||||
|
||||
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
|
||||
node = getEditing()?.targetElement;
|
||||
}
|
||||
|
||||
if (!node) return null;
|
||||
|
||||
const container = getEditing()?.targetElement || node;
|
||||
const presentation = resolveInlineStylesAtRange(range, container);
|
||||
const styles = getComputedStyle(node);
|
||||
const containerStyles = getComputedStyle(container);
|
||||
const resolvedColor = resolveTextColor(node, container);
|
||||
|
||||
return {
|
||||
color: resolvedColor.value,
|
||||
isGradient: resolvedColor.isGradient,
|
||||
fontFamily: styles.fontFamily,
|
||||
fontSize: styles.fontSize,
|
||||
textAlign: containerStyles.textAlign,
|
||||
...presentation,
|
||||
};
|
||||
}
|
||||
|
||||
/** Updates toolbar and parent when the selection moves during inline edit. */
|
||||
export function onSelectionChange(force = false) {
|
||||
const editing = getEditing();
|
||||
if (!editing?.targetElement?.hasAttribute("contenteditable")) return;
|
||||
if (!force && document.activeElement?.closest(EDITOR_UI_SELECTOR)) return;
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0 && !editing.targetElement.contains(selection.getRangeAt(0).commonAncestorContainer)) {
|
||||
return;
|
||||
}
|
||||
stopFontSizeEdit();
|
||||
hideAllPanels();
|
||||
const styles = getStylesAtCursor();
|
||||
if (!styles) return;
|
||||
updateTextFormatToolbar(styles);
|
||||
scheduleRepositionEditOverlays();
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
import { getEditing, setEditing, clearEditing } from "../../state/editing-state.js";
|
||||
import { normalizeContentEditableHtml, patchRemoveChild, serializeContentEditableHtml } from "../../utils/html-utils.js";
|
||||
import { postToParent } from "../../utils/parent-frame.js";
|
||||
import { ParentMessage } from "../../constants/messages.js";
|
||||
import { beginEditSession, recordEdit } from "../../state/history-state.js";
|
||||
import { notifyDraftStateChanged } from "../../api/draft-snapshot.js";
|
||||
import { showTextFormatToolbar, hideTextFormatToolbar, scheduleRepositionEditOverlays } from "../text-format/toolbar/toolbar.js";
|
||||
import { onSelectionChange } from "./cursor-styles.js";
|
||||
import { captureElementMetadata } from "../../utils/selection-mode-metadata.js";
|
||||
|
||||
|
||||
/**
|
||||
* Converts a CSS property name to its camelCase JS form
|
||||
* (e.g. `background-clip` → `backgroundClip`), matching the keys used by the
|
||||
* JSX style objects in the source write-back.
|
||||
*/
|
||||
function cssPropertyToCamelCase(cssProperty) {
|
||||
return cssProperty.replace(/^-/, '').replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
const TRANSPARENT_TEXT_CARET_COLOR = "#6B7280";
|
||||
|
||||
function isTransparentColor(color) {
|
||||
if (!color || color === "transparent") return true;
|
||||
const match = color.replace(/\s+/g, "").match(/^rgba?\(([^)]+)\)$/i);
|
||||
if (!match) return false;
|
||||
const segments = match[1].split(",");
|
||||
return segments.length === 4 && parseFloat(segments[3]) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether the element or any descendant renders transparent text
|
||||
* (e.g. gradient text via `background-clip: text` + `color: transparent`),
|
||||
* where the caret would otherwise be invisible.
|
||||
* @param {HTMLElement} element
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasTransparentText(element) {
|
||||
if (isTransparentColor(getComputedStyle(element).color)) return true;
|
||||
return Array.from(element.querySelectorAll("*")).some(
|
||||
descendant => isTransparentColor(getComputedStyle(descendant).color)
|
||||
);
|
||||
}
|
||||
|
||||
/** `caret-color: currentColor` is invisible on transparent/gradient text, so use a visible caret there. */
|
||||
function applyEditingCaretColor(element) {
|
||||
if (hasTransparentText(element)) {
|
||||
element.style.caretColor = TRANSPARENT_TEXT_CARET_COLOR;
|
||||
} else {
|
||||
element.style.removeProperty("caret-color");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshots every style property set inline on the element itself.
|
||||
* @param {HTMLElement} element
|
||||
* @returns {Record<string, string>} camelCase property → value
|
||||
*/
|
||||
function captureInlineStyles(element) {
|
||||
const styles = {};
|
||||
for (let index = 0; index < element.style.length; index++) {
|
||||
const cssProperty = element.style.item(index);
|
||||
styles[cssPropertyToCamelCase(cssProperty)] = element.style.getPropertyValue(cssProperty);
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diffs the element's current inline styles against a snapshot, covering any
|
||||
* panel action that styles the element itself ('' marks a removed property).
|
||||
* `textAlign` is excluded here — it is compared against computed style instead,
|
||||
* so re-applying an alignment inherited from CSS is not logged as a change.
|
||||
* @param {HTMLElement} element
|
||||
* @param {Record<string, string>} original
|
||||
* @returns {{ changed: Record<string, string>, previous: Record<string, string> }|null}
|
||||
*/
|
||||
function getInlineStyleDiff(element, original) {
|
||||
const current = captureInlineStyles(element);
|
||||
const changed = {};
|
||||
const previous = {};
|
||||
for (const key of new Set([...Object.keys(current), ...Object.keys(original)])) {
|
||||
if (key === 'textAlign') continue;
|
||||
// Transient editing caret-color must never be written back to source.
|
||||
if (key === 'caretColor') continue;
|
||||
if ((current[key] ?? '') !== (original[key] ?? '')) {
|
||||
changed[key] = current[key] ?? '';
|
||||
previous[key] = original[key] ?? '';
|
||||
}
|
||||
}
|
||||
return Object.keys(changed).length > 0 ? { changed, previous } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes element-level style changes (alignment plus any other inline style
|
||||
* the panel set on the element) since the edit session's last snapshot.
|
||||
* @param {object} editing - Current editing state.
|
||||
* @returns {{ style: object|null }}
|
||||
*/
|
||||
function buildStyleDiff(editing) {
|
||||
const { targetElement, originalTextAlign, originalInlineTextAlign, originalInlineStyles } = editing;
|
||||
const alignChanged = getComputedStyle(targetElement).textAlign !== originalTextAlign;
|
||||
const inlineDiff = getInlineStyleDiff(targetElement, originalInlineStyles);
|
||||
if (!alignChanged && !inlineDiff) return { style: null, oldStyle: null };
|
||||
|
||||
const style = {};
|
||||
const oldStyle = {};
|
||||
if (alignChanged) {
|
||||
style.textAlign = targetElement.style.textAlign;
|
||||
// Keep previous inline value around so re-applying the inherited default still serializes.
|
||||
if (!style.textAlign) style.textAlign = originalInlineTextAlign ?? '';
|
||||
oldStyle.textAlign = originalInlineTextAlign ?? '';
|
||||
}
|
||||
if (inlineDiff) {
|
||||
Object.assign(style, inlineDiff.changed);
|
||||
Object.assign(oldStyle, inlineDiff.previous);
|
||||
}
|
||||
return { style, oldStyle };
|
||||
}
|
||||
|
||||
/** Re-snapshots the editing state's style baselines after a soft-save. */
|
||||
function refreshStyleSnapshots(editing) {
|
||||
const { targetElement } = editing;
|
||||
editing.originalTextAlign = getComputedStyle(targetElement).textAlign;
|
||||
editing.originalInlineTextAlign = targetElement.style.textAlign || '';
|
||||
editing.originalInlineStyles = captureInlineStyles(targetElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshots child nodes so DOM mutations during edit can be restored if needed.
|
||||
* @param {HTMLElement} element
|
||||
* @returns {Array<{ node: Node, value: string|null }>}
|
||||
*/
|
||||
export function saveChildNodes(element) {
|
||||
const saved = [];
|
||||
for (const child of element.childNodes) {
|
||||
saved.push({
|
||||
node: child,
|
||||
value: child.nodeType === Node.TEXT_NODE ? child.nodeValue : null,
|
||||
});
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Places a collapsed caret at the end of the editable container's contents.
|
||||
* @param {HTMLElement} container
|
||||
*/
|
||||
export function placeCursorAtEnd(container) {
|
||||
if (!container.childNodes.length) {
|
||||
container.appendChild(document.createTextNode(''));
|
||||
}
|
||||
|
||||
let target = container;
|
||||
while (target.lastChild) {
|
||||
target = target.lastChild;
|
||||
}
|
||||
|
||||
const range = document.createRange();
|
||||
if (target.nodeType === Node.TEXT_NODE) {
|
||||
range.setStart(target, target.textContent.length);
|
||||
} else {
|
||||
range.selectNodeContents(target);
|
||||
}
|
||||
range.collapse(false);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Places the text caret at viewport coordinates, clamped to the editable container.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {HTMLElement} [container]
|
||||
*/
|
||||
export function placeCursorAtPoint(x, y, container) {
|
||||
let range = null;
|
||||
if (document.caretPositionFromPoint) {
|
||||
const position = document.caretPositionFromPoint(x, y);
|
||||
if (position) {
|
||||
range = document.createRange();
|
||||
range.setStart(position.offsetNode, position.offset);
|
||||
range.collapse(true);
|
||||
}
|
||||
} else if (document.caretRangeFromPoint) {
|
||||
range = document.caretRangeFromPoint(x, y);
|
||||
}
|
||||
|
||||
// If the caret resolved outside our editable target (e.g. an absolute overlay
|
||||
// sat on top of the click point), fall back to placing it at the end of the
|
||||
// target so typing actually goes into the contenteditable.
|
||||
if (container && (!range || !container.contains(range.startContainer))) {
|
||||
range = document.createRange();
|
||||
range.selectNodeContents(container);
|
||||
range.collapse(false);
|
||||
}
|
||||
|
||||
if (!range) return;
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables contenteditable on the target and wires edit listeners and toolbar.
|
||||
* @param {HTMLElement} targetElement
|
||||
* @param {string} editId
|
||||
* @param {string} currentContent - Initial HTML content.
|
||||
* @param {{ focusTarget?: boolean }} [options] - Pass `focusTarget: false` when the caller focuses its own UI instead.
|
||||
*/
|
||||
export function startInlineEdit(targetElement, editId, currentContent, { focusTarget = true } = {}) {
|
||||
if (getEditing()?.targetElement === targetElement) return;
|
||||
|
||||
const savedChildren = saveChildNodes(targetElement);
|
||||
const normalizedOriginal = normalizeContentEditableHtml(currentContent);
|
||||
const sessionId = beginEditSession();
|
||||
const selectionMode = captureElementMetadata(targetElement);
|
||||
setEditing({
|
||||
editId,
|
||||
targetElement,
|
||||
sessionId,
|
||||
originalContent: normalizedOriginal,
|
||||
originalTextAlign: getComputedStyle(targetElement).textAlign,
|
||||
originalInlineTextAlign: targetElement.style.textAlign || '',
|
||||
originalInlineStyles: captureInlineStyles(targetElement),
|
||||
savedChildren,
|
||||
selectionMode,
|
||||
});
|
||||
document.getElementById("root")?.setAttribute("data-inline-editing", "true");
|
||||
targetElement.setAttribute("data-editing", "true");
|
||||
patchRemoveChild(targetElement);
|
||||
|
||||
targetElement.setAttribute("contenteditable", "true");
|
||||
applyEditingCaretColor(targetElement);
|
||||
|
||||
if (focusTarget) {
|
||||
targetElement.focus();
|
||||
placeCursorAtEnd(targetElement);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (getEditing()?.targetElement !== targetElement) return;
|
||||
placeCursorAtEnd(targetElement);
|
||||
targetElement.focus();
|
||||
});
|
||||
}
|
||||
|
||||
postToParent(ParentMessage.EDIT_ENTER, {
|
||||
currentText: currentContent,
|
||||
elementType: targetElement.tagName.toLowerCase(),
|
||||
isDirectlyEditable: targetElement.hasAttribute('data-edit-id'),
|
||||
});
|
||||
showTextFormatToolbar(targetElement);
|
||||
targetElement.addEventListener("keydown", onInlineEditKeyDown);
|
||||
targetElement.addEventListener("beforeinput", onInlineEditBeforeInput);
|
||||
targetElement.addEventListener("input", onInlineEditInput);
|
||||
targetElement.addEventListener("tft-color-commit", saveCurrentEdit);
|
||||
document.addEventListener("selectionchange", onSelectionChange);
|
||||
setTimeout(onSelectionChange, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-saves the current edit without tearing down contenteditable (e.g. color commit),
|
||||
* recording it into the in-iframe edit history so it can be undone/redone and
|
||||
* later batch-persisted on draft save.
|
||||
*/
|
||||
export function saveCurrentEdit() {
|
||||
const editing = getEditing();
|
||||
if (!editing) return;
|
||||
|
||||
const { editId, targetElement, originalContent, sessionId, selectionMode } = editing;
|
||||
const newHTML = serializeContentEditableHtml(targetElement);
|
||||
const { style, oldStyle } = buildStyleDiff(editing);
|
||||
const isAssisted = targetElement.hasAttribute('data-edit-assisted-id');
|
||||
|
||||
if (newHTML !== originalContent || style) {
|
||||
recordEdit(editId, { beforeContent: originalContent, afterContent: newHTML }, { style, oldStyle, element: targetElement, sessionId, isAssisted, selectionMode });
|
||||
notifyDraftStateChanged();
|
||||
editing.originalContent = newHTML;
|
||||
editing.hasSoftSaved = true;
|
||||
refreshStyleSnapshots(editing);
|
||||
}
|
||||
}
|
||||
|
||||
/** Finalizes the active inline edit, records it into history, and clears editing state. */
|
||||
export function commitCurrentEdit() {
|
||||
const editing = getEditing();
|
||||
if (!editing) return;
|
||||
|
||||
const { editId, targetElement, originalContent, sessionId, selectionMode } = editing;
|
||||
const newHTML = serializeContentEditableHtml(targetElement);
|
||||
const { style, oldStyle } = buildStyleDiff(editing);
|
||||
const isAssisted = targetElement.hasAttribute('data-edit-assisted-id');
|
||||
|
||||
targetElement.removeAttribute("contenteditable");
|
||||
targetElement.removeAttribute("data-editing");
|
||||
targetElement.style.removeProperty("caret-color");
|
||||
document.getElementById("root")?.removeAttribute("data-inline-editing");
|
||||
targetElement.removeEventListener("keydown", onInlineEditKeyDown);
|
||||
targetElement.removeEventListener("beforeinput", onInlineEditBeforeInput);
|
||||
targetElement.removeEventListener("input", onInlineEditInput);
|
||||
targetElement.removeEventListener("tft-color-commit", saveCurrentEdit);
|
||||
document.removeEventListener("selectionchange", onSelectionChange);
|
||||
hideTextFormatToolbar();
|
||||
|
||||
patchRemoveChild(targetElement);
|
||||
targetElement.innerHTML = newHTML;
|
||||
|
||||
const { hasSoftSaved } = editing;
|
||||
|
||||
if (newHTML !== originalContent || style) {
|
||||
recordEdit(editId, { beforeContent: originalContent, afterContent: newHTML }, { style, oldStyle, element: targetElement, sessionId, isAssisted, selectionMode });
|
||||
notifyDraftStateChanged();
|
||||
} else if (!hasSoftSaved) {
|
||||
postToParent(ParentMessage.EDIT_CANCEL, {});
|
||||
}
|
||||
|
||||
clearEditing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks browser replacement-text input during inline edit.
|
||||
* @param {InputEvent} e
|
||||
*/
|
||||
export function onInlineEditBeforeInput(event) {
|
||||
if (event.inputType === "insertReplacementText") {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
scheduleRepositionEditOverlays();
|
||||
}
|
||||
|
||||
/** Repositions the toolbar as the editable content grows/shrinks. */
|
||||
export function onInlineEditInput() {
|
||||
if (!getEditing()) return;
|
||||
saveCurrentEdit();
|
||||
scheduleRepositionEditOverlays();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Escape/Enter to commit and stops propagation for other keys.
|
||||
* @param {KeyboardEvent} e
|
||||
*/
|
||||
export function onInlineEditKeyDown(event) {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
commitCurrentEdit();
|
||||
return;
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
if (event.shiftKey) {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
commitCurrentEdit();
|
||||
return;
|
||||
}
|
||||
if (event.key === " " && event.currentTarget.tagName.toLowerCase() === "button") {
|
||||
// Native <button> treats Space as activation, not text input, so the
|
||||
// browser never inserts a character. Prevent the activation and insert
|
||||
// the space manually (execCommand keeps the input event).
|
||||
event.preventDefault();
|
||||
document.execCommand("insertText", false, " ");
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { EDIT_BORDER_COLOR, EDIT_BACKGROUND_COLOR, PANEL_BG, BORDER_COLOR, COLOR_WHITE, Z_INDEX_EDITOR_OVERLAY } from '../../constants/theme.js';
|
||||
import { ICON_TEXT_EDIT_CURSOR } from '../../constants/icons.js';
|
||||
import { HOVER_OUTLINE_STROKE_WIDTH } from '../../constants/layout.js';
|
||||
import { EDITOR_UI_SELECTOR } from '../../constants/selectors.js';
|
||||
|
||||
/** Injected CSS for edit-mode hover outlines, contenteditable affordances, and the element-type tooltip. */
|
||||
export const EDIT_MODE_STYLES = `
|
||||
#root[data-edit-mode-enabled="true"] [data-edit-id],
|
||||
#root[data-edit-mode-enabled="true"] [data-edit-assisted-id] {
|
||||
cursor: default !important;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
#root[data-edit-mode-enabled="true"] [data-edit-id][contenteditable="true"],
|
||||
#root[data-edit-mode-enabled="true"] [data-edit-assisted-id][contenteditable="true"] {
|
||||
outline: 1px solid ${EDIT_BORDER_COLOR};
|
||||
caret-color: currentColor;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
cursor: url('data:image/svg+xml,${ICON_TEXT_EDIT_CURSOR}') 12 12, text !important;
|
||||
}
|
||||
#root[data-edit-mode-enabled="true"] [data-edit-id][contenteditable="true"] *,
|
||||
#root[data-edit-mode-enabled="true"] [data-edit-assisted-id][contenteditable="true"] * {
|
||||
cursor: url('data:image/svg+xml,${ICON_TEXT_EDIT_CURSOR}') 12 12, text !important;
|
||||
}
|
||||
|
||||
/* Suppress native cursor on actionable elements while editing. */
|
||||
#root[data-edit-mode-enabled="true"] a,
|
||||
#root[data-edit-mode-enabled="true"] button,
|
||||
#root[data-edit-mode-enabled="true"] [role="button"],
|
||||
#root[data-edit-mode-enabled="true"] [type="submit"] {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.inline-editor-hover-outline,
|
||||
.inline-editor-locked-outline {
|
||||
outline-offset: -${HOVER_OUTLINE_STROKE_WIDTH}px;
|
||||
box-shadow: inset 0 0 0 999px ${EDIT_BACKGROUND_COLOR};
|
||||
}
|
||||
|
||||
.inline-editor-hover-outline {
|
||||
outline: ${HOVER_OUTLINE_STROKE_WIDTH}px dashed ${EDIT_BORDER_COLOR};
|
||||
}
|
||||
|
||||
.inline-editor-locked-outline {
|
||||
outline: ${HOVER_OUTLINE_STROKE_WIDTH}px solid ${EDIT_BORDER_COLOR};
|
||||
}
|
||||
|
||||
@keyframes element-type-tooltip-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.element-type-tooltip {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: ${PANEL_BG};
|
||||
color: ${COLOR_WHITE};
|
||||
padding: 0 4px;
|
||||
border-radius: 6px;
|
||||
font-family: DM Sans, sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
border: 1px solid ${BORDER_COLOR};
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
z-index: ${Z_INDEX_EDITOR_OVERLAY};
|
||||
}
|
||||
|
||||
.element-type-tooltip.active {
|
||||
display: block;
|
||||
animation: element-type-tooltip-fade-in 0.2s ease-out forwards;
|
||||
}
|
||||
|
||||
${EDITOR_UI_SELECTOR} {
|
||||
pointer-events: auto;
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user