mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
Closes #6470 Closes #4806 Closes #4392 Closes #4804 Closes #4616 Closes #4863 Closes #4912 Ref #1798 Advanced styles now uses a CSS block editor in the panel and a centered editor. Local declarations are editable. Cascaded or inherited, preset, and global declarations are grouped below them and are read-only. The editor suggests properties and values, highlights search matches, shows source details and color swatches, and previews valid local edits on the canvas. Keyframe CSS declarations use the reusable local block editor. The old Add styles field, property-row editor, declaration menu actions, and Navigator CSS Preview are removed. ### Delivery checklist - [x] Edit and validate local CSS declarations, including shorthand display and likely typo hints - [x] Show other style sources with source colors, tooltips, and read-only protection - [x] Add search navigation, color swatches, and the centered editor shortcut - [x] Reuse the block editor for animation keyframes and preserve transform controls - [x] Preserve uploaded background layers when editing gradients or reordering layers - [x] Remove superseded editor and Navigator UI - [x] Review and update the Style panel and Navigator guides - [x] Remove the outdated CSS Preview sentence from the Builder anatomy guide - [x] Rename the stale Code view test title to match the Advanced editor - [x] Regenerate fixtures and keep the Vercel build manifest through regeneration - [x] Resolve review threads and verify required CI ### Implementation notes Styles remain stored as individual properties; equivalent shorthand text is reconstructed for editing. Unknown property names become CSS variables unless they resemble a known property, in which case the editor warns and skips the typo. Editing is limited to the selected local style block; read-only source groups remain visible for reference. Existing uploaded image and hidden background layers are retained when neighboring layers change. No schema migration is needed. The Vercel manifest setting lives in the React Router template, so fixture regeneration produces a clean worktree. The shared template also generates the setting for the Docker fixture. ### Verification On commit `7fb44d358`, both fixture checks, `check-size`, the main workflow, all ten end-to-end shards, and the label-approved visual comparison passed. The fixture regeneration, Vercel build, Builder typecheck, focused lint, and formatting checks passed locally before the final documentation and test-title cleanup; `git diff --check` passed afterward. No local tests were run for the final cleanup, which changes no behavior.
689 lines
18 KiB
TypeScript
689 lines
18 KiB
TypeScript
import {
|
|
useEffect,
|
|
useRef,
|
|
type ReactNode,
|
|
forwardRef,
|
|
type ComponentProps,
|
|
type RefObject,
|
|
useImperativeHandle,
|
|
} from "react";
|
|
import {
|
|
Annotation,
|
|
Compartment,
|
|
EditorSelection,
|
|
EditorState,
|
|
StateEffect,
|
|
Transaction,
|
|
type Extension,
|
|
} from "@codemirror/state";
|
|
import {
|
|
EditorView,
|
|
drawSelection,
|
|
dropCursor,
|
|
lineNumbers,
|
|
keymap,
|
|
} from "@codemirror/view";
|
|
import {
|
|
defaultKeymap,
|
|
history,
|
|
historyKeymap,
|
|
indentWithTab,
|
|
} from "@codemirror/commands";
|
|
import { foldGutter, syntaxHighlighting } from "@codemirror/language";
|
|
import {
|
|
theme,
|
|
textVariants,
|
|
css,
|
|
cssVar,
|
|
selectedItemBackground,
|
|
declareCssVar,
|
|
IconButton,
|
|
Grid,
|
|
Flex,
|
|
rawTheme,
|
|
Kbd,
|
|
Text,
|
|
FloatingPanel,
|
|
PanelContent,
|
|
} from "@webstudio-is/design-system";
|
|
import { MaximizeIcon } from "@webstudio-is/icons";
|
|
import { ChevronDownIcon, ChevronRightIcon } from "@webstudio-is/icons/svg";
|
|
import { codeHighlightStyle } from "./code-highlight";
|
|
|
|
// This undocumented flag is required to keep contenteditable fields editable after the first activation of EditorView.
|
|
// To reproduce the issue, open any Binding dialog and then try to edit a Navigation Item in the Navigation menu.
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-expect-error
|
|
EditorView.EDIT_CONTEXT = false;
|
|
|
|
export const ExternalChange = Annotation.define<boolean>();
|
|
|
|
const minHeightVar = declareCssVar("--code-editor-min-height");
|
|
const maxHeightVar = declareCssVar("--code-editor-max-height");
|
|
const maximizeIconVisibilityVar = declareCssVar(
|
|
"--code-editor-maximize-icon-visibility"
|
|
);
|
|
|
|
export const clampEditorSelection = (
|
|
selection: EditorSelection,
|
|
length: number
|
|
) => {
|
|
return EditorSelection.create(
|
|
selection.ranges.map((range) =>
|
|
EditorSelection.range(
|
|
Math.min(range.anchor, length),
|
|
Math.min(range.head, length)
|
|
)
|
|
),
|
|
selection.mainIndex
|
|
);
|
|
};
|
|
|
|
export const normalizeEditorValue = (value: undefined | string) => value ?? "";
|
|
|
|
export const getCodeEditorCssVars = ({
|
|
minHeight,
|
|
maxHeight,
|
|
}: {
|
|
minHeight: string;
|
|
maxHeight: string;
|
|
}) => ({
|
|
[minHeightVar]: minHeight,
|
|
[maxHeightVar]: maxHeight,
|
|
});
|
|
|
|
const editorContentStyle = css({
|
|
...textVariants.mono,
|
|
// fit editor into parent if stretched
|
|
display: "flex",
|
|
position: "relative",
|
|
minHeight: 0,
|
|
boxSizing: "border-box",
|
|
color: cssVar("--foreground-primary"),
|
|
borderRadius: theme.borderRadius[4],
|
|
border: `1px solid transparent`,
|
|
background: cssVar("--background-secondary"),
|
|
paddingTop: 4,
|
|
paddingBottom: 2,
|
|
paddingRight: theme.spacing[2],
|
|
paddingLeft: theme.spacing[3],
|
|
// required to support copying selected text
|
|
userSelect: "text",
|
|
"&:hover": {
|
|
borderColor: cssVar("--border-default"),
|
|
},
|
|
"&:focus-within": {
|
|
borderColor: cssVar("--border-focus"),
|
|
},
|
|
'&[data-chromeless="true"]': {
|
|
borderRadius: 0,
|
|
},
|
|
'&[data-chromeless="true"]:not([data-invalid="true"]):is(:hover, :focus-within)':
|
|
{
|
|
borderColor: "transparent",
|
|
},
|
|
'&[data-invalid="true"]': {
|
|
borderColor: cssVar("--border-negative"),
|
|
outlineColor: cssVar("--border-negative"),
|
|
},
|
|
"[data-editor-dialog-content] &": {
|
|
background: "transparent",
|
|
borderWidth: 0,
|
|
padding: 0,
|
|
},
|
|
"& .cm-focused": {
|
|
outline: "none",
|
|
},
|
|
"& .cm-cursor, & .cm-dropCursor": {
|
|
borderLeftColor: "currentColor",
|
|
},
|
|
"& .cm-cursor": {
|
|
marginLeft: 0,
|
|
},
|
|
"& .cm-activeLine": {
|
|
backgroundColor: cssVar("--overlay-interaction-hover"),
|
|
},
|
|
"& .cm-selectionBackground, & .cm-content ::selection": {
|
|
backgroundColor: `${cssVar("--background-text-selection")} !important`,
|
|
},
|
|
"& .cm-content ::selection": {
|
|
color: `${cssVar("--foreground-on-text-selection")} !important`,
|
|
},
|
|
// fix scrolls appear on mount
|
|
"& .cm-scroller": {
|
|
overflowX: "hidden",
|
|
},
|
|
"& .cm-content": {
|
|
padding: 0,
|
|
// makes sure you can click to focus when editor content is smaller than the container
|
|
minHeight: "100%",
|
|
},
|
|
"& .cm-line": {
|
|
padding: 0,
|
|
},
|
|
"& .cm-editor": {
|
|
width: "100%",
|
|
// avoid modifying height in .cm-content
|
|
// because it breaks scroll events and makes scrolling laggy
|
|
minHeight: cssVar(minHeightVar, "auto"),
|
|
maxHeight: cssVar(maxHeightVar, "none"),
|
|
},
|
|
"fieldset[disabled] & .cm-editor": {
|
|
opacity: 0.3,
|
|
},
|
|
".cm-lintRange-error": {
|
|
textDecoration: `underline wavy ${cssVar("--foreground-negative")}`,
|
|
backgroundColor: cssVar("--background-negative-subtle"),
|
|
},
|
|
".cm-lintRange-warning": {
|
|
textDecoration: `underline wavy ${cssVar("--foreground-warning")}`,
|
|
backgroundColor: cssVar("--background-warning-subtle"),
|
|
},
|
|
".cm-gutters": {
|
|
backgroundColor: "transparent",
|
|
border: 0,
|
|
},
|
|
});
|
|
|
|
const shortcutStyle = css({
|
|
position: "absolute",
|
|
left: 0,
|
|
bottom: 0,
|
|
width: "100%",
|
|
paddingInline: theme.spacing[3],
|
|
background: `oklch(from ${cssVar("--background-primary")} l c h / 50%)`,
|
|
zIndex: 1,
|
|
pointerEvents: "none",
|
|
});
|
|
|
|
const autocompletionTooltipTheme = EditorView.theme({
|
|
".cm-tooltip.cm-tooltip-autocomplete": {
|
|
...textVariants.mono,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
width: "max-content",
|
|
minWidth: "160px",
|
|
maxWidth: "260px",
|
|
boxSizing: "border-box",
|
|
borderRadius: rawTheme.borderRadius[6],
|
|
border: `1px solid ${cssVar("--border-default")}`,
|
|
backgroundColor: cssVar("--background-primary"),
|
|
boxShadow: rawTheme.shadows.menuDropShadow,
|
|
padding: `${rawTheme.spacing[3]} 0`,
|
|
// override none set on body by radix popover
|
|
pointerEvents: "auto",
|
|
},
|
|
".cm-tooltip.cm-tooltip-autocomplete ul": {
|
|
width: "100%",
|
|
boxSizing: "border-box",
|
|
maxHeight: rawTheme.spacing[34],
|
|
padding: 0,
|
|
order: 1,
|
|
},
|
|
".cm-tooltip.cm-tooltip-autocomplete ul li": {
|
|
...textVariants.labels,
|
|
textTransform: "none",
|
|
position: "relative",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
color: cssVar("--foreground-primary"),
|
|
marginInline: rawTheme.spacing[3],
|
|
padding: rawTheme.spacing[3],
|
|
paddingLeft: rawTheme.spacing[11],
|
|
borderRadius: rawTheme.borderRadius[3],
|
|
},
|
|
".cm-tooltip.cm-tooltip-autocomplete li:hover": {
|
|
color: cssVar("--foreground-primary"),
|
|
backgroundColor: cssVar("--overlay-interaction-hover"),
|
|
},
|
|
'.cm-tooltip.cm-tooltip-autocomplete li[aria-selected="true"]': {
|
|
backgroundColor: selectedItemBackground,
|
|
},
|
|
".cm-tooltip.cm-tooltip-autocomplete .cm-completionLabel": {
|
|
flexGrow: 1,
|
|
},
|
|
".cm-tooltip.cm-tooltip-autocomplete .cm-completionDetail": {
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
fontStyle: "normal",
|
|
color: cssVar("--foreground-secondary"),
|
|
},
|
|
".cm-tooltip.cm-completionInfo": {
|
|
position: "static",
|
|
width: "100%",
|
|
maxWidth: "100%",
|
|
boxSizing: "border-box",
|
|
border: "none",
|
|
borderTop: `1px solid ${cssVar("--border-default")}`,
|
|
backgroundColor: "transparent",
|
|
boxShadow: "none",
|
|
order: 2,
|
|
padding: 0,
|
|
whiteSpace: "normal",
|
|
},
|
|
".cm-tooltip.cm-completionInfo.cm-completionInfo-above": {
|
|
borderTop: "none",
|
|
borderBottom: `1px solid ${cssVar("--border-default")}`,
|
|
order: 0,
|
|
},
|
|
});
|
|
|
|
const keyBindings = [
|
|
...defaultKeymap.filter((binding) => {
|
|
// We are redefining it later and CodeMirror won't take an override
|
|
return binding.key !== "Mod-Enter";
|
|
}),
|
|
...historyKeymap,
|
|
indentWithTab,
|
|
];
|
|
|
|
export const foldGutterExtension = foldGutter({
|
|
markerDOM: (isOpen) => {
|
|
const div = document.createElement("div");
|
|
div.style.width = "16px";
|
|
div.style.height = "16px";
|
|
div.style.cursor = "pointer";
|
|
div.innerHTML = isOpen ? ChevronDownIcon : ChevronRightIcon;
|
|
return div;
|
|
},
|
|
});
|
|
|
|
export const linterTooltipTheme = EditorView.theme({
|
|
".cm-tooltip:has(.cm-tooltip-lint)": {
|
|
backgroundColor: "transparent",
|
|
borderWidth: 0,
|
|
paddingTop: rawTheme.spacing[5],
|
|
paddingBottom: rawTheme.spacing[5],
|
|
pointerEvents: "none",
|
|
},
|
|
".cm-tooltip-lint": {
|
|
backgroundColor: cssVar("--background-inverse"),
|
|
color: cssVar("--foreground-on-inverse"),
|
|
borderRadius: rawTheme.borderRadius[7],
|
|
padding: rawTheme.spacing[5],
|
|
},
|
|
".cm-tooltip-lint .cm-diagnostic": {
|
|
borderWidth: 0,
|
|
padding: 0,
|
|
margin: 0,
|
|
...textVariants.regular,
|
|
},
|
|
});
|
|
|
|
export type EditorApi = {
|
|
replaceSelection: (string: string) => void;
|
|
insertTemplate: (template: {
|
|
prefix: string;
|
|
suffix?: string;
|
|
placeholder: string;
|
|
}) => void;
|
|
focus: () => void;
|
|
};
|
|
|
|
export const getTemplateInsertion = ({
|
|
from,
|
|
selectedText,
|
|
prefix,
|
|
suffix = "",
|
|
placeholder,
|
|
}: {
|
|
from: number;
|
|
selectedText: string;
|
|
prefix: string;
|
|
suffix?: string;
|
|
placeholder: string;
|
|
}) => {
|
|
const content = selectedText || placeholder;
|
|
const selectionFrom = from + prefix.length;
|
|
return {
|
|
text: `${prefix}${content}${suffix}`,
|
|
selection: EditorSelection.range(
|
|
selectionFrom,
|
|
selectionFrom + content.length
|
|
),
|
|
};
|
|
};
|
|
|
|
type EditorContentProps = {
|
|
editorApiRef?: RefObject<undefined | EditorApi>;
|
|
ariaLabel?: string;
|
|
extensions?: Extension[];
|
|
readOnly?: boolean;
|
|
autoFocus?: boolean;
|
|
initialSelection?: number;
|
|
invalid?: boolean;
|
|
showShortcuts?: boolean;
|
|
showLineNumbers?: boolean;
|
|
chromeless?: boolean;
|
|
resetHistoryOnExternalChange?: boolean;
|
|
value?: string;
|
|
onChange: (value: string) => void;
|
|
onChangeComplete: (value: string, event?: FocusEvent) => void;
|
|
};
|
|
|
|
export const EditorContent = ({
|
|
editorApiRef,
|
|
ariaLabel,
|
|
extensions = [],
|
|
readOnly = false,
|
|
autoFocus = false,
|
|
initialSelection,
|
|
invalid = false,
|
|
showShortcuts = false,
|
|
showLineNumbers = true,
|
|
chromeless = false,
|
|
resetHistoryOnExternalChange = false,
|
|
value,
|
|
onChange,
|
|
onChangeComplete,
|
|
}: EditorContentProps) => {
|
|
const editorRef = useRef<HTMLDivElement>(null);
|
|
const viewRef = useRef<undefined | EditorView>(undefined);
|
|
const historyCompartment = useRef(new Compartment());
|
|
|
|
const onChangeRef = useRef(onChange);
|
|
onChangeRef.current = onChange;
|
|
const onChangeCompleteRef = useRef(onChangeComplete);
|
|
onChangeCompleteRef.current = onChangeComplete;
|
|
|
|
useEffect(() => {
|
|
const abortController = new AbortController();
|
|
|
|
document.addEventListener(
|
|
// https://github.com/radix-ui/primitives/blob/dac4fd8ab0c1974020e316c865db258ab10d2279/packages/react/dismissable-layer/src/DismissableLayer.tsx#L14
|
|
"dismissableLayer.pointerDownOutside",
|
|
(event) => {
|
|
if (
|
|
event.target instanceof Element &&
|
|
// Prevent radix dialogs and popups from closing when clicking on the editor's autocomplete items
|
|
event.target.closest(".cm-tooltip.cm-tooltip-autocomplete")
|
|
) {
|
|
event.preventDefault();
|
|
}
|
|
},
|
|
{
|
|
capture: true,
|
|
signal: abortController.signal,
|
|
}
|
|
);
|
|
return () => {
|
|
abortController.abort();
|
|
};
|
|
}, []);
|
|
|
|
// setup editor
|
|
useEffect(() => {
|
|
if (editorRef.current === null) {
|
|
return;
|
|
}
|
|
const view = new EditorView({
|
|
doc: "",
|
|
parent: editorRef.current,
|
|
});
|
|
|
|
viewRef.current = view;
|
|
return () => {
|
|
view.destroy();
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (autoFocus) {
|
|
viewRef.current?.focus();
|
|
}
|
|
}, [autoFocus]);
|
|
|
|
// update extensions whenever variables data is changed
|
|
|
|
useEffect(() => {
|
|
const view = viewRef.current;
|
|
if (view === undefined) {
|
|
return;
|
|
}
|
|
const hasDisabledFieldset =
|
|
editorRef.current?.closest("fieldset[disabled]");
|
|
|
|
view.dispatch({
|
|
effects: StateEffect.reconfigure.of([
|
|
...(showLineNumbers ? [lineNumbers()] : []),
|
|
...extensions,
|
|
...(hasDisabledFieldset ? [EditorView.editable.of(false)] : []),
|
|
autocompletionTooltipTheme,
|
|
historyCompartment.current.of(history()),
|
|
drawSelection(),
|
|
dropCursor(),
|
|
syntaxHighlighting(codeHighlightStyle, { fallback: true }),
|
|
keymap.of([
|
|
...keyBindings,
|
|
{
|
|
key: "Mod-Enter",
|
|
run(view) {
|
|
onChangeCompleteRef.current(view.state.doc.toString());
|
|
return true;
|
|
},
|
|
},
|
|
{
|
|
key: "Mod-s",
|
|
run(view) {
|
|
onChangeCompleteRef.current(view.state.doc.toString());
|
|
return true;
|
|
},
|
|
},
|
|
]),
|
|
EditorView.lineWrapping,
|
|
// Read-only editors must keep keyboard focus instead of leaving
|
|
// destructive Builder shortcuts active after clicking their content.
|
|
EditorView.contentAttributes.of({ tabindex: "0" }),
|
|
...(ariaLabel === undefined
|
|
? []
|
|
: [EditorView.contentAttributes.of({ "aria-label": ariaLabel })]),
|
|
EditorView.editable.of(readOnly === false),
|
|
EditorState.readOnly.of(readOnly === true),
|
|
// https://github.com/uiwjs/react-codemirror/blob/5d7a37245ce70e61f215b77dc42a7eaf295c46e7/core/src/useCodeMirror.ts#L57-L70
|
|
EditorView.updateListener.of((update) => {
|
|
if (
|
|
// prevent invoking callback when focus or selection is changed
|
|
update.docChanged &&
|
|
// prevent invoking callback when the change came from react value
|
|
update.transactions.some((trx) =>
|
|
trx.annotation(ExternalChange)
|
|
) === false
|
|
) {
|
|
onChangeRef.current(update.state.doc.toString());
|
|
}
|
|
}),
|
|
EditorView.domEventHandlers({
|
|
blur(event) {
|
|
onChangeCompleteRef.current(view.state.doc.toString(), event);
|
|
},
|
|
cut(event) {
|
|
// prevent catching cut by global copy paste
|
|
// with target outside of contenteditable
|
|
event.stopPropagation();
|
|
},
|
|
}),
|
|
]),
|
|
});
|
|
}, [ariaLabel, readOnly, extensions, showLineNumbers]);
|
|
|
|
// update editor with react value
|
|
// https://github.com/uiwjs/react-codemirror/blob/5d7a37245ce70e61f215b77dc42a7eaf295c46e7/core/src/useCodeMirror.ts#L158-L169
|
|
useEffect(() => {
|
|
const view = viewRef.current;
|
|
if (view === undefined) {
|
|
return;
|
|
}
|
|
const nextValue = normalizeEditorValue(value);
|
|
// prevent updating when editor has the same state
|
|
// and can be the source of new value
|
|
if (nextValue === view.state.doc.toString()) {
|
|
return;
|
|
}
|
|
|
|
view.dispatch({
|
|
changes: { from: 0, to: view.state.doc.length, insert: nextValue },
|
|
selection: clampEditorSelection(view.state.selection, nextValue.length),
|
|
annotations: [
|
|
ExternalChange.of(true),
|
|
...(resetHistoryOnExternalChange
|
|
? [Transaction.addToHistory.of(false)]
|
|
: []),
|
|
],
|
|
});
|
|
if (resetHistoryOnExternalChange) {
|
|
// A full replacement changes declaration offsets, so old undo events cannot be mapped safely.
|
|
view.dispatch({ effects: historyCompartment.current.reconfigure([]) });
|
|
view.dispatch({
|
|
effects: historyCompartment.current.reconfigure(history()),
|
|
});
|
|
}
|
|
}, [value, resetHistoryOnExternalChange]);
|
|
|
|
useEffect(() => {
|
|
if (initialSelection !== undefined) {
|
|
const view = viewRef.current;
|
|
if (view !== undefined) {
|
|
view.dispatch({
|
|
selection: {
|
|
anchor: Math.min(initialSelection, view.state.doc.length),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}, [initialSelection]);
|
|
|
|
useImperativeHandle(editorApiRef, () => ({
|
|
replaceSelection: (string) => {
|
|
const view = viewRef.current;
|
|
if (view === undefined) {
|
|
return;
|
|
}
|
|
|
|
view.dispatch(view.state.replaceSelection(string));
|
|
view.focus();
|
|
},
|
|
insertTemplate({ prefix, suffix, placeholder }) {
|
|
const view = viewRef.current;
|
|
if (view === undefined) {
|
|
return;
|
|
}
|
|
const { from, to } = view.state.selection.main;
|
|
const insertion = getTemplateInsertion({
|
|
from,
|
|
selectedText: view.state.doc.sliceString(from, to),
|
|
prefix,
|
|
suffix,
|
|
placeholder,
|
|
});
|
|
view.dispatch({
|
|
changes: { from, to, insert: insertion.text },
|
|
selection: insertion.selection,
|
|
});
|
|
view.focus();
|
|
},
|
|
focus() {
|
|
viewRef.current?.focus();
|
|
},
|
|
}));
|
|
|
|
return (
|
|
<div
|
|
className={editorContentStyle()}
|
|
data-invalid={invalid}
|
|
data-chromeless={chromeless ? "true" : undefined}
|
|
ref={editorRef}
|
|
>
|
|
{showShortcuts && (
|
|
<Flex align="center" justify="end" gap="1" className={shortcutStyle()}>
|
|
<Text variant="small">Submit</Text>
|
|
<Kbd value={["meta", "enter"]} />
|
|
</Flex>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const editorDialogControlStyle = css({
|
|
position: "relative",
|
|
"&:hover, &:focus-within": {
|
|
[maximizeIconVisibilityVar]: "visible",
|
|
},
|
|
});
|
|
|
|
export const EditorDialogControl = ({ children }: { children: ReactNode }) => {
|
|
return <div className={editorDialogControlStyle()}>{children}</div>;
|
|
};
|
|
|
|
export const EditorDialogButton = forwardRef<
|
|
HTMLButtonElement,
|
|
ComponentProps<typeof IconButton>
|
|
>((props, ref) => {
|
|
return (
|
|
<IconButton
|
|
type="button"
|
|
{...props}
|
|
ref={ref}
|
|
css={{
|
|
position: "absolute",
|
|
top: 4,
|
|
right: 4,
|
|
visibility: cssVar(maximizeIconVisibilityVar, "hidden"),
|
|
}}
|
|
>
|
|
<MaximizeIcon />
|
|
</IconButton>
|
|
);
|
|
});
|
|
EditorDialogButton.displayName = "EditorDialogButton";
|
|
|
|
export const EditorDialog = ({
|
|
content,
|
|
children,
|
|
placement = "center",
|
|
width = 640,
|
|
height = 480,
|
|
contentPadding = true,
|
|
...panelProps
|
|
}: {
|
|
title: ReactNode;
|
|
titleSuffix?: ReactNode;
|
|
content: ReactNode;
|
|
children?: ReactNode;
|
|
width?: number;
|
|
height?: number;
|
|
contentPadding?: boolean;
|
|
placement?: ComponentProps<typeof FloatingPanel>["placement"];
|
|
resize?: ComponentProps<typeof FloatingPanel>["resize"];
|
|
open?: boolean;
|
|
modal?: boolean;
|
|
onOpenChange?: (newOpen: boolean) => void;
|
|
}) => {
|
|
return (
|
|
<FloatingPanel
|
|
{...panelProps}
|
|
width={width}
|
|
height={height}
|
|
placement={placement}
|
|
maximizable
|
|
resize="both"
|
|
content={
|
|
<PanelContent
|
|
data-editor-dialog-content
|
|
as={Grid}
|
|
align="stretch"
|
|
css={{
|
|
padding: contentPadding ? undefined : 0,
|
|
height: "100%",
|
|
overflow: "auto",
|
|
boxSizing: "border-box",
|
|
}}
|
|
>
|
|
{content}
|
|
</PanelContent>
|
|
}
|
|
>
|
|
{children}
|
|
</FloatingPanel>
|
|
);
|
|
};
|