Feature/presentation_poc (#7374)

This commit is contained in:
Elian Doran 2025-10-16 18:40:48 +03:00 committed by GitHub
commit 2f49d315c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 528 additions and 32 deletions

View File

@ -58,6 +58,7 @@
"panzoom": "9.4.3",
"preact": "10.27.2",
"react-i18next": "16.0.1",
"reveal.js": "5.2.1",
"split.js": "1.6.5",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
@ -71,6 +72,7 @@
"@types/leaflet": "1.9.21",
"@types/leaflet-gpx": "1.3.8",
"@types/mark.js": "8.11.12",
"@types/reveal.js": "5.2.1",
"@types/tabulator-tables": "6.2.11",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.0.2",

View File

@ -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;

View File

@ -29,7 +29,7 @@ interface Options {
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(
{

View File

@ -27,7 +27,8 @@ export const byBookType: Record<ViewTypeOptions, string | null> = {
calendar: "xWbu3jpNWapp",
table: "2FvYrpmOXm29",
geoMap: "81SGnPGMk7Xc",
board: "CtBQqbwXDx1w"
board: "CtBQqbwXDx1w",
presentation: null
};
export function getHelpUrlForNote(note: FNote | null | undefined) {

View File

@ -4,6 +4,7 @@ import appContext, { type NoteCommandData } from "../components/app_context.js";
import froca from "./froca.js";
import utils from "./utils.js";
import { ALLOWED_PROTOCOLS } from "@triliumnext/commons";
import { openInCurrentNoteContext } from "../components/note_context.js";
function getNotePathFromUrl(url: string) {
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
@ -316,21 +317,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
viewScope
});
} else if (isLeftClick) {
const ntxId = $(evt?.target as any)
.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 });
}
openInCurrentNoteContext(evt, notePath, viewScope);
}
} else if (hrefLink) {
const withinEditLink = $link?.hasClass("ck-link-actions__preview");

View File

@ -168,7 +168,8 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
}
for (const templateNote of childNotes) {
if (templateNote.hasLabel("collection") !== filterCollections) {
if (templateNote.hasLabel("collection") !== filterCollections ||
!templateNote.hasLabel("template")) {
continue;
}

View File

@ -768,6 +768,7 @@
"table": "Table",
"geo-map": "Geo Map",
"board": "Board",
"presentation": "Presentation",
"include_archived_notes": "Show archived notes"
},
"edited_notes": {
@ -2029,6 +2030,11 @@
"edit-note-title": "Click to edit note 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": {
"tree-action-name": "Tree: {{name}}",
"export_note_title": "Export Note",

View File

@ -12,8 +12,9 @@ import BoardView from "./board";
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
import { WebSocketMessage } from "@triliumnext/commons";
import froca from "../../services/froca";
import PresentationView from "./presentation";
interface NoteListProps<T extends object> {
interface NoteListProps {
note: FNote | null | undefined;
notePath: string | null | undefined;
highlightedTokens?: string[] | null;
@ -23,17 +24,17 @@ interface NoteListProps<T extends object> {
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 isEnabled = noteContext?.hasNoteList();
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} />
}
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 viewType = useNoteViewType(note);
const noteIds = useNoteIds(note, viewType, ntxId);
@ -104,6 +105,8 @@ function getComponentByViewType(viewType: ViewTypeOptions, props: ViewModeProps<
return <TableView {...props} />
case "board":
return <BoardView {...props} />
case "presentation":
return <PresentationView {...props} />
}
}

View File

@ -1,6 +1,6 @@
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 interface ViewModeProps<T extends object> {

View File

@ -0,0 +1,10 @@
.presentation-button-bar {
position: absolute;
top: 1em;
right: 1em;
}
.presentation-container {
width: 100%;
height: 100%;
}

View 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;
}

View 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() };
}

View 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; }

View File

@ -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)));
});
});

View 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;
}

View File

@ -5,16 +5,18 @@ import keyboard_actions from "../../services/keyboard_actions";
export interface ActionButtonProps {
text: string;
titlePosition?: "bottom" | "left";
titlePosition?: "top" | "right" | "bottom" | "left";
icon: string;
className?: string;
onClick?: (e: MouseEvent) => void;
triggerCommand?: CommandNames;
noIconActionClass?: 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 [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
@ -32,8 +34,9 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo
return <button
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}
data-trigger-command={triggerCommand}
disabled={disabled}
/>;
}

View 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} />
}

View File

@ -19,7 +19,8 @@ const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
calendar: t("book_properties.calendar"),
table: t("book_properties.table"),
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) {

View File

@ -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 { ViewTypeOptions } from "../collections/interface";
import { FilterLabelsByType } from "@triliumnext/commons";
import { DEFAULT_THEME, getPresentationThemes } from "../collections/presentation/themes";
interface BookConfig {
properties: BookProperty[];
@ -159,6 +160,20 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
},
board: {
properties: []
},
presentation: {
properties: [
{
label: "Theme",
type: "combobox",
bindToLabel: "presentation:theme",
defaultValue: DEFAULT_THEME,
options: getPresentationThemes().map(theme => ({
value: theme.id,
label: theme.name
}))
}
]
}
};

View File

@ -423,7 +423,11 @@
"board_note_third": "Third note",
"board_status_todo": "To Do",
"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": {
"db_not_initialized_desktop": "DB not initialized, please follow on-screen instructions.",

View File

@ -345,24 +345,31 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtree
let branch;
if (!note) {
// Missing item, add it.
({ note, branch } = noteService.createNewNote({
noteId: item.id,
title: item.title,
type: item.type,
parentNoteId: parentNoteId,
content: "",
content: item.content ?? "",
ignoreForbiddenParents: true
}));
} else {
// Existing item, check if it's in the right state.
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
// For hidden subtree notes, we want to ensure they only exist in their designated locations
if (item.enforceBranches || item.id.startsWith("_help")) {
// 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
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({
noteId: item.id,
parentNoteId: parentNoteId,
@ -466,7 +473,7 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtree
}).save();
} else if (attr.name === "docName" || (existingAttribute.noteId.startsWith("_help") && attr.name === "iconClass")) {
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.save();
}

View File

@ -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"
}
]
}
]
}
]
};

View File

@ -12,7 +12,7 @@ describe("Migration", () => {
const migration = (await import("./migration.js")).default;
await migration.migrateIfNecessary();
expect(sql.getValue("SELECT count(*) FROM blobs")).toBe(116);
expect(sql.getValue("SELECT count(*) FROM blobs")).toBe(118);
resolve();
});
});

View File

@ -39,6 +39,7 @@ type Labels = {
"board:groupBy": string;
maxNestingDepth: number;
includeArchived: boolean;
"presentation:theme": string;
}
/**

View File

@ -54,4 +54,10 @@ export interface HiddenSubtreeItem {
* definitions will be removed.
*/
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
View File

@ -253,6 +253,9 @@ importers:
react-i18next:
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)
reveal.js:
specifier: 5.2.1
version: 5.2.1
split.js:
specifier: 1.6.5
version: 1.6.5
@ -287,6 +290,9 @@ importers:
'@types/mark.js':
specifier: 8.11.12
version: 8.11.12
'@types/reveal.js':
specifier: 5.2.1
version: 5.2.1
'@types/tabulator-tables':
specifier: 6.2.11
version: 6.2.11
@ -5162,6 +5168,9 @@ packages:
'@types/retry@0.12.2':
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
'@types/reveal.js@5.2.1':
resolution: {integrity: sha512-egr+amW5iilXo94kEGyJv24bJozsu/XAOHnhMHLnaJkHVxoui2gsWqzByaltA5zfXDTH2F4WyWnAkhHRcpytIQ==}
'@types/safe-compare@1.1.2':
resolution: {integrity: sha512-kK/IM1+pvwCMom+Kezt/UlP8LMEwm8rP6UgGbRc6zUnhU/csoBQ5rWgmD2CJuHxiMiX+H1VqPGpo0kDluJGXYA==}
@ -11964,6 +11973,10 @@ packages:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
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:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
@ -19715,6 +19728,8 @@ snapshots:
'@types/retry@0.12.2': {}
'@types/reveal.js@5.2.1': {}
'@types/safe-compare@1.1.2': {}
'@types/sanitize-html@2.16.0':
@ -28091,6 +28106,8 @@ snapshots:
reusify@1.1.0: {}
reveal.js@5.2.1: {}
rfdc@1.4.1: {}
rgb2hex@0.2.5: {}