mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
feat: Presentation mode (#11678)
* wip * fix scaling, query string, icons, refactor * refactor
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
CaseSensitiveIcon,
|
||||
RestoreIcon,
|
||||
EditIcon,
|
||||
EmbedIcon,
|
||||
} from "outline-icons";
|
||||
import { toast } from "sonner";
|
||||
import Icon from "@shared/components/Icon";
|
||||
@@ -944,6 +945,25 @@ export const printDocument = createAction({
|
||||
},
|
||||
});
|
||||
|
||||
export const presentDocument = createAction({
|
||||
name: ({ t, isMenu }) => (isMenu ? t("Present") : t("Present document")),
|
||||
analyticsName: "Present document",
|
||||
section: ActiveDocumentSection,
|
||||
icon: <EmbedIcon />,
|
||||
shortcut: ["Meta+Alt+p"],
|
||||
visible: ({ activeDocumentId }) => !!activeDocumentId,
|
||||
perform: ({ activeDocumentId, stores }) => {
|
||||
const document = activeDocumentId
|
||||
? stores.documents.get(activeDocumentId)
|
||||
: undefined;
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
|
||||
stores.ui.setPresentingDocument(document);
|
||||
},
|
||||
});
|
||||
|
||||
export const importDocument = createAction({
|
||||
name: ({ t }) => t("Import document"),
|
||||
analyticsName: "Import document",
|
||||
@@ -1487,6 +1507,7 @@ export const rootDocumentActions = [
|
||||
openRandomDocument,
|
||||
permanentlyDeleteDocument,
|
||||
permanentlyDeleteDocumentsInTrash,
|
||||
presentDocument,
|
||||
printDocument,
|
||||
pinDocumentToCollection,
|
||||
pinDocumentToHome,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { Suspense } from "react";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import lazyWithRetry from "~/utils/lazyWithRetry";
|
||||
|
||||
const PresentationMode = lazyWithRetry(
|
||||
() => import("~/scenes/Document/components/PresentationMode")
|
||||
);
|
||||
|
||||
function Presentation() {
|
||||
const { ui } = useStores();
|
||||
|
||||
if (!ui.presentationData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<PresentationMode
|
||||
title={ui.presentationData.title}
|
||||
icon={ui.presentationData.icon}
|
||||
iconColor={ui.presentationData.color}
|
||||
data={ui.presentationData.data}
|
||||
onClose={() => {
|
||||
ui.setPresentingDocument(null);
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Presentation);
|
||||
@@ -2,7 +2,7 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { transparentize } from "polished";
|
||||
import * as React from "react";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import { s, depths } from "@shared/styles";
|
||||
import useMobile from "~/hooks/useMobile";
|
||||
import { useTooltipContext } from "./TooltipContext";
|
||||
|
||||
@@ -267,7 +267,7 @@ const StyledContent = styled(TooltipPrimitive.Content)`
|
||||
white-space: normal;
|
||||
outline: 0;
|
||||
padding: 5px 9px;
|
||||
z-index: 9999;
|
||||
z-index: ${depths.tooltip};
|
||||
max-width: calc(100vw - 10px);
|
||||
|
||||
/* Animation */
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
openDocumentInsights,
|
||||
downloadDocument,
|
||||
copyDocument,
|
||||
presentDocument,
|
||||
printDocument,
|
||||
searchInDocument,
|
||||
deleteDocument,
|
||||
@@ -106,6 +107,7 @@ export function useDocumentMenuAction({
|
||||
openDocumentComments,
|
||||
openDocumentHistory,
|
||||
openDocumentInsights,
|
||||
presentDocument,
|
||||
downloadDocument,
|
||||
copyDocument,
|
||||
printDocument,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Router } from "react-router-dom";
|
||||
import stores from "~/stores";
|
||||
import Analytics from "~/components/Analytics";
|
||||
import Dialogs from "~/components/Dialogs";
|
||||
import Presentation from "~/components/Presentation";
|
||||
import ErrorBoundary from "~/components/ErrorBoundary";
|
||||
import PageTheme from "~/components/PageTheme";
|
||||
import ScrollToTop from "~/components/ScrollToTop";
|
||||
@@ -72,6 +73,7 @@ if (element) {
|
||||
</ScrollToTop>
|
||||
<Toasts />
|
||||
<Dialogs />
|
||||
<Presentation />
|
||||
<Desktop />
|
||||
</PageScroll>
|
||||
</LazyMotion>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useDocumentContext } from "~/components/DocumentContext";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useQuery from "~/hooks/useQuery";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import type { Properties } from "~/types";
|
||||
import Logger from "~/utils/Logger";
|
||||
@@ -88,6 +89,7 @@ function DataLoader({ match, children }: Props) {
|
||||
const isEditing = isEditRoute || !user?.separateEditMode;
|
||||
const can = usePolicy(document);
|
||||
const location = useLocation<LocationState>();
|
||||
const query = useQuery();
|
||||
const missingPolicy = !can || Object.keys(can).length === 0;
|
||||
|
||||
useDocumentSidebar();
|
||||
@@ -205,6 +207,13 @@ function DataLoader({ match, children }: Props) {
|
||||
revisionId,
|
||||
]);
|
||||
|
||||
// Auto-enter presentation mode when ?present=true query param is set
|
||||
React.useEffect(() => {
|
||||
if (document && query.has("present") && !ui.presentationData) {
|
||||
ui.setPresentingDocument(document);
|
||||
}
|
||||
}, [document, query, ui]);
|
||||
|
||||
if (error) {
|
||||
return error instanceof OfflineError ? (
|
||||
<ErrorOffline />
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
import * as React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ShrinkIcon, GrowIcon, CloseIcon } from "outline-icons";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import Icon from "@shared/components/Icon";
|
||||
import { richExtensions } from "@shared/editor/nodes";
|
||||
import { s, depths, hover } from "@shared/styles";
|
||||
import type { ProsemirrorData } from "@shared/types";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import Editor from "~/components/Editor";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import Text from "~/components/Text";
|
||||
import Flex from "~/components/Flex";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
import useIdle from "~/hooks/useIdle";
|
||||
import useKeyDown from "~/hooks/useKeyDown";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "~/components/Icons/ArrowIcon";
|
||||
|
||||
/** Activity events that reset the idle timer — excludes keyboard to stay idle during navigation. */
|
||||
const idleEvents = [
|
||||
"click",
|
||||
"mousemove",
|
||||
"mousedown",
|
||||
"touchstart",
|
||||
"touchmove",
|
||||
];
|
||||
|
||||
type Slide =
|
||||
| {
|
||||
type: "title";
|
||||
title: string;
|
||||
icon?: string | null;
|
||||
iconColor?: string | null;
|
||||
}
|
||||
| { type: "content"; content: ProsemirrorData[] };
|
||||
|
||||
interface Props {
|
||||
/** The document title. */
|
||||
title: string;
|
||||
/** The document icon. */
|
||||
icon?: string | null;
|
||||
/** The document icon color. */
|
||||
iconColor?: string | null;
|
||||
/** The prosemirror data for the document. */
|
||||
data: ProsemirrorData;
|
||||
/** Callback when presentation mode is closed. */
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a ProseMirror document into slides based on heading and divider nodes.
|
||||
* A dedicated title slide is prepended. Each h1/h2 heading or horizontal rule
|
||||
* starts a new content slide. Divider nodes are consumed as separators and not
|
||||
* rendered on slides.
|
||||
*
|
||||
* @param data the prosemirror document data.
|
||||
* @param title the document title.
|
||||
* @param icon the document icon.
|
||||
* @param iconColor the document icon color.
|
||||
* @returns an array of slides.
|
||||
*/
|
||||
function splitIntoSlides(
|
||||
data: ProsemirrorData,
|
||||
title: string,
|
||||
icon?: string | null,
|
||||
iconColor?: string | null
|
||||
): Slide[] {
|
||||
const content = data.content ?? [];
|
||||
const slides: Slide[] = [{ type: "title", title, icon, iconColor }];
|
||||
let currentNodes: ProsemirrorData[] = [];
|
||||
|
||||
for (const node of content) {
|
||||
const isDivider = node.type === "horizontal_rule" || node.type === "hr";
|
||||
const isHeadingBreak =
|
||||
node.type === "heading" &&
|
||||
node.attrs &&
|
||||
typeof node.attrs.level === "number" &&
|
||||
node.attrs.level <= 2;
|
||||
|
||||
if (isDivider) {
|
||||
if (currentNodes.length > 0) {
|
||||
slides.push({ type: "content", content: currentNodes });
|
||||
currentNodes = [];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isHeadingBreak && currentNodes.length > 0) {
|
||||
slides.push({ type: "content", content: currentNodes });
|
||||
currentNodes = [];
|
||||
}
|
||||
|
||||
currentNodes.push(node);
|
||||
}
|
||||
|
||||
if (currentNodes.length > 0) {
|
||||
slides.push({ type: "content", content: currentNodes });
|
||||
}
|
||||
|
||||
return slides;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-screen presentation mode that splits a document into slides by headings
|
||||
* and dividers, and allows navigating through them with keyboard controls.
|
||||
*/
|
||||
function PresentationMode({ title, icon, iconColor, data, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [currentSlide, setCurrentSlide] = React.useState(0);
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const slideContentRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isFullscreen, setIsFullscreen] = React.useState(false);
|
||||
const isIdle = useIdle(3000, idleEvents);
|
||||
|
||||
const slides = React.useMemo(
|
||||
() => splitIntoSlides(data, title, icon, iconColor),
|
||||
[data, title, icon, iconColor]
|
||||
);
|
||||
|
||||
const totalSlides = slides.length;
|
||||
|
||||
const goNext = React.useCallback(() => {
|
||||
setCurrentSlide((prev) => Math.min(prev + 1, totalSlides - 1));
|
||||
}, [totalSlides]);
|
||||
|
||||
const goPrev = React.useCallback(() => {
|
||||
setCurrentSlide((prev) => Math.max(prev - 1, 0));
|
||||
}, []);
|
||||
|
||||
const goFirst = React.useCallback(() => {
|
||||
setCurrentSlide(0);
|
||||
}, []);
|
||||
|
||||
const goLast = React.useCallback(() => {
|
||||
setCurrentSlide(totalSlides - 1);
|
||||
}, [totalSlides]);
|
||||
|
||||
const toggleFullscreen = React.useCallback(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(() => {
|
||||
// ignore
|
||||
});
|
||||
} else {
|
||||
el.requestFullscreen().catch(() => {
|
||||
// ignore
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useKeyDown("Escape", onClose);
|
||||
useKeyDown("ArrowRight", goNext);
|
||||
useKeyDown("ArrowDown", goNext);
|
||||
useKeyDown("PageDown", goNext);
|
||||
useKeyDown("ArrowLeft", goPrev);
|
||||
useKeyDown("ArrowUp", goPrev);
|
||||
useKeyDown("PageUp", goPrev);
|
||||
useKeyDown("Home", goFirst);
|
||||
useKeyDown("End", goLast);
|
||||
useKeyDown(" ", goNext);
|
||||
useKeyDown("f", toggleFullscreen);
|
||||
|
||||
// Prevent body scrolling while presentation is open
|
||||
React.useEffect(() => {
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Track fullscreen state changes
|
||||
React.useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setIsFullscreen(!!document.fullscreenElement);
|
||||
};
|
||||
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(() => {
|
||||
// ignore
|
||||
});
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Measure natural size once per slide, then apply scale directly to the DOM
|
||||
// to avoid React re-render loops during window resize.
|
||||
const naturalSize = React.useRef({ width: 0, height: 0 });
|
||||
|
||||
React.useEffect(() => {
|
||||
const el = slideContentRef.current;
|
||||
const container = containerRef.current;
|
||||
if (!el || !container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const applyScale = () => {
|
||||
const { width, height } = naturalSize.current;
|
||||
if (width === 0 || height === 0) {
|
||||
el.style.transform = "scale(1)";
|
||||
return;
|
||||
}
|
||||
|
||||
const availableWidth = container.clientWidth - 160;
|
||||
const availableHeight = container.clientHeight - 48 - 160;
|
||||
const scaleX = availableWidth / width;
|
||||
const scaleY = availableHeight / height;
|
||||
const newScale = Math.min(scaleX, scaleY, 1.5);
|
||||
el.style.transform = `scale(${Math.max(newScale, 0.5)})`;
|
||||
};
|
||||
|
||||
// Measure natural size with scale removed, then apply
|
||||
el.style.transform = "none";
|
||||
requestAnimationFrame(() => {
|
||||
naturalSize.current = {
|
||||
width: el.scrollWidth,
|
||||
height: el.scrollHeight,
|
||||
};
|
||||
applyScale();
|
||||
window.addEventListener("resize", applyScale);
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", applyScale);
|
||||
};
|
||||
}, [currentSlide]);
|
||||
|
||||
const slide = slides[currentSlide];
|
||||
|
||||
const slideData: ProsemirrorData | undefined = React.useMemo(
|
||||
() =>
|
||||
slide.type === "content"
|
||||
? { type: "doc", content: slide.content }
|
||||
: undefined,
|
||||
[slide]
|
||||
);
|
||||
|
||||
const extensions = React.useMemo(() => richExtensions, []);
|
||||
|
||||
return createPortal(
|
||||
<Container ref={containerRef} $background={theme.background} $idle={isIdle}>
|
||||
<TopBar $idle={isIdle}>
|
||||
<Flex align="center" gap={12}>
|
||||
<Tooltip content={t("Previous slide")} delay={500}>
|
||||
<Button onClick={goPrev} disabled={currentSlide === 0}>
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<SlideCounter>
|
||||
{currentSlide + 1} / {totalSlides}
|
||||
</SlideCounter>
|
||||
<Tooltip content={t("Next slide")} delay={500}>
|
||||
<Button
|
||||
onClick={goNext}
|
||||
disabled={currentSlide === totalSlides - 1}
|
||||
>
|
||||
<ArrowRightIcon color="currentColor" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<RightButtons>
|
||||
<Tooltip content={t("Toggle fullscreen")} delay={500}>
|
||||
<Button onClick={toggleFullscreen}>
|
||||
{isFullscreen ? (
|
||||
<ShrinkIcon color="currentColor" />
|
||||
) : (
|
||||
<GrowIcon color="currentColor" />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content={t("Close")} delay={500}>
|
||||
<Button onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</RightButtons>
|
||||
</TopBar>
|
||||
<SlideArea onClick={goNext}>
|
||||
<SlideContent ref={slideContentRef}>
|
||||
{slide.type === "title" ? (
|
||||
<TitleSlide>
|
||||
{slide.icon && (
|
||||
<TitleIcon>
|
||||
<Icon
|
||||
value={slide.icon}
|
||||
color={slide.iconColor ?? colorPalette[0]}
|
||||
size={64}
|
||||
initial={slide.title[0]}
|
||||
/>
|
||||
</TitleIcon>
|
||||
)}
|
||||
<TitleText>{slide.title}</TitleText>
|
||||
</TitleSlide>
|
||||
) : slideData ? (
|
||||
<Editor
|
||||
key={currentSlide}
|
||||
defaultValue={slideData}
|
||||
extensions={extensions}
|
||||
readOnly
|
||||
grow={false}
|
||||
placeholder=""
|
||||
/>
|
||||
) : null}
|
||||
</SlideContent>
|
||||
</SlideArea>
|
||||
</Container>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div<{ $background: string; $idle: boolean }>`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: ${depths.presentation};
|
||||
background: ${(props) => props.$background};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
cursor: ${(props) => (props.$idle ? "none" : "default")};
|
||||
|
||||
* {
|
||||
cursor: inherit;
|
||||
}
|
||||
`;
|
||||
|
||||
const SlideArea = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
padding: 80px;
|
||||
`;
|
||||
|
||||
const SlideContent = styled.div`
|
||||
max-width: 960px;
|
||||
width: 100%;
|
||||
transform-origin: center center;
|
||||
|
||||
.ProseMirror {
|
||||
padding: 0;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.4em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
`;
|
||||
|
||||
const TitleSlide = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 24px;
|
||||
min-height: 200px;
|
||||
`;
|
||||
|
||||
const TitleIcon = styled.div`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const TitleText = styled.h1`
|
||||
font-size: 3em;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
margin: 0;
|
||||
color: ${s("text")};
|
||||
`;
|
||||
|
||||
const TopBar = styled.div<{ $idle: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
opacity: ${(props) => (props.$idle ? 0 : 1)};
|
||||
transition: opacity 300ms ease;
|
||||
`;
|
||||
|
||||
const SlideCounter = styled(Text)`
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: ${s("textTertiary")};
|
||||
font-size: 14px;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const RightButtons = styled(Flex).attrs({ align: "center", gap: 16 })`
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
`;
|
||||
|
||||
const Button = styled(NudeButton).attrs({ size: 32 })`
|
||||
&:not(:disabled) {
|
||||
color: ${s("textTertiary")};
|
||||
|
||||
&:${hover},
|
||||
&:active {
|
||||
color: ${s("text")};
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: ${s("textTertiary")};
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
|
||||
export default PresentationMode;
|
||||
@@ -108,6 +108,15 @@ function KeyboardShortcuts({ defaultQuery = "" }: Props) {
|
||||
),
|
||||
label: t("Go to link"),
|
||||
},
|
||||
{
|
||||
shortcut: (
|
||||
<>
|
||||
<Key symbol>{metaDisplay}</Key> + <Key symbol>{altDisplay}</Key>{" "}
|
||||
+ <Key>p</Key>
|
||||
</>
|
||||
),
|
||||
label: t("Present document"),
|
||||
},
|
||||
{
|
||||
shortcut: (
|
||||
<>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { action, computed, observable } from "mobx";
|
||||
import { flushSync } from "react-dom";
|
||||
import { light as defaultTheme } from "@shared/styles/theme";
|
||||
import type { ProsemirrorData } from "@shared/types";
|
||||
import Storage from "@shared/utils/Storage";
|
||||
import Document from "~/models/Document";
|
||||
import type Model from "~/models/base/Model";
|
||||
@@ -92,6 +93,32 @@ class UiStore {
|
||||
@observable
|
||||
debugSafeArea = false;
|
||||
|
||||
/** Data for the currently active presentation, if any. */
|
||||
@observable
|
||||
presentationData: {
|
||||
title: string;
|
||||
icon?: string | null;
|
||||
color?: string | null;
|
||||
data: ProsemirrorData;
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
* Enter presentation mode for the given document.
|
||||
*
|
||||
* @param document the document to present, or null to exit.
|
||||
*/
|
||||
@action
|
||||
setPresentingDocument = (document: Document | null): void => {
|
||||
this.presentationData = document
|
||||
? {
|
||||
title: document.title,
|
||||
icon: document.icon,
|
||||
color: document.color,
|
||||
data: document.data,
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
/** Tracks active export toasts for in-place updates when export completes */
|
||||
exportToasts = observable.map<
|
||||
string,
|
||||
|
||||
+1
-1
@@ -180,7 +180,7 @@
|
||||
"node-fetch": "2.7.0",
|
||||
"nodemailer": "^7.0.11",
|
||||
"octokit": "^3.2.2",
|
||||
"outline-icons": "^4.1.0",
|
||||
"outline-icons": "^4.2.0",
|
||||
"oy-vey": "^0.12.1",
|
||||
"pako": "^2.1.0",
|
||||
"passport": "^0.7.0",
|
||||
|
||||
@@ -14,6 +14,8 @@ const depths = {
|
||||
titleBarDivider: 10000,
|
||||
loadingIndicatorBar: 20000,
|
||||
commandBar: 30000,
|
||||
presentation: 40000,
|
||||
tooltip: 50000,
|
||||
};
|
||||
|
||||
export default depths;
|
||||
|
||||
@@ -17580,12 +17580,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"outline-icons@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "outline-icons@npm:4.1.0"
|
||||
"outline-icons@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "outline-icons@npm:4.2.0"
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
checksum: 10c0/883c72d53b3b81e71748c0abd60f164034e86f9723b538c211fc437aa2c8efbfce3b1fe1d41bc608ed402f09142ad8383b1bde2506266a7116a949b575664d04
|
||||
checksum: 10c0/e607c1542e99aa675c1268763e6a442fc74f2aefacb81aa6a3a318fa853c76c062e288a9e03d963a1c2b90c2e06a40028b4c6c1ae319d0d1fe93cf8d7feb25a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -17811,7 +17811,7 @@ __metadata:
|
||||
nodemailer: "npm:^7.0.11"
|
||||
nodemon: "npm:^3.1.11"
|
||||
octokit: "npm:^3.2.2"
|
||||
outline-icons: "npm:^4.1.0"
|
||||
outline-icons: "npm:^4.2.0"
|
||||
oxlint: "npm:1.11.2"
|
||||
oxlint-tsgolint: "npm:^0.1.6"
|
||||
oy-vey: "npm:^0.12.1"
|
||||
|
||||
Reference in New Issue
Block a user