diff --git a/apps/builder/app/builder/builder.css b/apps/builder/app/builder/builder.css index 31ddefad6587..de9d18b8e4a1 100644 --- a/apps/builder/app/builder/builder.css +++ b/apps/builder/app/builder/builder.css @@ -22,3 +22,8 @@ body { [data-radix-scroll-area-viewport]::-webkit-scrollbar { display: none; } + +* { + scrollbar-width: thin; + scrollbar-color: var(--colors-foregroundScrollBar) transparent; +} diff --git a/apps/builder/app/builder/features/project-settings/project-settings.tsx b/apps/builder/app/builder/features/project-settings/project-settings.tsx index 3651a26715c1..5bf95e9bd8dd 100644 --- a/apps/builder/app/builder/features/project-settings/project-settings.tsx +++ b/apps/builder/app/builder/features/project-settings/project-settings.tsx @@ -11,6 +11,7 @@ import { List, ListItem, Text, + rawTheme, } from "@webstudio-is/design-system"; import { $openProjectSettings, @@ -53,11 +54,11 @@ export const ProjectSettingsView = ({ onOpenChange={onOpenChange} >
diff --git a/apps/builder/app/builder/features/project-settings/utils.ts b/apps/builder/app/builder/features/project-settings/utils.ts index a115a74ae1f8..14bf18280b66 100644 --- a/apps/builder/app/builder/features/project-settings/utils.ts +++ b/apps/builder/app/builder/features/project-settings/utils.ts @@ -1,8 +1,8 @@ -import { theme, type CSS } from "@webstudio-is/design-system"; +import { rawTheme, theme, type CSS } from "@webstudio-is/design-system"; import { getPagePath, type Pages } from "@webstudio-is/sdk"; -export const leftPanelWidth = theme.spacing[26]; -export const rightPanelWidth = theme.spacing[35]; +export const leftPanelWidth = rawTheme.spacing[26]; +export const rightPanelWidth = rawTheme.spacing[35]; export const sectionSpacing: CSS = { paddingInline: theme.panel.paddingInline, }; diff --git a/apps/builder/app/builder/features/settings-panel/variable-popover.tsx b/apps/builder/app/builder/features/settings-panel/variable-popover.tsx index 287d94fa3add..a692f59950df 100644 --- a/apps/builder/app/builder/features/settings-panel/variable-popover.tsx +++ b/apps/builder/app/builder/features/settings-panel/variable-popover.tsx @@ -1,6 +1,8 @@ import { z } from "zod"; import { nanoid } from "nanoid"; +import { computed } from "nanostores"; import { useStore } from "@nanostores/react"; +import { javascript } from "@codemirror/lang-javascript"; import { type ReactNode, type Ref, @@ -13,6 +15,7 @@ import { createContext, useEffect, useCallback, + useMemo, } from "react"; import { CopyIcon, RefreshIcon, UpgradeIcon } from "@webstudio-is/icons"; import { @@ -20,6 +23,7 @@ import { Button, Combobox, DialogClose, + DialogMaximize, DialogTitle, DialogTitleActions, Flex, @@ -58,13 +62,19 @@ import { $userPlanFeatures, $instances, $props, + $variableValuesByInstanceSelector, } from "~/shared/nano-states"; -import { $selectedInstance } from "~/shared/awareness"; +import { + $selectedInstance, + $selectedInstanceKeyWithRoot, +} from "~/shared/awareness"; import { BindingPopoverProvider } from "~/builder/shared/binding-popover"; import { + EditorContent, EditorDialog, EditorDialogButton, EditorDialogControl, + foldGutterExtension, } from "~/builder/shared/code-editor-base"; import { updateWebstudioData } from "~/shared/instance-utils"; import { @@ -666,8 +676,8 @@ const VariablePanel = forwardRef< direction="column" css={{ overflow: "hidden", + padding: theme.panel.padding, gap: theme.spacing[7], - p: theme.panel.padding, }} > @@ -707,6 +717,40 @@ const areAllFormErrorsVisible = (form: null | HTMLFormElement) => { return true; }; +const $instanceVariableValues = computed( + [$selectedInstanceKeyWithRoot, $variableValuesByInstanceSelector], + (instanceKey, variableValuesByInstanceSelector) => + variableValuesByInstanceSelector.get(instanceKey ?? "") ?? + new Map() +); + +const VariablePreview = ({ variable }: { variable: DataSource }) => { + const variableValues = useStore($instanceVariableValues); + const extensions = useMemo(() => [javascript({}), foldGutterExtension], []); + const editorProps = { + readOnly: true, + extensions, + // compute value as json lazily only when dialog is open + // by spliting into separate component which is invoked + // only when dialog content is rendered + value: JSON.stringify(variableValues.get(variable.id), null, 2), + onChange: () => {}, + onChangeComplete: () => {}, + }; + return ( + + + + ); +}; + export const VariablePopoverTrigger = ({ variable, children, @@ -724,6 +768,9 @@ export const VariablePopoverTrigger = ({ return ( { if (newOpen) { @@ -788,6 +835,7 @@ export const VariablePopoverTrigger = ({ )} + } @@ -797,57 +845,61 @@ export const VariablePopoverTrigger = ({ ) } content={ - -
{ - event.preventDefault(); - if (isSystemVariable) { - return; - } - const nameElement = - event.currentTarget.elements.namedItem("name"); - // make sure only name is valid and allow to save everything else - // to avoid loosing complex configuration when closed accidentally - if ( - nameElement instanceof HTMLInputElement && - nameElement.checkValidity() - ) { - const formData = new FormData(event.currentTarget); - panelRef.current?.save(formData); - // close popover whenever new variable is created - // to prevent creating duplicated variable - if (variable === undefined) { - setOpen(false); - } - } - }} + - {/* submit is not triggered when press enter on input without submit button */} - -
{ + event.preventDefault(); + if (isSystemVariable) { + return; + } + const nameElement = + event.currentTarget.elements.namedItem("name"); + // make sure only name is valid and allow to save everything else + // to avoid loosing complex configuration when closed accidentally + if ( + nameElement instanceof HTMLInputElement && + nameElement.checkValidity() + ) { + const formData = new FormData(event.currentTarget); + panelRef.current?.save(formData); + // close popover whenever new variable is created + // to prevent creating duplicated variable + if (variable === undefined) { + setOpen(false); + } + } + }} > -
+ +
+ {variable && } + } > {children} diff --git a/apps/builder/app/builder/features/settings-panel/variables-section.tsx b/apps/builder/app/builder/features/settings-panel/variables-section.tsx index 90d7d54096ce..45bafa30e57e 100644 --- a/apps/builder/app/builder/features/settings-panel/variables-section.tsx +++ b/apps/builder/app/builder/features/settings-panel/variables-section.tsx @@ -39,10 +39,7 @@ import { CollapsibleSectionRoot, useOpenState, } from "~/builder/shared/collapsible-section"; -import { - ValuePreviewDialog, - formatValuePreview, -} from "~/builder/shared/expression-editor"; +import { formatValuePreview } from "~/builder/shared/expression-editor"; import { VariablePopoverProvider, VariablePopoverTrigger, @@ -145,7 +142,6 @@ const VariablesItem = ({ usageCount: number; }) => { const selectedPage = useStore($selectedPage); - const [inspectDialogOpen, setInspectDialogOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); return ( @@ -169,69 +165,61 @@ const VariablesItem = ({ data-state={isMenuOpen ? "open" : undefined} buttons={ <> - - {undefined} - - - - - {/* a11y is completely broken here - focus is not restored to button invoker - @todo fix it eventually and consider restoring from closed value preview dialog - */} - } - onClick={() => {}} - /> - - event.preventDefault()} - > - setInspectDialogOpen(true)}> - Inspect - - {source === "local" && variable.type !== "parameter" && ( - { - if (usageCount > 0) { - setIsDeleteDialogOpen(true); - } else { - updateWebstudioData((data) => { - deleteVariableMutable(data, variable.id); - }); - } - }} - > - Delete {usageCount > 0 && `(${usageCount} bindings)`} - - )} - {source === "local" && - variable.id === selectedPage?.systemDataSourceId && ( + {((source === "local" && variable.type !== "parameter") || + (source === "local" && + variable.id === selectedPage?.systemDataSourceId)) && ( + + + {/* a11y is completely broken here + focus is not restored to button invoker + @todo fix it eventually and consider restoring from closed value preview dialog + */} + } + onClick={() => {}} + /> + + event.preventDefault()} + > + {source === "local" && variable.type !== "parameter" && ( { - updateWebstudioData((data) => { - const page = findPageByIdOrPath( - selectedPage.id, - data.pages - ); - delete page?.systemDataSourceId; - deleteVariableMutable(data, variable.id); - }); + if (usageCount > 0) { + setIsDeleteDialogOpen(true); + } else { + updateWebstudioData((data) => { + deleteVariableMutable(data, variable.id); + }); + } }} > - Delete + Delete {usageCount > 0 && `(${usageCount} bindings)`} )} - - + {source === "local" && + variable.id === selectedPage?.systemDataSourceId && ( + { + updateWebstudioData((data) => { + const page = findPageByIdOrPath( + selectedPage.id, + data.pages + ); + delete page?.systemDataSourceId; + deleteVariableMutable(data, variable.id); + }); + }} + > + Delete + + )} + + + )} { title="Background" content={} > -
Trigger
+
diff --git a/apps/builder/app/builder/shared/expression-editor.tsx b/apps/builder/app/builder/shared/expression-editor.tsx index 8bd81d060f9f..5920e38e086c 100644 --- a/apps/builder/app/builder/shared/expression-editor.tsx +++ b/apps/builder/app/builder/shared/expression-editor.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, type ReactNode, type RefObject } from "react"; +import { useEffect, useMemo, type RefObject } from "react"; import { matchSorter } from "match-sorter"; import type { SyntaxNode } from "@lezer/common"; import { Facet, RangeSetBuilder } from "@codemirror/state"; @@ -33,7 +33,6 @@ import { EditorDialog, EditorDialogButton, EditorDialogControl, - foldGutterExtension, type EditorApi, } from "./code-editor-base"; import { @@ -494,45 +493,3 @@ export const ExpressionEditor = ({ ); }; - -// compute value as json lazily only when dialog is open -// by spliting into separate component which is invoked -// only when dialog content is rendered -const ValuePreviewEditor = ({ value }: { value: unknown }) => { - const extensions = useMemo(() => [javascript({}), foldGutterExtension], []); - return ( - {}} - onChangeComplete={() => {}} - /> - ); -}; - -export const ValuePreviewDialog = ({ - title, - value, - children, - open, - onOpenChange, -}: { - title?: ReactNode; - value: unknown; - open?: boolean; - onOpenChange?: (newOpen: boolean) => void; - children: ReactNode; -}) => { - return ( - } - resize="both" - > - {children} - - ); -}; diff --git a/packages/design-system/src/components/dialog.tsx b/packages/design-system/src/components/dialog.tsx index cc7ddb47d50a..0231fb1bfac3 100644 --- a/packages/design-system/src/components/dialog.tsx +++ b/packages/design-system/src/components/dialog.tsx @@ -137,14 +137,6 @@ type Point = { x: number; y: number }; type Size = { width: number; height: number }; type Rect = Point & Size; -const centeredContent = { - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: "100vw", - height: "100vh", -} as const; - type UseDraggableProps = { isMaximized: boolean; minWidth?: number; @@ -174,12 +166,27 @@ const useDraggable = ({ const calcStyle = useCallback(() => { const style: CSSProperties = isMaximized - ? centeredContent - : { - ...centeredContent, - width, - height, - }; + ? { + top: "calc(20px + 50% - 50vh)", + left: "calc(20px + 50% - 50vw)", + width: "calc(100vw - 40px)", + height: "calc(100vh - 40px)", + } + : !width || !height + ? { + inset: 0, + margin: "auto", + width, + height, + } + : { + top: `max(20px, calc(50% - ${height / 2}px))`, + bottom: `max(20px, calc(50% - ${height / 2}px))`, + left: `max(20px, calc(50% - ${width / 2}px))`, + right: `max(20px, calc(50% - ${width / 2}px))`, + width, + height, + }; if (minWidth !== undefined) { style.minWidth = minWidth; @@ -191,11 +198,13 @@ const useDraggable = ({ if (isMaximized === false) { if (x !== undefined) { style.left = x; - style.transform = "none"; + style.right = "auto"; + style.margin = 0; } if (y !== undefined) { style.top = y; - style.transform = "none"; + style.bottom = "auto"; + style.margin = 0; } } return style; @@ -230,7 +239,6 @@ const useDraggable = ({ const rect = target.getBoundingClientRect(); target.style.left = `${rect.x}px`; target.style.top = `${rect.y}px`; - target.style.transform = "none"; lastDragDataRef.current = { point: { x: event.pageX, y: event.pageY }, rect, @@ -441,6 +449,7 @@ const overlayStyle = css({ const contentStyle = css(panelStyle, { position: "fixed", width: "min-content", + height: "min-content", minWidth: theme.sizes.sidebarWidth, minHeight: theme.spacing[22], maxWidth: `calc(100vw - ${theme.spacing[15]})`, diff --git a/packages/design-system/src/components/floating-panel.tsx b/packages/design-system/src/components/floating-panel.tsx index 903b0d7d2064..65b5006e87fd 100644 --- a/packages/design-system/src/components/floating-panel.tsx +++ b/packages/design-system/src/components/floating-panel.tsx @@ -7,7 +7,6 @@ import { useState, useRef, useLayoutEffect, - useCallback, } from "react"; import { css, theme } from "../stitches.config"; import { @@ -68,6 +67,8 @@ const contentStyle = css({ width: theme.sizes.sidebarWidth, }); +const defaultOffset = { mainAxis: 10 }; + export const FloatingPanel = ({ title, content, @@ -77,7 +78,7 @@ export const FloatingPanel = ({ width, height, placement = "left-start", - offset: offsetProp = { mainAxis: 10 }, + offset: offsetProp = defaultOffset, open, onOpenChange, }: FloatingPanelProps) => { @@ -87,60 +88,25 @@ export const FloatingPanel = ({ ); const triggerRef = useRef(null); const [position, setPosition] = useState<{ x: number; y: number }>(); - const positionIsSetRef = useRef(false); - const calcPosition = useCallback(() => { + useLayoutEffect(() => { if ( triggerRef.current === null || containerRef.current === null || contentElement === null || // When centering the dialog, we don't need to calculate the position - placement === "center" || - // After we positioned it once, we leave it alone to avoid jumps when user is scrolling the trigger - positionIsSetRef.current + placement === "center" ) { return; } - const triggerRect = triggerRef.current.getBoundingClientRect(); - const containerRect = containerRef.current.getBoundingClientRect(); - - const anchor = { - getBoundingClientRect() { - return { - width: containerRect.width, - height: triggerRect.height, - x: containerRect.x, - y: triggerRect.y, - left: containerRect.left, - right: containerRect.right, - top: triggerRect.top, - bottom: triggerRect.bottom, - }; - }, - }; - - computePosition(anchor, contentElement, { + computePosition(triggerRef.current, contentElement, { placement, - middleware: [ - shift(), - placement === "bottom" && flip(), - offset(offsetProp), - ], + middleware: [shift(), flip(), offset(offsetProp)], }).then(({ x, y }) => { setPosition({ x, y }); - positionIsSetRef.current = true; }); - }, [ - positionIsSetRef, - contentElement, - triggerRef, - containerRef, - placement, - offsetProp, - ]); - - useLayoutEffect(calcPosition, [calcPosition]); + }, [contentElement, triggerRef, containerRef, placement, offsetProp]); return (