feat: expand advanced widget experiences

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-14 11:07:09 +02:00
1 parent 9437d211c8
commit c099a8eef6
119 files changed
+3624 -844

No files matched your search

@@ -0,0 +1,18 @@
import { describe, expect, test } from "vitest";
import { getNotebookDisplay } from "./display";
describe("Notebook advanced disclosure", () => {
test("shows the complete toolbar and document stats regardless of compact preferences", () => {
expect(
getNotebookDisplay({ height: 100, isAdvanced: true, isEditing: true, isSaving: false, showToolbar: false }),
).toEqual({ showToolbar: true, toolbarMaxHeight: undefined, showDocumentStats: true });
});
test("does not expose editing controls outside an active editable session", () => {
expect(
getNotebookDisplay({ height: 300, isAdvanced: true, isEditing: false, isSaving: false, showToolbar: true })
.showToolbar,
).toBe(false);
});
});
+13
View File
@@ -0,0 +1,13 @@
interface NotebookDisplayInput {
height: number;
isAdvanced: boolean;
isEditing: boolean;
isSaving: boolean;
showToolbar: boolean;
}
export const getNotebookDisplay = ({ height, isAdvanced, isEditing, isSaving, showToolbar }: NotebookDisplayInput) => ({
showToolbar: isEditing && !isSaving && (isAdvanced || showToolbar),
toolbarMaxHeight: isAdvanced ? undefined : height < 180 ? "4.5rem" : "10rem",
showDocumentStats: isAdvanced || height >= 180,
});
+1 -1
View File
@@ -5,7 +5,7 @@ import { optionsBuilder } from "../options";
import { defaultContent } from "./default-content";
export const { definition, componentLoader } = createWidgetDefinition("notebook", {
supportsAdvancedFocus: false,
supportsAdvancedFocus: true,
icon: IconNotes,
createOptions() {
return optionsBuilder.from(
+38 -11
View File
@@ -70,6 +70,7 @@ import type { TablerIcon } from "@homarr/ui";
import type { WidgetComponentProps } from "../definition";
import actionTargetClasses from "../common/action-target.module.css";
import { createReadOnlyTaskItemTransaction, ReadOnlyTaskItem } from "./read-only-task-item";
import { getNotebookDisplay } from "./display";
import { NotebookTextDirection, setTextDirection } from "./text-direction";
import "@mantine/tiptap/styles.css";
@@ -98,6 +99,7 @@ export function Notebook({
boardId,
itemId,
height,
displayMode = "compact",
}: WidgetComponentProps<"notebook">) {
const [content, setContent] = useState(options.content);
const previousContentRef = useRef(options.content);
@@ -339,6 +341,13 @@ export function Notebook({
characters: documentText.length,
words: documentText.length === 0 ? 0 : documentText.split(/\s+/).length,
};
const display = getNotebookDisplay({
height,
isAdvanced: displayMode === "advanced",
isEditing,
isSaving,
showToolbar: options.showToolbar,
});
return (
<Box className="homarr-notebook" h="100%" onDoubleClick={handleDoubleClick}>
@@ -375,9 +384,9 @@ export function Notebook({
>
<RichTextEditor.Toolbar
style={{
display: isEditing && !isSaving && options.showToolbar === true ? "flex" : "none",
maxHeight: height < 180 ? "4.5rem" : "10rem",
overflowY: "auto",
display: display.showToolbar ? "flex" : "none",
maxHeight: display.toolbarMaxHeight,
overflowY: display.toolbarMaxHeight === undefined ? undefined : "auto",
}}
>
<RichTextEditor.ControlsGroup>
@@ -505,7 +514,7 @@ export function Notebook({
<RichTextEditor.Content />
</ScrollArea>
</RichTextEditor>
{(saveError || height >= 180) && (
{(saveError || display.showDocumentStats) && (
<Group pos="absolute" bottom={4} right={8} gap="xs" style={{ pointerEvents: saveError ? undefined : "none" }}>
{saveError && (
<Tooltip label={saveError} multiline>
@@ -521,7 +530,7 @@ export function Notebook({
</Text>
</Tooltip>
)}
{height >= 180 && (
{display.showDocumentStats && (
<Text size="xs" c="dimmed">
{t("widget.notebook.documentStats", documentStats)}
</Text>
@@ -873,9 +882,18 @@ function ListIndentIncrease() {
editor?.chain().focus().sinkListItem(itemType).run();
}, [editor, itemType]);
editor?.on("selectionUpdate", ({ editor }) => {
setItemType(editor.isActive("taskItem") ? "taskItem" : "listItem");
});
useEffect(() => {
if (!editor) return;
const handleSelectionUpdate = ({ editor: updatedEditor }: { editor: Editor }) => {
setItemType(updatedEditor.isActive("taskItem") ? "taskItem" : "listItem");
};
editor.on("selectionUpdate", handleSelectionUpdate);
return () => {
editor.off("selectionUpdate", handleSelectionUpdate);
};
}, [editor]);
return (
<RichTextEditor.Control
@@ -897,9 +915,18 @@ function ListIndentDecrease() {
editor?.chain().focus().liftListItem(itemType).run();
}, [editor, itemType]);
editor?.on("selectionUpdate", ({ editor }) => {
setItemType(editor.isActive("taskItem") ? "taskItem" : "listItem");
});
useEffect(() => {
if (!editor) return;
const handleSelectionUpdate = ({ editor: updatedEditor }: { editor: Editor }) => {
setItemType(updatedEditor.isActive("taskItem") ? "taskItem" : "listItem");
};
editor.on("selectionUpdate", handleSelectionUpdate);
return () => {
editor.off("selectionUpdate", handleSelectionUpdate);
};
}, [editor]);
return (
<RichTextEditor.Control