mirror of
https://github.com/zadam/trilium.git
synced 2025-10-21 15:49:00 +02:00
Feature/presentation_poc (#7374)
This commit is contained in:
commit
2f49d315c1
@ -58,6 +58,7 @@
|
|||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"preact": "10.27.2",
|
"preact": "10.27.2",
|
||||||
"react-i18next": "16.0.1",
|
"react-i18next": "16.0.1",
|
||||||
|
"reveal.js": "5.2.1",
|
||||||
"split.js": "1.6.5",
|
"split.js": "1.6.5",
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
"tabulator-tables": "6.3.1",
|
"tabulator-tables": "6.3.1",
|
||||||
@ -71,6 +72,7 @@
|
|||||||
"@types/leaflet": "1.9.21",
|
"@types/leaflet": "1.9.21",
|
||||||
"@types/leaflet-gpx": "1.3.8",
|
"@types/leaflet-gpx": "1.3.8",
|
||||||
"@types/mark.js": "8.11.12",
|
"@types/mark.js": "8.11.12",
|
||||||
|
"@types/reveal.js": "5.2.1",
|
||||||
"@types/tabulator-tables": "6.2.11",
|
"@types/tabulator-tables": "6.2.11",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"happy-dom": "20.0.2",
|
"happy-dom": "20.0.2",
|
||||||
|
@ -438,4 +438,22 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openInCurrentNoteContext(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, notePath: string, viewScope?: ViewScope) {
|
||||||
|
const ntxId = $(evt?.target as Element)
|
||||||
|
.closest("[data-ntx-id]")
|
||||||
|
.attr("data-ntx-id");
|
||||||
|
|
||||||
|
const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext();
|
||||||
|
|
||||||
|
if (noteContext) {
|
||||||
|
noteContext.setNote(notePath, { viewScope }).then(() => {
|
||||||
|
if (noteContext !== appContext.tabManager.getActiveContext()) {
|
||||||
|
appContext.tabManager.activateNoteContext(noteContext.ntxId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
appContext.tabManager.openContextWithNote(notePath, { viewScope, activate: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default NoteContext;
|
export default NoteContext;
|
||||||
|
@ -29,7 +29,7 @@ interface Options {
|
|||||||
|
|
||||||
const CODE_MIME_TYPES = new Set(["application/json"]);
|
const CODE_MIME_TYPES = new Set(["application/json"]);
|
||||||
|
|
||||||
async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) {
|
export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) {
|
||||||
|
|
||||||
options = Object.assign(
|
options = Object.assign(
|
||||||
{
|
{
|
||||||
|
@ -27,7 +27,8 @@ export const byBookType: Record<ViewTypeOptions, string | null> = {
|
|||||||
calendar: "xWbu3jpNWapp",
|
calendar: "xWbu3jpNWapp",
|
||||||
table: "2FvYrpmOXm29",
|
table: "2FvYrpmOXm29",
|
||||||
geoMap: "81SGnPGMk7Xc",
|
geoMap: "81SGnPGMk7Xc",
|
||||||
board: "CtBQqbwXDx1w"
|
board: "CtBQqbwXDx1w",
|
||||||
|
presentation: null
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getHelpUrlForNote(note: FNote | null | undefined) {
|
export function getHelpUrlForNote(note: FNote | null | undefined) {
|
||||||
|
@ -4,6 +4,7 @@ import appContext, { type NoteCommandData } from "../components/app_context.js";
|
|||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import { ALLOWED_PROTOCOLS } from "@triliumnext/commons";
|
import { ALLOWED_PROTOCOLS } from "@triliumnext/commons";
|
||||||
|
import { openInCurrentNoteContext } from "../components/note_context.js";
|
||||||
|
|
||||||
function getNotePathFromUrl(url: string) {
|
function getNotePathFromUrl(url: string) {
|
||||||
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
|
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
|
||||||
@ -316,21 +317,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
|
|||||||
viewScope
|
viewScope
|
||||||
});
|
});
|
||||||
} else if (isLeftClick) {
|
} else if (isLeftClick) {
|
||||||
const ntxId = $(evt?.target as any)
|
openInCurrentNoteContext(evt, notePath, viewScope);
|
||||||
.closest("[data-ntx-id]")
|
|
||||||
.attr("data-ntx-id");
|
|
||||||
|
|
||||||
const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext();
|
|
||||||
|
|
||||||
if (noteContext) {
|
|
||||||
noteContext.setNote(notePath, { viewScope }).then(() => {
|
|
||||||
if (noteContext !== appContext.tabManager.getActiveContext()) {
|
|
||||||
appContext.tabManager.activateNoteContext(noteContext.ntxId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
appContext.tabManager.openContextWithNote(notePath, { viewScope, activate: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (hrefLink) {
|
} else if (hrefLink) {
|
||||||
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
||||||
|
@ -168,7 +168,8 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const templateNote of childNotes) {
|
for (const templateNote of childNotes) {
|
||||||
if (templateNote.hasLabel("collection") !== filterCollections) {
|
if (templateNote.hasLabel("collection") !== filterCollections ||
|
||||||
|
!templateNote.hasLabel("template")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -768,6 +768,7 @@
|
|||||||
"table": "Table",
|
"table": "Table",
|
||||||
"geo-map": "Geo Map",
|
"geo-map": "Geo Map",
|
||||||
"board": "Board",
|
"board": "Board",
|
||||||
|
"presentation": "Presentation",
|
||||||
"include_archived_notes": "Show archived notes"
|
"include_archived_notes": "Show archived notes"
|
||||||
},
|
},
|
||||||
"edited_notes": {
|
"edited_notes": {
|
||||||
@ -2029,6 +2030,11 @@
|
|||||||
"edit-note-title": "Click to edit note title",
|
"edit-note-title": "Click to edit note title",
|
||||||
"edit-column-title": "Click to edit column title"
|
"edit-column-title": "Click to edit column title"
|
||||||
},
|
},
|
||||||
|
"presentation_view": {
|
||||||
|
"edit-slide": "Edit this slide",
|
||||||
|
"start-presentation": "Start presentation",
|
||||||
|
"slide-overview": "Toggle an overview of the slides"
|
||||||
|
},
|
||||||
"command_palette": {
|
"command_palette": {
|
||||||
"tree-action-name": "Tree: {{name}}",
|
"tree-action-name": "Tree: {{name}}",
|
||||||
"export_note_title": "Export Note",
|
"export_note_title": "Export Note",
|
||||||
|
@ -12,8 +12,9 @@ import BoardView from "./board";
|
|||||||
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
|
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
|
||||||
import { WebSocketMessage } from "@triliumnext/commons";
|
import { WebSocketMessage } from "@triliumnext/commons";
|
||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
|
import PresentationView from "./presentation";
|
||||||
|
|
||||||
interface NoteListProps<T extends object> {
|
interface NoteListProps {
|
||||||
note: FNote | null | undefined;
|
note: FNote | null | undefined;
|
||||||
notePath: string | null | undefined;
|
notePath: string | null | undefined;
|
||||||
highlightedTokens?: string[] | null;
|
highlightedTokens?: string[] | null;
|
||||||
@ -23,17 +24,17 @@ interface NoteListProps<T extends object> {
|
|||||||
ntxId: string | null | undefined;
|
ntxId: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NoteList<T extends object>(props: Pick<NoteListProps<T>, "displayOnlyCollections">) {
|
export default function NoteList<T extends object>(props: Pick<NoteListProps, "displayOnlyCollections">) {
|
||||||
const { note, noteContext, notePath, ntxId } = useNoteContext();
|
const { note, noteContext, notePath, ntxId } = useNoteContext();
|
||||||
const isEnabled = noteContext?.hasNoteList();
|
const isEnabled = noteContext?.hasNoteList();
|
||||||
return <CustomNoteList note={note} isEnabled={!!isEnabled} notePath={notePath} ntxId={ntxId} {...props} />
|
return <CustomNoteList note={note} isEnabled={!!isEnabled} notePath={notePath} ntxId={ntxId} {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchNoteList<T extends object>(props: Omit<NoteListProps<T>, "isEnabled">) {
|
export function SearchNoteList<T extends object>(props: Omit<NoteListProps, "isEnabled">) {
|
||||||
return <CustomNoteList {...props} isEnabled={true} />
|
return <CustomNoteList {...props} isEnabled={true} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId }: NoteListProps<T>) {
|
function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId }: NoteListProps) {
|
||||||
const widgetRef = useRef<HTMLDivElement>(null);
|
const widgetRef = useRef<HTMLDivElement>(null);
|
||||||
const viewType = useNoteViewType(note);
|
const viewType = useNoteViewType(note);
|
||||||
const noteIds = useNoteIds(note, viewType, ntxId);
|
const noteIds = useNoteIds(note, viewType, ntxId);
|
||||||
@ -104,6 +105,8 @@ function getComponentByViewType(viewType: ViewTypeOptions, props: ViewModeProps<
|
|||||||
return <TableView {...props} />
|
return <TableView {...props} />
|
||||||
case "board":
|
case "board":
|
||||||
return <BoardView {...props} />
|
return <BoardView {...props} />
|
||||||
|
case "presentation":
|
||||||
|
return <PresentationView {...props} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
|
|
||||||
export const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board"] as const;
|
export const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board", "presentation"] as const;
|
||||||
export type ViewTypeOptions = typeof allViewTypes[number];
|
export type ViewTypeOptions = typeof allViewTypes[number];
|
||||||
|
|
||||||
export interface ViewModeProps<T extends object> {
|
export interface ViewModeProps<T extends object> {
|
||||||
|
10
apps/client/src/widgets/collections/presentation/index.css
Normal file
10
apps/client/src/widgets/collections/presentation/index.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.presentation-button-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 1em;
|
||||||
|
right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
183
apps/client/src/widgets/collections/presentation/index.tsx
Normal file
183
apps/client/src/widgets/collections/presentation/index.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { ViewModeProps } from "../interface";
|
||||||
|
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import Reveal from "reveal.js";
|
||||||
|
import slideBaseStylesheet from "reveal.js/dist/reveal.css?raw";
|
||||||
|
import slideCustomStylesheet from "./slidejs.css?raw";
|
||||||
|
import { buildPresentationModel, PresentationModel, PresentationSlideBaseModel } from "./model";
|
||||||
|
import ShadowDom from "../../react/ShadowDom";
|
||||||
|
import ActionButton from "../../react/ActionButton";
|
||||||
|
import "./index.css";
|
||||||
|
import { RefObject } from "preact";
|
||||||
|
import { openInCurrentNoteContext } from "../../../components/note_context";
|
||||||
|
import { useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
|
||||||
|
import { t } from "../../../services/i18n";
|
||||||
|
import { DEFAULT_THEME, loadPresentationTheme } from "./themes";
|
||||||
|
import FNote from "../../../entities/fnote";
|
||||||
|
|
||||||
|
export default function PresentationView({ note, noteIds }: ViewModeProps<{}>) {
|
||||||
|
const [ presentation, setPresentation ] = useState<PresentationModel>();
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [ api, setApi ] = useState<Reveal.Api>();
|
||||||
|
const stylesheets = usePresentationStylesheets(note);
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
buildPresentationModel(note).then(setPresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
if (loadResults.getNoteIds().find(noteId => noteIds.includes(noteId))) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useLayoutEffect(refresh, [ note, noteIds ]);
|
||||||
|
|
||||||
|
return presentation && stylesheets && (
|
||||||
|
<>
|
||||||
|
<ShadowDom
|
||||||
|
className="presentation-container"
|
||||||
|
containerRef={containerRef}
|
||||||
|
>
|
||||||
|
{stylesheets.map(stylesheet => <style>{stylesheet}</style>)}
|
||||||
|
<Presentation presentation={presentation} setApi={setApi} />
|
||||||
|
</ShadowDom>
|
||||||
|
<ButtonOverlay containerRef={containerRef} api={api} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePresentationStylesheets(note: FNote) {
|
||||||
|
const [ themeName ] = useNoteLabelWithDefault(note, "presentation:theme", DEFAULT_THEME);
|
||||||
|
const [ stylesheets, setStylesheets ] = useState<string[]>();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
loadPresentationTheme(themeName).then((themeStylesheet) => {
|
||||||
|
setStylesheets([
|
||||||
|
slideBaseStylesheet,
|
||||||
|
themeStylesheet,
|
||||||
|
slideCustomStylesheet
|
||||||
|
].map(stylesheet => stylesheet.replace(/:root/g, ":host")));
|
||||||
|
});
|
||||||
|
}, [ themeName ]);
|
||||||
|
|
||||||
|
return stylesheets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ButtonOverlay({ containerRef, api }: { containerRef: RefObject<HTMLDivElement>, api: Reveal.Api | undefined }) {
|
||||||
|
const [ isOverviewActive, setIsOverviewActive ] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api) return;
|
||||||
|
setIsOverviewActive(api.isOverview());
|
||||||
|
const onEnabled = () => setIsOverviewActive(true);
|
||||||
|
const onDisabled = () => setIsOverviewActive(false);
|
||||||
|
api.on("overviewshown", onEnabled);
|
||||||
|
api.on("overviewhidden", onDisabled);
|
||||||
|
return () => {
|
||||||
|
api.off("overviewshown", onEnabled);
|
||||||
|
api.off("overviewhidden", onDisabled);
|
||||||
|
};
|
||||||
|
}, [ api ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="presentation-button-bar">
|
||||||
|
<div className="floating-buttons-children">
|
||||||
|
<ActionButton
|
||||||
|
className="floating-button"
|
||||||
|
icon="bx bx-edit"
|
||||||
|
text={t("presentation_view.edit-slide")}
|
||||||
|
noIconActionClass
|
||||||
|
onClick={e => {
|
||||||
|
const currentSlide = api?.getCurrentSlide();
|
||||||
|
const noteId = getNoteIdFromSlide(currentSlide);
|
||||||
|
|
||||||
|
if (noteId) {
|
||||||
|
openInCurrentNoteContext(e, noteId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
className="floating-button"
|
||||||
|
icon="bx bx-grid-horizontal"
|
||||||
|
text={t("presentation_view.slide-overview")}
|
||||||
|
active={isOverviewActive}
|
||||||
|
noIconActionClass
|
||||||
|
onClick={() => api?.toggleOverview()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
className="floating-button"
|
||||||
|
icon="bx bx-fullscreen"
|
||||||
|
text={t("presentation_view.start-presentation")}
|
||||||
|
noIconActionClass
|
||||||
|
onClick={() => containerRef.current?.requestFullscreen()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Presentation({ presentation, setApi } : { presentation: PresentationModel, setApi: (api: Reveal.Api | undefined) => void }) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [revealApi, setRevealApi] = useState<Reveal.Api>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const api = new Reveal(containerRef.current, {
|
||||||
|
transition: "slide",
|
||||||
|
embedded: true,
|
||||||
|
keyboardCondition(event) {
|
||||||
|
// Full-screen requests sometimes fail, we rely on the UI button instead.
|
||||||
|
if (event.key === "f") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
api.initialize().then(() => {
|
||||||
|
setRevealApi(revealApi);
|
||||||
|
setApi(api);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api.destroy();
|
||||||
|
setRevealApi(undefined);
|
||||||
|
setApi(undefined);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
revealApi?.sync();
|
||||||
|
}, [ presentation, revealApi ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="reveal">
|
||||||
|
<div className="slides">
|
||||||
|
{presentation.slides?.map(slide => {
|
||||||
|
if (!slide.verticalSlides) {
|
||||||
|
return <Slide key={slide.noteId} slide={slide} />
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<Slide key={slide.noteId} slide={slide} />
|
||||||
|
{slide.verticalSlides.map(slide => <Slide key={slide.noteId} slide={slide} /> )}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function Slide({ slide }: { slide: PresentationSlideBaseModel }) {
|
||||||
|
return <section data-note-id={slide.noteId} dangerouslySetInnerHTML={slide.content} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNoteIdFromSlide(slide: HTMLElement | undefined) {
|
||||||
|
if (!slide) return;
|
||||||
|
return slide.dataset.noteId;
|
||||||
|
}
|
49
apps/client/src/widgets/collections/presentation/model.ts
Normal file
49
apps/client/src/widgets/collections/presentation/model.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import FNote from "../../../entities/fnote";
|
||||||
|
import contentRenderer from "../../../services/content_renderer";
|
||||||
|
|
||||||
|
type DangerouslySetInnerHTML = { __html: string; };
|
||||||
|
|
||||||
|
/** A top-level slide with optional vertical slides. */
|
||||||
|
interface PresentationSlideModel extends PresentationSlideBaseModel {
|
||||||
|
verticalSlides: PresentationSlideBaseModel[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Either a top-level slide or a vertical slide. */
|
||||||
|
export interface PresentationSlideBaseModel {
|
||||||
|
noteId: string;
|
||||||
|
content: DangerouslySetInnerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresentationModel {
|
||||||
|
slides: PresentationSlideModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildPresentationModel(note: FNote): Promise<PresentationModel> {
|
||||||
|
const slideNotes = await note.getChildNotes();
|
||||||
|
const slides: PresentationSlideModel[] = await Promise.all(slideNotes.map(async slideNote => ({
|
||||||
|
noteId: slideNote.noteId,
|
||||||
|
content: await processContent(slideNote),
|
||||||
|
verticalSlides: await buildVerticalSlides(slideNote)
|
||||||
|
})))
|
||||||
|
|
||||||
|
return { slides };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildVerticalSlides(parentSlideNote: FNote): Promise<undefined | PresentationSlideBaseModel[]> {
|
||||||
|
const children = await parentSlideNote.getChildNotes();
|
||||||
|
if (!children.length) return;
|
||||||
|
|
||||||
|
const slides: PresentationSlideBaseModel[] = await Promise.all(children.map(async childNote => ({
|
||||||
|
noteId: childNote.noteId,
|
||||||
|
content: await processContent(childNote)
|
||||||
|
})));
|
||||||
|
|
||||||
|
return slides;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processContent(note: FNote): Promise<DangerouslySetInnerHTML> {
|
||||||
|
const { $renderedContent } = await contentRenderer.getRenderedContent(note, {
|
||||||
|
|
||||||
|
});
|
||||||
|
return { __html: $renderedContent.html() };
|
||||||
|
}
|
20
apps/client/src/widgets/collections/presentation/slidejs.css
Normal file
20
apps/client/src/widgets/collections/presentation/slidejs.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
figure img {
|
||||||
|
aspect-ratio: unset !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.katex-html {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:has(span.text-tiny),
|
||||||
|
p:has(span.text-small),
|
||||||
|
p:has(span.text-big),
|
||||||
|
p:has(span.text-huge) {
|
||||||
|
line-height: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.text-tiny { font-size: 0.5em; }
|
||||||
|
span.text-small { font-size: 0.75em; }
|
||||||
|
span.text-big { font-size: 1.5em; }
|
||||||
|
span.text-huge { font-size: 2em; }
|
@ -0,0 +1,10 @@
|
|||||||
|
import { it, describe } from "vitest";
|
||||||
|
import { getPresentationThemes, loadPresentationTheme } from "./themes";
|
||||||
|
|
||||||
|
describe("Presentation themes", () => {
|
||||||
|
it("can load all themes", async () => {
|
||||||
|
const themes = getPresentationThemes();
|
||||||
|
|
||||||
|
await Promise.all(themes.map(theme => loadPresentationTheme(theme.id)));
|
||||||
|
});
|
||||||
|
});
|
58
apps/client/src/widgets/collections/presentation/themes.ts
Normal file
58
apps/client/src/widgets/collections/presentation/themes.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
export const DEFAULT_THEME = "white";
|
||||||
|
|
||||||
|
const themes = {
|
||||||
|
black: {
|
||||||
|
name: "Black",
|
||||||
|
loadTheme: () => import("reveal.js/dist/theme/black.css?raw")
|
||||||
|
},
|
||||||
|
white: {
|
||||||
|
name: "White",
|
||||||
|
loadTheme: () => import("reveal.js/dist/theme/white.css?raw")
|
||||||
|
},
|
||||||
|
beige: {
|
||||||
|
name: "Beige",
|
||||||
|
loadTheme: () => import("reveal.js/dist/theme/beige.css?raw")
|
||||||
|
},
|
||||||
|
serif: {
|
||||||
|
name: "Serif",
|
||||||
|
loadTheme: () => import("reveal.js/dist/theme/serif.css?raw")
|
||||||
|
},
|
||||||
|
simple: {
|
||||||
|
name: "Simple",
|
||||||
|
loadTheme: () => import("reveal.js/dist/theme/simple.css?raw")
|
||||||
|
},
|
||||||
|
solarized: {
|
||||||
|
name: "Solarized",
|
||||||
|
loadTheme: () => import("reveal.js/dist/theme/solarized.css?raw")
|
||||||
|
},
|
||||||
|
moon: {
|
||||||
|
name: "Moon",
|
||||||
|
loadTheme: () => import("reveal.js/dist/theme/moon.css?raw")
|
||||||
|
},
|
||||||
|
dracula: {
|
||||||
|
name: "Dracula",
|
||||||
|
loadTheme: () => import("reveal.js/dist/theme/dracula.css?raw")
|
||||||
|
},
|
||||||
|
sky: {
|
||||||
|
name: "Sky",
|
||||||
|
loadTheme: () => import("reveal.js/dist/theme/sky.css?raw")
|
||||||
|
},
|
||||||
|
blood: {
|
||||||
|
name: "Blood",
|
||||||
|
loadTheme: () => import("reveal.js/dist/theme/blood.css?raw")
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function getPresentationThemes() {
|
||||||
|
return Object.entries(themes).map(([ id, theme ]) => ({
|
||||||
|
id: id,
|
||||||
|
name: theme.name
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadPresentationTheme(name: keyof typeof themes | string) {
|
||||||
|
let theme = themes[name];
|
||||||
|
if (!theme) theme = themes[DEFAULT_THEME];
|
||||||
|
|
||||||
|
return (await theme.loadTheme()).default;
|
||||||
|
}
|
@ -5,16 +5,18 @@ import keyboard_actions from "../../services/keyboard_actions";
|
|||||||
|
|
||||||
export interface ActionButtonProps {
|
export interface ActionButtonProps {
|
||||||
text: string;
|
text: string;
|
||||||
titlePosition?: "bottom" | "left";
|
titlePosition?: "top" | "right" | "bottom" | "left";
|
||||||
icon: string;
|
icon: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (e: MouseEvent) => void;
|
onClick?: (e: MouseEvent) => void;
|
||||||
triggerCommand?: CommandNames;
|
triggerCommand?: CommandNames;
|
||||||
noIconActionClass?: boolean;
|
noIconActionClass?: boolean;
|
||||||
frame?: boolean;
|
frame?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass, frame }: ActionButtonProps) {
|
export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled }: ActionButtonProps) {
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
|
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
|
||||||
|
|
||||||
@ -32,8 +34,9 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo
|
|||||||
|
|
||||||
return <button
|
return <button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
class={`${className ?? ""} ${!noIconActionClass ? "icon-action" : "btn"} ${icon} ${frame ? "btn btn-primary" : ""}`}
|
class={`${className ?? ""} ${!noIconActionClass ? "icon-action" : "btn"} ${icon} ${frame ? "btn btn-primary" : ""} ${disabled ? "disabled" : ""} ${active ? "active" : ""}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
data-trigger-command={triggerCommand}
|
data-trigger-command={triggerCommand}
|
||||||
|
disabled={disabled}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
28
apps/client/src/widgets/react/ShadowDom.tsx
Normal file
28
apps/client/src/widgets/react/ShadowDom.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { ComponentChildren, HTMLAttributes, JSX, RefObject, render } from "preact";
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { useSyncedRef } from "./hooks";
|
||||||
|
|
||||||
|
interface ShadowDomProps extends Omit<HTMLAttributes<HTMLDivElement>, "ref"> {
|
||||||
|
children: ComponentChildren;
|
||||||
|
containerRef?: RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShadowDom({ children, containerRef: externalContainerRef, ...containerProps }: ShadowDomProps) {
|
||||||
|
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
|
||||||
|
const [ shadowRoot, setShadowRoot ] = useState<ShadowRoot | null>(null);
|
||||||
|
|
||||||
|
// Create the shadow root.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const shadow = containerRef.current.attachShadow({ mode: "open" });
|
||||||
|
setShadowRoot(shadow);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Render the child elements.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shadowRoot) return;
|
||||||
|
render(<>{children}</>, shadowRoot);
|
||||||
|
}, [ shadowRoot, children ]);
|
||||||
|
|
||||||
|
return <div ref={containerRef} {...containerProps} />
|
||||||
|
}
|
@ -19,7 +19,8 @@ const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
|
|||||||
calendar: t("book_properties.calendar"),
|
calendar: t("book_properties.calendar"),
|
||||||
table: t("book_properties.table"),
|
table: t("book_properties.table"),
|
||||||
geoMap: t("book_properties.geo-map"),
|
geoMap: t("book_properties.geo-map"),
|
||||||
board: t("book_properties.board")
|
board: t("book_properties.board"),
|
||||||
|
presentation: t("book_properties.presentation")
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CollectionPropertiesTab({ note }: TabContext) {
|
export default function CollectionPropertiesTab({ note }: TabContext) {
|
||||||
|
@ -5,6 +5,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget";
|
|||||||
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, type MapLayer } from "../collections/geomap/map_layer";
|
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, type MapLayer } from "../collections/geomap/map_layer";
|
||||||
import { ViewTypeOptions } from "../collections/interface";
|
import { ViewTypeOptions } from "../collections/interface";
|
||||||
import { FilterLabelsByType } from "@triliumnext/commons";
|
import { FilterLabelsByType } from "@triliumnext/commons";
|
||||||
|
import { DEFAULT_THEME, getPresentationThemes } from "../collections/presentation/themes";
|
||||||
|
|
||||||
interface BookConfig {
|
interface BookConfig {
|
||||||
properties: BookProperty[];
|
properties: BookProperty[];
|
||||||
@ -159,6 +160,20 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
|||||||
},
|
},
|
||||||
board: {
|
board: {
|
||||||
properties: []
|
properties: []
|
||||||
|
},
|
||||||
|
presentation: {
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
label: "Theme",
|
||||||
|
type: "combobox",
|
||||||
|
bindToLabel: "presentation:theme",
|
||||||
|
defaultValue: DEFAULT_THEME,
|
||||||
|
options: getPresentationThemes().map(theme => ({
|
||||||
|
value: theme.id,
|
||||||
|
label: theme.name
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -423,7 +423,11 @@
|
|||||||
"board_note_third": "Third note",
|
"board_note_third": "Third note",
|
||||||
"board_status_todo": "To Do",
|
"board_status_todo": "To Do",
|
||||||
"board_status_progress": "In Progress",
|
"board_status_progress": "In Progress",
|
||||||
"board_status_done": "Done"
|
"board_status_done": "Done",
|
||||||
|
"presentation": "Presentation",
|
||||||
|
"presentation_slide": "Presentation slide",
|
||||||
|
"presentation_slide_first": "First slide",
|
||||||
|
"presentation_slide_second": "Second slide"
|
||||||
},
|
},
|
||||||
"sql_init": {
|
"sql_init": {
|
||||||
"db_not_initialized_desktop": "DB not initialized, please follow on-screen instructions.",
|
"db_not_initialized_desktop": "DB not initialized, please follow on-screen instructions.",
|
||||||
|
@ -345,24 +345,31 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtree
|
|||||||
let branch;
|
let branch;
|
||||||
|
|
||||||
if (!note) {
|
if (!note) {
|
||||||
|
// Missing item, add it.
|
||||||
({ note, branch } = noteService.createNewNote({
|
({ note, branch } = noteService.createNewNote({
|
||||||
noteId: item.id,
|
noteId: item.id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
parentNoteId: parentNoteId,
|
parentNoteId: parentNoteId,
|
||||||
content: "",
|
content: item.content ?? "",
|
||||||
ignoreForbiddenParents: true
|
ignoreForbiddenParents: true
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
// Existing item, check if it's in the right state.
|
||||||
branch = note.getParentBranches().find((branch) => branch.parentNoteId === parentNoteId);
|
branch = note.getParentBranches().find((branch) => branch.parentNoteId === parentNoteId);
|
||||||
|
|
||||||
|
if (item.content && note.getContent() !== item.content) {
|
||||||
|
log.info(`Updating content of ${item.id}.`);
|
||||||
|
note.setContent(item.content);
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up any branches that shouldn't exist according to the meta definition
|
// Clean up any branches that shouldn't exist according to the meta definition
|
||||||
// For hidden subtree notes, we want to ensure they only exist in their designated locations
|
// For hidden subtree notes, we want to ensure they only exist in their designated locations
|
||||||
if (item.enforceBranches || item.id.startsWith("_help")) {
|
if (item.enforceBranches || item.id.startsWith("_help")) {
|
||||||
// If the note exists but doesn't have a branch in the expected parent,
|
// If the note exists but doesn't have a branch in the expected parent,
|
||||||
// create the missing branch to ensure it's in the correct location
|
// create the missing branch to ensure it's in the correct location
|
||||||
if (!branch) {
|
if (!branch) {
|
||||||
console.log("Creating missing branch for note", item.id, "under parent", parentNoteId);
|
log.info(`Creating missing branch for note ${item.id} under parent ${parentNoteId}.`);
|
||||||
branch = new BBranch({
|
branch = new BBranch({
|
||||||
noteId: item.id,
|
noteId: item.id,
|
||||||
parentNoteId: parentNoteId,
|
parentNoteId: parentNoteId,
|
||||||
@ -466,7 +473,7 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtree
|
|||||||
}).save();
|
}).save();
|
||||||
} else if (attr.name === "docName" || (existingAttribute.noteId.startsWith("_help") && attr.name === "iconClass")) {
|
} else if (attr.name === "docName" || (existingAttribute.noteId.startsWith("_help") && attr.name === "iconClass")) {
|
||||||
if (existingAttribute.value !== attr.value) {
|
if (existingAttribute.value !== attr.value) {
|
||||||
console.log(`Updating attribute ${attrId} from "${existingAttribute.value}" to "${attr.value}"`);
|
log.info(`Updating attribute ${attrId} from "${existingAttribute.value}" to "${attr.value}"`);
|
||||||
existingAttribute.value = attr.value ?? "";
|
existingAttribute.value = attr.value ?? "";
|
||||||
existingAttribute.save();
|
existingAttribute.save();
|
||||||
}
|
}
|
||||||
|
@ -234,6 +234,72 @@ export default function buildHiddenSubtreeTemplates() {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "_template_presentation_slide",
|
||||||
|
type: "text",
|
||||||
|
title: t("hidden_subtree_templates.presentation_slide"),
|
||||||
|
icon: "bx bx-rectangle",
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
name: "slide",
|
||||||
|
type: "label"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "_template_presentation",
|
||||||
|
type: "book",
|
||||||
|
title: t("hidden_subtree_templates.presentation"),
|
||||||
|
icon: "bx bx-slideshow",
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
name: "template",
|
||||||
|
type: "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "viewType",
|
||||||
|
type: "label",
|
||||||
|
value: "presentation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "collection",
|
||||||
|
type: "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "child:template",
|
||||||
|
type: "relation",
|
||||||
|
value: "_template_presentation_slide"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "_template_presentation_first",
|
||||||
|
type: "text",
|
||||||
|
title: t("hidden_subtree_templates.presentation_slide_first"),
|
||||||
|
content: t("hidden_subtree_templates.presentation_slide_first"),
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
name: "template",
|
||||||
|
type: "relation",
|
||||||
|
value: "_template_presentation_slide"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "_template_presentation_second",
|
||||||
|
type: "text",
|
||||||
|
title: t("hidden_subtree_templates.presentation_slide_second"),
|
||||||
|
content: t("hidden_subtree_templates.presentation_slide_second"),
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
name: "template",
|
||||||
|
type: "relation",
|
||||||
|
value: "_template_presentation_slide"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ describe("Migration", () => {
|
|||||||
|
|
||||||
const migration = (await import("./migration.js")).default;
|
const migration = (await import("./migration.js")).default;
|
||||||
await migration.migrateIfNecessary();
|
await migration.migrateIfNecessary();
|
||||||
expect(sql.getValue("SELECT count(*) FROM blobs")).toBe(116);
|
expect(sql.getValue("SELECT count(*) FROM blobs")).toBe(118);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -39,6 +39,7 @@ type Labels = {
|
|||||||
"board:groupBy": string;
|
"board:groupBy": string;
|
||||||
maxNestingDepth: number;
|
maxNestingDepth: number;
|
||||||
includeArchived: boolean;
|
includeArchived: boolean;
|
||||||
|
"presentation:theme": string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,4 +54,10 @@ export interface HiddenSubtreeItem {
|
|||||||
* definitions will be removed.
|
* definitions will be removed.
|
||||||
*/
|
*/
|
||||||
enforceAttributes?: boolean;
|
enforceAttributes?: boolean;
|
||||||
|
/**
|
||||||
|
* Optionally, a content to be set in the hidden note. If undefined, an empty string will be set instead.
|
||||||
|
*
|
||||||
|
* The value is also checked at every startup to ensure that it's kept up to date according to the definition.
|
||||||
|
*/
|
||||||
|
content?: string;
|
||||||
}
|
}
|
||||||
|
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@ -253,6 +253,9 @@ importers:
|
|||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: 16.0.1
|
specifier: 16.0.1
|
||||||
version: 16.0.1(i18next@25.6.0(typescript@5.9.3))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.3)
|
version: 16.0.1(i18next@25.6.0(typescript@5.9.3))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.3)
|
||||||
|
reveal.js:
|
||||||
|
specifier: 5.2.1
|
||||||
|
version: 5.2.1
|
||||||
split.js:
|
split.js:
|
||||||
specifier: 1.6.5
|
specifier: 1.6.5
|
||||||
version: 1.6.5
|
version: 1.6.5
|
||||||
@ -287,6 +290,9 @@ importers:
|
|||||||
'@types/mark.js':
|
'@types/mark.js':
|
||||||
specifier: 8.11.12
|
specifier: 8.11.12
|
||||||
version: 8.11.12
|
version: 8.11.12
|
||||||
|
'@types/reveal.js':
|
||||||
|
specifier: 5.2.1
|
||||||
|
version: 5.2.1
|
||||||
'@types/tabulator-tables':
|
'@types/tabulator-tables':
|
||||||
specifier: 6.2.11
|
specifier: 6.2.11
|
||||||
version: 6.2.11
|
version: 6.2.11
|
||||||
@ -5162,6 +5168,9 @@ packages:
|
|||||||
'@types/retry@0.12.2':
|
'@types/retry@0.12.2':
|
||||||
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
|
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
|
||||||
|
|
||||||
|
'@types/reveal.js@5.2.1':
|
||||||
|
resolution: {integrity: sha512-egr+amW5iilXo94kEGyJv24bJozsu/XAOHnhMHLnaJkHVxoui2gsWqzByaltA5zfXDTH2F4WyWnAkhHRcpytIQ==}
|
||||||
|
|
||||||
'@types/safe-compare@1.1.2':
|
'@types/safe-compare@1.1.2':
|
||||||
resolution: {integrity: sha512-kK/IM1+pvwCMom+Kezt/UlP8LMEwm8rP6UgGbRc6zUnhU/csoBQ5rWgmD2CJuHxiMiX+H1VqPGpo0kDluJGXYA==}
|
resolution: {integrity: sha512-kK/IM1+pvwCMom+Kezt/UlP8LMEwm8rP6UgGbRc6zUnhU/csoBQ5rWgmD2CJuHxiMiX+H1VqPGpo0kDluJGXYA==}
|
||||||
|
|
||||||
@ -11964,6 +11973,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||||
|
|
||||||
|
reveal.js@5.2.1:
|
||||||
|
resolution: {integrity: sha512-r7//6mIM5p34hFiDMvYfXgyjXqGRta+/psd9YtytsgRlrpRzFv4RbH76TXd2qD+7ZPZEbpBDhdRhJaFgfQ7zNQ==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
rfdc@1.4.1:
|
rfdc@1.4.1:
|
||||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
@ -19715,6 +19728,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/retry@0.12.2': {}
|
'@types/retry@0.12.2': {}
|
||||||
|
|
||||||
|
'@types/reveal.js@5.2.1': {}
|
||||||
|
|
||||||
'@types/safe-compare@1.1.2': {}
|
'@types/safe-compare@1.1.2': {}
|
||||||
|
|
||||||
'@types/sanitize-html@2.16.0':
|
'@types/sanitize-html@2.16.0':
|
||||||
@ -28091,6 +28106,8 @@ snapshots:
|
|||||||
|
|
||||||
reusify@1.1.0: {}
|
reusify@1.1.0: {}
|
||||||
|
|
||||||
|
reveal.js@5.2.1: {}
|
||||||
|
|
||||||
rfdc@1.4.1: {}
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
rgb2hex@0.2.5: {}
|
rgb2hex@0.2.5: {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user