[WIKI-307]chore: update page icon placement #6916

This commit is contained in:
Aaryan Khandelwal 2025-04-11 18:07:03 +05:30 committed by GitHub
parent 1d5b93cebd
commit 915e374485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 225 additions and 83 deletions

View file

@ -151,11 +151,11 @@
/* end font size and style */ /* end font size and style */
/* layout config */ /* layout config */
#page-header-container { #page-toolbar-container {
container-name: page-header-container; container-name: page-toolbar-container;
container-type: inline-size; container-type: inline-size;
.page-header-content { .page-toolbar-content {
--header-width: var(--normal-content-width); --header-width: var(--normal-content-width);
&.wide-layout { &.wide-layout {
@ -186,23 +186,23 @@
} }
/* keep a static padding of 96px for wide layouts for container width >912px and <1344px */ /* keep a static padding of 96px for wide layouts for container width >912px and <1344px */
@container page-header-container (min-width: 912px) and (max-width: 1344px) { @container page-toolbar-container (min-width: 912px) and (max-width: 1344px) {
.page-header-content.wide-layout { .page-toolbar-content.wide-layout {
padding-left: var(--wide-content-margin) !important; padding-left: var(--wide-content-margin) !important;
} }
} }
/* keep a static padding of 96px for wide layouts for container width <912px */ /* keep a static padding of 96px for wide layouts for container width <912px */
@container page-header-container (max-width: 912) { @container page-toolbar-container (max-width: 912) {
.page-header-content.wide-layout { .page-toolbar-content.wide-layout {
padding-left: var(--wide-content-margin) !important; padding-left: var(--wide-content-margin) !important;
} }
} }
/* end layout config */ /* end layout config */
/* keep a static padding of 20px for wide layouts for container width <760px */ /* keep a static padding of 20px for wide layouts for container width <760px */
@container page-header-container (max-width: 760px) { @container page-toolbar-container (max-width: 760px) {
.page-header-content { .page-toolbar-content {
padding-left: var(--normal-content-margin) !important; padding-left: var(--normal-content-margin) !important;
} }
} }
@ -211,7 +211,7 @@
/* keep a static padding of 96px for wide layouts for container width >912px and <1344px */ /* keep a static padding of 96px for wide layouts for container width >912px and <1344px */
@container page-content-container (min-width: 912px) and (max-width: 1344px) { @container page-content-container (min-width: 912px) and (max-width: 1344px) {
.editor-container.wide-layout, .editor-container.wide-layout,
.page-title-container { .page-header-container {
padding-left: var(--wide-content-margin); padding-left: var(--wide-content-margin);
padding-right: var(--wide-content-margin); padding-right: var(--wide-content-margin);
} }
@ -220,7 +220,7 @@
/* keep a static padding of 20px for wide layouts for container width <912px */ /* keep a static padding of 20px for wide layouts for container width <912px */
@container page-content-container (max-width: 912px) { @container page-content-container (max-width: 912px) {
.editor-container.wide-layout, .editor-container.wide-layout,
.page-title-container { .page-header-container {
padding-left: var(--normal-content-margin); padding-left: var(--normal-content-margin);
padding-right: var(--normal-content-margin); padding-right: var(--normal-content-margin);
} }
@ -229,7 +229,7 @@
/* keep a static padding of 20px for normal layouts for container width <760px */ /* keep a static padding of 20px for normal layouts for container width <760px */
@container page-content-container (max-width: 760px) { @container page-content-container (max-width: 760px) {
.editor-container:not(.wide-layout), .editor-container:not(.wide-layout),
.page-title-container { .page-header-container {
padding-left: var(--normal-content-margin); padding-left: var(--normal-content-margin);
padding-right: var(--normal-content-margin); padding-right: var(--normal-content-margin);
} }

View file

@ -30,6 +30,8 @@ import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed"; import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
// store // store
import { TPageInstance } from "@/store/pages/base-page"; import { TPageInstance } from "@/store/pages/base-page";
// local imports
import { PageEditorHeaderRoot } from "./header";
export type TEditorBodyConfig = { export type TEditorBodyConfig = {
fileHandler: TFileHandler; fileHandler: TFileHandler;
@ -161,10 +163,10 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
return ( return (
<Row <Row
className="relative size-full flex flex-col pt-[64px] overflow-y-auto overflow-x-hidden vertical-scrollbar scrollbar-md duration-200" className="relative size-full flex flex-col overflow-y-auto overflow-x-hidden vertical-scrollbar scrollbar-md duration-200"
variant={ERowVariant.HUGGING} variant={ERowVariant.HUGGING}
> >
<div id="page-content-container" className="relative w-full flex-shrink-0 space-y-4"> <div id="page-content-container" className="relative w-full flex-shrink-0">
{/* table of content */} {/* table of content */}
<div className="page-summary-container absolute h-full right-0 top-[64px] z-[5]"> <div className="page-summary-container absolute h-full right-0 top-[64px] z-[5]">
<div className="sticky top-[72px]"> <div className="sticky top-[72px]">
@ -178,13 +180,17 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
</div> </div>
</div> </div>
</div> </div>
<div className="page-header-container group/page-header">
<div className={blockWidthClassName}>
<PageEditorHeaderRoot page={page} />
<PageEditorTitle <PageEditorTitle
editorRef={editorRef} editorRef={editorRef}
readOnly={!isContentEditable} readOnly={!isContentEditable}
title={pageTitle} title={pageTitle}
updateTitle={updateTitle} updateTitle={updateTitle}
widthClassName={blockWidthClassName}
/> />
</div>
</div>
<CollaborativeDocumentEditorWithRef <CollaborativeDocumentEditorWithRef
editable={isContentEditable} editable={isContentEditable}
id={pageId} id={pageId}

View file

@ -1,7 +1 @@
export * from "./color-dropdown";
export * from "./extra-options";
export * from "./info-popover";
export * from "./options-dropdown";
export * from "./root"; export * from "./root";
export * from "./mobile-root";
export * from "./toolbar";

View file

@ -0,0 +1,53 @@
import { useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { EmojiIconPicker, EmojiIconPickerTypes } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { Logo } from "@/components/common";
// store
import { TPageInstance } from "@/store/pages/base-page";
type Props = {
className?: string;
page: TPageInstance;
};
export const PageEditorHeaderLogoPicker: React.FC<Props> = observer((props) => {
const { className, page } = props;
// states
const [isLogoPickerOpen, setIsLogoPickerOpen] = useState(false);
// derived values
const { logo_props, isContentEditable, updatePageLogo } = page;
const isLogoSelected = !!logo_props?.in_use;
return (
<div
className={cn(className, "max-h-0 pointer-events-none transition-all ease-linear duration-200", {
"max-h-[56px] pointer-events-auto": isLogoSelected,
})}
>
<EmojiIconPicker
isOpen={isLogoPickerOpen}
handleToggle={(val) => setIsLogoPickerOpen(val)}
className="flex items-center justify-center"
buttonClassName="flex items-center justify-center"
label={
<div
className={cn("-ml-[8px] size-[56px] grid place-items-center rounded transition-colors", {
"hover:bg-custom-background-80": isContentEditable,
})}
>
{isLogoSelected && <Logo logo={logo_props} size={48} type="lucide" />}
</div>
}
onChange={updatePageLogo}
defaultIconColor={logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined}
defaultOpen={
logo_props?.in_use && logo_props?.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON
}
disabled={!isContentEditable}
/>
</div>
);
});

View file

@ -1,56 +1,70 @@
import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EditorRefApi } from "@plane/editor"; import { SmilePlus } from "lucide-react";
// components // plane imports
import { PageEditorMobileHeaderRoot, PageExtraOptions, PageToolbar } from "@/components/pages"; import { EmojiIconPicker, EmojiIconPickerTypes } from "@plane/ui";
// helpers import { cn } from "@plane/utils";
import { cn } from "@/helpers/common.helper";
// hooks
import { usePageFilters } from "@/hooks/use-page-filters";
// plane web hooks
import { EPageStoreType } from "@/plane-web/hooks/store";
// store // store
import { TPageInstance } from "@/store/pages/base-page"; import { TPageInstance } from "@/store/pages/base-page";
// local imports
import { PageEditorHeaderLogoPicker } from "./logo-picker";
type Props = { type Props = {
editorReady: boolean;
editorRef: React.RefObject<EditorRefApi>;
page: TPageInstance; page: TPageInstance;
storeType: EPageStoreType;
}; };
export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => { export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
const { editorReady, editorRef, page, storeType } = props; const { page } = props;
// states
const [isLogoPickerOpen, setIsLogoPickerOpen] = useState(false);
// derived values // derived values
const { isContentEditable } = page; const { isContentEditable, logo_props, name, updatePageLogo } = page;
// page filters const isLogoSelected = !!logo_props?.in_use;
const { isFullWidth, isStickyToolbarEnabled } = usePageFilters(); const isTitleEmpty = !name || name.trim() === "";
// derived values
const resolvedEditorRef = editorRef.current;
if (!resolvedEditorRef) return null;
return ( return (
<div id="page-header-container"> <>
<div className="h-[48px] flex items-end text-left">
{!isLogoSelected && (
<div <div
className={cn("opacity-0 group-hover/page-header:opacity-100 transition-all duration-200", {
"opacity-100": isTitleEmpty,
})}
>
<EmojiIconPicker
isOpen={isLogoPickerOpen}
handleToggle={(val) => setIsLogoPickerOpen(val)}
className="flex items-center justify-center"
buttonClassName="flex items-center justify-center"
label={
<button
type="button"
className={cn( className={cn(
"hidden md:flex items-center relative min-h-[52px] page-header-content border-b border-custom-border-200 px-page-x transition-all duration-200 ease-in-out", "flex items-center gap-1 p-1 rounded font-medium text-sm hover:bg-custom-background-80 text-custom-text-300 outline-none transition-colors",
{ {
"wide-layout": isFullWidth, "bg-custom-background-80": isLogoPickerOpen,
} }
)} )}
> >
<div className="max-w-full w-full flex items-center justify-between"> <SmilePlus className="flex-shrink-0 size-4" />
<div> Icon
{isStickyToolbarEnabled && editorReady && isContentEditable && editorRef.current && ( </button>
<PageToolbar editorRef={editorRef?.current} /> }
onChange={updatePageLogo}
defaultIconColor={
logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined
}
defaultOpen={
logo_props?.in_use && logo_props?.in_use === "emoji"
? EmojiIconPickerTypes.EMOJI
: EmojiIconPickerTypes.ICON
}
disabled={!isContentEditable}
/>
</div>
)} )}
</div> </div>
<PageExtraOptions editorRef={resolvedEditorRef} page={page} storeType={storeType} /> <PageEditorHeaderLogoPicker className="flex-shrink-0 w-full mt-2 flex" page={page} />
</div> </>
</div>
<div className="md:hidden">
<PageEditorMobileHeaderRoot editorRef={resolvedEditorRef} page={page} storeType={storeType} />
</div>
</div>
); );
}); });

View file

@ -1,5 +1,5 @@
export * from "./header";
export * from "./summary";
export * from "./editor-body"; export * from "./editor-body";
export * from "./title";
export * from "./page-root"; export * from "./page-root";
export * from "./summary";
export * from "./title";
export * from "./toolbar";

View file

@ -7,7 +7,7 @@ import { EditorRefApi } from "@plane/editor";
import { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types"; import { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types";
// components // components
import { import {
PageEditorHeaderRoot, PageEditorToolbarRoot,
PageEditorBody, PageEditorBody,
PageVersionsOverlay, PageVersionsOverlay,
PagesVersionEditor, PagesVersionEditor,
@ -103,7 +103,7 @@ export const PageRoot = observer((props: TPageRootProps) => {
pageId={page.id ?? ""} pageId={page.id ?? ""}
restoreEnabled={isContentEditable} restoreEnabled={isContentEditable}
/> />
<PageEditorHeaderRoot editorReady={editorReady} editorRef={editorRef} page={page} storeType={storeType} /> <PageEditorToolbarRoot editorReady={editorReady} editorRef={editorRef} page={page} storeType={storeType} />
<PageEditorBody <PageEditorBody
config={config} config={config}
editorReady={editorReady} editorReady={editorReady}

View file

@ -17,11 +17,10 @@ type Props = {
readOnly: boolean; readOnly: boolean;
title: string | undefined; title: string | undefined;
updateTitle: (title: string) => void; updateTitle: (title: string) => void;
widthClassName: string;
}; };
export const PageEditorTitle: React.FC<Props> = observer((props) => { export const PageEditorTitle: React.FC<Props> = observer((props) => {
const { editorRef, readOnly, title, updateTitle, widthClassName } = props; const { editorRef, readOnly, title, updateTitle } = props;
// states // states
const [isLengthVisible, setIsLengthVisible] = useState(false); const [isLengthVisible, setIsLengthVisible] = useState(false);
// page filters // page filters
@ -33,12 +32,11 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
}); });
return ( return (
<div className="relative w-full flex-shrink-0 py-3 page-title-container"> <div className="relative w-full flex-shrink-0 py-3">
{readOnly ? ( {readOnly ? (
<h6 <h6
className={cn( className={cn(
titleFontClassName, titleFontClassName,
widthClassName,
{ {
"text-custom-text-400": !title, "text-custom-text-400": !title,
}, },
@ -48,7 +46,7 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
{getPageName(title)} {getPageName(title)}
</h6> </h6>
) : ( ) : (
<div className={cn("relative", widthClassName)}> <div className="relative">
<TextArea <TextArea
className={cn(titleFontClassName, "block w-full border-none outline-none p-0 resize-none rounded-none")} className={cn(titleFontClassName, "block w-full border-none outline-none p-0 resize-none rounded-none")}
placeholder="Untitled" placeholder="Untitled"

View file

@ -0,0 +1,7 @@
export * from "./color-dropdown";
export * from "./extra-options";
export * from "./info-popover";
export * from "./options-dropdown";
export * from "./root";
export * from "./mobile-root";
export * from "./toolbar";

View file

@ -0,0 +1,56 @@
import { observer } from "mobx-react";
import { EditorRefApi } from "@plane/editor";
// components
import { PageEditorMobileHeaderRoot, PageExtraOptions, PageToolbar } from "@/components/pages";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { usePageFilters } from "@/hooks/use-page-filters";
// plane web hooks
import { EPageStoreType } from "@/plane-web/hooks/store";
// store
import { TPageInstance } from "@/store/pages/base-page";
type Props = {
editorReady: boolean;
editorRef: React.RefObject<EditorRefApi>;
page: TPageInstance;
storeType: EPageStoreType;
};
export const PageEditorToolbarRoot: React.FC<Props> = observer((props) => {
const { editorReady, editorRef, page, storeType } = props;
// derived values
const { isContentEditable } = page;
// page filters
const { isFullWidth, isStickyToolbarEnabled } = usePageFilters();
// derived values
const resolvedEditorRef = editorRef.current;
if (!resolvedEditorRef) return null;
return (
<div id="page-toolbar-container">
<div
className={cn(
"hidden md:flex items-center relative min-h-[52px] page-toolbar-content border-b border-custom-border-200 px-page-x transition-all duration-200 ease-in-out",
{
"wide-layout": isFullWidth,
}
)}
>
<div className="max-w-full w-full flex items-center justify-between">
<div>
{isStickyToolbarEnabled && editorReady && isContentEditable && editorRef.current && (
<PageToolbar editorRef={editorRef?.current} />
)}
</div>
<PageExtraOptions editorRef={resolvedEditorRef} page={page} storeType={storeType} />
</div>
</div>
<div className="md:hidden">
<PageEditorMobileHeaderRoot editorRef={resolvedEditorRef} page={page} storeType={storeType} />
</div>
</div>
);
});

View file

@ -1,9 +1,10 @@
import set from "lodash/set"; import set from "lodash/set";
import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx"; import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx";
// constants // plane imports
import { EPageAccess } from "@plane/constants"; import { EPageAccess } from "@plane/constants";
// types
import { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@plane/types"; import { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@plane/types";
import { TChangeHandlerProps } from "@plane/ui";
import { convertHexEmojiToDecimal } from "@plane/utils";
// plane web store // plane web store
import { RootStore } from "@/plane-web/store/root.store"; import { RootStore } from "@/plane-web/store/root.store";
@ -27,7 +28,7 @@ export type TBasePage = TPage & {
unlock: (shouldSync?: boolean) => Promise<void>; unlock: (shouldSync?: boolean) => Promise<void>;
archive: (shouldSync?: boolean) => Promise<void>; archive: (shouldSync?: boolean) => Promise<void>;
restore: (shouldSync?: boolean) => Promise<void>; restore: (shouldSync?: boolean) => Promise<void>;
updatePageLogo: (logo_props: TLogoProps) => Promise<void>; updatePageLogo: (value: TChangeHandlerProps) => Promise<void>;
addToFavorites: () => Promise<void>; addToFavorites: () => Promise<void>;
removePageFromFavorites: () => Promise<void>; removePageFromFavorites: () => Promise<void>;
duplicate: () => Promise<TPage | undefined>; duplicate: () => Promise<TPage | undefined>;
@ -424,12 +425,25 @@ export class BasePage implements TBasePage {
} }
}; };
updatePageLogo = async (logo_props: TLogoProps) => { updatePageLogo = async (value: TChangeHandlerProps) => {
let logoValue = {};
if (value?.type === "emoji")
logoValue = {
value: convertHexEmojiToDecimal(value.value.unified),
url: value.value.imageUrl,
};
else if (value?.type === "icon") logoValue = value.value;
const logoProps: TLogoProps = {
in_use: value?.type,
[value?.type]: logoValue,
};
await this.services.update({ await this.services.update({
logo_props, logo_props: logoProps,
}); });
runInAction(() => { runInAction(() => {
this.logo_props = logo_props; this.logo_props = logoProps;
}); });
}; };