mirror of
https://github.com/makeplane/plane.git
synced 2026-01-27 16:49:09 -06:00
[WEB-2050] dev: added new information panels to a page (#5409)
* dev: added new information panels to pages * refactor: update function name
This commit is contained in:
committed by
GitHub
parent
fb2a04dc14
commit
7efda1c392
@@ -32,6 +32,7 @@
|
||||
"@plane/ui": "*",
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"@tiptap/extension-blockquote": "^2.1.13",
|
||||
"@tiptap/extension-character-count": "^2.6.5",
|
||||
"@tiptap/extension-collaboration": "^2.3.2",
|
||||
"@tiptap/extension-image": "^2.1.13",
|
||||
"@tiptap/extension-list-item": "^2.1.13",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import CharacterCount from "@tiptap/extension-character-count";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import TaskItem from "@tiptap/extension-task-item";
|
||||
import TaskList from "@tiptap/extension-task-list";
|
||||
@@ -157,4 +158,5 @@ export const CoreEditorExtensions = ({
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
CharacterCount,
|
||||
];
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Extensions, generateJSON, getSchema } from "@tiptap/core";
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
import { EditorState, Selection } from "@tiptap/pm/state";
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { CoreEditorExtensionsWithoutProps } from "src/core/extensions/core-without-props";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface EditorClassNames {
|
||||
@@ -61,3 +59,12 @@ export const isValidHttpUrl = (string: string): boolean => {
|
||||
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
};
|
||||
|
||||
export const getParagraphCount = (editorState: EditorState | undefined) => {
|
||||
if (!editorState) return 0;
|
||||
let paragraphCount = 0;
|
||||
editorState.doc.descendants((node) => {
|
||||
if (node.type.name === "paragraph" && node.content.size > 0) paragraphCount++;
|
||||
});
|
||||
return paragraphCount;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getEditorMenuItems } from "@/components/menus";
|
||||
// extensions
|
||||
import { CoreEditorExtensions } from "@/extensions";
|
||||
// helpers
|
||||
import { getParagraphCount } from "@/helpers/common";
|
||||
import { insertContentAtSavedSelection } from "@/helpers/insert-content-at-cursor-position";
|
||||
import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
|
||||
// plane editor providers
|
||||
@@ -249,6 +250,11 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
|
||||
}
|
||||
},
|
||||
documentInfo: {
|
||||
characters: editorRef.current?.storage?.characterCount?.characters?.() ?? 0,
|
||||
paragraphs: getParagraphCount(editorRef.current?.state),
|
||||
words: editorRef.current?.storage?.characterCount?.words?.() ?? 0,
|
||||
},
|
||||
}),
|
||||
[editorRef, savedSelection, fileHandler.upload]
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
||||
// extensions
|
||||
import { CoreReadOnlyEditorExtensions } from "@/extensions";
|
||||
// helpers
|
||||
import { getParagraphCount } from "@/helpers/common";
|
||||
import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
|
||||
// props
|
||||
import { CoreReadOnlyEditorProps } from "@/props";
|
||||
@@ -81,6 +82,11 @@ export const useReadOnlyEditor = ({
|
||||
if (!editorRef.current) return;
|
||||
scrollSummary(editorRef.current, marking);
|
||||
},
|
||||
documentInfo: {
|
||||
characters: editorRef.current?.storage?.characterCount?.characters?.() ?? 0,
|
||||
paragraphs: getParagraphCount(editorRef.current?.state),
|
||||
words: editorRef.current?.storage?.characterCount?.words?.() ?? 0,
|
||||
},
|
||||
}));
|
||||
|
||||
if (!editor) {
|
||||
|
||||
@@ -9,6 +9,11 @@ export type EditorReadOnlyRefApi = {
|
||||
clearEditor: (emitUpdate?: boolean) => void;
|
||||
setEditorValue: (content: string) => void;
|
||||
scrollSummary: (marking: IMarking) => void;
|
||||
documentInfo: {
|
||||
characters: number;
|
||||
paragraphs: number;
|
||||
words: number;
|
||||
};
|
||||
};
|
||||
|
||||
export interface EditorRefApi extends EditorReadOnlyRefApi {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TLogoProps } from "@plane/types";
|
||||
import { Breadcrumbs, Button, EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { PageEditInformationPopover } from "@/components/pages";
|
||||
// helpers
|
||||
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
||||
// hooks
|
||||
@@ -30,7 +31,8 @@ export const PageDetailsHeader = observer(() => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// store hooks
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const { isContentEditable, isSubmitting, name, logo_props, updatePageLogo } = usePage(pageId?.toString() ?? "");
|
||||
const page = usePage(pageId?.toString() ?? "");
|
||||
const { isContentEditable, isSubmitting, name, logo_props, updatePageLogo } = page;
|
||||
// use platform
|
||||
const { isMobile, platform } = usePlatformOS();
|
||||
// derived values
|
||||
@@ -159,6 +161,7 @@ export const PageDetailsHeader = observer(() => {
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
<PageEditInformationPopover page={page} />
|
||||
<PageDetailsHeaderExtraActions />
|
||||
{isContentEditable && !isVersionHistoryOverlayActive && (
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// helpers
|
||||
import { calculateTimeAgoShort, renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
// store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
|
||||
type Props = {
|
||||
page: IPage;
|
||||
};
|
||||
|
||||
export const PageEditInformationPopover: React.FC<Props> = observer((props) => {
|
||||
const { page } = props;
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
const editorInformation = page.updated_by ? getUserDetails(page.updated_by) : undefined;
|
||||
const creatorInformation = page.created_by ? getUserDetails(page.created_by) : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0 relative group/edit-information whitespace-nowrap">
|
||||
<span className="text-sm text-custom-text-300">Edited {calculateTimeAgoShort(page.updated_at ?? "")} ago</span>
|
||||
<div className="hidden group-hover/edit-information:block absolute z-10 top-full right-0 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-2 shadow-custom-shadow-rg space-y-2">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-custom-text-300">Edited by</p>
|
||||
<Link
|
||||
href={`/${workspaceSlug?.toString()}/profile/${page.updated_by}`}
|
||||
className="mt-2 flex items-center gap-1.5 text-sm font-medium"
|
||||
>
|
||||
<Avatar
|
||||
src={editorInformation?.avatar}
|
||||
name={editorInformation?.display_name}
|
||||
className="flex-shrink-0"
|
||||
size="sm"
|
||||
/>
|
||||
<span>
|
||||
{editorInformation?.display_name}{" "}
|
||||
<span className="text-custom-text-300">{renderFormattedDate(page.updated_at)}</span>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-custom-text-300">Created by</p>
|
||||
<Link
|
||||
href={`/${workspaceSlug?.toString()}/profile/${page.created_by}`}
|
||||
className="mt-2 flex items-center gap-1.5 text-sm font-medium"
|
||||
>
|
||||
<Avatar
|
||||
src={creatorInformation?.avatar}
|
||||
name={creatorInformation?.display_name}
|
||||
className="flex-shrink-0"
|
||||
size="sm"
|
||||
/>
|
||||
<span>
|
||||
{creatorInformation?.display_name}{" "}
|
||||
<span className="text-custom-text-300">{renderFormattedDate(page.created_at)}</span>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./edit-information-popover";
|
||||
export * from "./quick-actions";
|
||||
|
||||
@@ -72,7 +72,7 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
|
||||
className="!min-w-[38rem]"
|
||||
/>
|
||||
)}
|
||||
<PageInfoPopover page={page} />
|
||||
<PageInfoPopover editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current} />
|
||||
<PageOptionsDropdown
|
||||
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
|
||||
handleDuplicatePage={handleDuplicatePage}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { useState } from "react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Calendar, History, Info } from "lucide-react";
|
||||
import { Info } from "lucide-react";
|
||||
// plane editor
|
||||
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
import { getReadTimeFromWordsCount } from "@/helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
page: IPage;
|
||||
editorRef: EditorRefApi | EditorReadOnlyRefApi | null;
|
||||
};
|
||||
|
||||
export const PageInfoPopover: React.FC<Props> = (props) => {
|
||||
const { page } = props;
|
||||
const { editorRef } = props;
|
||||
// states
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
// refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -21,35 +21,54 @@ export const PageInfoPopover: React.FC<Props> = (props) => {
|
||||
const { styles: infoPopoverStyles, attributes: infoPopoverAttributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-start",
|
||||
});
|
||||
// derived values
|
||||
const { created_at, updated_at } = page;
|
||||
|
||||
const secondsToReadableTime = () => {
|
||||
const wordsCount = editorRef?.documentInfo.words || 0;
|
||||
const readTimeInSeconds = Number(getReadTimeFromWordsCount(wordsCount).toFixed(0));
|
||||
return readTimeInSeconds < 60 ? `${readTimeInSeconds}s` : `${Math.ceil(readTimeInSeconds / 60)}m`;
|
||||
};
|
||||
|
||||
const documentInfoCards = [
|
||||
{
|
||||
key: "words-count",
|
||||
title: "Words",
|
||||
info: editorRef?.documentInfo.words,
|
||||
},
|
||||
{
|
||||
key: "characters-count",
|
||||
title: "Characters",
|
||||
info: editorRef?.documentInfo.characters,
|
||||
},
|
||||
{
|
||||
key: "paragraphs-count",
|
||||
title: "Paragraphs",
|
||||
info: editorRef?.documentInfo.paragraphs,
|
||||
},
|
||||
{
|
||||
key: "read-time",
|
||||
title: "Read time",
|
||||
info: secondsToReadableTime(),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div onMouseEnter={() => setIsPopoverOpen(true)} onMouseLeave={() => setIsPopoverOpen(false)}>
|
||||
<button type="button" ref={setReferenceElement} className="block">
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
<Info className="size-3.5" />
|
||||
</button>
|
||||
{isPopoverOpen && (
|
||||
<div
|
||||
className="z-10 w-64 space-y-2.5 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg"
|
||||
className="z-10 w-64 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-2 shadow-custom-shadow-rg grid grid-cols-2 gap-1.5"
|
||||
ref={setPopperElement}
|
||||
style={infoPopoverStyles.popper}
|
||||
{...infoPopoverAttributes.popper}
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<h6 className="text-xs text-custom-text-400">Last updated on</h6>
|
||||
<h5 className="flex items-center gap-1 text-sm">
|
||||
<History className="h-3 w-3" />
|
||||
{renderFormattedDate(updated_at)}
|
||||
</h5>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<h6 className="text-xs text-custom-text-400">Created on</h6>
|
||||
<h5 className="flex items-center gap-1 text-sm">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{renderFormattedDate(created_at)}
|
||||
</h5>
|
||||
</div>
|
||||
{documentInfoCards.map((card) => (
|
||||
<div key={card.key} className="p-2 bg-custom-background-90 rounded">
|
||||
<h6 className="text-base font-semibold">{card.info}</h6>
|
||||
<p className="mt-1.5 text-sm text-custom-text-300">{card.title}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -344,3 +344,16 @@ export const convertMinutesToHoursMinutesString = (totalMinutes: number): string
|
||||
|
||||
return `${hours ? `${hours}h ` : ``}${minutes ? `${minutes}m ` : ``}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description calculates the read time for a document using the words count
|
||||
* @param {number} wordsCount
|
||||
* @returns {number} total number of seconds
|
||||
* @example getReadTimeFromWordsCount(400) // Output: 120
|
||||
* @example getReadTimeFromWordsCount(100) // Output: 30s
|
||||
*/
|
||||
export const getReadTimeFromWordsCount = (wordsCount: number): number => {
|
||||
const wordsPerMinute = 200;
|
||||
const minutes = wordsCount / wordsPerMinute;
|
||||
return minutes * 60;
|
||||
};
|
||||
|
||||
45
yarn.lock
45
yarn.lock
@@ -3750,6 +3750,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.4.0.tgz#60eea05b5ac8c8e8d615c057559fddb95033abeb"
|
||||
integrity sha512-9S5DLIvFRBoExvmZ+/ErpTvs4Wf1yOEs8WXlKYUCcZssK7brTFj99XDwpHFA29HKDwma5q9UHhr2OB2o0JYAdw==
|
||||
|
||||
"@tiptap/extension-character-count@^2.6.5":
|
||||
version "2.6.5"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-character-count/-/extension-character-count-2.6.5.tgz#8ccecf900c0c89a0b14de137b224c7b26def959e"
|
||||
integrity sha512-UZlSzfZ6Vq0zOGhNOAzLIEjAhS46dSsHXFnhgw8l61tLknVeIyXbcdi8hBxWzOQ6XkH2PA3QSnahEGwYks73WQ==
|
||||
|
||||
"@tiptap/extension-code-block@^2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.4.0.tgz#b7f1da4825677a2ea6b8e970a1197877551e5dc8"
|
||||
@@ -4377,7 +4382,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@18.2.48", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.42", "@types/react@^18.2.48":
|
||||
"@types/react@*", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.42", "@types/react@^18.2.48":
|
||||
version "18.2.48"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1"
|
||||
integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==
|
||||
@@ -10535,7 +10540,7 @@ prelude-ls@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
"prettier-fallback@npm:prettier@^3":
|
||||
"prettier-fallback@npm:prettier@^3", prettier@^3.1.1, prettier@^3.2.5, prettier@latest:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.1.tgz#e68935518dd90bb7ec4821ba970e68f8de16e1ac"
|
||||
integrity sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==
|
||||
@@ -10562,11 +10567,6 @@ prettier@^2.8.8:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
|
||||
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
||||
|
||||
prettier@^3.1.1, prettier@^3.2.5, prettier@latest:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.1.tgz#e68935518dd90bb7ec4821ba970e68f8de16e1ac"
|
||||
integrity sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==
|
||||
|
||||
pretty-error@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6"
|
||||
@@ -11950,16 +11950,7 @@ string-argv@~0.3.2:
|
||||
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6"
|
||||
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -12046,14 +12037,7 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -13337,16 +13321,7 @@ wordwrap@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
||||
Reference in New Issue
Block a user