Make magnify icon always visible, transition from magnify to minimize (#197)

I'm updating the magnify button to be always visible and animate a
transition between being a "Magnify" button and a "Minimize" button.

This also cleans up some text shrinking behavior in the block frame
header so the end icons are always visible.

Also fixes some height discrepancies in the block frame header.

Also implements a `prefers-reduced-motion` query for the tilelayout and
block frame to ensure transitions are not set if the user does not want
them.
This commit is contained in:
Evan Simkowitz 2024-08-05 16:13:26 -07:00 committed by GitHub
parent 2f8642db5b
commit e29cb2debe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 131 additions and 16 deletions

View File

@ -3,7 +3,6 @@ import type { Preview } from "@storybook/react";
import React from "react"; import React from "react";
import { DndProvider } from "react-dnd"; import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend"; import { HTML5Backend } from "react-dnd-html5-backend";
import "./global.css"; import "./global.css";
const preview: Preview = { const preview: Preview = {

View File

@ -61,15 +61,15 @@ export default defineConfig({
plugins: [ plugins: [
ViteImageOptimizer(), ViteImageOptimizer(),
tsconfigPaths(), tsconfigPaths(),
svgr({
svgrOptions: { exportType: "default", ref: true, svgo: false, titleProp: true },
include: "**/*.svg",
}),
react({}), react({}),
flow(), flow(),
viteStaticCopy({ viteStaticCopy({
targets: [{ src: "node_modules/monaco-editor/min/vs/*", dest: "monaco" }], targets: [{ src: "node_modules/monaco-editor/min/vs/*", dest: "monaco" }],
}), }),
svgr({
svgrOptions: { exportType: "default", ref: true, svgo: false, titleProp: true },
include: "**/*.svg",
}),
], ],
}, },
}); });

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg2" version="1.1" fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g id="g2" transform="matrix(1 0 0 1 -4 -4)" fill="#000">
<path id="arrow1" class="arrow"
d="m10.208 13.003c0.4262 0 0.7723 0.34592 0.7723 0.77195v5.4527c0 0.42604-0.3461 0.77195-0.7723 0.77195-0.42619 0-0.77227-0.34592-0.77227-0.77195v-4.6825l-4.6832-0.0017c-0.4262 0-0.77228-0.34592-0.77228-0.77185 0-0.42604 0.34608-0.77195 0.77228-0.77195h5.4554v0.0034z" />
<path id="arrow2" class="arrow"
d="m13.772 10.997c-0.42624 0-0.77233-0.34581-0.77233-0.77184v-5.4527c0-0.426 0.34609-0.77191 0.77233-0.77191 0.42614 0 0.77223 0.34591 0.77223 0.77191v4.6826l4.6832 0.0017c0.42625 0 0.77223 0.34591 0.77223 0.77187 0 0.42603-0.34599 0.77195-0.77223 0.77195h-5.4554v-0.0035z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@ -70,6 +70,8 @@
flex-direction: column; flex-direction: column;
.block-frame-default-header { .block-frame-default-header {
max-height: var(--header-height);
min-height: var(--header-height);
display: flex; display: flex;
padding: 4px 5px 4px 10px; padding: 4px 5px 4px 10px;
align-items: center; align-items: center;
@ -90,8 +92,10 @@
.block-frame-default-header-iconview { .block-frame-default-header-iconview {
display: flex; display: flex;
flex-shrink: 1;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
overflow-x: hidden;
.block-frame-view-icon { .block-frame-view-icon {
font-size: var(--header-icon-size); font-size: var(--header-icon-size);
@ -103,10 +107,9 @@
} }
.block-frame-view-type { .block-frame-view-type {
line-height: 12px;
font-weight: 700; font-weight: 700;
text-wrap: nowrap;
overflow-x: hidden; overflow-x: hidden;
text-wrap: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@ -119,8 +122,8 @@
font: var(--fixed-font); font: var(--fixed-font);
font-size: 11px; font-size: 11px;
opacity: 0.7; opacity: 0.7;
text-wrap: nowrap;
overflow-x: hidden; overflow-x: hidden;
text-wrap: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@ -148,6 +151,7 @@
min-width: 0; min-width: 0;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
overflow-x: hidden;
.block-frame-div { .block-frame-div {
display: flex; display: flex;
@ -200,6 +204,7 @@
.block-frame-end-icons { .block-frame-end-icons {
display: flex; display: flex;
flex-shrink: 0;
.iconbutton { .iconbutton {
display: flex; display: flex;
@ -207,6 +212,19 @@
padding: 4px 6px; padding: 4px 6px;
align-items: center; align-items: center;
} }
.block-frame-magnify {
justify-content: center;
align-items: center;
padding: 0;
svg {
#arrow1,
#arrow2 {
fill: var(--main-text-color);
}
}
}
} }
} }
@ -220,7 +238,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
i { .iconbutton {
opacity: 0.7; opacity: 0.7;
font-size: 45px; font-size: 45px;
margin: -30px 0 0 0; margin: -30px 0 0 0;

View File

@ -6,6 +6,7 @@ import { Button } from "@/app/element/button";
import { ContextMenuModel } from "@/app/store/contextmenu"; import { ContextMenuModel } from "@/app/store/contextmenu";
import { atoms, globalStore, useBlockAtom, WOS } from "@/app/store/global"; import { atoms, globalStore, useBlockAtom, WOS } from "@/app/store/global";
import * as services from "@/app/store/services"; import * as services from "@/app/store/services";
import { MagnifyIcon } from "@/element/magnify";
import { LayoutTreeState } from "@/layout/index"; import { LayoutTreeState } from "@/layout/index";
import { getLayoutStateAtomForTab } from "@/layout/lib/layoutAtom"; import { getLayoutStateAtomForTab } from "@/layout/lib/layoutAtom";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
@ -77,13 +78,11 @@ const OptMagnifyButton = React.memo(
const tabId = globalStore.get(atoms.activeTabId); const tabId = globalStore.get(atoms.activeTabId);
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId)); const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
const layoutTreeState = util.useAtomValueSafe(getLayoutStateAtomForTab(tabId, tabAtom)); const layoutTreeState = util.useAtomValueSafe(getLayoutStateAtomForTab(tabId, tabAtom));
if (!isBlockMagnified(layoutTreeState, blockData.oid)) { const isMagnified = isBlockMagnified(layoutTreeState, blockData.oid);
return null;
}
const magnifyDecl: HeaderIconButton = { const magnifyDecl: HeaderIconButton = {
elemtype: "iconbutton", elemtype: "iconbutton",
icon: "regular@magnifying-glass-minus", icon: <MagnifyIcon enabled={isMagnified} />,
title: "Minimize", title: isMagnified ? "Minimize" : "Magnify",
click: layoutModel?.onMagnifyToggle, click: layoutModel?.onMagnifyToggle,
}; };
return <IconButton key="magnify" decl={magnifyDecl} className="block-frame-magnify" />; return <IconButton key="magnify" decl={magnifyDecl} className="block-frame-magnify" />;

View File

@ -163,7 +163,7 @@ export const IconButton = React.memo(({ decl, className }: { decl: HeaderIconBut
useLongClick(buttonRef, decl.click, decl.longClick); useLongClick(buttonRef, decl.click, decl.longClick);
return ( return (
<div ref={buttonRef} className={clsx("iconbutton", className)} title={decl.title}> <div ref={buttonRef} className={clsx("iconbutton", className)} title={decl.title}>
<i className={util.makeIconClass(decl.icon, true)} /> {typeof decl.icon === "string" ? <i className={util.makeIconClass(decl.icon, true)} /> : decl.icon}
</div> </div>
); );
}); });

View File

@ -0,0 +1,36 @@
.magnify-icon {
display: inline-block;
width: 15px;
height: 15px;
svg {
#arrow1 {
transform: rotate(180deg);
transform-origin: calc(29.167% + 4px) calc(70.833% + 4px); // account for path offset in the svg itself
}
#arrow2 {
transform: rotate(-180deg);
transform-origin: calc(70.833% + 4px) calc(29.167% + 4px);
}
#arrow1,
#arrow2 {
transition: transform 300ms ease-in;
}
}
&.enabled {
svg {
#arrow1,
#arrow2 {
transform: rotate(0deg);
}
}
}
}
@media (prefers-reduced-motion) {
.magnify-icon svg {
#arrow1,
#arrow2 {
transition: none;
}
}
}

View File

@ -0,0 +1,25 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MagnifyIcon } from "./magnify";
const meta = {
title: "Icons/Magnify",
component: MagnifyIcon,
args: {
enabled: true,
},
} satisfies Meta<typeof MagnifyIcon>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Enabled: Story = {
args: {
enabled: true,
},
};
export const Disabled: Story = {
args: {
enabled: false,
},
};

View File

@ -0,0 +1,15 @@
import clsx from "clsx";
import MagnifySVG from "../asset/magnify.svg";
import "./magnify.less";
interface MagnifyIconProps {
enabled: boolean;
}
export function MagnifyIcon({ enabled }: MagnifyIconProps) {
return (
<div className={clsx("magnify-icon", { enabled })}>
<MagnifySVG />
</div>
);
}

View File

@ -32,6 +32,7 @@
--header-font: 700 11px / normal "Inter", sans-serif; --header-font: 700 11px / normal "Inter", sans-serif;
--header-icon-size: 14px; --header-icon-size: 14px;
--header-icon-width: 16px; --header-icon-width: 16px;
--header-height: 30px;
--tab-green: rgb(88, 193, 66); --tab-green: rgb(88, 193, 66);

View File

@ -117,3 +117,16 @@
border-radius: calc(var(--block-border-radius) + 2px); border-radius: calc(var(--block-border-radius) + 2px);
} }
} }
@media (prefers-reduced-motion) {
.tile-layout {
&.animate {
.tile-node,
.placeholder {
transition-duration: none;
transition-timing-function: none;
transition-property: none;
}
}
}
}

View File

@ -123,7 +123,7 @@ declare global {
type HeaderIconButton = { type HeaderIconButton = {
elemtype: "iconbutton"; elemtype: "iconbutton";
icon: string; icon: string | React.ReactNode;
className?: string; className?: string;
title?: string; title?: string;
click?: (e: React.MouseEvent<any>) => void; click?: (e: React.MouseEvent<any>) => void;