diff --git a/.github/workflows/fonts.yml b/.github/workflows/fonts.yml
new file mode 100644
index 0000000000..401b40a814
--- /dev/null
+++ b/.github/workflows/fonts.yml
@@ -0,0 +1,19 @@
+name: Fonts
+on:
+ push:
+ paths:
+ - "packages/fonts/**/*"
+ - ".github/workflows/fonts.yml"
+ - ".github/actions/publish/action.yaml"
+ branches:
+ - main
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./.github/actions/publish
+ with:
+ workdir: ./packages/fonts
+ filter: "@webstudio-is/fonts"
+ token: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/package-tests.yml b/.github/workflows/package-tests.yml
index c9a3210ef7..4db3e6127f 100644
--- a/.github/workflows/package-tests.yml
+++ b/.github/workflows/package-tests.yml
@@ -57,4 +57,16 @@ jobs:
with:
workdir: ./packages/tsconfig
filter: "@webstudio-is/tsconfig"
+ token: ${{ secrets.NPM_TOKEN }}
+ - uses: ./.github/actions/publish-test
+ if: always()
+ with:
+ workdir: ./packages/fonts
+ filter: "@webstudio-is/fonts"
+ token: ${{ secrets.NPM_TOKEN }}
+ - uses: ./.github/actions/publish-test
+ if: always()
+ with:
+ workdir: ./packages/prisma-client
+ filter: "@webstudio-is/prisma-client"
token: ${{ secrets.NPM_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/prisma-client.yml b/.github/workflows/prisma-client.yml
new file mode 100644
index 0000000000..3ad6b77db3
--- /dev/null
+++ b/.github/workflows/prisma-client.yml
@@ -0,0 +1,19 @@
+name: Prisma Client
+on:
+ push:
+ paths:
+ - "packages/prisma-client/**/*"
+ - ".github/workflows/prisma-client.yml"
+ - ".github/actions/publish/action.yaml"
+ branches:
+ - main
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./.github/actions/publish
+ with:
+ workdir: ./packages/prisma-client
+ filter: "@webstudio-is/prisma-client"
+ token: ${{ secrets.NPM_TOKEN }}
diff --git a/.lostpixel/baseline/components-combobox--simple.png b/.lostpixel/baseline/components-combobox--simple.png
index 3ab0f6293d..c5bdaf570a 100644
Binary files a/.lostpixel/baseline/components-combobox--simple.png and b/.lostpixel/baseline/components-combobox--simple.png differ
diff --git a/.lostpixel/baseline/components-search-field--empty.png b/.lostpixel/baseline/components-search-field--empty.png
new file mode 100644
index 0000000000..47f4b13d0e
Binary files /dev/null and b/.lostpixel/baseline/components-search-field--empty.png differ
diff --git a/.lostpixel/baseline/components-search-field--reset.png b/.lostpixel/baseline/components-search-field--reset.png
new file mode 100644
index 0000000000..0a9b4ba5ff
Binary files /dev/null and b/.lostpixel/baseline/components-search-field--reset.png differ
diff --git a/.lostpixel/baseline/components-search-field--update-value.png b/.lostpixel/baseline/components-search-field--update-value.png
new file mode 100644
index 0000000000..4aa4b4be53
Binary files /dev/null and b/.lostpixel/baseline/components-search-field--update-value.png differ
diff --git a/.lostpixel/baseline/components-search-field--with-initial-value.png b/.lostpixel/baseline/components-search-field--with-initial-value.png
new file mode 100644
index 0000000000..d6f0bf0876
Binary files /dev/null and b/.lostpixel/baseline/components-search-field--with-initial-value.png differ
diff --git a/.lostpixel/baseline/components-text-field--click-capture.png b/.lostpixel/baseline/components-text-field--click-capture.png
index 673451fbef..470a6171cc 100644
Binary files a/.lostpixel/baseline/components-text-field--click-capture.png and b/.lostpixel/baseline/components-text-field--click-capture.png differ
diff --git a/.lostpixel/baseline/components-text-field--focus-events.png b/.lostpixel/baseline/components-text-field--focus-events.png
index 8ec53c2226..5542e8888a 100644
Binary files a/.lostpixel/baseline/components-text-field--focus-events.png and b/.lostpixel/baseline/components-text-field--focus-events.png differ
diff --git a/.lostpixel/baseline/components-text-field--interactive.png b/.lostpixel/baseline/components-text-field--interactive.png
index 165508e4e0..ce559ea3f9 100644
Binary files a/.lostpixel/baseline/components-text-field--interactive.png and b/.lostpixel/baseline/components-text-field--interactive.png differ
diff --git a/.lostpixel/baseline/components-text-field--layout.png b/.lostpixel/baseline/components-text-field--layout.png
index 70e5007b91..6f721d6ca4 100644
Binary files a/.lostpixel/baseline/components-text-field--layout.png and b/.lostpixel/baseline/components-text-field--layout.png differ
diff --git a/.lostpixel/baseline/components-text-field--native-props.png b/.lostpixel/baseline/components-text-field--native-props.png
index 6f0764f33b..d0e9a63194 100644
Binary files a/.lostpixel/baseline/components-text-field--native-props.png and b/.lostpixel/baseline/components-text-field--native-props.png differ
diff --git a/.lostpixel/baseline/components-text-field--prefix-suffix.png b/.lostpixel/baseline/components-text-field--prefix-suffix.png
index 0117b94737..7a46df00c1 100644
Binary files a/.lostpixel/baseline/components-text-field--prefix-suffix.png and b/.lostpixel/baseline/components-text-field--prefix-suffix.png differ
diff --git a/.lostpixel/baseline/components-text-field--variants.png b/.lostpixel/baseline/components-text-field--variants.png
index f7db5735a2..ea8be5c4ec 100644
Binary files a/.lostpixel/baseline/components-text-field--variants.png and b/.lostpixel/baseline/components-text-field--variants.png differ
diff --git a/apps/designer/app/canvas/canvas.tsx b/apps/designer/app/canvas/canvas.tsx
index 2c00da02c4..93e3dfc247 100644
--- a/apps/designer/app/canvas/canvas.tsx
+++ b/apps/designer/app/canvas/canvas.tsx
@@ -65,6 +65,7 @@ const useElementsTree = () => {
if (rootInstance === undefined) return;
return createElementsTree({
+ sandbox: true,
instance: rootInstance,
breakpoints,
Component: WrapperComponentDev,
diff --git a/apps/designer/app/canvas/shared/sync/use-sync.ts b/apps/designer/app/canvas/shared/sync/use-sync.ts
index 824858b64c..96c5734760 100644
--- a/apps/designer/app/canvas/shared/sync/use-sync.ts
+++ b/apps/designer/app/canvas/shared/sync/use-sync.ts
@@ -16,7 +16,7 @@ export const useSync = ({
if (entries.length === 0) return;
// @todo this entire queueing logic needs to be gone, it's a workaround,
- // because prisma can't do atomic updates yet with embeded documents
+ // because prisma can't do atomic updates yet with sandbox documents
// and backend fetches and updates big objects, so if we send quickly,
// we end up overwriting things
enqueue(() =>
diff --git a/apps/designer/app/designer/features/breakpoints/use-subscribe-shortcuts.ts b/apps/designer/app/designer/features/breakpoints/use-subscribe-shortcuts.ts
index 8849e6a5ad..48e8a2799c 100644
--- a/apps/designer/app/designer/features/breakpoints/use-subscribe-shortcuts.ts
+++ b/apps/designer/app/designer/features/breakpoints/use-subscribe-shortcuts.ts
@@ -3,6 +3,8 @@ import { useBreakpoints } from "~/shared/nano-states";
import { useZoom, useSelectedBreakpoint } from "../../shared/nano-states";
import { minZoom } from "./zoom-setting";
import { utils } from "@webstudio-is/project";
+import { shortcuts } from "~/shared/shortcuts";
+import { useHotkeys } from "react-hotkeys-hook";
export const useSubscribeSelectBreakpointFromShortcut = () => {
const [breakpoints] = useBreakpoints();
@@ -17,6 +19,17 @@ const zoomStep = 20;
export const useSubscribeZoomFromShortcut = () => {
const [zoom, setZoom] = useZoom();
+
+ // We need to prevent browser default behavior for zooming in/out
+ // Maybe we should use shift modifier and keep the default behaivor functional?
+ useHotkeys(
+ shortcuts.zoom,
+ (event) => {
+ event.preventDefault();
+ },
+ []
+ );
+
useSubscribe("zoom", (direction) => {
if (direction === "zoomIn") {
setZoom(Math.min(zoom + zoomStep, 100));
diff --git a/apps/designer/app/designer/features/style-panel/controls/font-family/font-family-control.tsx b/apps/designer/app/designer/features/style-panel/controls/font-family/font-family-control.tsx
index 20b8af6320..a4026cb7c2 100644
--- a/apps/designer/app/designer/features/style-panel/controls/font-family/font-family-control.tsx
+++ b/apps/designer/app/designer/features/style-panel/controls/font-family/font-family-control.tsx
@@ -3,6 +3,7 @@ import { FontsManager } from "~/designer/shared/fonts-manager";
import type { ControlProps } from "../../style-sections";
import { getFinalValue } from "../../shared/get-final-value";
import { ValuePickerPopover } from "../../shared/value-picker-popover";
+import { useState } from "react";
export const FontFamilyControl = ({
currentStyle,
@@ -16,6 +17,7 @@ export const FontFamilyControl = ({
inheritedStyle,
property: styleConfig.property,
});
+ const [isOpen, setIsOpen] = useState(false);
if (value === undefined) return null;
@@ -25,9 +27,13 @@ export const FontFamilyControl = ({
}
+ onOpenChange={setIsOpen}
>
{/* @todo this should be part of the design system, probably a varian="button" */}
-
+
);
};
diff --git a/apps/designer/app/designer/features/style-panel/search.tsx b/apps/designer/app/designer/features/style-panel/search.tsx
deleted file mode 100644
index 299ce19933..0000000000
--- a/apps/designer/app/designer/features/style-panel/search.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { useRef, useState } from "react";
-import { IconButton, TextField } from "@webstudio-is/design-system";
-import { Cross2Icon, MagnifyingGlassIcon } from "@webstudio-is/icons";
-import { components } from "@webstudio-is/react-sdk";
-import type { SelectedInstanceData } from "~/shared/canvas-components";
-
-type OnSearch = (search: string) => void;
-
-const useSearch = (onSearch: OnSearch): [string, OnSearch] => {
- const [search, setSearch] = useState("");
- return [
- search,
- (search: string) => {
- setSearch(search);
- onSearch(search);
- },
- ];
-};
-
-type SearchProps = {
- onSearch: OnSearch;
- selectedInstanceData: SelectedInstanceData;
-};
-
-export const Search = ({ onSearch, selectedInstanceData }: SearchProps) => {
- const [search, setSearch] = useSearch(onSearch);
- const inputRef = useRef(null);
- return (
-
-
-
- }
- suffix={
- search.length > 0 && (
- {
- setSearch("");
- inputRef.current?.focus();
- }}
- >
-
-
- )
- }
- placeholder={components[selectedInstanceData.component].label}
- onChange={(event) => {
- const { value } = event.target;
- setSearch(value.toLowerCase());
- }}
- />
- );
-};
diff --git a/apps/designer/app/designer/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/designer/app/designer/features/style-panel/shared/css-value-input/css-value-input.tsx
index 2ea10e83a3..d576837bda 100644
--- a/apps/designer/app/designer/features/style-panel/shared/css-value-input/css-value-input.tsx
+++ b/apps/designer/app/designer/features/style-panel/shared/css-value-input/css-value-input.tsx
@@ -7,8 +7,8 @@ import {
ComboboxPopperAnchor,
ComboboxListbox,
ComboboxListboxItem,
- IconButton,
numericScrubControl,
+ TextFieldIconButton,
} from "@webstudio-is/design-system";
import { ChevronDownIcon } from "@webstudio-is/icons";
import {
@@ -270,9 +270,12 @@ export const CssValueInput = ({
const suffix =
value.type === "keyword" ? (
-
+
-
+
) : value.type === "unit" ? (
unitSelectElement
) : null;
diff --git a/apps/designer/app/designer/features/style-panel/shared/css-value-input/unit-select.tsx b/apps/designer/app/designer/features/style-panel/shared/css-value-input/unit-select.tsx
index 9b2a1c4ea4..965de5a12d 100644
--- a/apps/designer/app/designer/features/style-panel/shared/css-value-input/unit-select.tsx
+++ b/apps/designer/app/designer/features/style-panel/shared/css-value-input/unit-select.tsx
@@ -13,6 +13,7 @@ import {
SelectViewport,
SelectItem,
SelectContent,
+ TextFieldIconButton,
} from "@webstudio-is/design-system";
import { ChevronDownIcon, ChevronUpIcon } from "@webstudio-is/icons";
import { isValid } from "../parse-css-value";
@@ -88,23 +89,8 @@ export const useUnitSelect = ({
return [isOpen, select];
};
-const StyledTrigger = styled(SelectPrimitive.SelectTrigger, {
- all: "unset",
- height: "$5",
- width: "auto",
- px: "$1",
- borderRadius: 2,
- display: "inline-flex",
- alignItems: "center",
- color: "$hiContrast",
- justifyContent: "center",
- "&:hover": {
- backgroundColor: "$slate6",
- },
- "&:focus": {
- backgroundColor: "$blue10",
- color: "$loContrast",
- },
+const StyledTrigger = styled(TextFieldIconButton, {
+ px: 3,
});
type UnitSelectProps = {
@@ -131,9 +117,11 @@ const UnitSelect = ({
onOpenChange={onOpenChange}
open={open}
>
-
- {value}
-
+
+
+ {value}
+
+
diff --git a/apps/designer/app/designer/features/style-panel/shared/value-picker-popover.tsx b/apps/designer/app/designer/features/style-panel/shared/value-picker-popover.tsx
index 72811d8bd9..9fecafec57 100644
--- a/apps/designer/app/designer/features/style-panel/shared/value-picker-popover.tsx
+++ b/apps/designer/app/designer/features/style-panel/shared/value-picker-popover.tsx
@@ -17,27 +17,32 @@ const usePickerSideOffset = (
return [ref, sideOffset];
};
-type AssetPickerProps = {
+type ValuePickerPopoverProps = {
title: string;
content: JSX.Element;
children: JSX.Element;
+ onOpenChange?: (isOpen: boolean) => void;
};
export const ValuePickerPopover = ({
title,
content,
children,
-}: AssetPickerProps) => {
+ onOpenChange,
+}: ValuePickerPopoverProps) => {
const [isOpen, setIsOpen] = useState(false);
const [ref, sideOffset] = usePickerSideOffset(isOpen);
-
+ const handleOpenChange = (open: boolean) => {
+ setIsOpen(open);
+ onOpenChange?.(open);
+ };
return (
-
+
{
- setIsOpen(true);
+ handleOpenChange(true);
}}
>
{children}
@@ -47,7 +52,8 @@ export const ValuePickerPopover = ({
sideOffset={sideOffset}
side="right"
hideArrow
- css={{ minHeight: 500 }}
+ align="start"
+ css={{ width: PANEL_WIDTH }}
>
{content}
diff --git a/apps/designer/app/designer/features/style-panel/style-panel.tsx b/apps/designer/app/designer/features/style-panel/style-panel.tsx
index b326756ce3..1e36ab7d72 100644
--- a/apps/designer/app/designer/features/style-panel/style-panel.tsx
+++ b/apps/designer/app/designer/features/style-panel/style-panel.tsx
@@ -1,10 +1,9 @@
import type { Publish } from "~/shared/pubsub";
import { willRender } from "~/designer/shared/breakpoints";
-import { Box, Card, Paragraph } from "@webstudio-is/design-system";
+import { Box, Card, Paragraph, SearchField } from "@webstudio-is/design-system";
import type { SelectedInstanceData } from "~/shared/canvas-components";
import { useStyleData } from "./shared/use-style-data";
import { StyleSettings } from "./style-settings";
-import { Search } from "./search";
import { useState } from "react";
import {
useCanvasWidth,
@@ -56,9 +55,14 @@ export const StylePanel = ({
return (
<>
- {
+ setSearch(event.target.value);
+ }}
+ onCancel={() => {
+ setSearch("");
+ }}
/>
diff --git a/apps/designer/app/designer/shared/fonts-manager/fonts-manager.tsx b/apps/designer/app/designer/shared/fonts-manager/fonts-manager.tsx
index 2161be9a49..796c13daa9 100644
--- a/apps/designer/app/designer/shared/fonts-manager/fonts-manager.tsx
+++ b/apps/designer/app/designer/shared/fonts-manager/fonts-manager.tsx
@@ -1,70 +1,57 @@
-/* This entire module is WIP */
import type { Asset } from "@webstudio-is/asset-uploader";
import {
Flex,
- Box,
- ComboboxListboxItem,
- TextField,
- DropdownMenu,
- DropdownMenuTrigger,
- IconButton,
- DropdownMenuContent,
- DropdownMenuItem,
- Text,
- DropdownMenuPortal,
useCombobox,
+ Separator,
+ SearchField,
} from "@webstudio-is/design-system";
import { AssetUpload, PreviewAsset, useAssets } from "~/designer/shared/assets";
import { SYSTEM_FONTS } from "@webstudio-is/fonts";
-import { DotsHorizontalIcon, MagnifyingGlassIcon } from "@webstudio-is/icons";
-import { useMemo } from "react";
+import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
+import { ItemMenu } from "./item-menu";
+import { Listbox, ListboxItem } from "./listbox";
-const getItems = (
- assets: Array
-): Array<{ label: string }> => {
- const system = Array.from(SYSTEM_FONTS.keys()).map((label) => ({ label }));
+type Item = {
+ label: string;
+ type: "uploaded" | "system";
+};
+
+const toItems = (assets: Array): Array- => {
+ const system = Array.from(SYSTEM_FONTS.keys()).map((label) => ({
+ label,
+ type: "system",
+ }));
// We can have 2+ assets with the same family name, so we use a map to dedupe.
const uploaded = new Map();
for (const asset of assets) {
// @todo need to teach ts the right type from useAssets
if ("meta" in asset && "family" in asset.meta) {
- uploaded.set(asset.meta.family, { label: asset.meta.family });
+ uploaded.set(asset.meta.family, {
+ label: asset.meta.family,
+ type: "uploaded",
+ });
}
}
- return [...system, ...uploaded.values()];
+ return [...uploaded.values(), ...system];
};
-const ItemMenu = ({ onDelete }: { onDelete: () => void }) => {
+const NotFound = () => {
return (
-
-
-
-
-
-
-
-
- {
- onDelete();
- }}
- >
- Delete font
-
-
-
-
+
+ Font not found
+
);
};
-type FontsManagerProps = {
+const useLogic = ({
+ value,
+ onChange,
+}: {
value: string;
onChange: (value: string) => void;
-};
-
-export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
+}) => {
+ const [search, setSearch] = useState("");
const { assets, handleDelete } = useAssets("font");
-
const handleDeleteByLabel = (family: string) => {
// One family may have multiple assets for different formats, so we need to delete them all.
const ids = assets
@@ -79,63 +66,169 @@ export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
handleDelete(ids);
};
- const fontItems = useMemo(() => getItems(assets), [assets]);
+ const fontItems = useMemo(() => toItems(assets), [assets]);
+
const selectedItem =
fontItems.find((item) => item.label === value) ?? fontItems[0];
- const { items, getComboboxProps, getMenuProps, getItemProps, getInputProps } =
- useCombobox({
- items: fontItems,
- value: selectedItem,
- itemToString: (item) => item?.label ?? "",
- onItemSelect: (value) => {
- if (value !== null) {
- onChange(value.label);
- }
- },
- });
+ const {
+ items: filteredItems,
+ resetFilter,
+ highlightedIndex,
+ isOpen,
+ inputValue,
+ ...comboboxProps
+ } = useCombobox({
+ // Makes sure first keydown selectsthe item instead of opening the list first otherwise.
+ isOpen: true,
+ items: fontItems,
+ value: selectedItem,
+ itemToString(item) {
+ return item?.label ?? "";
+ },
+ onItemSelect(value) {
+ if (value !== null) {
+ onChange(value.label);
+ handleCancelSearch();
+ }
+ },
+ });
- const { isEmpty, ...menuProps } = getMenuProps();
+ const { uploadedItems, systemItems } = useMemo(() => {
+ const uploadedItems = filteredItems.filter(
+ (item) => item.type === "uploaded"
+ );
+ const systemItems = filteredItems.filter((item) => item.type === "system");
+ return { uploadedItems, systemItems };
+ }, [filteredItems]);
+
+ useEffect(() => {
+ setSearch(inputValue);
+ }, [inputValue]);
+
+ const handleCancelSearch = useCallback(() => {
+ resetFilter();
+ setSearch("");
+ }, [resetFilter]);
+
+ useEffect(handleCancelSearch, [fontItems, selectedItem, handleCancelSearch]);
+
+ return {
+ filteredItems,
+ uploadedItems,
+ systemItems,
+ handleDelete: handleDeleteByLabel,
+ handleCancelSearch,
+ search,
+ ...comboboxProps,
+ };
+};
+
+type FontsManagerProps = {
+ value: string;
+ onChange: (value: string) => void;
+};
+
+export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
+ const {
+ filteredItems,
+ uploadedItems,
+ systemItems,
+ getInputProps,
+ handleDelete,
+ handleCancelSearch,
+ getComboboxProps,
+ getMenuProps,
+ getItemProps,
+ setHighlightedIndex,
+ search,
+ } = useLogic({ value, onChange });
return (
-
-
+
+
-
-
+ {
+ setHighlightedIndex(-1);
+ }}
+ onCancel={handleCancelSearch}
+ />
+
+
+ {filteredItems.length === 0 && }
- }
- {...getInputProps()}
- />
-
+ }
+ onFocus={(event) => {
+ // We need to ignore focus on a menu button inside
+ if (event.target === event.currentTarget) {
+ setHighlightedIndex(index);
+ }
+ }}
+ >
+ {item.label}
+
+ );
+ })}
+ {systemItems.length !== 0 && (
+ <>
+
+ {"System"}
+ >
+ )}
+ {systemItems.map((item, index) => {
+ const globalIndex = uploadedItems.length + index;
+ return (
+ {
+ setHighlightedIndex(globalIndex);
+ }}
+ >
+ {item.label}
+
+ );
+ })}
+
);
diff --git a/apps/designer/app/designer/shared/fonts-manager/item-menu.tsx b/apps/designer/app/designer/shared/fonts-manager/item-menu.tsx
new file mode 100644
index 0000000000..5d3abc0ea4
--- /dev/null
+++ b/apps/designer/app/designer/shared/fonts-manager/item-menu.tsx
@@ -0,0 +1,73 @@
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ IconButton,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ Text,
+ DropdownMenuPortal,
+ styled,
+} from "@webstudio-is/design-system";
+import { DotsHorizontalIcon } from "@webstudio-is/icons";
+import { cssVars } from "@webstudio-is/css-vars";
+import { type FocusEventHandler, useState } from "react";
+
+const visibilityVar = cssVars.define("visibility");
+
+export const getItemMenuVars = (state: "visible" | "hidden") => {
+ return {
+ [visibilityVar]: state,
+ };
+};
+
+const MenuButton = styled(IconButton, {
+ visibility: cssVars.use(visibilityVar, "hidden"),
+ color: "$hint",
+ "&:hover, &:focus": {
+ color: "$hiContrast",
+ },
+});
+
+type ItemMenuProps = {
+ onDelete: () => void;
+ onOpenChange: (open: boolean) => void;
+ onFocusTrigger?: FocusEventHandler;
+};
+
+export const ItemMenu = ({
+ onDelete,
+ onOpenChange,
+ onFocusTrigger,
+}: ItemMenuProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+ return (
+ {
+ setIsOpen(open);
+ onOpenChange(open);
+ }}
+ >
+
+
+
+
+
+
+
+ {
+ onDelete();
+ }}
+ >
+ Delete font
+
+
+
+
+ );
+};
diff --git a/apps/designer/app/designer/shared/fonts-manager/listbox.tsx b/apps/designer/app/designer/shared/fonts-manager/listbox.tsx
new file mode 100644
index 0000000000..78acce1ee8
--- /dev/null
+++ b/apps/designer/app/designer/shared/fonts-manager/listbox.tsx
@@ -0,0 +1,79 @@
+import { Flex, styled, Text } from "@webstudio-is/design-system";
+import { CheckIcon } from "@webstudio-is/icons";
+import { ComponentProps, forwardRef } from "react";
+import { getItemMenuVars } from "./item-menu";
+
+export const Listbox = styled("ul", {
+ display: "flex",
+ flexDirection: "column",
+ margin: 0,
+ padding: 0,
+ variants: {
+ empty: {
+ true: {
+ display: "none",
+ },
+ },
+ },
+});
+
+const ListboxItemBase = styled("li", {
+ display: "grid",
+ gridTemplateColumns: "$4 1fr",
+ alignItems: "center",
+ justifyContent: "space-between",
+ height: "$5",
+ px: "$1",
+ listStyle: "none",
+ borderRadius: "$1",
+ outline: 0,
+ "&[data-highlighted]": {
+ boxShadow: "0px 0px 0px 2px $colors$blue10, 0px 0px 0px 2px $colors$blue10",
+ ...getItemMenuVars("visible"),
+ },
+ "&:focus-within": getItemMenuVars("visible"),
+ "&[disabled]": {
+ pointerEvents: "none",
+ },
+});
+
+export const ListboxItem = forwardRef<
+ HTMLLIElement,
+ ComponentProps & {
+ highlighted?: boolean;
+ disabled?: boolean;
+ selected?: boolean;
+ suffix?: JSX.Element;
+ }
+>(
+ (
+ {
+ highlighted,
+ disabled = false,
+ selected = false,
+ children,
+ suffix,
+ ...props
+ },
+ ref
+ ) => {
+ return (
+
+ {selected && }
+
+
+ {children}
+
+ {suffix}
+
+
+ );
+ }
+);
+ListboxItem.displayName = "ListboxItem";
diff --git a/apps/designer/app/designer/shared/shortcuts/use-publish-shortcuts.ts b/apps/designer/app/designer/shared/shortcuts/use-publish-shortcuts.ts
index 8ba8c3da72..9eaf306c1a 100644
--- a/apps/designer/app/designer/shared/shortcuts/use-publish-shortcuts.ts
+++ b/apps/designer/app/designer/shared/shortcuts/use-publish-shortcuts.ts
@@ -20,7 +20,6 @@ export const usePublishShortcuts = (publish: Publish) => {
useHotkeys(
shortcuts[name],
(event) => {
- event.preventDefault();
publish({
type: "shortcut",
payload: { name, key: event.key },
diff --git a/apps/designer/app/entry.client.tsx b/apps/designer/app/entry.client.tsx
index af4f800575..65822500b7 100644
--- a/apps/designer/app/entry.client.tsx
+++ b/apps/designer/app/entry.client.tsx
@@ -21,12 +21,18 @@ initSentry({
// Forward DEBUG env variable defined on the server to the client-side debug() utility.
// This way you can set it once server-side and have the logging or feature flags enabled on both, server and client
-if (
- typeof localStorage !== "undefined" &&
- // Client-side value should override the server-side one
- (localStorage.debug == null || localStorage.debug === "")
-) {
- localStorage.debug = env.DEBUG;
+// https://github.com/debug-js/debug#browser-support
+try {
+ if (
+ // Client-side value should override the server-side one
+ localStorage.debug == null ||
+ localStorage.debug === ""
+ ) {
+ localStorage.debug = env.DEBUG;
+ }
+} catch (e) {
+ // eslint-disable-next-line no-console
+ console.warn("Failed to set localStorage.debug due to Error:", e);
}
hydrate(, document);
diff --git a/apps/designer/public/logo.svg b/apps/designer/public/logo.svg
deleted file mode 100644
index 6f5559de65..0000000000
--- a/apps/designer/public/logo.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-
diff --git a/apps/designer/public/logo.webp b/apps/designer/public/logo.webp
new file mode 100644
index 0000000000..fa18ef216d
Binary files /dev/null and b/apps/designer/public/logo.webp differ
diff --git a/packages/design-system/src/components/button.tsx b/packages/design-system/src/components/button.tsx
index eb9ad25417..ade004b281 100644
--- a/packages/design-system/src/components/button.tsx
+++ b/packages/design-system/src/components/button.tsx
@@ -7,7 +7,7 @@ export const Button = styled("button", {
boxSizing: "border-box",
border: "none",
userSelect: "none",
- transition: "all 200ms ease-out",
+ transition: "all 150ms ease-out",
"&::before": {
boxSizing: "border-box",
},
@@ -40,7 +40,7 @@ export const Button = styled("button", {
variants: {
size: {
"1": {
- borderRadius: 2,
+ borderRadius: "$1",
height: "$5",
px: "$3",
fontSize: "$1",
@@ -82,7 +82,7 @@ export const Button = styled("button", {
},
blue: {
backgroundColor: "$blue10",
- color: "$loContrast",
+ color: "white",
"@hover": {
"&:hover": {
backgroundColor: "$loContrast",
@@ -269,7 +269,8 @@ export const Button = styled("button", {
backgroundColor: "$blueA4",
},
"&:focus": {
- boxShadow: "inset 0 0 0 1px $colors$blueA8, 0 0 0 1px $colors$blueA8",
+ boxShadow:
+ "0px 0px 0px 2px $colors$blue10, 0px 0px 0px 2px $colors$blue10",
},
'&[data-state="open"]': {
backgroundColor: "$blueA4",
diff --git a/packages/design-system/src/components/combobox.tsx b/packages/design-system/src/components/combobox.tsx
index fbf53839cf..3e61b6c3fc 100644
--- a/packages/design-system/src/components/combobox.tsx
+++ b/packages/design-system/src/components/combobox.tsx
@@ -10,8 +10,9 @@ import React, {
import { CheckIcon, ChevronDownIcon } from "@webstudio-is/icons";
import { Popper, PopperContent, PopperAnchor } from "@radix-ui/react-popper";
import {
- DownshiftState,
- UseComboboxStateChangeOptions,
+ type DownshiftState,
+ type UseComboboxStateChangeOptions,
+ type UseComboboxProps as UseDownshiftComboboxProps,
useCombobox as useDownshiftCombobox,
} from "downshift";
import { matchSorter } from "match-sorter";
@@ -38,7 +39,7 @@ const Listbox = styled("ul", panelStyles, {
display: "none",
},
},
- isEmpty: {
+ empty: {
true: {
display: "none",
},
@@ -129,7 +130,7 @@ const useFilter =
- ({
};
};
-type UseComboboxProps
- = {
+type UseComboboxProps
- = UseDownshiftComboboxProps
- & {
items: Array
- ;
itemToString: (item: Item | null) => string;
value: Item | null; // This is to prevent: "downshift: A component has changed the uncontrolled prop "selectedItem" to be controlled."
@@ -152,6 +153,7 @@ export const useCombobox =
- ({
onItemHighlight,
stateReducer = (state, { changes }) => changes,
match,
+ ...rest
}: UseComboboxProps
- ) => {
const { filteredItems, filter, resetFilter } = useFilter
- ({
items,
@@ -160,7 +162,9 @@ export const useCombobox =
- ({
});
const downshiftProps = useDownshiftCombobox({
+ ...rest,
items: filteredItems,
+ defaultHighlightedIndex: -1,
selectedItem: value, // Prevent downshift warning about switching controlled mode
stateReducer,
itemToString,
@@ -204,7 +208,7 @@ export const useCombobox =
- ({
return {
...getMenuProps(options),
state: isOpen ? "open" : "closed",
- isEmpty: filteredItems.length === 0,
+ empty: filteredItems.length === 0,
};
},
[getMenuProps, isOpen, filteredItems.length]
@@ -215,6 +219,7 @@ export const useCombobox =
- ({
items: filteredItems,
getItemProps: enhancedGetItemProps,
getMenuProps: enhancedGetMenuProps,
+ resetFilter,
};
};
diff --git a/packages/design-system/src/components/icon-button.tsx b/packages/design-system/src/components/icon-button.tsx
index 77c7c429db..e1169ad5ec 100644
--- a/packages/design-system/src/components/icon-button.tsx
+++ b/packages/design-system/src/components/icon-button.tsx
@@ -30,6 +30,8 @@ export const IconButton = styled("button", {
"&:focus": {
backgroundColor: "$slate6",
outline: "none",
+ border: "2px solid white",
+ boxShadow: "0px 0px 0px 2px $colors$blue10, 0px 0px 0px 2px $colors$blue10",
},
"&:disabled": {
pointerEvents: "none",
@@ -41,8 +43,12 @@ export const IconButton = styled("button", {
size: {
"1": {
borderRadius: 2,
- height: "$5",
- width: "$5",
+ height: "$3",
+ width: "$3",
+ "&:hover, &:focus": {
+ background: "none",
+ borderColor: "transparent",
+ },
},
"2": {
borderRadius: "$1",
@@ -60,12 +66,6 @@ export const IconButton = styled("button", {
width: "$8",
},
},
- state: {
- active: {
- color: "$loContrast",
- backgroundColor: "$blue10",
- },
- },
},
defaultVariants: {
size: "1",
diff --git a/packages/design-system/src/components/panel.tsx b/packages/design-system/src/components/panel.tsx
index b4a361629b..d4abe937ef 100644
--- a/packages/design-system/src/components/panel.tsx
+++ b/packages/design-system/src/components/panel.tsx
@@ -2,10 +2,9 @@ import { css, styled } from "../stitches.config";
export const panelStyles = css({
overflow: "auto",
- backgroundColor: "$colors$slate4",
+ backgroundColor: "$slate4",
borderRadius: "$1",
- boxShadow:
- "0px 2px 7px rgba(0, 0, 0, 0.1), 0px 5px 17px rgba(0, 0, 0, 0.15), inset 0 0 1px 1px $colors$slate1, 0 0 0 1px $colors$slate8",
+ boxShadow: "0px 2px 7px rgba(0, 0, 0, 0.1), 0px 5px 17px rgba(0, 0, 0, 0.15)",
});
export const Panel = styled("div", panelStyles);
diff --git a/packages/design-system/src/components/popover.tsx b/packages/design-system/src/components/popover.tsx
index 1e7bf750f0..7ef6455061 100644
--- a/packages/design-system/src/components/popover.tsx
+++ b/packages/design-system/src/components/popover.tsx
@@ -74,12 +74,12 @@ export const PopoverHeader = ({ title }: PopoverHeaderProps) => {
>
{title}
-
+
-
+
>
);
};
diff --git a/packages/design-system/src/components/primitives/dnd/placement-indicator.tsx b/packages/design-system/src/components/primitives/dnd/placement-indicator.tsx
index 43b51fe45a..c9770086eb 100644
--- a/packages/design-system/src/components/primitives/dnd/placement-indicator.tsx
+++ b/packages/design-system/src/components/primitives/dnd/placement-indicator.tsx
@@ -4,7 +4,7 @@ import { type Placement } from "./geometry-utils";
const placementStyle = {
boxSizing: "content-box",
position: "absolute",
- background: "$dropPlacement",
+ background: "$blue10",
pointerEvents: "none",
};
diff --git a/packages/design-system/src/components/search-field.stories.tsx b/packages/design-system/src/components/search-field.stories.tsx
new file mode 100644
index 0000000000..275f521221
--- /dev/null
+++ b/packages/design-system/src/components/search-field.stories.tsx
@@ -0,0 +1,45 @@
+import { expect } from "@storybook/jest";
+import { type ComponentStory } from "@storybook/react";
+import { userEvent, waitFor, within } from "@storybook/testing-library";
+import React from "react";
+import { SearchField } from "./search-field";
+
+export default {
+ component: SearchField,
+};
+
+export const Empty: ComponentStory = () => {
+ return ;
+};
+
+export const WithInitialValue: ComponentStory = () => {
+ return ;
+};
+
+let updateValue: (value: string) => void;
+
+export const UpdateValue: ComponentStory = () => {
+ const [value, setValue] = React.useState("something");
+ updateValue = setValue;
+ return ;
+};
+
+UpdateValue.play = async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const search = canvas.getByTitle("Search");
+ expect(search.value).toBe("something");
+ updateValue?.("test");
+ await waitFor(() => expect(search.value).toBe("test"));
+};
+
+export const Reset: ComponentStory = () => {
+ return ;
+};
+
+Reset.play = async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const search = canvas.getByTitle("Search");
+ await userEvent.click(canvas.getByTitle("Reset search"));
+ await waitFor(() => expect(search.value).toBe(""));
+ expect(await canvas.queryByTitle("Reset search")).toBeNull();
+};
diff --git a/packages/design-system/src/components/search-field.tsx b/packages/design-system/src/components/search-field.tsx
new file mode 100644
index 0000000000..2bd137cfd6
--- /dev/null
+++ b/packages/design-system/src/components/search-field.tsx
@@ -0,0 +1,75 @@
+import {
+ type ComponentProps,
+ type ForwardRefRenderFunction,
+ forwardRef,
+ useRef,
+ useState,
+ useEffect,
+} from "react";
+import { Cross2Icon, MagnifyingGlassIcon } from "@webstudio-is/icons";
+import { TextField } from "./text-field";
+import { IconButton } from "./icon-button";
+import { styled } from "../stitches.config";
+
+const SearchIcon = styled(MagnifyingGlassIcon, {
+ color: "$hint",
+ padding: "$1",
+});
+
+const SearchFieldBase: ForwardRefRenderFunction<
+ HTMLInputElement,
+ ComponentProps & { onCancel?: () => void }
+> = (props, ref) => {
+ const {
+ onChange,
+ onCancel,
+ value: propsValue = "",
+ onKeyDown,
+ ...rest
+ } = props;
+ const [value, setValue] = useState(String(propsValue));
+ const inputRef = useRef(null);
+ useEffect(() => {
+ setValue(String(propsValue));
+ }, [propsValue]);
+ return (
+ }
+ suffix={
+ value.length > 0 ? (
+ {
+ inputRef.current?.focus();
+ setValue("");
+ onCancel?.();
+ }}
+ >
+
+
+ ) : null
+ }
+ onChange={(event) => {
+ setValue(event.target.value);
+ onChange?.(event);
+ }}
+ onKeyDown={(event) => {
+ // Make sure we clear the search on esc and preserve the default browser behavior
+ if (event.key === "Escape" && value.length !== 0) {
+ event.stopPropagation();
+ return;
+ }
+ onKeyDown?.(event);
+ }}
+ />
+ );
+};
+
+export const SearchField = forwardRef(SearchFieldBase);
diff --git a/packages/design-system/src/components/text-area.tsx b/packages/design-system/src/components/text-area.tsx
index 191ad5438c..f04dcbeb2d 100644
--- a/packages/design-system/src/components/text-area.tsx
+++ b/packages/design-system/src/components/text-area.tsx
@@ -11,7 +11,7 @@ export const TextArea = styled("textarea", {
width: "100%",
WebkitTapHighlightColor: "rgba(0,0,0,0)",
backgroundColor: "$loContrast",
- boxShadow: "inset 0 0 0 1px $colors$slate7",
+ boxShadow: "inset 0 0 0 1px $colors$muted",
color: "$hiContrast",
fontVariantNumeric: "tabular-nums",
position: "relative",
diff --git a/packages/design-system/src/components/text-field.stories.tsx b/packages/design-system/src/components/text-field.stories.tsx
index f49a1c0adb..0a7fa39955 100644
--- a/packages/design-system/src/components/text-field.stories.tsx
+++ b/packages/design-system/src/components/text-field.stories.tsx
@@ -30,6 +30,7 @@ export const NativeProps: ComponentStory = () => {
+
@@ -41,6 +42,8 @@ export const Variants: ComponentStory = () => {
+
+
);
};
diff --git a/packages/design-system/src/components/text-field.tsx b/packages/design-system/src/components/text-field.tsx
index 078cd2edd6..6f6574903c 100644
--- a/packages/design-system/src/components/text-field.tsx
+++ b/packages/design-system/src/components/text-field.tsx
@@ -1,8 +1,71 @@
import { mergeRefs } from "@react-aria/utils";
import React from "react";
import { useFocusWithin } from "@react-aria/interactions";
-import { styled } from "../stitches.config";
-import { Flex } from "./flex";
+import { css, styled } from "../stitches.config";
+import { ChevronLeftIcon } from "@webstudio-is/icons";
+import { cssVars } from "@webstudio-is/css-vars";
+
+const backgroundColorVar = cssVars.define("background-color");
+const colorVar = cssVars.define("color");
+
+const getTextFieldSuffixCssVars = (state: "focus" | "hover") => {
+ if (state === "focus") {
+ return {
+ [backgroundColorVar]: "$colors$blue10",
+ [colorVar]: "white",
+ };
+ }
+
+ return {
+ [backgroundColorVar]: "$colors$slate7",
+ [colorVar]: "$colors$hiContrast",
+ };
+};
+
+const textFieldIconBaseStyle = css({
+ height: "$5",
+ minWidth: "$2",
+ display: "inline-flex",
+ alignItems: "center",
+ justifyContent: "center",
+ borderRadius: 2,
+});
+
+// Trigger can be used as a button, which is focusable/hoverable itself or as an icon,
+// which has the same states but activated over the parent component element.
+export const TextFieldIconButton = styled(
+ "button",
+ {
+ all: "unset",
+ "&:hover": {
+ backgroundColor: "$slate7",
+ color: "$hiContrast",
+ },
+ "&:focus": {
+ backgroundColor: "$blue10",
+ color: "white",
+ },
+ variants: {
+ state: {
+ active: {
+ backgroundColor: "$blue10",
+ color: "white",
+ "&:hover": {
+ backgroundColor: "$blue10",
+ color: "white",
+ },
+ },
+ },
+ },
+ },
+ textFieldIconBaseStyle
+);
+
+export const TextFieldIcon = styled("span", textFieldIconBaseStyle, {
+ // Icon receives colors from parent.
+ backgroundColor: cssVars.use(backgroundColorVar),
+ color: cssVars.use(colorVar),
+});
const InputBase = styled("input", {
// Reset
@@ -52,7 +115,7 @@ const InputBase = styled("input", {
},
"&::placeholder": {
- color: "$slate9",
+ color: "$hint",
},
"&:disabled": {
@@ -78,12 +141,10 @@ const TextFieldBase = styled("div", {
fontSize: "$1",
height: 28, // @todo waiting for the sizing scale
lineHeight: 1,
-
- "&:focus-within, &[data-state=focus]": {
+ "&:focus-within": {
boxShadow:
"inset 0px 0px 0px 1px $colors$blue10, 0px 0px 0px 1px $colors$blue10",
},
-
"&[aria-disabled=true]": {
pointerEvents: "none",
backgroundColor: "$slate2",
@@ -94,7 +155,6 @@ const TextFieldBase = styled("div", {
boxShadow: "inset 0px 0px 0px 1px $colors$slate7",
},
},
-
variants: {
variant: {
ghost: {
@@ -108,7 +168,7 @@ const TextFieldBase = styled("div", {
"&:focus": {
backgroundColor: "$loContrast",
boxShadow:
- "inset 0px 0px 0px 1px $colors$blue8, 0px 0px 0px 1px $colors$blue10",
+ "inset 0px 0px 0px 1px $colors$blue10, 0px 0px 0px 1px $colors$blue10",
},
"&:disabled": {
backgroundColor: "transparent",
@@ -117,6 +177,10 @@ const TextFieldBase = styled("div", {
backgroundColor: "transparent",
},
},
+ button: {
+ "&:hover": getTextFieldSuffixCssVars("hover"),
+ "&:focus-within": getTextFieldSuffixCssVars("focus"),
+ },
},
state: {
invalid: {
@@ -133,20 +197,46 @@ const TextFieldBase = styled("div", {
"inset 0px 0px 0px 1px $colors$green8, 0px 0px 0px 1px $colors$green8",
},
},
+ active: {
+ boxShadow:
+ "inset 0px 0px 0px 1px $colors$blue10, 0px 0px 0px 1px $colors$blue10",
+ ...getTextFieldSuffixCssVars("focus"),
+ },
},
+ // Preffix and suffix are responsible for their spacing
withPrefix: {
true: {
- paddingLeft: 2,
+ paddingLeft: 0,
},
},
withSuffix: {
true: {
- paddingRight: 2,
+ paddingRight: 0,
},
},
},
});
+const PrefixSlot = styled("div", {
+ display: "flex",
+ alignItems: "center",
+ flexShrink: 0,
+ order: 0,
+ padding: 2,
+ paddingRight: 0,
+ borderRadius: 2,
+});
+
+const SuffixSlot = styled("div", {
+ display: "flex",
+ alignItems: "center",
+ flexShrink: 0,
+ order: 2,
+ padding: 2,
+ paddingLeft: 0,
+ borderRadius: 2,
+});
+
export type TextFieldProps = Pick<
React.ComponentProps,
"variant" | "state" | "css"
@@ -161,16 +251,22 @@ export const TextField = React.forwardRef(
(props, forwardedRef) => {
const {
prefix,
- suffix,
css,
disabled,
inputRef,
state,
- variant,
+ variant: variantProp,
onFocus,
onBlur,
+ onClick,
+ type,
+ // prevent spreading it into the dom
+ suffix: suffixProp,
...textFieldProps
} = props;
+ let suffix = suffixProp;
+ const variant =
+ type === "button" && variantProp === undefined ? "button" : variantProp;
const internalInputRef = React.useRef(null);
@@ -180,16 +276,23 @@ export const TextField = React.forwardRef(
const { focusWithinProps } = useFocusWithin({
isDisabled: disabled,
- onFocusWithin: (event) => {
- // @ts-expect-error Type mismatch from react-aria
- onFocus?.(event);
- },
- onBlurWithin: (event) => {
- // @ts-expect-error Type mismatch from react-aria
- onBlur?.(event);
- },
+ // @ts-expect-error Type mismatch from react-aria
+ onFocusWithin: onFocus,
+ // @ts-expect-error Type mismatch from react-aria
+ onBlurWithin: onBlur,
});
+ if (type === "button" && suffix === undefined) {
+ suffix = (
+ {
+ internalInputRef.current?.click();
+ }}
+ />
+ );
+ }
+
return (
(
>
{/* We want input to be the first element in DOM so it receives the focus first */}
- {prefix && (
-
- {prefix}
-
- )}
-
- {suffix && (
-
- {suffix}
-
- )}
+ {prefix && {prefix}}
+ {suffix && {suffix}}
);
}
diff --git a/packages/design-system/src/components/tree/placement-indicator.tsx b/packages/design-system/src/components/tree/placement-indicator.tsx
index de38e5d226..b9ef27e8b1 100644
--- a/packages/design-system/src/components/tree/placement-indicator.tsx
+++ b/packages/design-system/src/components/tree/placement-indicator.tsx
@@ -23,7 +23,7 @@ const Circle = styled(Box, {
width: CIRCLE_SIZE,
height: CIRCLE_SIZE,
position: "absolute",
- border: "solid $dropPlacement",
+ border: "solid $blue10",
borderWidth: 2,
borderRadius: "50%",
pointerEvents: "none",
@@ -32,7 +32,7 @@ const Circle = styled(Box, {
const Line = styled(Box, {
boxSizing: "content-box",
position: "absolute",
- background: "$dropPlacement",
+ background: "$blue10",
pointerEvents: "none",
outline: "solid $loContrast",
outlineWidth: OUTLINE_WIDTH,
diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts
index 4d6604439f..4cf5adcdbd 100644
--- a/packages/design-system/src/index.ts
+++ b/packages/design-system/src/index.ts
@@ -10,7 +10,8 @@ export { Label } from "./components/label";
export * as ToggleGroup from "./components/toggle-group";
export * from "./components/progress-radial";
export * from "./components/select";
-export { TextField } from "./components/text-field";
+export * from "./components/text-field";
+export { SearchField } from "./components/search-field";
export { Flex } from "./components/flex";
export { Image } from "./components/image";
export { Grid } from "./components/grid";
diff --git a/packages/design-system/src/stitches.config.ts b/packages/design-system/src/stitches.config.ts
index 460364c816..614df53171 100644
--- a/packages/design-system/src/stitches.config.ts
+++ b/packages/design-system/src/stitches.config.ts
@@ -195,9 +195,9 @@ const {
// Semantic colors
hiContrast: "$slate12",
loContrast: "$slate1",
- hint: "$slate10",
+ hint: "$slate9",
muted: "$slate6",
- primary: "$blue12",
+ primary: "$blue10",
canvas: "hsl(0 0% 93%)",
panel: "white",
panelOutline: "hsl(0deg 0% 0% / 10%)",
@@ -206,7 +206,6 @@ const {
shadowDark: "hsl(206 22% 7% / 20%)",
background: "$slate1",
text: "$slate12",
- dropPlacement: "$blue9",
},
fonts: {
sans: "Inter, -apple-system, system-ui, sans-serif",
@@ -478,12 +477,11 @@ export const darkTheme = createTheme({
...goldDarkA,
// Semantic colors
- hint: "$slate10",
- muted: "$slate8",
- primary: "$blue9",
hiContrast: "$slate12",
- // radix colors in this range are either too dark/too light, nothing gray/black in the 16-17% luminosity range
loContrast: "hsl(0 0% 17%)",
+ hint: "$slate9",
+ muted: "$slate8",
+ primary: "$blue10",
canvas: "hsl(0 0% 15%)",
panel: "$slate3",
panelOutline: "hsl(0deg 0% 100% / 10%)",
@@ -492,6 +490,5 @@ export const darkTheme = createTheme({
shadowDark: "hsl(206 22% 7% / 20%)",
background: "$slate12",
text: "$slate1",
- dropPlacement: "$blue9",
},
});
diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json
index ade03fcd41..800af1f8f0 100644
--- a/packages/react-sdk/package.json
+++ b/packages/react-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@webstudio-is/react-sdk",
- "version": "0.6.1",
+ "version": "0.7.0",
"description": "Webstudio JavaScript / TypeScript API",
"author": "Webstudio ",
"homepage": "https://webstudio.is",
diff --git a/packages/react-sdk/src/tree/create-elements-tree.tsx b/packages/react-sdk/src/tree/create-elements-tree.tsx
index 8ca8adf138..9518354046 100644
--- a/packages/react-sdk/src/tree/create-elements-tree.tsx
+++ b/packages/react-sdk/src/tree/create-elements-tree.tsx
@@ -4,6 +4,7 @@ import type { Instance } from "../db";
import type { Breakpoint } from "../css";
import { type WrapperComponentProps } from "./wrapper-component";
import { Scripts, ScrollRestoration } from "@remix-run/react";
+import { SessionStoragePolyfill } from "./session-storage-polyfill";
export type ChildrenUpdates = Array<
| string
@@ -24,11 +25,13 @@ export type OnChangeChildren = (change: {
}) => void;
export const createElementsTree = ({
+ sandbox,
instance,
breakpoints,
Component,
onChangeChildren,
}: {
+ sandbox?: boolean;
instance: Instance;
breakpoints: Array;
Component: (props: WrapperComponentProps) => JSX.Element;
@@ -46,6 +49,7 @@ export const createElementsTree = ({
children: [
{children}
+ {sandbox && }
,
diff --git a/packages/react-sdk/src/tree/session-storage-polyfill.tsx b/packages/react-sdk/src/tree/session-storage-polyfill.tsx
new file mode 100644
index 0000000000..6e39e4a1b6
--- /dev/null
+++ b/packages/react-sdk/src/tree/session-storage-polyfill.tsx
@@ -0,0 +1,50 @@
+// This is a temporary work around for https://github.com/remix-run/remix/issues/3659
+
+// The code is based on this discussion https://bugs.chromium.org/p/chromium/issues/detail?id=357625
+const polyfill = function () {
+ try {
+ const key = "__session_storage_availability_test__";
+ sessionStorage.setItem(key, "test"); // test
+ sessionStorage.removeItem(key); // cleanup
+ } catch (error) {
+ alert(
+ 'It looks like you have disabled cookies in your browser. Webstudio designer may not work properly.\n\nTo enable cookies, go to "Setting" > "Privacy and security" > "Cookies and other site data", and make sure neither "Block all cookies" nor "Block third-party cookies" are selected.\n\nRead more at https://support.google.com/chrome/answer/95647'
+ );
+ const data = new Map();
+ Object.defineProperty(window, "sessionStorage", {
+ value: {
+ setItem: (key: string, val: unknown) => {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `Session storage is unavailable due to Error "${
+ (error as Error).message
+ }". A polyfill is used to set value of "${key}". The value will be lost when the page is reloaded.`
+ );
+ data.set(key, String(val));
+ },
+ getItem: (key: string) => {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `Session storage is unavailable due to Error "${
+ (error as Error).message
+ }". A polyfill is used to get value of "${key}". The value will be undefined if the page was reloaded after it was set.`
+ );
+ return data.get(key);
+ },
+ removeItem: (key: string) => {
+ data.delete(key);
+ },
+ clear: () => {
+ data.clear();
+ },
+ },
+ });
+ }
+}.toString();
+
+export const SessionStoragePolyfill = () => (
+
+);