From 512229ec7415a19143cfba68c232a26fb89d4fd0 Mon Sep 17 00:00:00 2001 From: rlazarini <remi.lazarini@cea.fr> Date: Thu, 12 Dec 2024 14:41:36 +0100 Subject: [PATCH] [Ivette] added Help component with test in sandbox and adapting the markdown component --- ivette/electron.vite.config.ts | 1 + ivette/package.json | 1 + ivette/src/dome/renderer/dialogs.tsx | 2 +- ivette/src/dome/renderer/help.tsx | 122 ++++++++++++++++++++ ivette/src/dome/renderer/style.css | 81 +++++++++----- ivette/src/dome/renderer/text/markdown.tsx | 55 ++++++++- ivette/src/dome/renderer/text/style.css | 15 +++ ivette/src/sandbox/help.tsx | 61 ++++++++++ ivette/src/sandbox/panel.tsx | 11 +- ivette/src/sandbox/sandbox.md | 124 +++++++++++++++++++++ 10 files changed, 436 insertions(+), 37 deletions(-) create mode 100644 ivette/src/dome/renderer/help.tsx create mode 100644 ivette/src/sandbox/help.tsx create mode 100644 ivette/src/sandbox/sandbox.md diff --git a/ivette/electron.vite.config.ts b/ivette/electron.vite.config.ts index 58835c0b2ba..6637f2640d2 100644 --- a/ivette/electron.vite.config.ts +++ b/ivette/electron.vite.config.ts @@ -62,6 +62,7 @@ export default defineConfig({ "dome/controls": path.resolve(DOME, "renderer", "controls"), "dome/data": path.resolve(DOME, "renderer", "data"), "dome/dialogs": path.resolve(DOME, "renderer", "dialogs"), + "dome/help": path.resolve(DOME, "renderer", "help"), "dome/dnd": path.resolve(DOME, "renderer", "dnd"), "dome/errors": path.resolve(DOME, "renderer", "errors"), "dome/frame": path.resolve(DOME, "renderer", "frame"), diff --git a/ivette/package.json b/ivette/package.json index 6978f96fcb8..3d54725bdb6 100644 --- a/ivette/package.json +++ b/ivette/package.json @@ -44,6 +44,7 @@ "diff": "^5", "lodash": "^4", "react": "^18", + "react-code-blocks": "0.1.6", "react-cytoscapejs": "", "react-dom": "^18", "react-draggable": "^4.4.6", diff --git a/ivette/src/dome/renderer/dialogs.tsx b/ivette/src/dome/renderer/dialogs.tsx index d5b7dfb0b40..67bf1faf6f3 100644 --- a/ivette/src/dome/renderer/dialogs.tsx +++ b/ivette/src/dome/renderer/dialogs.tsx @@ -315,7 +315,7 @@ export async function showOpenDir( export function showModal(val: React.ReactNode): void { modal.setValue(val); } export function closeModal(): void { showModal(undefined); } -interface ModalProps { +export interface ModalProps { /** Text of the label. Prepend to other children elements. */ label: string; /** Icon identifier. Displayed on the left side of the label. */ diff --git a/ivette/src/dome/renderer/help.tsx b/ivette/src/dome/renderer/help.tsx new file mode 100644 index 00000000000..8af05746dd5 --- /dev/null +++ b/ivette/src/dome/renderer/help.tsx @@ -0,0 +1,122 @@ +/* ************************************************************************ */ +/* */ +/* This file is part of Frama-C. */ +/* */ +/* Copyright (C) 2007-2024 */ +/* CEA (Commissariat à l'énergie atomique et aux énergies */ +/* alternatives) */ +/* */ +/* you can redistribute it and/or modify it under the terms of the GNU */ +/* Lesser General Public License as published by the Free Software */ +/* Foundation, version 2.1. */ +/* */ +/* It is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU Lesser General Public License for more details. */ +/* */ +/* See the GNU Lesser General Public License version 2.1 */ +/* for more details (enclosed in the file licenses/LGPLv2.1). */ +/* */ +/* ************************************************************************ */ + +/** + @packageDocumentation + @module dome/help + */ + +import React from 'react'; +import { classes } from 'dome/misc/utils'; +import { IconButton, IconButtonKind } from './controls/buttons'; +import { Modal, showModal, ModalProps } from './dialogs'; +import { iconTag, Markdown, Pattern } from './text/markdown'; + +/* --------------------------------------------------------------------------*/ +/* --- Panel List */ +/* --------------------------------------------------------------------------*/ +interface HelpMarkdownProps { + /** classes for Doc component */ + className?: string; + /** Tab of patterns */ + patterns?: Pattern[]; + /** + * scroll to title h1 or h2 when component is render. + * The value must be the id of the balise html. + * Id is calculate by title.toLowerCase().replaceAll(' ','-') + * where title is the content of h1 or h2 if it is a string + */ + initialScrollTo?: string; + /** Markdown content. */ + children?: string; +} + +export function HelpMarkdown(props: HelpMarkdownProps): JSX.Element { + const { patterns = [iconTag], className, initialScrollTo, children } = props; + const classNames = classes('dome-xHelp', className); + + const scrollableDivRef = React.useRef<HTMLDivElement>(null); + const anchorsRef = React.useRef<{ + [key: string] : HTMLHeadingElement | null + }>({}); + + const scrollToAnchor = (id: string): void => { + const scrollableDiv = scrollableDivRef.current; + const anchor = anchorsRef.current[id]; + const top = scrollableDiv?.offsetTop || 0; + + if (scrollableDiv && anchor) { + const anchorPosition = anchor.offsetTop - top; + scrollableDiv.scrollTo({ + top: anchorPosition, + behavior: 'smooth', + }); + } + }; + + React.useEffect(() => { + if(initialScrollTo) scrollToAnchor(initialScrollTo); + }, [initialScrollTo]); + + return ( + <div ref={scrollableDivRef} className={classNames}> + <Markdown + patterns={patterns || [iconTag]} + anchorsRef={anchorsRef} + >{ children }</Markdown> + </div> + ); +} + +interface IconModalMdProps extends HelpMarkdownProps { + /** Icon props */ + kind?: IconButtonKind; + title?: string; + size?: number; + /** Properties of Modal component */ + modal: Omit<ModalProps, 'children'>; +} + +export function IconHelpModalMd(props: IconModalMdProps): JSX.Element { + const { title, kind, size, + patterns, initialScrollTo, + modal, children + } = props; + + return ( + <IconButton + icon='HELP' + className='dome-xDoc-icon' + title={title} + kind={kind} + size={size} + onClick={() => showModal( + <Modal {...modal} > + <HelpMarkdown + patterns={patterns} + initialScrollTo={initialScrollTo} + >{ children }</HelpMarkdown> + </Modal>) + } + /> + ); +} diff --git a/ivette/src/dome/renderer/style.css b/ivette/src/dome/renderer/style.css index f0d5e43fdb9..991a903e8b3 100644 --- a/ivette/src/dome/renderer/style.css +++ b/ivette/src/dome/renderer/style.css @@ -225,45 +225,55 @@ input[type="checkbox"]:checked { border-radius: 10px; background-color: var(--background); max-width: calc(100% - 100px); - max-height: calc(100% - 50px); - overflow: hidden; + max-height: calc(100vh - 55px); + overflow-y: hidden; - .dome-xModal-header { + .dome-xModal-content { display: flex; - justify-content: space-between; - background-color: var(--background-profound); - padding: 5px; - font-size: medium; - align-items: center; - - .dome-xModal-title { - margin-right: 20px; - - .dome-xIcon { - padding-right: .3em; - - svg { - height: 16px; + flex-direction: column; + max-height: 100%; + overflow-y: hidden; + + .dome-xModal-header { + display: flex; + justify-content: space-between; + background-color: var(--background-profound); + padding: 5px; + font-size: medium; + align-items: center; + + .dome-xModal-title { + margin-right: 20px; + + .dome-xIcon { + padding-right: .3em; + + svg { + height: 16px; + } } } - } - &>.dome-xIcon:hover { - cursor: pointer; + &>.dome-xIcon:hover { + cursor: pointer; + } } - } - .dome-xModal-body { - background-color: var(--background); + .dome-xModal-body { + flex:1; + max-height: 100%; + overflow-y: hidden; + background-color: var(--background); - .dome-xModal-waiting { - display: flex; - justify-content: center; - align-items: center; - padding: 20px; + .dome-xModal-waiting { + display: flex; + justify-content: center; + align-items: center; + padding: 20px; - svg { bottom: 0 !important; } - } + svg { bottom: 0 !important; } + } + } } } @@ -276,4 +286,15 @@ input[type="checkbox"]:checked { bottom: 0; background-color: rgba(50, 50, 50, .4); } + +/* -------------------------------------------------------------------------- */ +/* --- Doc --- */ +/* -------------------------------------------------------------------------- */ + +.dome-xHelp { + max-height: 100%; + overflow-y: auto; + padding: 10px; +} + /* -------------------------------------------------------------------------- */ diff --git a/ivette/src/dome/renderer/text/markdown.tsx b/ivette/src/dome/renderer/text/markdown.tsx index ca97fcc4159..3151d7cc919 100644 --- a/ivette/src/dome/renderer/text/markdown.tsx +++ b/ivette/src/dome/renderer/text/markdown.tsx @@ -23,18 +23,24 @@ import React from 'react'; import ReactMarkdown, { Options } from 'react-markdown'; +import * as Themes from 'dome/themes'; import { classes } from 'dome/misc/utils'; import { Icon } from 'dome/controls/icons'; - +import { + CodeBlock, atomOneDark, atomOneLight +} from "react-code-blocks"; export interface Pattern { pattern: RegExp, replace: (key: number, match?: RegExpExecArray) => JSX.Element | null } export const iconTag: Pattern = { - pattern: /\[icon-([^\]]+)\]/g, + pattern: /(\[ex:\])?\[icon-([^\]]+)\]/g, replace: (key: number, match?: RegExpExecArray) => { - return match ? <Icon key={key} id={match[1]}/> : null; + if(match && match[1] === "[ex:]") { + return <span key={key}>{`[icon-${match[2]}]`}</span>; + } + return match ? <Icon key={key} id={match[2]}/> : null; } }; @@ -99,6 +105,9 @@ interface MarkdownProps { className?: string; /** Tab of patterns */ patterns?: Pattern[]; + /** Anchors ref */ + anchorsRef: React.MutableRefObject< + {[key: string] : HTMLHeadingElement | null}>; /** Children */ children?: string | null; } @@ -106,15 +115,51 @@ interface MarkdownProps { export function Markdown( props: MarkdownProps ): JSX.Element { - const { className, patterns, children } = props; + const { className, patterns, anchorsRef, children } = props; + const theme = Themes.useColorTheme()[0]; const markdownClasses = classes( "dome-xMarkdown", "dome-pages", className ); + /** + * If children is a string this function return the + * heading element with an id and save ref in anchorsRef + */ + const getHtmlTitle = ( + children: React.ReactNode, + tag: "h1" | "h2" = 'h1' + ): JSX.Element => { + const Tag = tag; + const id = typeof children === "string" ? + children.toLowerCase().replaceAll(' ', '-') : undefined; + + return id ? + <Tag id={id} ref={(el) => anchorsRef.current[id] = el}>{children}</Tag>: + <Tag>{children}</Tag>; + }; + const options: Options = { className: markdownClasses }; if(patterns && patterns.length > 0) options.components = { p: ({ children }) => <div>{replaceTags(children, patterns)}</div>, - li: ({ children }) => <li>{replaceTags(children, patterns)}</li> + li: ({ children }) => <li>{replaceTags(children, patterns)}</li>, + h1: ({ children }) => getHtmlTitle(children), + h2: ({ children }) => getHtmlTitle(children, 'h2'), + /** Uses codeBlock if ```` is used in markdown with a language, + * otherwise the code-inline class is added */ + code: ({ className, children }) => { + if (className && className.includes("language-") + && typeof children === "string" + ) { + const language = className.split("language-")[1]; + return <CodeBlock + text={children} + language={language} + showLineNumbers={false} + theme={theme === 'dark' ? atomOneDark : atomOneLight} + />; + } + return <code className='code-inline'>{children}</code>; + }, }; return <ReactMarkdown {...options}>{ children }</ReactMarkdown>; diff --git a/ivette/src/dome/renderer/text/style.css b/ivette/src/dome/renderer/text/style.css index b3a5e8e8d93..28aca401af6 100644 --- a/ivette/src/dome/renderer/text/style.css +++ b/ivette/src/dome/renderer/text/style.css @@ -75,6 +75,21 @@ } +.dome-pages a +{ + color: var(--text-highlighted); + font-weight: 600; + text-decoration: none; +} + +.dome-pages code { + &.code-inline { + background-color: var(--background-interaction); + padding: 1px 5px; + border-radius: 5px; + } +} + /* -------------------------------------------------------------------------- */ /* --- Styling CodeMirror 6 editor --- */ /* -------------------------------------------------------------------------- */ diff --git a/ivette/src/sandbox/help.tsx b/ivette/src/sandbox/help.tsx new file mode 100644 index 00000000000..7109452f478 --- /dev/null +++ b/ivette/src/sandbox/help.tsx @@ -0,0 +1,61 @@ +/* ************************************************************************ */ +/* */ +/* This file is part of Frama-C. */ +/* */ +/* Copyright (C) 2007-2024 */ +/* CEA (Commissariat à l'énergie atomique et aux énergies */ +/* alternatives) */ +/* */ +/* you can redistribute it and/or modify it under the terms of the GNU */ +/* Lesser General Public License as published by the Free Software */ +/* Foundation, version 2.1. */ +/* */ +/* It is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU Lesser General Public License for more details. */ +/* */ +/* See the GNU Lesser General Public License version 2.1 */ +/* for more details (enclosed in the file licenses/LGPLv2.1). */ +/* */ +/* ************************************************************************ */ + +/* -------------------------------------------------------------------------- */ +/* --- Sandbox Testing for Force Graph component --- */ +/* -------------------------------------------------------------------------- */ + +import React from 'react'; +import { registerSandbox, TitleBar } from 'ivette'; +import { HelpMarkdown, IconHelpModalMd } from 'dome/help'; +import docSandbox from './sandbox.md?raw'; + +// -------------------------------------------------------------------------- +// --- Main force graph component +// -------------------------------------------------------------------------- + +function SandboxHelp(): JSX.Element { + return ( + <> + <TitleBar> + <IconHelpModalMd + modal={{ label: 'docsandbox - Help' }} + initialScrollTo={'help'} + >{ docSandbox }</IconHelpModalMd> + </TitleBar> + <HelpMarkdown initialScrollTo={'help'}>{ docSandbox }</HelpMarkdown> + </> + ); +} + +/* -------------------------------------------------------------------------- */ +/* --- Sandbox --- */ +/* -------------------------------------------------------------------------- */ + +registerSandbox({ + id: 'sandbox.help', + label: 'Help', + preferredPosition: 'ABCD', + children: <SandboxHelp />, +}); + +// -------------------------------------------------------------------------- diff --git a/ivette/src/sandbox/panel.tsx b/ivette/src/sandbox/panel.tsx index bbae94cf524..80dea7996be 100644 --- a/ivette/src/sandbox/panel.tsx +++ b/ivette/src/sandbox/panel.tsx @@ -37,6 +37,8 @@ import { Icon } from 'dome/controls/icons'; import './style.css'; import { Label } from 'dome/controls/labels'; import { Modal, showModal } from 'dome/dialogs'; +import { IconHelpModalMd } from 'dome/help'; +import docSandbox from './sandbox.md?raw'; /* -------------------------------------------------------------------------- */ /* --- Use Panel --- */ @@ -75,12 +77,19 @@ function UsePanel(): JSX.Element { }) } /> - <IconButton icon="SIDEBAR" title={"show or hide the panel"} onClick={flipVisible} /> + <IconHelpModalMd + modal={{ + label: 'docsandbox - Panel' + }} + initialScrollTo={'panel'} + > + { docSandbox } + </IconHelpModalMd> </TitleBar> <div style={{ position: 'relative', height: '100%' }}> <Panel visible={visible} position={position}> diff --git a/ivette/src/sandbox/sandbox.md b/ivette/src/sandbox/sandbox.md new file mode 100644 index 00000000000..64e853f4bf9 --- /dev/null +++ b/ivette/src/sandbox/sandbox.md @@ -0,0 +1,124 @@ +# Sandbox + +The sandbox part of Ivette is only available in development mode. +It allows you to test new modules and discover a simplified form of the basic modules before using them. + +## Dot Diagram + +Documentation is not yet available for this module. + +## ForceGraph + +Documentation is not yet available for this module. + +## Icons + +Documentation is not yet available for this module. + +## Panel + +The Panel component allows the addition of a retractable panel to a positioned block. + +The panel can be displayed on any side of the block using the position prop, which defaults to the right. The visible prop allows hiding or showing the panel. + +### Props + ``` javascript + export type PanelPosition = 'top' | 'bottom' | 'left' | 'right'; + + interface PanelProps { + /** Additional class. */ + className?: string; + /** Position to displayed the panel. Default 'tr' */ + position?: PanelPosition; + /** Defaults to `true`. */ + visible?: boolean; + /** Defaults to `true`. */ + display?: boolean; + /** Panel children. */ + children: JSX.Element[]; +} +``` + + +## Qsplit + +Documentation is not yet available for this module. + +## Text + +Documentation is not yet available for this module. + +## UseDnd + +Documentation is not yet available for this module. + +## Help + +the documentation is written in [Markdown](#markdown). It must be in a `*.md` file, the raw content of which will be retrieved via an import. + +For example, for the documentation of a sandbox module +``` javascript +import docSandbox from './sandbox.md?raw'; +``` +Here, `?raw` is used to indicate that we want the raw content of the file. + +Typically, the documentation will be displayed in the application's modal. + +### help.tsx + +This file contains components that make it easier to display documentation in your components. + +#### HelpMarkdown + +This component is used to display the markdown help, it is used by `IconodalMd` and you can see an example of it out of modal in the `help` sandbox. +It takes the following props: +``` javascript +interface DocMarkdownProps { + /** classes for Doc component */ + className?: string; + /** Tab of patterns */ + patterns?: Pattern[]; + /** + * scroll to title h1 or h2 when component is render. + * The value must be the id of the balise html. + * Id is calculate by title.toLowerCase().replaceAll(' ','-') + * where title is the content of h1 or h2 if it is a string + */ + initialScrollTo?: string; + /** Markdown content. */ + children?: string; +} +``` + +#### IconModalMd + +Allows you to add a `HELP` icon ([icon-HELP]) which will open a modal window with the chosen document when clicked. + +``` javascript +interface IconModalMdProps extends DocMarkdownProps { + /** Icon props */ + kind?: IconButtonKind; + title?: string; + size?: number; + /** Properties of Modal component */ + modal: Omit<ModalProps, 'children'>; +} +``` + + +## Markdown + +TO BE COMPLETED + +### Pattern +You can used patterns to replace parts of the text by JSX Element. + +#### Icons + +There is one basic pattern to replace tags by an `Icon`, it name `iconTag` in markdown component . + +* [icon-TUNINGS] : [ex:][icon-TUNINGS] +* [icon-TARGET] : [ex:][icon-TARGET] +* [icon-PIN] : [ex:][icon-PIN] + +or inline [icon-TUNINGS], [icon-TARGET], [icon-PIN] -- GitLab