mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
experimental: Style panel focus mode (#4835)
https://github.com/webstudio-is/webstudio/issues/4816 ## Description - implements accordion pattern - adds a dropdown switch ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
This commit is contained in:
1 parent
609fdb47da
commit
a8bfa4d37a
15 files changed
+266
-109
No files matched your search
@@ -19,7 +19,7 @@ import {
|
||||
Kbd,
|
||||
FloatingPanelProvider,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { StylePanel } from "~/builder/features/style-panel";
|
||||
import { ModeMenu, StylePanel } from "~/builder/features/style-panel";
|
||||
import { SettingsPanelContainer } from "~/builder/features/settings-panel";
|
||||
import {
|
||||
$registeredComponentMetas,
|
||||
@@ -33,6 +33,7 @@ import { getInstanceLabel } from "~/shared/instance-utils";
|
||||
import { BindingPopoverProvider } from "~/builder/shared/binding-popover";
|
||||
import { $activeInspectorPanel } from "~/builder/shared/nano-states";
|
||||
import { $selectedInstance, $selectedPage } from "~/shared/awareness";
|
||||
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
|
||||
|
||||
const InstanceInfo = ({ instance }: { instance: Instance }) => {
|
||||
const metas = useStore($registeredComponentMetas);
|
||||
@@ -42,16 +43,7 @@ const InstanceInfo = ({ instance }: { instance: Instance }) => {
|
||||
}
|
||||
const label = getInstanceLabel(instance, componentMeta);
|
||||
return (
|
||||
<Flex
|
||||
shrink="false"
|
||||
gap="1"
|
||||
align="center"
|
||||
css={{
|
||||
p: theme.panel.padding,
|
||||
pb: 0,
|
||||
color: theme.colors.foregroundSubtle,
|
||||
}}
|
||||
>
|
||||
<Flex shrink={false} gap="1" align="center">
|
||||
<MetaIcon icon={componentMeta.icon} />
|
||||
<Text truncate variant="labelsSentenceCase">
|
||||
{label}
|
||||
@@ -175,7 +167,18 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
|
||||
</PanelTabsList>
|
||||
<Separator />
|
||||
<PanelTabsContent value="style" css={contentStyle} tabIndex={-1}>
|
||||
<InstanceInfo instance={selectedInstance} />
|
||||
<Flex
|
||||
justify="between"
|
||||
align="center"
|
||||
shrink={false}
|
||||
css={{
|
||||
paddingInline: theme.panel.paddingInline,
|
||||
height: theme.spacing[13],
|
||||
}}
|
||||
>
|
||||
<InstanceInfo instance={selectedInstance} />
|
||||
{isFeatureEnabled("stylePanelModes") && <ModeMenu />}
|
||||
</Flex>
|
||||
<StylePanel />
|
||||
</PanelTabsContent>
|
||||
<PanelTabsContent
|
||||
@@ -184,7 +187,17 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ScrollArea>
|
||||
<InstanceInfo instance={selectedInstance} />
|
||||
<Flex
|
||||
justify="between"
|
||||
align="center"
|
||||
shrink={false}
|
||||
css={{
|
||||
paddingInline: theme.panel.paddingInline,
|
||||
height: theme.spacing[13],
|
||||
}}
|
||||
>
|
||||
<InstanceInfo instance={selectedInstance} />
|
||||
</Flex>
|
||||
<SettingsPanelContainer
|
||||
// Re-render when instance changes
|
||||
key={selectedInstance.id}
|
||||
|
||||
@@ -310,13 +310,15 @@ const VariablesList = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const label = "Data Variables";
|
||||
|
||||
export const VariablesSection = () => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = useOpenState({ label: "variables" });
|
||||
const [isOpen, setIsOpen] = useOpenState(label);
|
||||
return (
|
||||
<VariablePopoverProvider value={{ containerRef }}>
|
||||
<CollapsibleSectionRoot
|
||||
label="Data Variables"
|
||||
label={label}
|
||||
fullWidth={true}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
|
||||
@@ -199,7 +199,7 @@ export const PropertyInfo = ({
|
||||
<ResetIcon />
|
||||
</Flex>
|
||||
}
|
||||
suffix={<Kbd value={["option", "click"]} color="moreSubtle" />}
|
||||
suffix={<Kbd value={["alt", "click"]} color="moreSubtle" />}
|
||||
css={{ gridTemplateColumns: "1fr max-content 1fr" }}
|
||||
onClick={onReset}
|
||||
>
|
||||
|
||||
@@ -83,7 +83,7 @@ const AdvancedStyleSection = (props: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const { label, children, properties, onAdd } = props;
|
||||
const [isOpen, setIsOpen] = useOpenState(props);
|
||||
const [isOpen, setIsOpen] = useOpenState(label);
|
||||
const styles = useComputedStyles(properties);
|
||||
return (
|
||||
<CollapsibleSectionRoot
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
forwardRef,
|
||||
useState,
|
||||
type ElementRef,
|
||||
type ComponentProps,
|
||||
} from "react";
|
||||
import { forwardRef, type ElementRef, type ComponentProps } from "react";
|
||||
import type { StyleProperty } from "@webstudio-is/css-engine";
|
||||
import { propertyDescriptions } from "@webstudio-is/css-data";
|
||||
import {
|
||||
@@ -32,7 +27,10 @@ import {
|
||||
EyeOpenIcon,
|
||||
EllipsesIcon,
|
||||
} from "@webstudio-is/icons";
|
||||
import { CollapsibleSectionRoot } from "~/builder/shared/collapsible-section";
|
||||
import {
|
||||
CollapsibleSectionRoot,
|
||||
useOpenState,
|
||||
} from "~/builder/shared/collapsible-section";
|
||||
import {
|
||||
addDefaultsForTransormSection,
|
||||
isTransformPanelPropertyUsed,
|
||||
@@ -138,7 +136,7 @@ const TransformAdvancedPopover = () => {
|
||||
};
|
||||
|
||||
export const Section = () => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [isOpen, setIsOpen] = useOpenState(label);
|
||||
|
||||
const styles = useComputedStyles(properties);
|
||||
const isAnyTransformPropertyAdded = transformPanels.some((panel) =>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from "react";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import { PlusIcon } from "@webstudio-is/icons";
|
||||
import {
|
||||
@@ -12,7 +11,10 @@ import {
|
||||
type LayerValueItem,
|
||||
type StyleProperty,
|
||||
} from "@webstudio-is/css-engine";
|
||||
import { CollapsibleSectionRoot } from "~/builder/shared/collapsible-section";
|
||||
import {
|
||||
CollapsibleSectionRoot,
|
||||
useOpenState,
|
||||
} from "~/builder/shared/collapsible-section";
|
||||
import { $selectedOrLastStyleSourceSelector } from "~/shared/nano-states";
|
||||
import { humanizeString } from "~/shared/string-utils";
|
||||
import { repeatUntil } from "~/shared/array-utils";
|
||||
@@ -83,7 +85,7 @@ const getLayerLabel = ({
|
||||
};
|
||||
|
||||
export const Section = () => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [isOpen, setIsOpen] = useOpenState(label);
|
||||
|
||||
const selectedOrLastStyleSourceSelector = useStore(
|
||||
$selectedOrLastStyleSourceSelector
|
||||
|
||||
@@ -41,11 +41,9 @@ export const StyleSection = (props: {
|
||||
// @todo remove to keep sections consistent
|
||||
fullWidth?: boolean;
|
||||
children: ReactNode;
|
||||
accordion?: string;
|
||||
initialOpen?: string;
|
||||
}) => {
|
||||
const { label, children, properties, fullWidth } = props;
|
||||
const [isOpen, setIsOpen] = useOpenState(props);
|
||||
const [isOpen, setIsOpen] = useOpenState(label);
|
||||
const styles = useComputedStyles(properties);
|
||||
return (
|
||||
<CollapsibleSectionRoot
|
||||
@@ -74,7 +72,7 @@ export const RepeatedStyleSection = (props: {
|
||||
}) => {
|
||||
const { label, description, children, properties, onAdd, collapsible } =
|
||||
props;
|
||||
const [isOpen, setIsOpen] = useOpenState(props);
|
||||
const [isOpen, setIsOpen] = useOpenState(label);
|
||||
const styles = useComputedStyles(properties);
|
||||
const dots = getDots(styles);
|
||||
|
||||
|
||||
@@ -5,6 +5,18 @@ import {
|
||||
Text,
|
||||
Separator,
|
||||
ScrollArea,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioItem,
|
||||
MenuCheckedIcon,
|
||||
DropdownMenuRadioGroup,
|
||||
rawTheme,
|
||||
Kbd,
|
||||
Flex,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuItem,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import { computed } from "nanostores";
|
||||
@@ -14,8 +26,15 @@ import { sections } from "./sections";
|
||||
import { toValue } from "@webstudio-is/css-engine";
|
||||
import { $instanceTags, useParentComputedStyleDecl } from "./shared/model";
|
||||
import { $selectedInstance } from "~/shared/awareness";
|
||||
import { CollapsibleSectionContext } from "~/builder/shared/collapsible-section";
|
||||
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
|
||||
import { CollapsibleProvider } from "~/builder/shared/collapsible-section";
|
||||
import { EllipsesIcon } from "@webstudio-is/icons";
|
||||
import {
|
||||
$settings,
|
||||
getSetting,
|
||||
setSetting,
|
||||
type Settings,
|
||||
} from "~/builder/shared/client-settings";
|
||||
import { useState } from "react";
|
||||
|
||||
const $selectedInstanceTag = computed(
|
||||
[$selectedInstance, $instanceTags],
|
||||
@@ -27,7 +46,67 @@ const $selectedInstanceTag = computed(
|
||||
}
|
||||
);
|
||||
|
||||
export const ModeMenu = () => {
|
||||
const value = getSetting("stylePanelMode");
|
||||
const [focusedValue, setFocusedValue] = useState<string>(value);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton>
|
||||
<EllipsesIcon />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
sideOffset={Number.parseFloat(rawTheme.spacing[5])}
|
||||
css={{ width: theme.spacing[25] }}
|
||||
>
|
||||
<DropdownMenuRadioGroup
|
||||
value={value}
|
||||
onValueChange={(value) => {
|
||||
setSetting("stylePanelMode", value as Settings["stylePanelMode"]);
|
||||
}}
|
||||
>
|
||||
<DropdownMenuRadioItem
|
||||
value="default"
|
||||
icon={<MenuCheckedIcon />}
|
||||
onFocus={() => setFocusedValue("default")}
|
||||
>
|
||||
Default
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem
|
||||
value="focus"
|
||||
icon={<MenuCheckedIcon />}
|
||||
onFocus={() => setFocusedValue("focus")}
|
||||
>
|
||||
<Flex justify="between" grow>
|
||||
<Text>Focus mode</Text> <Kbd value={["alt", "shift", "s"]} />
|
||||
</Flex>
|
||||
</DropdownMenuRadioItem>
|
||||
{/*
|
||||
<DropdownMenuRadioItem value="advanced" icon={<MenuCheckedIcon />}>
|
||||
Advanced mode
|
||||
</DropdownMenuRadioItem> */}
|
||||
</DropdownMenuRadioGroup>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{focusedValue === "default" && (
|
||||
<DropdownMenuItem hint>
|
||||
All sections are open by default.
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{focusedValue === "focus" && (
|
||||
<DropdownMenuItem hint>
|
||||
Only one section is open at a time.
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export const StylePanel = () => {
|
||||
const { stylePanelMode } = useStore($settings);
|
||||
const selectedInstanceRenderState = useStore($selectedInstanceRenderState);
|
||||
const tag = useStore($selectedInstanceTag);
|
||||
const display = useParentComputedStyleDecl("display");
|
||||
@@ -75,14 +154,12 @@ export const StylePanel = () => {
|
||||
</Box>
|
||||
<Separator />
|
||||
<ScrollArea>
|
||||
<CollapsibleSectionContext.Provider
|
||||
value={{
|
||||
accordion: isFeatureEnabled("stylePanelModes"),
|
||||
initialOpen: "Layout",
|
||||
}}
|
||||
<CollapsibleProvider
|
||||
accordion={stylePanelMode === "focus"}
|
||||
initialOpen={stylePanelMode === "focus" ? "Layout" : "*"}
|
||||
>
|
||||
{all}
|
||||
</CollapsibleSectionContext.Provider>
|
||||
</CollapsibleProvider>
|
||||
</ScrollArea>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -41,7 +41,7 @@ const PagesButton = () => {
|
||||
content={
|
||||
<Text>
|
||||
{"Pages or page settings "}
|
||||
<Kbd value={["option", "click"]} color="moreSubtle" />
|
||||
<Kbd value={["alt", "click"]} color="moreSubtle" />
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
|
||||
+2
-2
@@ -203,7 +203,7 @@ export const TemplatesMenu = ({
|
||||
display: hasChildren ? undefined : "none",
|
||||
}}
|
||||
>
|
||||
<Kbd value={["option", "click"]} /> <Text>to add before</Text>
|
||||
<Kbd value={["alt", "click"]} /> <Text>to add before</Text>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</div>
|
||||
@@ -303,7 +303,7 @@ export const BlockChildHoveredInstanceOutline = () => {
|
||||
gap={1}
|
||||
css={{ order: 1, display: !hasChildren ? "none" : undefined }}
|
||||
>
|
||||
<Kbd value={["option", "click"]} color="contrast" />{" "}
|
||||
<Kbd value={["alt", "click"]} color="contrast" />{" "}
|
||||
<Text color="subtle">to delete</Text>
|
||||
</Flex>
|
||||
</Grid>
|
||||
|
||||
@@ -5,6 +5,7 @@ const Settings = z.object({
|
||||
navigatorLayout: z.enum(["docked", "undocked"]).default("undocked"),
|
||||
isAiMenuOpen: z.boolean().default(true),
|
||||
isAiCommandBarVisible: z.boolean().default(false),
|
||||
stylePanelMode: z.enum(["default", "focus", "advanced"]).default("default"),
|
||||
});
|
||||
|
||||
export type Settings = z.infer<typeof Settings>;
|
||||
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
} from "@webstudio-is/design-system";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
@@ -20,46 +23,94 @@ import type { Simplify } from "type-fest";
|
||||
|
||||
type Label = string;
|
||||
|
||||
type UseOpenStateProps = {
|
||||
label: Label;
|
||||
isOpen?: boolean;
|
||||
type State = {
|
||||
[label: string]: boolean;
|
||||
};
|
||||
|
||||
export const CollapsibleSectionContext = createContext<{
|
||||
// Preserves the open/close state even when component gets unmounted
|
||||
const $state = atom<State>({});
|
||||
|
||||
type HandleOpenState = (
|
||||
label: Label,
|
||||
isOpenForced?: boolean
|
||||
) => [boolean, (value: boolean) => void];
|
||||
|
||||
const CollapsibleSectionContext = createContext<
|
||||
| undefined
|
||||
| {
|
||||
handleOpenState: HandleOpenState;
|
||||
}
|
||||
>(undefined);
|
||||
|
||||
export const CollapsibleProvider = ({
|
||||
children,
|
||||
accordion,
|
||||
initialOpen,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
accordion?: boolean;
|
||||
initialOpen?: Label;
|
||||
}>({});
|
||||
}) => {
|
||||
const state = useStore($state);
|
||||
useEffect(() => {
|
||||
const nextState = { ...$state.get() };
|
||||
|
||||
const $stateContainer = atom<{
|
||||
[label: string]: boolean;
|
||||
}>({});
|
||||
if (initialOpen === "*") {
|
||||
for (const key in nextState) {
|
||||
nextState[key] = true;
|
||||
}
|
||||
$state.set(nextState);
|
||||
return;
|
||||
}
|
||||
|
||||
// Preserves the open/close state even when component gets unmounted
|
||||
export const useOpenState = ({
|
||||
label,
|
||||
isOpen: isOpenForced,
|
||||
}: UseOpenStateProps): [boolean, (value: boolean) => void] => {
|
||||
const state = useStore($stateContainer);
|
||||
const { accordion, initialOpen } = useContext(CollapsibleSectionContext);
|
||||
const setIsOpen = (isOpen: boolean) => {
|
||||
const update = { ...state };
|
||||
if (isOpen && accordion) {
|
||||
// In accordion mode we close everything else within that accordion.
|
||||
for (const key in update) {
|
||||
update[key] = false;
|
||||
if (accordion) {
|
||||
for (const key in nextState) {
|
||||
nextState[key] = false;
|
||||
}
|
||||
}
|
||||
update[label] = isOpen;
|
||||
$stateContainer.set(update);
|
||||
};
|
||||
|
||||
// Set initial value for accordion mode.
|
||||
if (accordion && state[label] === undefined) {
|
||||
state[label] = initialOpen === label;
|
||||
if (initialOpen) {
|
||||
nextState[initialOpen] = true;
|
||||
}
|
||||
$state.set(nextState);
|
||||
}, [accordion, initialOpen]);
|
||||
|
||||
const handleOpenState: HandleOpenState = useCallback(
|
||||
(label, isOpenForced) => {
|
||||
const nextState = { ...state };
|
||||
if (nextState[label] === undefined) {
|
||||
nextState[label] = accordion ? false : true;
|
||||
}
|
||||
|
||||
const setIsOpen = (isOpen: boolean) => {
|
||||
if (accordion) {
|
||||
for (const key in nextState) {
|
||||
nextState[key] = false;
|
||||
}
|
||||
}
|
||||
nextState[label] = isOpen;
|
||||
$state.set(nextState);
|
||||
};
|
||||
|
||||
return [isOpenForced ?? nextState[label], setIsOpen];
|
||||
},
|
||||
[state, accordion]
|
||||
);
|
||||
|
||||
return (
|
||||
<CollapsibleSectionContext.Provider value={{ handleOpenState }}>
|
||||
{children}
|
||||
</CollapsibleSectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useOpenState: HandleOpenState = (label, isOpen) => {
|
||||
const context = useContext(CollapsibleSectionContext);
|
||||
const localState = useState(isOpen ?? true);
|
||||
if (context === undefined) {
|
||||
return localState;
|
||||
}
|
||||
|
||||
const isOpenCurrent = state[label];
|
||||
return [isOpenForced ?? isOpenCurrent ?? true, setIsOpen];
|
||||
return context.handleOpenState(label, isOpen);
|
||||
};
|
||||
|
||||
type CollapsibleSectionBaseProps = {
|
||||
@@ -67,8 +118,8 @@ type CollapsibleSectionBaseProps = {
|
||||
children: ReactNode;
|
||||
fullWidth?: boolean;
|
||||
label: string;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (value: boolean) => void;
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const CollapsibleSectionRoot = ({
|
||||
@@ -78,44 +129,47 @@ export const CollapsibleSectionRoot = ({
|
||||
fullWidth = false,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
}: CollapsibleSectionBaseProps) => (
|
||||
<Collapsible.Root open={isOpen} onOpenChange={onOpenChange}>
|
||||
<>
|
||||
<Collapsible.Trigger asChild>
|
||||
{trigger ?? (
|
||||
<SectionTitle>
|
||||
<SectionTitleLabel>{label}</SectionTitleLabel>
|
||||
</SectionTitle>
|
||||
)}
|
||||
</Collapsible.Trigger>
|
||||
}: CollapsibleSectionBaseProps) => {
|
||||
return (
|
||||
<Collapsible.Root open={isOpen} onOpenChange={onOpenChange}>
|
||||
<>
|
||||
<Collapsible.Trigger asChild>
|
||||
{trigger ?? (
|
||||
<SectionTitle>
|
||||
<SectionTitleLabel>{label}</SectionTitleLabel>
|
||||
</SectionTitle>
|
||||
)}
|
||||
</Collapsible.Trigger>
|
||||
|
||||
<Collapsible.Content asChild>
|
||||
<Flex
|
||||
gap="2"
|
||||
direction="column"
|
||||
css={{
|
||||
pb: theme.panel.paddingBlock,
|
||||
px: fullWidth ? 0 : theme.panel.paddingInline,
|
||||
paddingTop: 0,
|
||||
"&:empty": { display: "none" },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
</Collapsible.Content>
|
||||
<Separator />
|
||||
</>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
<Collapsible.Content asChild>
|
||||
<Flex
|
||||
gap="2"
|
||||
direction="column"
|
||||
css={{
|
||||
pb: theme.panel.paddingBlock,
|
||||
px: fullWidth ? 0 : theme.panel.paddingInline,
|
||||
paddingTop: 0,
|
||||
"&:empty": { display: "none" },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
</Collapsible.Content>
|
||||
<Separator />
|
||||
</>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
|
||||
type CollapsibleSectionProps = Simplify<
|
||||
Omit<CollapsibleSectionBaseProps, "isOpen" | "onOpenChange"> &
|
||||
UseOpenStateProps
|
||||
Omit<CollapsibleSectionBaseProps, "onOpenChange"> & {
|
||||
label: Label;
|
||||
}
|
||||
>;
|
||||
|
||||
export const CollapsibleSection = (props: CollapsibleSectionProps) => {
|
||||
const { label, trigger, children, fullWidth } = props;
|
||||
const [isOpen, setIsOpen] = useOpenState(props);
|
||||
const [isOpen, setIsOpen] = useOpenState(label, props.isOpen);
|
||||
return (
|
||||
<CollapsibleSectionRoot
|
||||
label={label}
|
||||
@@ -143,7 +197,7 @@ export const CollapsibleSectionWithAddButton = ({
|
||||
hasItems?: boolean | ComponentProps<typeof SectionTitle>["dots"];
|
||||
}) => {
|
||||
const { label, children } = props;
|
||||
const [isOpen, setIsOpen] = useOpenState(props);
|
||||
const [isOpen, setIsOpen] = useOpenState(label, props.isOpen);
|
||||
|
||||
const isEmpty =
|
||||
hasItems === false || (Array.isArray(hasItems) && hasItems.length === 0);
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
isInstanceDetachable,
|
||||
isTreeMatching,
|
||||
} from "~/shared/matcher";
|
||||
import { getSetting, setSetting } from "./client-settings";
|
||||
|
||||
const makeBreakpointCommand = <CommandName extends string>(
|
||||
name: CommandName,
|
||||
@@ -350,6 +351,17 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({
|
||||
},
|
||||
disableOnInputLikeControls: true,
|
||||
},
|
||||
{
|
||||
name: "toggleStylePanelFocusMode",
|
||||
defaultHotkeys: ["alt+shift+s"],
|
||||
handler: () => {
|
||||
setSetting(
|
||||
"stylePanelMode",
|
||||
getSetting("stylePanelMode") === "focus" ? "default" : "focus"
|
||||
);
|
||||
},
|
||||
disableOnInputLikeControls: true,
|
||||
},
|
||||
{
|
||||
name: "openSettingsPanel",
|
||||
defaultHotkeys: ["d"],
|
||||
|
||||
@@ -6,7 +6,7 @@ const isMac =
|
||||
const shortcutSymbolMap: Record<string, string> = {
|
||||
cmd: isMac ? "⌘" : "Ctrl",
|
||||
shift: "⇧",
|
||||
option: isMac ? "⌥" : "Alt",
|
||||
alt: isMac ? "⌥" : "Alt",
|
||||
backspace: "⌫",
|
||||
enter: "↵",
|
||||
tab: isMac ? "tab" : "Tab",
|
||||
|
||||
@@ -61,7 +61,7 @@ export const menuItemCss = css({
|
||||
borderRadius: theme.borderRadius[3],
|
||||
// override button default styles
|
||||
backgroundColor: "transparent",
|
||||
"&:focus, &[data-found], &[aria-selected=true], &[data-state=open], &[data-state=checked]":
|
||||
"&:focus, &[data-found], &[aria-selected=true], &[data-state=open], &[data-state=checked]:is(:hover,:focus)":
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundItemMenuItemHover,
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user