diff --git a/apps/web/index.html b/apps/web/index.html index 4aec42b95ecd04bab9e000f8755acdd13515ffa0..86f50622cd6a92f97da434bd3bd20a619588718a 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -1,30 +1,30 @@ <!doctype html> <html lang="en"> - <head> - <meta charset="utf-8" /> - <title>GraphPolaris</title> - <base href="/" /> - <meta name="description" content="GraphPolaris" /> +<head> + <meta charset="utf-8"/> + <title>GraphPolaris</title> + <base href="/"/> + <meta name="description" content="GraphPolaris"/> - <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> - <link rel="stylesheet" href="/favicon.ico" /> - <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> - <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> - <link rel="manifest" href="/site.webmanifest"> - <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#da532c"> - <meta name="msapplication-TileColor" content="#da532c"> - <meta name="theme-color" content="#ffffff"> + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="stylesheet" href="/favicon.ico"/> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="manifest" href="/site.webmanifest"> + <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#da532c"> + <meta name="msapplication-TileColor" content="#da532c"> + <meta name="theme-color" content="#ffffff"> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <link rel="preconnect" href="https://fonts.googleapis.com" /> - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> - <link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet" /> - </head> - <body> - <script> - globalThis.import_meta_env = JSON.parse('"import_meta_env_placeholder"'); - </script> - <div id="root" data-theme="graphpolaris"></div> - <script type="module" src="./src/main.tsx"></script> - </body> + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> + <link rel="preconnect" href="https://fonts.googleapis.com"/> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/> + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Roboto+Mono&display=swap" rel="stylesheet" /> +</head> +<body> +<script> + globalThis.import_meta_env = JSON.parse('"import_meta_env_placeholder"'); +</script> +<div id="root" data-theme="graphpolaris"></div> +<script type="module" src="./src/main.tsx"></script> +</body> </html> diff --git a/apps/web/src/app/app.module.scss b/apps/web/src/app/app.module.scss deleted file mode 100644 index 1dc1ca7dd87dd18d24b7dd61cf20512b52717888..0000000000000000000000000000000000000000 --- a/apps/web/src/app/app.module.scss +++ /dev/null @@ -1,44 +0,0 @@ -/* Your styles goes here. */ - -.mainContainer { - padding: 0.8rem; - gap: 0.7rem; - height: 100%; - display: flex; - flex-direction: row; - .schema { - width: 100%; - max-width: 33%; - height: 100%; - max-height: 100%; - h1 { - margin-left: 0.8em; - } - } - .panel { - display: flex; - gap: 0.7rem; - width: 100%; - max-width: 67%; - flex-direction: column; - height: 100%; - max-height: 100%; - .visualization { - height: 100%; - max-height: 70%; - overflow-y: clip; - h1 { - margin-left: 0.8em; - } - } - .queryBuilder { - height: 100%; - max-height: 30%; - overflow-y: clip; - h1 { - margin-top: 0.25em; - margin-left: 0.8em; - } - } - } -} diff --git a/apps/web/src/app/app.module.scss.d.ts b/apps/web/src/app/app.module.scss.d.ts deleted file mode 100644 index 6ff3621ba9b235bdb473345835b9e856c566da72..0000000000000000000000000000000000000000 --- a/apps/web/src/app/app.module.scss.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare const classNames: { - readonly mainContainer: 'mainContainer'; - readonly schema: 'schema'; - readonly panel: 'panel'; - readonly visualization: 'visualization'; - readonly queryBuilder: 'queryBuilder'; -}; -export = classNames; diff --git a/apps/web/src/app/app.spec.tsx b/apps/web/src/app/app.spec.tsx index 607e29ae595ee6909666bd361968ba46f25f2d10..5bb3582e53fe970127c7f5afe81a31ff47779024 100644 --- a/apps/web/src/app/app.spec.tsx +++ b/apps/web/src/app/app.spec.tsx @@ -5,14 +5,6 @@ import App from './app'; describe('App', () => { it('should render successfully', () => { - //const { baseElement } = render(<App />); - expect(true).toBeTruthy(); }); - - // it('should have a greeting as the title', () => { - // const { getByText } = render(<App />); - - // expect(getByText(/Welcome graphpolaris/gi)).toBeTruthy(); - // }); }); diff --git a/apps/web/src/app/app.stories.tsx b/apps/web/src/app/app.stories.tsx index cb1816ebeb3d5431944a104e308cd98237dbecc0..74396173e9309fc860934f456523812ff68f8220 100644 --- a/apps/web/src/app/app.stories.tsx +++ b/apps/web/src/app/app.stories.tsx @@ -1,19 +1,27 @@ import React from 'react'; -import { Meta, Story } from '@storybook/react'; +import { Meta } from '@storybook/react'; import { App } from './app'; import { Provider } from 'react-redux'; import { store } from '@graphpolaris/shared/lib/data-access/store'; +import { Route, BrowserRouter as Router, Routes } from 'react-router-dom'; export default { component: App, title: 'App', decorators: [ // using the real store here - (story) => <Provider store={store}>{story()}</Provider>, + (story) => ( + <Provider store={store}> + <Router> + <Routes> + <Route path="/" element={story()}></Route> + </Routes> + </Router> + </Provider> + ), ], -} as Meta; +} as Meta<typeof App>; -const Template: Story = (args) => <App {...args} />; - -export const Primary = Template.bind({}); -Primary.args = {}; +export const Primary = { + args: {}, +}; diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx index c66a944f1e48676d7906097345d1a9dd16554859..0969d61a0da4f451adb633e9baf5eba019669268 100644 --- a/apps/web/src/app/app.tsx +++ b/apps/web/src/app/app.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { readInSchemaFromBackend, useAuth, @@ -21,18 +21,14 @@ import { useQuerybuilderSettings, } from '@graphpolaris/shared/lib/data-access/store'; import { - GraphQueryResultFromBackend, GraphQueryResultFromBackendPayload, resetGraphQueryResults, queryingBackend, } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; import { Query2BackendQuery, QueryBuilder, QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder'; import { Schema } from '@graphpolaris/shared/lib/schema/panel'; -import { useCallback, useEffect, useRef, useState } from 'react'; import { Navbar } from '../components/navbar/navbar'; import { VisualizationPanel } from './panels/Visualization'; -import styles from './app.module.scss'; -import { logout } from '@graphpolaris/shared/lib/data-access/store/authSlice'; import { SchemaFromBackend } from '@graphpolaris/shared/lib/schema'; import { LinkPredictionInstance, setMLResult, allMLTypes } from '@graphpolaris/shared/lib/data-access/store/mlSlice'; import { Resizable } from '@graphpolaris/shared/lib/components/Resizable'; @@ -153,41 +149,6 @@ export function App(props: App) { </main> </div> </div> - {/*<GridLayout*/} - {/* className="layout"*/} - {/* cols={10}*/} - {/* rowHeight={30}*/} - {/* width={window.innerWidth}*/} - {/*>*/} - {/* <div*/} - {/* key="schema-panel"*/} - {/* data-grid={{ x: 0, y: 0, w: 3, h: 30, maxW: 5, isDraggable: false }}*/} - {/* >*/} - {/* <Panel content="Schema Panel" color="red">*/} - {/* <Schema />*/} - {/* </Panel>*/} - {/* </div>*/} - {/* <div*/} - {/* key="query-panel"*/} - {/* data-grid={{ x: 3, y: 20, w: 5, h: 10, maxH: 20, isDraggable: false }}*/} - {/* >*/} - {/* <Panel content="Query Panel" color="blue">*/} - {/* <QueryBuilder />*/} - {/* </Panel>*/} - {/* </div>*/} - {/* <div*/} - {/* key="visualisation-panel"*/} - {/* data-grid={{ x: 3, y: 0, w: 7, h: 20, isDraggable: false }}*/} - {/* >*/} - {/* <VisualizationPanel />*/} - {/* </div>*/} - {/* <div*/} - {/* key="history-panel"*/} - {/* data-grid={{ x: 8, y: 20, w: 2, h: 10, isDraggable: false }}*/} - {/* >*/} - {/* <Panel content="History Channel" color="purple"></Panel>*/} - {/* </div>*/} - {/*</GridLayout>*/} </div> ); } diff --git a/apps/web/src/app/panels/Visualization.tsx b/apps/web/src/app/panels/Visualization.tsx index da715a03e3e1566b114db1b140d746223945af3a..883093ed90b97f5166ddd774d327ea844de0c2ae 100644 --- a/apps/web/src/app/panels/Visualization.tsx +++ b/apps/web/src/app/panels/Visualization.tsx @@ -1,21 +1,36 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { RawJSONVis, NodeLinkVis, PaohVis, SemanticSubstrates, TableVis } from '@graphpolaris/shared/lib/vis'; import { useAppDispatch, useGraphQueryResult, useQuerybuilderGraph, useVisualizationState } from '@graphpolaris/shared/lib/data-access'; import { LoadingSpinner } from '@graphpolaris/shared/lib/components/LoadingSpinner'; import { Visualizations, setActiveVisualization } from '@graphpolaris/shared/lib/data-access/store/visualizationSlice'; -import { ArrowDropDown } from '@mui/icons-material'; +import { DropdownItem, DropdownItemContainer, MenuDropdown } from '@graphpolaris/shared/lib/components/dropdowns'; +import ControlContainer from '@graphpolaris/shared/lib/components/controls'; +import { Button } from '@graphpolaris/shared/lib/components/buttons'; + +type VisOptionFunction = () => { + payload: Visualizations; + type: 'visualization/setActiveVisualization'; +}; export const VisualizationPanel = () => { const vis = useVisualizationState(); const graphQueryResult = useGraphQueryResult(); const query = useQuerybuilderGraph(); const dispatch = useAppDispatch(); + const [visDropdownOpen, setVisDropdownOpen] = useState<boolean>(false); + + const visOptions: Record<string, VisOptionFunction> = { + Table: () => dispatch(setActiveVisualization(Visualizations.Table)), + NodeLink: () => dispatch(setActiveVisualization(Visualizations.NodeLink)), + PaohVis: () => dispatch(setActiveVisualization(Visualizations.Paohvis)), + 'JSON Structure': () => dispatch(setActiveVisualization(Visualizations.RawJSON)), + }; const visualizationComponent = useMemo(() => { switch (vis.activeVisualization) { case Visualizations.Table: return ( - <div id={Visualizations.Table} className="tabContent w-full h-full"> + <div id={Visualizations.Table} className="tabContent w-full h-full py-8"> <TableVis showBarplot={true} /> </div> ); @@ -44,47 +59,49 @@ export const VisualizationPanel = () => { return ( <div className="vis-panel h-full w-full overflow-y-auto" style={graphQueryResult.nodes.length === 0 ? { overflow: 'hidden' } : {}}> - <h1> - <span className="mr-2">Visualization Panel</span> - <div className="dropdown"> - <label - tabIndex={0} - className="text-sm s-1 bg-slate-100 hover:bg-slate-200 transition-colors duration-300 rounded cursor-pointer px-2 py-1" - > - {vis.activeVisualization} - <ArrowDropDown /> - </label> - <ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> - <li onClick={() => dispatch(setActiveVisualization(Visualizations.Table))}> - <a>Table</a> - </li> - <li onClick={() => dispatch(setActiveVisualization(Visualizations.NodeLink))}> - <a>Node Link</a> - </li> - <li onClick={() => dispatch(setActiveVisualization(Visualizations.Paohvis))}> - <a>PaohVis</a> - </li> - <li onClick={() => dispatch(setActiveVisualization(Visualizations.RawJSON))}> - <a>JSON Structure</a> - </li> - </ul> - </div> - </h1> - <div className="h-[calc(100%-2rem)]"> - {graphQueryResult.queryingBackend && ( - <div className="w-full h-full flex flex-col items-center justify-center overflow-hidden"> - <LoadingSpinner>Querying backend...</LoadingSpinner> - </div> - )} - {!graphQueryResult.queryingBackend && graphQueryResult.nodes.length === 0 ? ( - <div className="w-full h-full flex flex-col items-center justify-center"> - <p>No data available to be shown</p> - {query.nodes.length > 0 ? <p>Query resulted in empty dataset</p> : <p>Query for data to visualize</p>} - </div> - ) : ( - <div className="w-full h-full">{visualizationComponent}</div> - )} + <div className="sticky top-0 flex items-center justify-between z-[2] py-0 px-2 bg-secondary-100 border-b border-secondary-200"> + <h1 className="text-xs font-semibold text-secondary-800">{vis.activeVisualization} visualization</h1> + <ControlContainer> + <Button type="secondary" variant="ghost" size="xs" iconName="Settings" onClick={() => {}} /> + <Button + type="secondary" + variant="ghost" + size="xs" + iconName="Apps" + onClick={() => { + setVisDropdownOpen(!visDropdownOpen); + }} + /> + {visDropdownOpen && ( + <DropdownItemContainer align="top-6 right-6"> + {Object.keys(visOptions).map((key) => ( + <DropdownItem + key={key} + value={key} + onClick={() => { + visOptions[key](); + setVisDropdownOpen(false); + }} + /> + ))} + </DropdownItemContainer> + )} + </ControlContainer> </div> + + {graphQueryResult.queryingBackend && ( + <div className="w-full h-full flex flex-col items-center justify-center overflow-hidden"> + <LoadingSpinner>Querying backend...</LoadingSpinner> + </div> + )} + {!graphQueryResult.queryingBackend && graphQueryResult.nodes.length === 0 ? ( + <div className="w-full h-full flex flex-col items-center justify-center"> + <p>No data available to be shown</p> + {query.nodes.length > 0 ? <p>Query resulted in empty dataset</p> : <p>Query for data to visualize</p>} + </div> + ) : ( + <div className="w-full h-full">{visualizationComponent}</div> + )} </div> ); }; diff --git a/apps/web/src/components/navbar/DatabaseManagement/DatabaseSelector.tsx b/apps/web/src/components/navbar/DatabaseManagement/DatabaseSelector.tsx index 02c665e2cc02836a068b9f97da6f383dc7b36e12..47016885ba40132f90b13a2ff95c7a16dc62321d 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/DatabaseSelector.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/DatabaseSelector.tsx @@ -1,11 +1,12 @@ import React, { useEffect, useState } from 'react'; -import { Add, ArrowDropDown, Delete, Settings } from '@mui/icons-material'; +import { Add, Delete, Settings } from '@mui/icons-material'; import { DatabaseInfo, useAppDispatch, useDatabaseAPI, useSchemaGraph, useSessionCache } from '@graphpolaris/shared/lib/data-access'; import { updateCurrentDatabase } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; import { SettingsForm } from './forms/settings'; import { NewDatabaseForm } from './forms/AddDatabase/newdatabase'; import { LoadingSpinner } from '@graphpolaris/shared/lib/components/LoadingSpinner'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; +import { DropdownButton, DropdownContainer, DropdownItemContainer } from '@graphpolaris/shared/lib/components/dropdowns'; import { clearQB } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { clearSchema } from '@graphpolaris/shared/lib/data-access/store/schemaSlice'; @@ -51,9 +52,7 @@ export default function DatabaseSelector({}) { } return () => { - if (timeoutId) { - clearTimeout(timeoutId); - } + if (timeoutId) clearTimeout(timeoutId); }; }, [connecting]); @@ -72,56 +71,47 @@ export default function DatabaseSelector({}) { setAddDatabaseFormOpen(false); }} /> - <div - className="relative flex-shrink max-md:flex-grow border w-full xl:w-[30rem] min-w-0 max-h-full ml-auto mr-auto cursor-pointer" - ref={dbSelectionMenuRef} - > - <div - className="flex w-full pl-4 py-2 pr-7 hover:bg-slate-200 transition-colors duration-300" + <DropdownContainer ref={dbSelectionMenuRef} className="w-[20rem]"> + <DropdownButton + title={ + <div className="flex items-center"> + {connecting ? ( + <> + <LoadingSpinner /> + <p className="ml-2 truncate">Connecting to {session.currentDatabase}</p> + </> + ) : session.currentDatabase ? ( + <> + <div className="h-2 w-2 rounded-full bg-success-500" /> + <p className="ml-2 truncate">Connected DB: {session.currentDatabase}</p> + </> + ) : session.databases === undefined ? ( + <> + <LoadingSpinner /> + <p className="ml-2">Retrieving databases</p> + </> + ) : session.databases?.length === 0 ? ( + <> + <p className="ml-2">Add your first Database</p> + </> + ) : ( + <> + <div className="h-2 w-2 rounded-full bg-secondary-500" /> + <p className="ml-2">Select a database</p> + </> + )} + </div> + } onClick={() => { if (session.databases?.length === 0) setAddDatabaseFormOpen(true); else setDbSelectionMenuOpen(!dbSelectionMenuOpen); }} - > - <div className="flex items-center w-full shrink-0"> - {connecting ? ( - <> - <LoadingSpinner /> - <p className="ml-2 truncate"> - <span className="max-md:hidden">Connecting to </span> - {session.currentDatabase} - </p> - </> - ) : session.currentDatabase ? ( - <> - <div className="h-[8px] w-[8px] shrink-0 rounded-full bg-green-500" /> - <p className="ml-2 truncate"> - <span className="max-md:hidden">Connected DB: </span> - {session.currentDatabase} - </p> - </> - ) : session.databases === undefined ? ( - <> - <LoadingSpinner /> - <p className="ml-2">Retrieving databases</p> - </> - ) : session.databases?.length === 0 ? ( - <> - <p className="ml-2">Add your first Database</p> - </> - ) : ( - <> - <div className="h-2 w-2 rounded-full bg-slate-500" /> - <p className="ml-2">Select a database</p> - </> - )} - </div> - <ArrowDropDown /> - </div> - {dbSelectionMenuOpen && session.databases && ( - <div className="absolute w-full top-11 z-50 bg-slate-100 border"> - <div - className="flex items-center p-2 hover:bg-slate-200" + /> + + {dbSelectionMenuOpen && session.databases !== undefined && ( + <DropdownItemContainer align="top-10 w-full"> + <li + className="flex items-center p-2 hover:bg-secondary-50 cursor-pointer" onClick={(e) => { e.preventDefault(); setDbSelectionMenuOpen(false); @@ -141,11 +131,11 @@ export default function DatabaseSelector({}) { <p className="ml-2">Add database</p> </> )} - </div> + </li> {session.databases.map((db) => ( - <div + <li key={db.Name} - className="flex justify-between items-center px-4 py-2 hover:bg-slate-200 gap-2" + className="flex justify-between items-center px-4 py-2 hover:bg-primary-100 gap-2 cursor-pointer" onClick={(e) => { if (db.Name !== session.currentDatabase) { e.preventDefault(); @@ -162,10 +152,10 @@ export default function DatabaseSelector({}) { onMouseLeave={() => setHovered(null)} title={`Connect to ${db.Name}`} > - <div className={`h-[8px] w-[8px] rounded-full shrink-0 ${db.status ? 'bg-green-500' : 'bg-red-500'}`} /> + <div className={`h-[8px] w-[8px] rounded-full shrink-0 ${db.status ? 'bg-success-500' : 'bg-danger-500'}`} /> <div className="w-full shrink min-w-0 flex flex-col"> <p className="truncate w-full shrink-0 min-w-0">{db.Name}</p> - <p className="text-xs text-slate-400 truncate w-fit shrink-0 min-w-0 max-w-full h-full border border-slate-300 rounded-sm p-0.5"> + <p className="bg-light text-2xs text-secondary-500 truncate w-fit shrink-0 min-w-0 max-w-full h-full border border-secondary-200 rounded-sm p-0.5"> {db.Protocol} {db.URL} </p> @@ -173,7 +163,7 @@ export default function DatabaseSelector({}) { {hovered === db.Name && ( <div className="flex items-center ml-2"> <div - className="text-slate-700 hover:text-slate-400 transition-colors duration-300" + className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300" onClick={(e) => { e.preventDefault(); e.stopPropagation(); @@ -184,7 +174,7 @@ export default function DatabaseSelector({}) { <Settings /> </div> <div - className="text-slate-700 hover:text-slate-400 transition-colors duration-300" + className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300" onClick={(e) => { e.preventDefault(); dispatch(updateCurrentDatabase(undefined)); @@ -198,11 +188,11 @@ export default function DatabaseSelector({}) { </div> </div> )} - </div> + </li> ))} - </div> + </DropdownItemContainer> )} - </div> + </DropdownContainer> </> ); } diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/AddDatabase/newdatabase.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/AddDatabase/newdatabase.tsx index 67105ecc5cf540b8a311652b42aa6ee41f8fada0..ba97cb8333af8d6526aeb038a61dce7adf892774 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/AddDatabase/newdatabase.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/AddDatabase/newdatabase.tsx @@ -1,3 +1,4 @@ +import React, { useEffect, useRef, useState } from 'react'; import { AddDatabaseRequest, DatabaseType, @@ -7,12 +8,12 @@ import { useDatabaseAPI, useSchemaAPI, } from '@graphpolaris/shared/lib/data-access'; -import React, { useEffect, useRef, useState } from 'react'; -import { RequiredInput } from '@graphpolaris/shared/lib/components/forms/requiredinput'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; -import { ArrowBack, ErrorOutline } from '@mui/icons-material'; +import { ErrorOutline } from '@mui/icons-material'; import { mockDatabases } from './mockDatabases'; import { Dialog } from '@graphpolaris/shared/lib/components/Dialog'; +import { Button } from '@graphpolaris/shared/lib/components/buttons'; +import Input from '@graphpolaris/shared/lib/components/inputs'; const INITIAL_DB_STATE = { username: 'neo4j', @@ -148,13 +149,10 @@ export const NewDatabaseForm = (props: { onClose(): void; open: boolean }) => { return ( <Dialog open={props.open} onClose={props.onClose} className="lg:min-w-[50rem] "> <div className="flex justify-between align-center"> - <h1 className="card-title">New Database</h1> + <h1 className="text-xl font-bold">New Database</h1> <div> {sampleData ? ( - <button className="btn" onClick={() => setSampleData(false)}> - <ArrowBack /> - Go back - </button> + <Button variant="outline" label="Go back" onClick={() => setSampleData(false)} /> ) : ( <> <h1 className="font-light text-xs">No data?</h1> @@ -171,175 +169,166 @@ export const NewDatabaseForm = (props: { onClose(): void; open: boolean }) => { {mockDatabases.map((sample) => ( <div key={sample.name} - className="card hover:bg-base-100 cursor-pointer mb-2 border w-[15rem]" + className="card hover:bg-secondary-100 cursor-pointer mb-2 border w-[15rem]" onClick={() => loadMockDatabase(sample)} > <div className="card-body"> <h2 className="card-title">{sample.name}</h2> - <p className="font-light text-slate-400">{sample.subtitle}</p> + <p className="font-light text-secondary-400">{sample.subtitle}</p> </div> </div> ))} </div> ) : ( <> - <RequiredInput - errorText="This field is required" + <Input + type="text" label="Name of the database" - placeHolder="neo4j" value={state.name} + placeholder="neo4j" + required + errorText="This field is required" validate={(v) => { setHasError({ ...hasError, name: v.length === 0 }); return v.length > 0; }} - onChange={(value) => handleInputChange('name', value)} - type="text" + onChange={(value: string) => handleInputChange('name', value)} /> - <RequiredInput - errorText="This field is required" + <Input + type="text" label="Internal database name" - placeHolder="internal_database_name" value={state.internal_database_name} + placeholder="internal_database_name" + required + errorText="This field is required" validate={(v) => { setHasError({ ...hasError, internal_database_name: v.length === 0 }); return v.length > 0; }} - onChange={(value) => handleInputChange('internal_database_name', value)} - type="internalDatabaseName" + onChange={(value: string) => handleInputChange('internal_database_name', value)} /> <div className="flex w-full gap-2"> - <div className="w-full"> - <label className="label"> - <span className="label-text">Database Type</span> - </label> - <select - className="select select-bordered w-full max-w-xs" - value={databaseNameMapping[state.type]} - onChange={(event) => { - setState({ - ...state, - type: databaseNameMapping.indexOf(event.currentTarget.value), - }); - }} - > - {databaseNameMapping.map((dbName) => ( - <option value={dbName} key={dbName}> - {dbName} - </option> - ))} - </select> - </div> - <div className="w-full"> - <label className="label"> - <span className="label-text">Database Protocol</span> - </label> - <select - className="select select-bordered w-full max-w-xs" - value={state.protocol} - onChange={(event) => { - setState({ - ...state, - protocol: event.currentTarget.value, - }); - }} - > - {databaseProtocolMapping.map((protocol) => ( - <option value={protocol} key={protocol}> - {protocol} - </option> - ))} - </select> - </div> + <Input + type="dropdown" + label="Database Type" + required + value={databaseNameMapping[state.type]} + options={databaseNameMapping} + onChange={(value: string) => { + setState({ + ...state, + type: databaseNameMapping.indexOf(value), + }); + }} + /> + + <Input + type="dropdown" + label="Database Protocol" + required + value={state.protocol} + options={databaseProtocolMapping} + onChange={(value: string) => { + setState({ + ...state, + protocol: value, + }); + }} + /> </div> <div className="flex w-full gap-2"> - <RequiredInput - errorText="This field is required" + <Input + type="text" label="Hostname/IP" - placeHolder="neo4j" value={state.url} + placeholder="neo4j" + required + errorText="This field is required" validate={(v) => { setHasError({ ...hasError, url: v.length === 0 }); return v.length > 0; }} - onChange={(value) => handleInputChange('url', value)} - type="hostname" + onChange={(value: string) => handleInputChange('url', value)} /> - <RequiredInput - errorText="Must be between 1 and 9999" + <Input + type="text" label="Port" - placeHolder="neo4j" - value={state.port} + value={state.port.toString()} + placeholder="neo4j" + required + errorText="Must be between 1 and 9999" validate={(v) => { setHasError({ ...hasError, port: !(v <= 9999 && v > 0) }); return v <= 9999 && v > 0; }} - onChange={(value) => handlePortChanged(value)} - type="port" + onChange={(value: string) => handlePortChanged(value)} /> </div> <div className="flex w-full gap-2"> - <RequiredInput - errorText="This field is required" + <Input + type="text" label="Username" - placeHolder="username" value={state.username} + placeholder="username" + required + errorText="This field is required" validate={(v) => { setHasError({ ...hasError, username: v.length === 0 }); return v.length > 0; }} - onChange={(value) => handleInputChange('username', value)} - type="text" + onChange={(value: string) => handleInputChange('username', value)} /> - <RequiredInput - errorText="This field is required" + + <Input + type="text" label="Password" - placeHolder="password" value={state.password} + placeholder="password" + required + visible={false} + errorText="This field is required" validate={(v) => { setHasError({ ...hasError, password: v.length === 0 }); return v.length > 0; }} - onChange={(value) => handleInputChange('password', value)} - type="password" + onChange={(value: string) => handleInputChange('password', value)} /> </div> {!(connection.status === null) && ( <div className={`flex flex-col justify-center items-center`}> <div className="flex justify-center items-center"> - {connection.verified === false && <ErrorOutline className="text-slate-400" />} - <p className="font-light text-sm text-slate-400 ">{connection.status}</p> + {connection.verified === false && <ErrorOutline className="text-secondary-400" />} + <p className="font-light text-sm text-secondary-400 ">{connection.status}</p> </div> {connection.verified === null && <progress className="progress w-56"></progress>} </div> )} - <div className="card-actions w-full justify-center items-center"> - <button - className={`btn btn-primary ${Object.values(hasError).some((e) => e === true) ? 'btn-disabled' : ''}`} - type="button" - disabled={connection.connecting || Object.values(hasError).some((e) => e === true)} + <div className="grid gap-2"> + <Button + type="primary" + block + label={connection.connecting ? 'Connecting...' : 'Connect'} onClick={(event) => { event.preventDefault(); testDatabaseConnection(); }} - > - {connection.connecting ? 'Connecting...' : 'Connect'} - </button> - - <button - className="btn btn-outline" - onClick={(e) => { - e.preventDefault(); + disabled={connection.connecting || Object.values(hasError).some((e) => e === true)} + /> + <Button + variant="outline" + block + label="Cancel" + onClick={(event) => { + event.preventDefault(); closeDialog(); }} - > - Cancel - </button> + /> </div> </> )} diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx index 177dce55cf54e603df9d60cd4797714eb1858066..109aa2953249eb2be4159a3270027d784be9f1cc 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx @@ -7,10 +7,11 @@ import { useDatabaseAPI, DatabaseInfo, } from '@graphpolaris/shared/lib/data-access'; -import { RequiredInput } from '@graphpolaris/shared/lib/components/forms/requiredinput'; import { ErrorOutline } from '@mui/icons-material'; import { Dialog } from '@graphpolaris/shared/lib/components/Dialog'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; +import { Button } from '@graphpolaris/shared/lib/components/buttons'; +import Input from '@graphpolaris/shared/lib/components/inputs'; interface Connection { updating: boolean; @@ -135,161 +136,145 @@ export const SettingsForm = (props: { onClose(): void; open: boolean; database: return ( <Dialog open={props.open} onClose={props.onClose}> <div className="flex justify-between align-center"> - <h1 className="card-title">Update Credentials for {state.name} Database</h1> + <h1 className="card-title">Update {state.name} Database</h1> </div> <> - <div className="form-control w-full "> - <label className="label"> - <span className="label-text">Name of database</span> - </label> - <input type="text" className={`input input-bordered w-full`} value={state.name} disabled={true} /> - </div> + <Input type="text" label="Name of database" value={state.name} onChange={() => {}} disabled /> - <RequiredInput - errorText="This field is required" + <Input + type="text" label="Internal database name" - placeHolder="internal_database_name" value={state.internal_database_name} + placeholder="internal_database_name" + required + errorText="This field is required" validate={(v) => { setHasError({ ...hasError, internal_database_name: v.length === 0 }); return v.length > 0; }} - onChange={(value) => handleInputChange('internal_database_name', value)} - type="text" + onChange={(value: string) => handleInputChange('internal_database_name', value)} /> <div className="flex w-full gap-2"> - <div className="w-full"> - <label className="label"> - <span className="label-text">Database Type</span> - </label> - <select - className="select select-bordered w-full max-w-xs" - value={databaseNameMapping[state.type]} - onChange={(event) => { - setState({ - ...state, - type: databaseNameMapping.indexOf(event.currentTarget.value), - }); - }} - > - {databaseNameMapping.map((dbName) => ( - <option value={dbName} key={dbName}> - {dbName} - </option> - ))} - </select> - </div> - <div className="w-full"> - <label className="label"> - <span className="label-text">Database Protocol</span> - </label> - <select - className="select select-bordered w-full max-w-xs" - value={state.protocol} - onChange={(event) => { - setState({ - ...state, - protocol: event.currentTarget.value, - }); - }} - > - {databaseProtocolMapping.map((protocol) => ( - <option value={protocol} key={protocol}> - {protocol} - </option> - ))} - </select> - </div> + <Input + type="dropdown" + label="Database Type" + required + value={databaseNameMapping[state.type]} + options={databaseNameMapping} + onChange={(value: string) => { + setState({ + ...state, + type: databaseNameMapping.indexOf(value), + }); + }} + /> + + <Input + type="dropdown" + label="Database Protocol" + required + value={state.protocol} + options={databaseProtocolMapping} + onChange={(value: string) => { + setState({ + ...state, + protocol: value, + }); + }} + /> </div> <div className="flex w-full gap-2"> - <RequiredInput - errorText="This field is required" + <Input + type="text" label="Hostname/IP" - placeHolder="neo4j" value={state.url} + placeholder="neo4j" + required + errorText="This field is required" validate={(v) => { setHasError({ ...hasError, url: v.length === 0 }); return v.length > 0; }} - onChange={(value) => handleInputChange('url', value)} - type="hostname" + onChange={(value: string) => handleInputChange('url', value)} /> - <RequiredInput - errorText="Must be between 1 and 9999" + <Input + type="text" label="Port" - placeHolder="neo4j" - value={state.port} + value={state.port.toString()} + placeholder="neo4j" + required + errorText="Must be between 1 and 9999" validate={(v) => { setHasError({ ...hasError, port: !(v <= 9999 && v > 0) }); return v <= 9999 && v > 0; }} - onChange={(value) => handlePortChanged(value)} - type="port" + onChange={(value: string) => handlePortChanged(value)} /> </div> <div className="flex w-full gap-2"> - <RequiredInput - errorText="This field is required" + <Input + type="text" label="Username" - placeHolder="username" value={state.username} + placeholder="username" + required + errorText="This field is required" validate={(v) => { setHasError({ ...hasError, username: v.length === 0 }); return v.length > 0; }} - onChange={(value) => handleInputChange('username', value)} - type="text" + onChange={(value: string) => handleInputChange('username', value)} /> - <RequiredInput - errorText="This field is required" + + <Input + type="text" label="Password" - placeHolder="password" value={state.password} + visible={false} + placeholder="password" + required + errorText="This field is required" validate={(v) => { setHasError({ ...hasError, password: v.length === 0 }); return v.length > 0; }} - onChange={(value) => handleInputChange('password', value)} - type="password" + onChange={(value: string) => handleInputChange('password', value)} /> </div> {!(connection.status === null) && ( <div className={`flex flex-col justify-center items-center`}> <div className="flex justify-center items-center"> - {connection.verified === false && <ErrorOutline className="text-slate-400" />} - <p className="font-light text-sm text-slate-400 ">{connection.status}</p> + {connection.verified === false && <ErrorOutline className="text-secondary-400" />} + <p className="font-light text-sm text-secondary-400 ">{connection.status}</p> </div> {connection.verified === null && <progress className="progress w-56"></progress>} </div> )} - <div className="card-actions w-full justify-center items-center"> - <button - className={`btn btn-primary ${Object.values(hasError).some((e) => e === true) ? 'btn-disabled' : ''}`} - type="button" - disabled={connection.updating || Object.values(hasError).some((e) => e === true)} + + <div className="grid md:grid-cols-2 gap-3 card-actions w-full justify-stretch items-center"> + <Button + type="primary" + label={connection.updating ? 'Updating...' : 'Update'} onClick={(event) => { event.preventDefault(); handleSubmit(); }} - > - {connection.updating ? 'Updating...' : 'Update'} - </button> - - <button - className="btn btn-outline" - onClick={(e) => { - e.preventDefault(); + disabled={connection.updating || Object.values(hasError).some((e) => e === true)} + /> + <Button + variant="outline" + label="Cancel" + onClick={(event) => { + event.preventDefault(); closeDialog(); }} - > - Cancel - </button> + /> </div> </> </Dialog> diff --git a/apps/web/src/components/navbar/MenuList.scss b/apps/web/src/components/navbar/MenuList.scss deleted file mode 100644 index af7f9f647753753c0c3077e7a1994ada4454fcbb..0000000000000000000000000000000000000000 --- a/apps/web/src/components/navbar/MenuList.scss +++ /dev/null @@ -1,17 +0,0 @@ -/* This file is dependent on src/data/domain/entity/customization/colours.tsx -* You can match a class name with a colour palette by naming the class 'menuList-' + colour palette name -*/ - -.menuList-default{ - .MuiPaper-root{ - background-color: #fffdfa; - color: #000000; - } -} - -.menuList-dark{ - .MuiPaper-root{ - background-color: #171721; - color: #ffffff; - } -} \ No newline at end of file diff --git a/apps/web/src/components/navbar/databasemenu.tsx b/apps/web/src/components/navbar/databasemenu.tsx index f5ae99103f0c85f0377e34f41ab437f35b064012..9f53a853a5e2f973dfed45b0d35a927b33c24d8e 100644 --- a/apps/web/src/components/navbar/databasemenu.tsx +++ b/apps/web/src/components/navbar/databasemenu.tsx @@ -5,7 +5,7 @@ export const DatabaseMenu = (props: { onClick: (database: string) => void }) => const session = useSessionCache(); return ( - <ul className="menu dropdown-content absolute right-48 z-[1] p-2 shadow-xl bg-offwhite-100 rounded-box w-52" tabIndex={0}> + <ul className="menu dropdown-content absolute right-48 z-[1] p-2 shadow-xl bg-secondary-50 rounded-box w-52" tabIndex={0}> {session.databases && session.databases.map((db: any) => ( <li key={db.Name}> diff --git a/apps/web/src/components/navbar/navbar.module.scss b/apps/web/src/components/navbar/navbar.module.scss deleted file mode 100644 index 6504cf40b1b320ad2cf1ce7b1feeb8cc807859ba..0000000000000000000000000000000000000000 --- a/apps/web/src/components/navbar/navbar.module.scss +++ /dev/null @@ -1,42 +0,0 @@ -// .root { -// display: flex; -// overflow: hidden; -// height: 50px; -// } - -// .appBar { -// display: flex; -// flex-direction: column; -// justify-content: center; -// height: 50px; -// } - -// .menubox { -// display: flex; -// flex-direction: row; -// justify-content: flex-end; -// width: 100%; -// } - -// .menuText { -// margin: 5px; -// color: #181520; -// font-weight: 400; -// font-size: 15px; -// align-self: flex-end; -// text-transform: none; -// } - -// .logo { -// margin: auto; -// display: block; -// } - -// .loginButton { -// margin: 6px; -// } - -// .loginButtonText { -// color: black; -// font-weight: bold; -// } diff --git a/apps/web/src/components/navbar/navbar.module.scss.d.ts b/apps/web/src/components/navbar/navbar.module.scss.d.ts index d23c443282e8089b21d5fdf602c91f05378dc088..5fc8829cc55093ac225af1acd319611c23d9ac9f 100644 --- a/apps/web/src/components/navbar/navbar.module.scss.d.ts +++ b/apps/web/src/components/navbar/navbar.module.scss.d.ts @@ -1,10 +1,2 @@ -declare const classNames: { - readonly root: 'root'; - readonly appBar: 'appBar'; - readonly menubox: 'menubox'; - readonly menuText: 'menuText'; - readonly logo: 'logo'; - readonly loginButton: 'loginButton'; - readonly loginButtonText: 'loginButtonText'; -}; +declare const classNames: {}; export = classNames; diff --git a/apps/web/src/components/navbar/navbar.tsx b/apps/web/src/components/navbar/navbar.tsx index d5ae5ac8e2c297c1799da520cf1ff447d65d3df4..b1302896ec7bc7e8cc471403ff8794ba39f9aab7 100644 --- a/apps/web/src/components/navbar/navbar.tsx +++ b/apps/web/src/components/navbar/navbar.tsx @@ -8,55 +8,39 @@ /* The comment above was added so the code coverage wouldn't count this file towards code coverage. * We do not test components/renderfunctions/styling files. * See testing plan for more details.*/ -import React, { useState } from 'react'; -import { AccountCircle } from '@mui/icons-material'; +import React, { useState, useRef, useEffect } from 'react'; import logo_white from './gp-logo-white.svg'; import logo from './gp-logo.svg'; - -import { updateCurrentDatabase } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; -import { useAppDispatch, useAuthorizationCache, useDatabaseAPI, useSessionCache } from '@graphpolaris/shared/lib/data-access'; -import { DatabaseMenu } from './databasemenu'; -import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; +import { useAuthorizationCache } from '@graphpolaris/shared/lib/data-access'; import { SearchBar } from './search/SearchBar'; import DatabaseSelector from './DatabaseManagement/DatabaseSelector'; +import { DropdownItem, DropdownItemContainer } from '@graphpolaris/shared/lib/components/dropdowns'; -export interface NavbarComponentProps { - // changeColourPalette: () => void; FIXME move to redux -} - -/** NavbarComponentState is an interface containing the type of visualizations. */ -export interface NavbarComponentState { - // The anchor for rendering the user data (userID, sessionID, add database, etc) menu - userMenuAnchor?: Element; - // The anchor for rendering the menu for selecting a database to use - selectDatabaseMenuAnchor?: Element; - deleteDatabaseMenuAnchor?: Element; - // Determines if the addDatabaseForm will be shown - showAddDatabaseForm: boolean; -} - -export interface NavbarSubComponentState { - changeDb?: String; - deleteDb?: String; -} - -/** NavbarComponent is the View implementation for Navbar */ -export const Navbar = (props: NavbarComponentProps) => { +export const Navbar = () => { + const dropdownRef = useRef<HTMLDivElement>(null); const auth = useAuthorizationCache(); - const session = useSessionCache(); - const api = useDatabaseAPI(); - const dispatch = useAppDispatch(); const [menuOpen, setMenuOpen] = useState(false); - const [subMenuOpen, setSubMenuOpen] = useState<string | undefined>(undefined); const currentLogo = !'dark' ? logo_white : logo; // TODO: support dark mode + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setMenuOpen(false); + } + }; + if (menuOpen) document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [menuOpen]); + const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION; return ( <div className="w-full h-auto px-5"> - <div title="GraphPolaris" className="navbar flex items-center justify-between w-auto gap-2 xl:gap-10"> - <a href="https://graphpolaris.com/" target="_blank" className="xl:w-fit w-[5rem] shrink-0"> + <div className="navbar flex items-center justify-between w-auto gap-2 xl:gap-10"> + <a href="https://graphpolaris.com/" target="_blank" className="w-[10rem] md:w-fit shrink-0"> <img src={currentLogo} alt="GraphPolaris" className="h-9" /> </a> @@ -65,70 +49,54 @@ export const Navbar = (props: NavbarComponentProps) => { <div> <SearchBar /> - <div className="w-fit"> - <div className="menu-walkthrough"> - <button - tabIndex={0} - className="btn btn-circle btn-ghost hover:bg-gray-200" - onClick={(event) => { - setMenuOpen(!menuOpen); - setSubMenuOpen(undefined); - }} - > - <AccountCircle htmlColor="black" /> - </button> - {menuOpen && ( - <> - <div - className="z-10 bg-transparent absolute w-screen h-screen top-0 left-0" - onClick={() => { - setMenuOpen(false); - setSubMenuOpen(undefined); - }} - ></div> - <ul - tabIndex={0} - className="z-20 dropdown-content menu absolute right-4 p-2 shadow-xl bg-offwhite-100 rounded-box max-w-52 w-52" - > - {auth.authorized ? ( - <div className="w-full"> - <div className="menu-title"> - <h2>user: {auth.username}</h2> - <h3 className="text-xs break-words">session: {auth.sessionID}</h3> - </div> - - <div className="menu-title"> - <div className="absolute left-0 h-0.5 w-full bg-offwhite-300"></div> - <h3 className="text-xs mt-3">Version: {buildInfo}</h3> - </div> - </div> - ) : ( - <div className="w-full"> - <div className="menu-title"> - <h2>user: {auth.username}</h2> - <h3 className="text-xs">session: {auth.sessionID}</h3> - </div> - <div> - <button - className="btn btn-ghost" - onClick={() => { - setMenuOpen(false); - }} - > - <span>Login</span> - {/* !TODO */} - </button> - </div> - <div className="menu-title"> - <div className="absolute left-0 h-0.5 w-full bg-offwhite-300"></div> - <h3 className="text-xs mt-3">Version: {buildInfo}</h3> - </div> - </div> - )} - </ul> - </> - )} + <div className="w-fit menu-walkthrough" ref={dropdownRef}> + <div + className="relative inline-flex items-center justify-center w-8 h-8 overflow-hidden bg-secondary-500 rounded hover:bg-secondary-600 transition-colors duration-150 ease-in-out cursor-pointer" + onClick={() => setMenuOpen(!menuOpen)} + > + <span className="font-medium text-light">{auth.username?.slice(0, 2).toUpperCase()}</span> </div> + + {menuOpen && ( + <DropdownItemContainer className="w-56" align="right-7"> + <div className="menu-title border-b"> + <h2>user: {auth.username}</h2> + <h3 className="text-xs break-words">session: {auth.sessionID}</h3> + </div> + + {auth.authorized ? ( + <> + <DropdownItem + value="Share" + submenu={ + <> + <DropdownItem value="Visual" onClick={() => {}} /> + <DropdownItem value="Knowledge base" onClick={() => {}} /> + </> + } + /> + <DropdownItem + value="Advanced" + submenu={ + <> + <DropdownItem value="" onClick={() => {}} /> + </> + } + /> + <DropdownItem value="Settings" onClick={() => {}} /> + <DropdownItem value="Log out" onClick={() => {}} /> + </> + ) : ( + <> + <DropdownItem value="Login" onClick={() => {}} /> + </> + )} + + <div className="menu-title border-t"> + <h3 className="text-xs">Version: {buildInfo}</h3> + </div> + </DropdownItemContainer> + )} </div> </div> </div> diff --git a/apps/web/src/components/navbar/search/SearchBar.tsx b/apps/web/src/components/navbar/search/SearchBar.tsx index cebe1c1fc4c1ed0e61929542ba4ae3d2f54542ce..ccd3b30ac3f31f2ed7447798cf1923e6b0c470ab 100644 --- a/apps/web/src/components/navbar/search/SearchBar.tsx +++ b/apps/web/src/components/navbar/search/SearchBar.tsx @@ -6,16 +6,17 @@ import { useQuerybuilderGraph, useSearchResult, AppDispatch, + useRecentSearches, } from '@graphpolaris/shared/lib/data-access'; import { filterData } from './similarity'; import { addSearchResultData, addSearchResultSchema, addSearchResultQueryBuilder, - resetSearchResults, CATEGORY_KEYS, + addRecentSearch, } from '@graphpolaris/shared/lib/data-access/store/searchResultSlice'; -import { Close, Search } from '@mui/icons-material'; +import Icon from '@graphpolaris/shared/lib/components/icon'; const SIMILARITY_THRESHOLD = 0.7; @@ -40,6 +41,7 @@ export function SearchBar({}) { const searchbarRef = React.useRef<HTMLDivElement>(null); const dispatch = useAppDispatch(); const results = useSearchResult(); + const recentSearches = useRecentSearches(); const schema = useSchemaGraph(); const graphData = useGraphQueryResult(); const querybuilderData = useQuerybuilderGraph(); @@ -54,6 +56,58 @@ export function SearchBar({}) { querybuilder: querybuilderData, }; + const toggleSearch = () => { + setSearchOpen(true); + if (!searchOpen && inputRef.current) { + inputRef.current.focus(); + } + }; + + React.useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + if (searchOpen && search !== '') { + dispatch(addRecentSearch(search)); + } + } + }; + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, [searchOpen, search]); + + React.useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === '/') { + if (!searchOpen) { + setSearchOpen(true); + setSearch(''); + } + } + }; + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, [searchOpen]); + + React.useEffect(() => { + if (searchOpen && inputRef.current) { + inputRef.current.focus(); + setSearch(''); + } + }, [searchOpen]); + + React.useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + if (searchOpen) { + setSearchOpen(false); + setSearch(''); + } + } + }; + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, [searchOpen]); + React.useEffect(() => { handleSearch(); }, [search]); @@ -75,7 +129,6 @@ export function SearchBar({}) { nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), }; - categoryAction(payload, dispatch); } }); @@ -99,8 +152,6 @@ export function SearchBar({}) { if (inputRef.current && target && !inputRef.current.contains(target as Node) && !searchbarRef?.current?.contains(target as Node)) { setSearch(''); setSearchOpen(false); - // dispatch(resetSearchResults()); - // inputRef.current.blur(); } }; document.addEventListener('click', handleClickOutside); @@ -110,94 +161,99 @@ export function SearchBar({}) { }, []); return ( - <div className="relative" ref={searchbarRef}> - <a - className="btn btn-circle btn-ghost hover:bg-gray-200" - onClick={() => { - setSearchOpen(!searchOpen); - if (inputRef.current) { - inputRef.current.focus(); - } - }} - > - <Search /> - </a> - - <div className={`searchbar ${searchOpen ? 'absolute' : 'hidden'} right-0 -bottom-12 form-control w-[30rem]`}> - <div className="relative shadow-sm w-full"> - <input - ref={inputRef} - value={search} - onChange={(e) => setSearch(e.target.value)} - type="text" - placeholder="Search…" - className="block w-full border-0 py-1.5 pl-2 pr-8 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 sm:text-sm sm:leading-6" - /> - {search !== '' && ( - <div - className="absolute inset-y-0 right-0 flex items-center cursor-pointer" - onClick={() => { - dispatch(resetSearchResults()); - setSearch(''); - }} - > - <div className="py-0 px-2"> - <span className="text-gray-400 text-xs"> - <Close /> - </span> + <> + {searchOpen && <div className="fixed inset-0 bg-black bg-opacity-50 z-40"></div>} + <div className="mr-2" ref={searchbarRef}> + <div + className="flex items-center border border-secondary-300 hover:bg-secondary-50 px-2 rounded text-sm w-44 h-8 text-secondary-900 cursor-pointer" + onClick={toggleSearch} + > + <Icon name="Search" /> + <span className="ml-1 text-secondary-900"> + Type <span className="border border-secondary-900 rounded px-1">/</span> to search + </span> + </div> + + {searchOpen && ( + <div className="fixed flex flex-col left-1/2 -translate-x-1/2 w-9/12 max-h-1/2 top-2 items-start justify-center z-50 bg-secondary-200 rounded"> + <div className="p-3 w-full"> + {/* <label className="sr-only">Search</label> */} + <div className="relative"> + <div className="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none"> + <Icon name="Search" className="text-secondary500" /> + </div> + <input + type="text" + ref={inputRef} + value={search} + onChange={(e) => setSearch(e.target.value)} + id="input-group-search" + className="block w-full p-2 ps-10 text-sm text-secondary-900 border border-secondary-300 rounded bg-secondary-50 focus:ring-blue-500 focus:border-blue-500 focus:ring-0" + placeholder="Search database" + ></input> </div> </div> - )} - </div> - {search !== '' && ( - <div className="absolute top-[calc(100%+0.5rem)] z-10 bg-white rounded-none card-bordered w-full overflow-auto max-h-[60vh]"> - {SEARCH_CATEGORIES.every((category) => results[category].nodes.length === 0 && results[category].edges.length === 0) ? ( - <div className="p-2 text-lg"> - <p className="text-entity-500 font-bold">Found no matches...</p> + + {recentSearches.length !== 0 && ( + <div className="px-3 pb-3"> + <p className="text-sm">Recent searches</p> + {recentSearches.slice(0, 3).map((term) => ( + <p key={term} className="ml-1 text-sm text-secondary-500 cursor-pointer" onClick={() => setSearch(term)}> + {term} + </p> + ))} </div> - ) : ( - SEARCH_CATEGORIES.map((category, index) => { - if (results[category].nodes.length > 0 || results[category].edges.length > 0) { - return ( - <div key={index}> - <div className="flex justify-between p-2 text-lg"> - <p className="text-entity-500 font-bold">{category.charAt(0).toUpperCase() + category.slice(1)}</p> - <p className="font-light text-slate-800"> - {results[category].nodes.length + results[category].edges.length} results - </p> - </div> - <div className="h-[1px] w-full bg-offwhite-300"></div> - {Object.values(Object.values(results[category])) - .flat() - .map((item, index) => ( - <div - key={index} - className="flex flex-col hover:bg-slate-300 p-2 cursor-pointer" - title={JSON.stringify(item)} - onClick={() => { - CATEGORY_ACTIONS[category]( - { - nodes: results[category].nodes.includes(item) ? [item] : [], - edges: results[category].edges.includes(item) ? [item] : [], - }, - dispatch, - ); - }} - > - <div className="font-semibold text-md"> - {item?.key?.slice(0, 18) || item?.id?.slice(0, 18) || Object.values(item)?.[0]?.slice(0, 18)} - </div> - <div className="font-light text-slate-800 text-xs">{JSON.stringify(item).substring(0, 40)}...</div> + )} + {search !== '' && ( + <div className="z-10 rounded card-bordered w-full overflow-auto max-h-[60vh] px-3 pb-3"> + <p className="font-bold text-sm">Results</p> + {SEARCH_CATEGORIES.every((category) => results[category].nodes.length === 0 && results[category].edges.length === 0) ? ( + <div className="ml-1 text-sm"> + <p className="text-secondary-500">Found no matches...</p> + </div> + ) : ( + SEARCH_CATEGORIES.map((category, index) => { + if (results[category].nodes.length > 0 || results[category].edges.length > 0) { + return ( + <div key={index}> + <div className="flex justify-between p-2 text-lg"> + <p className="font-bold text-sm">{category.charAt(0).toUpperCase() + category.slice(1)}</p> + <p className="font-bold text-sm">{results[category].nodes.length + results[category].edges.length} results</p> </div> - ))} - </div> - ); - } else return <></>; - }) + <div className="h-[1px] w-full bg-secondary-200"></div> + {Object.values(Object.values(results[category])) + .flat() + .map((item, index) => ( + <div + key={index} + className="flex flex-col hover:bg-secondary-300 px-2 py-1 cursor-pointer rounded ml-2" + title={JSON.stringify(item)} + onClick={() => { + CATEGORY_ACTIONS[category]( + { + nodes: results[category].nodes.includes(item) ? [item] : [], + edges: results[category].edges.includes(item) ? [item] : [], + }, + dispatch, + ); + }} + > + <div className="font-bold text-sm"> + {item?.key?.slice(0, 18) || item?.id?.slice(0, 18) || Object.values(item)?.[0]?.slice(0, 18)} + </div> + <div className="font-light text-secondary-800 text-xs">{JSON.stringify(item).substring(0, 40)}...</div> + </div> + ))} + </div> + ); + } else return <></>; + }) + )} + </div> )} </div> )} </div> - </div> + </> ); } diff --git a/apps/web/src/components/onboarding/onboarding.tsx b/apps/web/src/components/onboarding/onboarding.tsx index 17a45905b5aaa5f2bf5974b74f1e7e037b871c80..c109e01aaf240f2bcd97380f12530ec816a3ecb8 100644 --- a/apps/web/src/components/onboarding/onboarding.tsx +++ b/apps/web/src/components/onboarding/onboarding.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import Joyride, { ACTIONS, EVENTS, STATUS } from 'react-joyride'; import { useLocation } from 'react-router-dom'; -import CloseIcon from '@mui/icons-material/Close'; +import { Button } from '@graphpolaris/shared/lib/components/buttons'; import { useCases } from './use-cases'; import { useAuthorizationCache } from '@graphpolaris/shared/lib/data-access'; @@ -54,13 +54,9 @@ export default function Onboarding({}) { return ( <div> {showWalkthrough && ( - <div className="alert absolute bottom-5 left-5 w-fit cursor-pointer z-50 bg-entity-400"> - <span className="btn btn-ghost" onClick={startWalkThrough}> - Start a Tour - </span> - <button className="btn btn-circle btn-sm btn-ghost" onClick={() => addWalkthroughCookie()}> - <CloseIcon fontSize="small" /> - </button> + <div className="bg-accent-light alert absolute bottom-5 left-5 w-fit cursor-pointer z-50"> + <Button onClick={startWalkThrough} label={'Start a Tour'} variant="ghost" /> + <Button onClick={() => addWalkthroughCookie()} iconName="Close" variant="ghost" rounded /> </div> )} <Joyride diff --git a/apps/web/src/styles.scss b/apps/web/src/main.css similarity index 75% rename from apps/web/src/styles.scss rename to apps/web/src/main.css index fd781f72ddefaab82c0588e9ac450c6115bcb19a..f4201aade4a641ccb95db651191cbbaa87e8c166 100644 --- a/apps/web/src/styles.scss +++ b/apps/web/src/main.css @@ -1,4 +1,34 @@ -/* You can add global styles to this file, and also import other style files */ +@import '../../../libs/config/styles.css'; + +/* You can add global styles for the webapp to this file, and also import other style files */ + +@layer base { + input[type='number']::-webkit-inner-spin-button, + input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } +} + +html * { + box-sizing: border-box; +} + +html { + @apply font-sans; +} + +.panel { + background-color: hsl(var(--clr-light)); + border: 1px solid hsl(var(--clr-sec--200)); +} + +.tooltip::before { + @apply z-50; + @apply content-[attr(data-tip)]; +} + +/* TODO: Find out if this is being used before removing. .react-grid-layout { position: relative; @@ -51,3 +81,4 @@ box-sizing: border-box; cursor: se-resize; } +*/ diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 942d3bfcefbdd023bce08400ca9e0fa73c08290e..2da0c03812f0dc4331295b249f77e5c3592a1afc 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -5,7 +5,7 @@ import { Provider } from 'react-redux'; import { store } from '@graphpolaris/shared/lib/data-access/store'; import App from './app/app'; import { createRoot } from 'react-dom/client'; -import './styles.css'; +import './main.css'; (window as any).global = window; const domNode = document.getElementById('root'); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css deleted file mode 100644 index c563ddb44d8157dcec0d42d04ddb8c4de469e554..0000000000000000000000000000000000000000 --- a/apps/web/src/styles.css +++ /dev/null @@ -1,27 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - input[type='number']::-webkit-inner-spin-button, - input[type='number']::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; - } -} - -* { - font-family: 'Inter', ubuntu, sans-serif; - box-sizing: border-box; -} - -.panel { - padding: 0.3rem; - background-color: #ffffff; - border: 1px solid #dddddd; -} - -.tooltip::before { - @apply z-50; - @apply content-[attr(data-tip)]; -} diff --git a/apps/web/src/variables.css b/apps/web/src/variables.css new file mode 100644 index 0000000000000000000000000000000000000000..15d7a4fb206a9d3e0a79be240300cba6d8eb8627 --- /dev/null +++ b/apps/web/src/variables.css @@ -0,0 +1,110 @@ +@layer base { + :root { + /* Color styles */ + /* https://tailwindcss.com/docs/customizing-colors. */ + /* tailwind config: hsl(198deg 93% 60% / <alpha-value>) */ + + /* PRIMITIVES */ + --indigo--900: 239deg 50.9% 34.3%; + --indigo--800: 241deg 59.2% 41.4%; + --indigo--700: 242deg 62.7% 50.6%; + --indigo--600: 240deg 81% 58.6%; + --indigo--500: 236deg 90.3% 63.5%; + --indigo--400: 232deg 97% 73.9%; + --indigo--300: 226deg 100% 81.6%; + --indigo--200: 224deg 100% 88.6%; + --indigo--100: 223deg 100% 93.7%; + --indigo--50: 220deg 100% 96.5%; + + --blue--900: 204deg 86.9% 23.9%; + --blue--800: 203deg 97.1% 27.5%; + --blue--700: 203deg 100% 32.7%; + --blue--600: 202deg 100% 40.4%; + --blue--500: 200deg 95.1% 48.4%; + --blue--400: 200deg 100% 60%; + --blue--300: 200deg 100% 73.5%; + --blue--200: 202deg 100% 86.1%; + --blue--100: 206deg 100% 93.7%; + --blue--50: 203deg 100% 96.9%; + + --neutral--900: 223deg 26.3% 26.1%; + --neutral--800: 222deg 28.1% 30%; + --neutral--700: 222deg 28.1% 34.9%; + --neutral--600: 221deg 28.4% 39.4%; + --neutral--500: 219deg 25.5% 48.4%; + --neutral--400: 219deg 27% 62.9%; + --neutral--300: 220deg 26.8% 78%; + --neutral--200: 220deg 26.3% 88.8%; + --neutral--100: 220deg 23.1% 94.9%; + --neutral--50: 225deg 40% 98%; + + --red--900: 3deg 80.4% 28%; + --red--800: 5deg 83.3% 32.9%; + --red--700: 7deg 83.3% 40%; + --red--600: 10deg 89.2% 52.9%; + --red--500: 12deg 100% 64.9%; + --red--400: 15deg 97.3% 71%; + --red--300: 17deg 100% 81.8%; + --red--200: 18deg 100% 88%; + --red--100: 16deg 100% 94.9%; + --red--50: 17deg 100% 97.3%; + + --purple--900: 268deg 73.6% 31.2%; + --purple--800: 268deg 68.2% 39.4%; + --purple--700: 267deg 68.3% 47.1%; + --purple--600: 266deg 72.4% 55.9%; + --purple--500: 265deg 74.2% 65.1%; + --purple--400: 264deg 82.5% 75.3%; + --purple--300: 264deg 94.7% 85.3%; + --purple--200: 263deg 95.2% 91.8%; + --purple--100: 263deg 100% 95.5%; + --purple--50: 264deg 100% 98%; + + --yellow--900: 23deg 92.6% 26.5%; + --yellow--800: 26deg 97.5% 31%; + --yellow--700: 28deg 98.9% 37.1%; + --yellow--600: 34deg 100% 43.7%; + --yellow--500: 38deg 97.6% 50.2%; + --yellow--400: 41deg 100% 57.1%; + --yellow--300: 44deg 100% 64.5%; + --yellow--200: 46deg 100% 76.7%; + --yellow--100: 46deg 100% 88.8%; + --yellow--50: 45deg 100% 96.1%; + + --green--900: 139deg 61.2% 20.2%; + --green--800: 137deg 64.2% 24.1%; + --green--700: 137deg 71.8% 29.2%; + --green--600: 137deg 76.2% 36.3%; + --green--500: 137deg 70.6% 45.3%; + --green--400: 137deg 69.5% 53.7%; + --green--300: 137deg 76.6% 73.1%; + --green--200: 136deg 78.9% 85.1%; + --green--100: 135deg 84.2% 92.5%; + --green--50: 134deg 76.5% 96.7%; + + --white: 0deg 0% 100%; + --black: 0deg 0% 0%; + + /* Effect styles */ + --shadow--sm: 0px 1px 4px hsl(var(--neutral--900) / 0.12); + --shadow--base: 0px 2px 6px hsl(var(--neutral--900) / 0.12); + --shadow--md: 0px 3px 8px hsl(var(--neutral--900) / 0.12); + --shadow--lg: 0px 6px 12px hsl(var(--neutral--900) / 0.12); + --shadow--xl: 0px 9px 18px hsl(var(--neutral--900) / 0.12); + --shadow--2xl: 0px 12px 24px hsl(var(--neutral--900) / 0.12); + + --box-shadow--sm: 0px 1px 3px 0 hsl(var(--neutral--900) / 0.12); + --box-shadow--base: 0px 2px 6px -1px hsl(var(--neutral--900) / 0.12); + --box-shadow--md: 0px 3px 8px -2px hsl(var(--neutral--900) / 0.12); + --box-shadow--lg: 0px 6px 12px -3px hsl(var(--neutral--900) / 0.12); + --box-shadow--xl: 0px 9px 18px -4px hsl(var(--neutral--900) / 0.12); + --box-shadow--2xl: 0px 12px 24px -5px hsl(var(--neutral--900) / 0.12); + + --btn-primary-bg: hsl(var(--indigo--500)); + + /* Transitions */ + --transition--fast: 0.15s ease-in-out; + --transition--base: 0.25s ease-in-out; + --transition--slow: 0.4s ease-in-out; + } +} diff --git a/libs/config/src/colors.js b/libs/config/src/colors.js index 2ca7a396fc18178f012ac0ed338d0c7a0e8c5034..5dab6bc1df1451c058d2acd0d283f2e8befe6b82 100644 --- a/libs/config/src/colors.js +++ b/libs/config/src/colors.js @@ -2,6 +2,101 @@ import * as defaultTheme from 'tailwindcss/defaultTheme'; export const tailwindColors = { ...defaultTheme.colors, + + primary: { + DEFAULT: 'hsl(var(--clr-pri) / <alpha-value>)', + 50: 'hsl(var(--clr-pri--50) / <alpha-value>)', + 100: 'hsl(var(--clr-pri--100) / <alpha-value>)', + 200: 'hsl(var(--clr-pri--200) / <alpha-value>)', + 300: 'hsl(var(--clr-pri--300) / <alpha-value>)', + 400: 'hsl(var(--clr-pri--400) / <alpha-value>)', + 500: 'hsl(var(--clr-pri--500) / <alpha-value>)', + 600: 'hsl(var(--clr-pri--600) / <alpha-value>)', + 700: 'hsl(var(--clr-pri--700) / <alpha-value>)', + 800: 'hsl(var(--clr-pri--800) / <alpha-value>)', + 900: 'hsl(var(--clr-pri--900) / <alpha-value>)', + }, + + secondary: { + DEFAULT: 'hsl(var(--clr-sec) / <alpha-value>)', + 0: 'hsl(var(--clr-sec--0) / <alpha-value>)', + 50: 'hsl(var(--clr-sec--50) / <alpha-value>)', + 100: 'hsl(var(--clr-sec--100) / <alpha-value>)', + 200: 'hsl(var(--clr-sec--200) / <alpha-value>)', + 300: 'hsl(var(--clr-sec--300) / <alpha-value>)', + 400: 'hsl(var(--clr-sec--400) / <alpha-value>)', + 500: 'hsl(var(--clr-sec--500) / <alpha-value>)', + 600: 'hsl(var(--clr-sec--600) / <alpha-value>)', + 700: 'hsl(var(--clr-sec--700) / <alpha-value>)', + 800: 'hsl(var(--clr-sec--800) / <alpha-value>)', + 900: 'hsl(var(--clr-sec--900) / <alpha-value>)', + 950: 'hsl(var(--clr-sec--950) / <alpha-value>)', + 1000: 'hsl(var(--clr-sec--1000) / <alpha-value>)', + }, + + accent: { + DEFAULT: 'hsl(var(--clr-acc) / <alpha-value>)', + 100: 'hsl(var(--clr-acc--100) / <alpha-value>)', + 200: 'hsl(var(--clr-acc--200) / <alpha-value>)', + 300: 'hsl(var(--clr-acc--300) / <alpha-value>)', + 400: 'hsl(var(--clr-acc--400) / <alpha-value>)', + 500: 'hsl(var(--clr-acc--500) / <alpha-value>)', + 600: 'hsl(var(--clr-acc--600) / <alpha-value>)', + 700: 'hsl(var(--clr-acc--700) / <alpha-value>)', + 800: 'hsl(var(--clr-acc--800) / <alpha-value>)', + }, + + info: { + DEFAULT: 'hsl(var(--clr-inf) / <alpha-value>)', + 100: 'hsl(var(--clr-info--100) / <alpha-value>)', + 200: 'hsl(var(--clr-info--200) / <alpha-value>)', + 300: 'hsl(var(--clr-info--300) / <alpha-value>)', + 400: 'hsl(var(--clr-info--400) / <alpha-value>)', + 500: 'hsl(var(--clr-info--500) / <alpha-value>)', + 600: 'hsl(var(--clr-info--600) / <alpha-value>)', + 700: 'hsl(var(--clr-info--700) / <alpha-value>)', + 800: 'hsl(var(--clr-info--800) / <alpha-value>)', + }, + + success: { + DEFAULT: 'hsl(var(--clr-succ) / <alpha-value>)', + 100: 'hsl(var(--clr-succ--100) / <alpha-value>)', + 200: 'hsl(var(--clr-succ--200) / <alpha-value>)', + 300: 'hsl(var(--clr-succ--300) / <alpha-value>)', + 400: 'hsl(var(--clr-succ--400) / <alpha-value>)', + 500: 'hsl(var(--clr-succ--500) / <alpha-value>)', + 600: 'hsl(var(--clr-succ--600) / <alpha-value>)', + 700: 'hsl(var(--clr-succ--700) / <alpha-value>)', + 800: 'hsl(var(--clr-succ--800) / <alpha-value>)', + }, + + warning: { + DEFAULT: 'hsl(var(--clr-warn) / <alpha-value>)', + 100: 'hsl(var(--clr-warn--100) / <alpha-value>)', + 200: 'hsl(var(--clr-warn--200) / <alpha-value>)', + 300: 'hsl(var(--clr-warn--300) / <alpha-value>)', + 400: 'hsl(var(--clr-warn--400) / <alpha-value>)', + 500: 'hsl(var(--clr-warn--500) / <alpha-value>)', + 600: 'hsl(var(--clr-warn--600) / <alpha-value>)', + 700: 'hsl(var(--clr-warn--700) / <alpha-value>)', + 800: 'hsl(var(--clr-warn--800) / <alpha-value>)', + }, + + danger: { + DEFAULT: 'hsl(var(--clr-dang) / <alpha-value>)', + 100: 'hsl(var(--clr-dang--100) / <alpha-value>)', + 200: 'hsl(var(--clr-dang--200) / <alpha-value>)', + 300: 'hsl(var(--clr-dang--300) / <alpha-value>)', + 400: 'hsl(var(--clr-dang--400) / <alpha-value>)', + 500: 'hsl(var(--clr-dang--500) / <alpha-value>)', + 600: 'hsl(var(--clr-dang--600) / <alpha-value>)', + 700: 'hsl(var(--clr-dang--700) / <alpha-value>)', + 800: 'hsl(var(--clr-dang--800) / <alpha-value>)', + }, + + light: 'hsl(var(--clr-light) / <alpha-value>)', + dark: 'hsl(var(--clr-dark) / <alpha-value>)', + entity: { // https://www.tailwindshades.com/#color=29.41176470588235%2C100%2C50&step-up=8&step-down=8&hue-shift=5&name=flush-orange&base-stop=6&v=1&overrides=e30%3D DEFAULT: '#FF7D00', @@ -33,30 +128,7 @@ export const tailwindColors = { 900: '#102C45', 950: '#0B1F30', }, - line: { - 100: '#dee9f0', - 300: '#ccd6dc', - }, - logic: { - DEFAULT: '#A36A30', - 50: '#FBF6F1', - 100: '#F5E9DD', - 200: '#E9D0B6', - 300: '#DEB68E', - 400: '#D29D67', - 500: '#C7843F', - 600: '#A36A30', - 700: '#7C5024', - 800: '#543719', - 900: '#2D1D0D', - 950: '#191007', - }, - offwhite: { - DEFAULT: '#F7F9FA', - 100: '#F7F9FA', - 200: '#f4f6f7', - 300: '#d4dce1', - }, + custom: { nodes: [ '#181520', // black @@ -82,3 +154,378 @@ export const tailwindColors = { ], }, }; + +export const dataColors = { + black: 'hsl(0 0 0%)', + white: 'hsl(0 0 100%)', + blue: { + 5: 'hsl(220 80% 98%)', + 10: 'hsl(220 71% 96%)', + 20: 'hsl(220 95% 92%)', + 30: 'hsl(220 92% 85%)', + 40: 'hsl(220 94% 75%)', + 50: 'hsl(220 92% 67%)', + 60: 'hsl(220 84% 58%)', + 70: 'hsl(220 79% 49%)', + 80: 'hsl(220 86% 36%)', + 90: 'hsl(220 80% 23%)', + 95: 'hsl(220 84% 17%)', + 100: 'hsl(220 61% 13%)', + }, + cyan: { + 5: 'hsl(207 40% 98%)', + 10: 'hsl(207 69% 95%)', + 20: 'hsl(207 80% 90%)', + 30: 'hsl(207 75% 81%)', + 40: 'hsl(207 67% 69%)', + 50: 'hsl(207 75% 60%)', + 60: 'hsl(207 80% 49%)', + 70: 'hsl(207 94% 39%)', + 80: 'hsl(207 97% 29%)', + 90: 'hsl(207 94% 20%)', + 95: 'hsl(207 89% 14%)', + 100: 'hsl(207 80% 10%)', + }, + gray: { + 5: 'hsl(210 20% 98%)', + 10: 'hsl(220 14% 96%)', + 20: 'hsl(223 14% 90%)', + 30: 'hsl(214 14% 80%)', + 40: 'hsl(219 10% 68%)', + 50: 'hsl(219 11% 57%)', + 60: 'hsl(217 10% 45%)', + 70: 'hsl(218 15% 35%)', + 80: 'hsl(216 21% 27%)', + 90: 'hsl(215 30% 17%)', + 95: 'hsl(221 33% 13%)', + 100: 'hsl(223 30% 9%)', + }, + green: { + 5: 'hsl(122 80% 98%)', + 10: 'hsl(122 80% 94%)', + 20: 'hsl(122 73% 87%)', + 30: 'hsl(122 59% 76%)', + 40: 'hsl(122 54% 63%)', + 50: 'hsl(122 50% 51%)', + 60: 'hsl(122 57% 40%)', + 70: 'hsl(122 58% 31%)', + 80: 'hsl(122 66% 22%)', + 90: 'hsl(122 82% 15%)', + 95: 'hsl(122 54% 12%)', + 100: 'hsl(122 56% 8%)', + }, + magenta: { + 5: 'hsl(334 100% 99%)', + 10: 'hsl(334 100% 97%)', + 20: 'hsl(334 100% 92%)', + 30: 'hsl(334 100% 84%)', + 40: 'hsl(334 98% 74%)', + 50: 'hsl(334 84% 64%)', + 60: 'hsl(334 75% 54%)', + 70: 'hsl(334 73% 44%)', + 80: 'hsl(334 75% 32%)', + 90: 'hsl(334 91% 21%)', + 95: 'hsl(334 74% 15%)', + 100: 'hsl(334 62% 10%)', + }, + neutral: { + 5: 'hsl(0 0 98%)', + 10: 'hsl(0 0 95%)', + 20: 'hsl(0 0 89%)', + 30: 'hsl(0 0 79%)', + 40: 'hsl(0 0 66%)', + 50: 'hsl(0 0 55%)', + 60: 'hsl(0 0 44%)', + 70: 'hsl(0 0 33%)', + 80: 'hsl(0 0 25%)', + 90: 'hsl(0 0 17%)', + 95: 'hsl(0 0 12%)', + 100: 'hsl(0 0 8%)', + }, + orange: { + 5: 'hsl(29 100% 98%)', + 10: 'hsl(29 100% 95%)', + 20: 'hsl(29 100% 87%)', + 30: 'hsl(29 100% 76%)', + 40: 'hsl(29 96% 60%)', + 50: 'hsl(29 100% 50%)', + 60: 'hsl(28 100% 43%)', + 70: 'hsl(25 100% 35%)', + 80: 'hsl(26 96% 27%)', + 90: 'hsl(25 100% 17%)', + 95: 'hsl(25 100% 12%)', + 100: 'hsl(25 100% 8%)', + }, + purple: { + 5: 'hsl(263 100% 99%)', + 10: 'hsl(263 100% 97%)', + 20: 'hsl(263 100% 93%)', + 30: 'hsl(263 97% 87%)', + 40: 'hsl(263 96% 78%)', + 50: 'hsl(263 96% 71%)', + 60: 'hsl(263 80% 61%)', + 70: 'hsl(263 65% 52%)', + 80: 'hsl(263 64% 38%)', + 90: 'hsl(263 67% 25%)', + 95: 'hsl(263 54% 16%)', + 100: 'hsl(263 52% 12%)', + }, + red: { + 5: 'hsl(0 100% 99%)', + 10: 'hsl(0 100% 97%)', + 20: 'hsl(0 100% 92%)', + 30: 'hsl(0 100% 85%)', + 40: 'hsl(0 98% 76%)', + 50: 'hsl(0 95% 68%)', + 60: 'hsl(0 89% 58%)', + 70: 'hsl(0 76% 48%)', + 80: 'hsl(0 82% 35%)', + 90: 'hsl(0 85% 24%)', + 95: 'hsl(0 77% 17%)', + 100: 'hsl(0 74% 12%)', + }, + teal: { + 5: 'hsl(180 80% 98%)', + 10: 'hsl(180 83% 93%)', + 20: 'hsl(180 79% 85%)', + 30: 'hsl(180 72% 71%)', + 40: 'hsl(180 62% 57%)', + 50: 'hsl(180 93% 40%)', + 60: 'hsl(180 93% 33%)', + 70: 'hsl(180 89% 26%)', + 80: 'hsl(180 71% 20%)', + 90: 'hsl(180 79% 13%)', + 95: 'hsl(180 61% 10%)', + 100: 'hsl(180 56% 7%)', + }, + yellow: { + 5: 'hsl(49 82% 98%)', + 10: 'hsl(49 86% 91%)', + 20: 'hsl(49 97% 76%)', + 30: 'hsl(49 95% 56%)', + 40: 'hsl(49 100% 44%)', + 50: 'hsl(49 100% 38%)', + 60: 'hsl(49 100% 31%)', + 70: 'hsl(49 89% 26%)', + 80: 'hsl(49 86% 19%)', + 90: 'hsl(49 91% 13%)', + 95: 'hsl(49 83% 9%)', + 100: 'hsl(49 83% 7%)', + }, +}; +export const divergenceColors = { + blueRed: [ + dataColors.blue[90], + dataColors.blue[80], + dataColors.blue[70], + dataColors.blue[60], + dataColors.blue[50], + dataColors.blue[40], + dataColors.blue[30], + dataColors.blue[20], + dataColors.blue[10], + dataColors.blue[5], + dataColors.red[5], + dataColors.red[10], + dataColors.red[20], + dataColors.red[30], + dataColors.red[40], + dataColors.red[50], + dataColors.red[60], + dataColors.red[70], + dataColors.red[80], + dataColors.red[90], + ], + blueRedMiddle: [ + dataColors.blue[90], + dataColors.blue[80], + dataColors.blue[70], + dataColors.blue[60], + dataColors.blue[50], + dataColors.blue[40], + dataColors.blue[30], + dataColors.blue[20], + dataColors.blue[10], + dataColors.blue[5], + dataColors.neutral[5], + dataColors.red[5], + dataColors.red[10], + dataColors.red[20], + dataColors.red[30], + dataColors.red[40], + dataColors.red[50], + dataColors.red[60], + dataColors.red[70], + dataColors.red[80], + dataColors.red[90], + ], + magentaGreen: [ + dataColors.magenta[90], + dataColors.magenta[80], + dataColors.magenta[70], + dataColors.magenta[60], + dataColors.magenta[50], + dataColors.magenta[40], + dataColors.magenta[30], + dataColors.magenta[20], + dataColors.magenta[10], + dataColors.magenta[5], + dataColors.green[5], + dataColors.green[10], + dataColors.green[20], + dataColors.green[30], + dataColors.green[40], + dataColors.green[50], + dataColors.green[60], + dataColors.green[70], + dataColors.green[80], + dataColors.green[90], + ], + magentaGreenMiddle: [ + dataColors.magenta[90], + dataColors.magenta[80], + dataColors.magenta[70], + dataColors.magenta[60], + dataColors.magenta[50], + dataColors.magenta[40], + dataColors.magenta[30], + dataColors.magenta[20], + dataColors.magenta[10], + dataColors.magenta[5], + dataColors.neutral[5], + dataColors.green[5], + dataColors.green[10], + dataColors.green[20], + dataColors.green[30], + dataColors.green[40], + dataColors.green[50], + dataColors.green[60], + dataColors.green[70], + dataColors.green[80], + dataColors.green[90], + ], + orangePurple: [ + dataColors.orange[90], + dataColors.orange[80], + dataColors.orange[70], + dataColors.orange[60], + dataColors.orange[50], + dataColors.orange[40], + dataColors.orange[30], + dataColors.orange[20], + dataColors.orange[10], + dataColors.orange[5], + dataColors.purple[5], + dataColors.purple[10], + dataColors.purple[20], + dataColors.purple[30], + dataColors.purple[40], + dataColors.purple[50], + dataColors.purple[60], + dataColors.purple[70], + dataColors.purple[80], + dataColors.purple[90], + ], + orangePurpleMiddle: [ + dataColors.orange[90], + dataColors.orange[80], + dataColors.orange[70], + dataColors.orange[60], + dataColors.orange[50], + dataColors.orange[40], + dataColors.orange[30], + dataColors.orange[20], + dataColors.orange[10], + dataColors.orange[5], + dataColors.neutral[5], + dataColors.purple[5], + dataColors.purple[10], + dataColors.purple[20], + dataColors.purple[30], + dataColors.purple[40], + dataColors.purple[50], + dataColors.purple[60], + dataColors.purple[70], + dataColors.purple[80], + dataColors.purple[90], + ], + yellowTeal: [ + dataColors.yellow[90], + dataColors.yellow[80], + dataColors.yellow[70], + dataColors.yellow[60], + dataColors.yellow[50], + dataColors.yellow[40], + dataColors.yellow[30], + dataColors.yellow[20], + dataColors.yellow[10], + dataColors.yellow[5], + dataColors.teal[5], + dataColors.teal[10], + dataColors.teal[20], + dataColors.teal[30], + dataColors.teal[40], + dataColors.teal[50], + dataColors.teal[60], + dataColors.teal[70], + dataColors.teal[80], + dataColors.teal[90], + ], + yellowTealMiddle: [ + dataColors.yellow[90], + dataColors.yellow[80], + dataColors.yellow[70], + dataColors.yellow[60], + dataColors.yellow[50], + dataColors.yellow[40], + dataColors.yellow[30], + dataColors.yellow[20], + dataColors.yellow[10], + dataColors.yellow[5], + dataColors.neutral[5], + dataColors.teal[5], + dataColors.teal[10], + dataColors.teal[20], + dataColors.teal[30], + dataColors.teal[40], + dataColors.teal[50], + dataColors.teal[60], + dataColors.teal[70], + dataColors.teal[80], + dataColors.teal[90], + ], +}; +export const categoricalColors = { + lightMode: { + 1: dataColors.orange[60], + 2: dataColors.purple[70], + 3: dataColors.green[70], + 4: dataColors.blue[50], + 5: dataColors.red[50], + 6: dataColors.yellow[50], + 7: dataColors.magenta[50], + 8: dataColors.teal[60], + 9: dataColors.orange[40], + 10: dataColors.red[80], + 11: dataColors.purple[40], + 12: dataColors.blue[80], + 13: dataColors.green[50], + 14: dataColors.magenta[70], + }, + darkMode: { + 1: dataColors.orange[50], + 2: dataColors.purple[50], + 3: dataColors.green[50], + 4: dataColors.blue[30], + 5: dataColors.red[40], + 6: dataColors.yellow[40], + 7: dataColors.magenta[40], + 8: dataColors.teal[50], + 9: dataColors.orange[30], + 10: dataColors.red[70], + 11: dataColors.purple[40], + 12: dataColors.blue[60], + 13: dataColors.green[30], + 14: dataColors.magenta[60], + }, +}; diff --git a/libs/config/styles.css b/libs/config/styles.css index 04b35af2af65c8f08f0ffa850b9147d1694bc94e..8c1d76389ebad20785225705afae2e86a98d8ac1 100644 --- a/libs/config/styles.css +++ b/libs/config/styles.css @@ -1,3 +1,7 @@ +@import "styling/primitives.css"; +@import "styling/variables.css"; + @tailwind base; @tailwind components; @tailwind utilities; + diff --git a/libs/config/styling/primitives.css b/libs/config/styling/primitives.css new file mode 100644 index 0000000000000000000000000000000000000000..133553dd7a6c3edf50122ad220ccb4dac393d988 --- /dev/null +++ b/libs/config/styling/primitives.css @@ -0,0 +1,155 @@ +@layer base { + :root { + /* Color styles */ + /* https://tailwindcss.com/docs/customizing-colors. */ + /* tailwind config: 198deg 93% 60% / <alpha-value>) */ + + /* PRIMITIVES */ + /* primitives are used to create other variables */ + --clr-white: 0 0% 100%; + --clr-black: 0 0% 0%; + + --clr-blue-5: 220deg 80% 98%; + --clr-blue-10: 220deg 71% 96%; + --clr-blue-20: 220deg 95% 92%; + --clr-blue-30: 220deg 92% 85%; + --clr-blue-40: 220deg 94% 75%; + --clr-blue-50: 220deg 92% 67%; + --clr-blue-60: 220deg 84% 58%; + --clr-blue-70: 220deg 79% 49%; + --clr-blue-80: 220deg 86% 36%; + --clr-blue-90: 220deg 80% 23%; + --clr-blue-95: 220deg 84% 17%; + --clr-blue-100: 220deg 61% 13%; + + --clr-gray-5: 210deg 20% 98%; + --clr-gray-10: 220deg 14% 96%; + --clr-gray-20: 223deg 14% 90%; + --clr-gray-30: 214deg 14% 80%; + --clr-gray-40: 219deg 10% 68%; + --clr-gray-50: 219deg 11% 57%; + --clr-gray-60: 217deg 10% 45%; + --clr-gray-70: 218deg 15% 35%; + --clr-gray-80: 216deg 21% 27%; + --clr-gray-90: 215deg 30% 17%; + --clr-gray-95: 221deg 33% 13%; + --clr-gray-100: 223deg 30% 9%; + + --clr-cyan-5: 207deg 40% 98%; + --clr-cyan-10: 207deg 69% 95%; + --clr-cyan-20: 207deg 80% 90%; + --clr-cyan-30: 207deg 75% 81%; + --clr-cyan-40: 207deg 67% 69%; + --clr-cyan-50: 207deg 75% 60%; + --clr-cyan-60: 207deg 80% 49%; + --clr-cyan-70: 207deg 94% 39%; + --clr-cyan-80: 207deg 97% 29%; + --clr-cyan-90: 207deg 94% 20%; + --clr-cyan-95: 207deg 89% 14%; + --clr-cyan-100: 207deg 80% 10%; + + --clr-neutral-5: 0deg 0% 98%; + --clr-neutral-10: 0deg 0% 95%; + --clr-neutral-20: 0deg 0% 89%; + --clr-neutral-30: 0deg 0% 79%; + --clr-neutral-40: 0deg 0% 66%; + --clr-neutral-50: 0deg 0% 55%; + --clr-neutral-60: 0deg 0% 44%; + --clr-neutral-70: 0deg 0% 33%; + --clr-neutral-80: 0deg 0% 25%; + --clr-neutral-90: 0deg 0% 17%; + --clr-neutral-95: 0deg 0% 12%; + --clr-neutral-100: 0deg 0% 8%; + + --clr-green-5: 122deg 80% 98%; + --clr-green-10: 122deg 80% 94%; + --clr-green-20: 122deg 73% 87%; + --clr-green-30: 122deg 59% 76%; + --clr-green-40: 122deg 54% 63%; + --clr-green-50: 122deg 50% 51%; + --clr-green-60: 122deg 57% 40%; + --clr-green-70: 122deg 58% 31%; + --clr-green-80: 122deg 66% 22%; + --clr-green-90: 122deg 82% 15%; + --clr-green-95: 122deg 54% 12%; + --clr-green-100: 122deg 56% 8%; + + --clr-magenta-5: 334deg 100% 99%; + --clr-magenta-10: 334deg 100% 97%; + --clr-magenta-20: 334deg 100% 92%; + --clr-magenta-30: 334deg 100% 84%; + --clr-magenta-40: 334deg 98% 74%; + --clr-magenta-50: 334deg 84% 64%; + --clr-magenta-60: 334deg 75% 54%; + --clr-magenta-70: 334deg 73% 44%; + --clr-magenta-80: 334deg 75% 32%; + --clr-magenta-90: 334deg 91% 21%; + --clr-magenta-95: 334deg 74% 15%; + --clr-magenta-100: 334deg 62% 10%; + + --clr-orange-5: 29deg 100% 98%; + --clr-orange-10: 29deg 100% 95%; + --clr-orange-20: 29deg 100% 87%; + --clr-orange-30: 29deg 100% 76%; + --clr-orange-40: 29deg 96% 60%; + --clr-orange-50: 29deg 100% 50%; + --clr-orange-60: 25deg 100% 43%; + --clr-orange-70: 25deg 100% 35%; + --clr-orange-80: 25deg 96% 27%; + --clr-orange-90: 25deg 100% 17%; + --clr-orange-95: 25deg 100% 12%; + --clr-orange-100: 25deg 100% 8%; + + --clr-purple-5: 263deg 100% 99%; + --clr-purple-10: 263deg 100% 97%; + --clr-purple-20: 263deg 100% 93%; + --clr-purple-30: 263deg 97% 87%; + --clr-purple-40: 263deg 96% 78%; + --clr-purple-50: 263deg 96% 71%; + --clr-purple-60: 263deg 80% 61%; + --clr-purple-70: 263deg 65% 52%; + --clr-purple-80: 263deg 64% 38%; + --clr-purple-90: 263deg 67% 25%; + --clr-purple-95: 263deg 54% 16%; + --clr-purple-100: 263deg 52% 12%; + + --clr-red-5: 0deg 100% 99%; + --clr-red-10: 0deg 100% 97%; + --clr-red-20: 0deg 100% 92%; + --clr-red-30: 0deg 100% 85%; + --clr-red-40: 0deg 98% 76%; + --clr-red-50: 0deg 95% 68%; + --clr-red-60: 0deg 89% 58%; + --clr-red-70: 0deg 76% 48%; + --clr-red-80: 0deg 82% 35%; + --clr-red-90: 0deg 85% 24%; + --clr-red-95: 0deg 77% 17%; + --clr-red-100: 0deg 74% 12%; + + --clr-teal-5: 180deg 80% 98%; + --clr-teal-10: 180deg 83% 93%; + --clr-teal-20: 180deg 79% 85%; + --clr-teal-30: 180deg 72% 71%; + --clr-teal-40: 180deg 62% 57%; + --clr-teal-50: 180deg 93% 40%; + --clr-teal-60: 180deg 93% 33%; + --clr-teal-70: 180deg 89% 26%; + --clr-teal-80: 180deg 71% 20%; + --clr-teal-90: 180deg 79% 13%; + --clr-teal-95: 180deg 61% 10%; + --clr-teal-100: 180deg 56% 7%; + + --clr-yellow-5: 49deg 82% 98%; + --clr-yellow-10: 49deg 86% 91%; + --clr-yellow-20: 49deg 97% 76%; + --clr-yellow-30: 49deg 95% 56%; + --clr-yellow-40: 49deg 100% 44%; + --clr-yellow-50: 49deg 100% 38%; + --clr-yellow-60: 49deg 100% 31%; + --clr-yellow-70: 49deg 89% 26%; + --clr-yellow-80: 49deg 86% 19%; + --clr-yellow-90: 49deg 91% 13%; + --clr-yellow-95: 49deg 83% 9%; + --clr-yellow-100: 49deg 83% 7%; + } +} diff --git a/libs/config/styling/variables.css b/libs/config/styling/variables.css new file mode 100644 index 0000000000000000000000000000000000000000..fc03f5721963bd8d64ee9495e5a1571b30d418a5 --- /dev/null +++ b/libs/config/styling/variables.css @@ -0,0 +1,102 @@ +@layer base { + :root { + --clr-light: var(--clr-gray-5); + --clr-dark: var(--clr-gray-100); + + --clr-pri--50: var(--clr-cyan-10); + --clr-pri--100: var(--clr-cyan-20); + --clr-pri--200: var(--clr-cyan-30); + --clr-pri--300: var(--clr-cyan-40); + --clr-pri--400: var(--clr-cyan-50); + --clr-pri--500: var(--clr-cyan-60); + --clr-pri--600: var(--clr-cyan-70); + --clr-pri--700: var(--clr-cyan-80); + --clr-pri--800: var(--clr-cyan-90); + --clr-pri--900: var(--clr-cyan-95); + --clr-pri: var(--clr-pri--600); + + --clr-sec--0: var(--clr-white); + --clr-sec--50: var(--clr-gray-5); + --clr-sec--100: var(--clr-gray-10); + --clr-sec--200: var(--clr-gray-20); + --clr-sec--300: var(--clr-gray-30); + --clr-sec--400: var(--clr-gray-40); + --clr-sec--500: var(--clr-gray-50); + --clr-sec--600: var(--clr-gray-60); + --clr-sec--700: var(--clr-gray-70); + --clr-sec--800: var(--clr-gray-80); + --clr-sec--900: var(--clr-gray-90); + --clr-sec--950: var(--clr-gray-95); + --clr-sec--1000: var(--clr-gray-100); + --clr-sec: var(--clr-sec--700); + + --clr-acc--100: var(--clr-orange-10); + --clr-acc--200: var(--clr-orange-20); + --clr-acc--300: var(--clr-orange-30); + --clr-acc--400: var(--clr-orange-40); + --clr-acc--500: var(--clr-orange-50); + --clr-acc--600: var(--clr-orange-60); + --clr-acc--700: var(--clr-orange-70); + --clr-acc--800: var(--clr-orange-80); + --clr-acc: var(--clr-acc--500); + + --clr-info--100: var(--clr-teal-10); + --clr-info--200: var(--clr-teal-20); + --clr-info--300: var(--clr-teal-30); + --clr-info--400: var(--clr-teal-40); + --clr-info--500: var(--clr-teal-50); + --clr-info--600: var(--clr-teal-60); + --clr-info--700: var(--clr-teal-70); + --clr-info--800: var(--clr-teal-80); + --clr-info: var(--clr-info--500); + + --clr-succ--100: var(--clr-green-10); + --clr-succ--200: var(--clr-green-20); + --clr-succ--300: var(--clr-green-30); + --clr-succ--400: var(--clr-green-40); + --clr-succ--500: var(--clr-green-50); + --clr-succ--600: var(--clr-green-60); + --clr-succ--700: var(--clr-green-70); + --clr-succ--800: var(--clr-green-80); + --clr-succ: var(--clr-succ--600); + + --clr-warn--100: var(--clr-yellow-5); + --clr-warn--200: var(--clr-yellow-10); + --clr-warn--300: var(--clr-yellow-20); + --clr-warn--400: var(--clr-yellow-30); + --clr-warn--500: var(--clr-yellow-40); + --clr-warn--600: var(--clr-yellow-50); + --clr-warn--700: var(--clr-yellow-60); + --clr-warn--800: var(--clr-yellow-70); + --clr-warn: var(--clr-warn--500); + + --clr-dang--100: var(--clr-red-20); + --clr-dang--200: var(--clr-red-30); + --clr-dang--300: var(--clr-red-40); + --clr-dang--400: var(--clr-red-50); + --clr-dang--500: var(--clr-red-60); + --clr-dang--600: var(--clr-red-70); + --clr-dang--700: var(--clr-red-80); + --clr-dang--800: var(--clr-red-90); + --clr-dang: var(--clr-dang--600); + + --focus-shadow-clr: var(--clr-gray-95); + + /* Graph data colors */ + + --clr-cat-01: var(--clr-cyan-70); + --clr-cat-02: var(--clr-orange-50); + --clr-cat-03: var(--clr-green-70); + --clr-cat-04: var(--clr-magenta-70); + --clr-cat-05: var(--clr-red-70); + --clr-cat-06: var(--clr-primary); + --clr-cat-07: var(--clr-secondary); + --clr-cat-08: var(--clr-accent); + --clr-cat-09: var(--clr-success); + --clr-cat-10: var(--clr-danger); + --clr-cat-11: var(--clr-gray-50); + --clr-cat-12: var(--clr-gray-50); + --clr-cat-13: var(--clr-gray-50); + --clr-cat-14: var(--clr-gray-50); + } +} diff --git a/libs/config/tailwind.config.js b/libs/config/tailwind.config.js index 59520b7a09e48e30c34f894097e069b514955207..ae29ad2c7425f293415f6676ddb6138a013047c4 100644 --- a/libs/config/tailwind.config.js +++ b/libs/config/tailwind.config.js @@ -3,12 +3,40 @@ import * as defaultTheme from 'tailwindcss/defaultTheme'; import { tailwindColors } from './src/colors.js'; export default { - content: ['./index.html', 'src/**/*.{js,ts,jsx,tsx}', '../../libs/*/lib/**/*.{js,ts,jsx,tsx}'], + content: ['./index.html', 'src/**/*.{js,ts,jsx,tsx,mdx}', '../../libs/*/lib/**/*.{js,ts,jsx,tsx,mdx}'], theme: { + borderRadius: { + none: 0, + sm: '0.125rem', + DEFAULT: '0.25rem', + md: '0.375rem', + lg: '0.5rem', + xl: '0.75rem', + '2xl': '1rem', + '3xl': '1.5rem', + full: '9999px', + }, + fontSize: { + '3xs': '0.625rem', + '2xs': '0.688rem', + xs: '0.75rem', + sm: '0.875rem', + base: '1rem', + lg: '1.125rem', + xl: '1.25rem', + '2xl': '1.5rem', + '3xl': '1.875rem', + '4xl': '2.25rem', + '5xl': '3rem', + '6xl': '3.75rem', + '7xl': '4.5rem', + '8xl': '6rem', + }, + fontFamily: { + sans: ['Inter', ...defaultTheme.fontFamily.sans], + mono: ['Roboto Mono', ...defaultTheme.fontFamily.mono], + }, extend: { - fontFamily: { - inter: ['"Inter"', ...defaultTheme.fontFamily.sans], - }, colors: tailwindColors, animation: { openmenu: 'openmenu 0.3s ease-out', diff --git a/libs/shared/lib/components/Design guides/styleGuide.mdx b/libs/shared/lib/components/Design guides/styleGuide.mdx new file mode 100644 index 0000000000000000000000000000000000000000..8d15b583616cd34b8e354de2959e16233e299118 --- /dev/null +++ b/libs/shared/lib/components/Design guides/styleGuide.mdx @@ -0,0 +1,1007 @@ +import { Meta, Unstyled } from '@storybook/blocks'; +import { ColorPalette, ColorItem, IconGallery, IconItem, Canvas } from '@storybook/blocks'; +import { Button } from '../buttons/.'; + +import { Text } from '../inputs/.'; +import { Icon } from '../icon/.'; + +import { + Delete as DeleteIcon, + DeleteOutlined as DeleteOutlinedIcon, + DeleteRounded as DeleteRoundedIcon, + DeleteTwoTone as DeleteTwoToneIcon, + DeleteSharp as DeleteSharpIcon, + Settings as SettingsIcon, + PhotoCamera as PhotoCameraIcon, + Add as AddIcon, + Fullscreen as FullscreenIcon, + FullscreenExit as FullscreenExitIcon, + ContentCopy as ContentCopyIcon, + Cached as CachedIcon, + Info as InfoIcon, + ErrorOutline as ErrorOutlineIcon, + CheckCircleOutline as CheckCircleOutlineIcon, + Search as SearchIcon, + ArrowDropDown as ArrowDropDownIcon, + ArrowDropUp as ArrowDropUpIcon, + ArrowBack as ArrowBackIcon, + ArrowForward as ArrowForwardIcon, + Close as CloseIcon, + Cancel as CancelIcon, + KeyboardArrowDown as KeyboardArrowDownIcon, + KeyboardArrowLeft as KeyboardArrowLeftIcon, + KeyboardArrowRight as KeyboardArrowRightIcon, + KeyboardArrowUp as KeyboardArrowUpIcon, + CheckCircle as CheckCircleIcon, +} from '@mui/icons-material'; + +<Meta title="Design guide" /> + +# Style Guide + +Table of contents: + +1. [Basic information](#Basic-information) +2. [Colors](#colors) +3. [Typography](#typography) +4. [Icons](#icons) +5. [Border radius](#border-radius) +6. [Spacing](#spacing) +7. [Shadows](#shadows) +8. [Develop components](#develop-components) + +## Basic information + +GraphPolaris uses Tailwind. DaisyUI usage is deprecated: + +Style files can be found in: + +- tailwind.config.js: For styling elements. E.g.: + +```jsx +<div className="btn-primary btn-lg"></div> +``` + +- colors.js: For getting colors palettes as dictionaries. E.g.: + +```jsx +import { tailwindColors, dataColors } from '../../../../config/src/colors.js'; +``` + +## Colors + +### UI components + +<ColorPalette> + <ColorItem + title="Main" + colors={{ + primary: 'hsl(var(--clr-pri))', + secondary: 'hsl(var(--clr-sec))', + accent: 'hsl(var(--clr-acc))', + info: 'hsl(var(--clr-info))', + success: 'hsl(var(--clr-succ))', + warning: 'hsl(var(--clr-warn))', + danger: 'hsl(var(--clr-dang))', + }} + /> +</ColorPalette> + +<ColorPalette> + <ColorItem + title="Extra" + colors={{ + light: 'hsl(var(--clr-light))', + dark: 'hsl(var(--clr-dark))', + }} + /> +</ColorPalette> + +And if you need variations of them: +You can use the Tailwind classes in the following way: +bg-primary-100, text-warning-500, border-danger-500, etc. + +<ColorPalette> +<ColorItem + title="Primary" + colors={{ + 50: 'hsl(var(--clr-pri--50))', + 100: 'hsl(var(--clr-pri--100))', + 200: 'hsl(var(--clr-pri--200))', + 300: 'hsl(var(--clr-pri--300))', + 400: 'hsl(var(--clr-pri--400))', + 500: 'hsl(var(--clr-pri--500))', + 600: 'hsl(var(--clr-pri--600))', + 700: 'hsl(var(--clr-pri--700))', + 800: 'hsl(var(--clr-pri--800))', + 900: 'hsl(var(--clr-pri--900))', + }} +/> +<ColorItem + title="Secondary" + colors={{ + 0: 'hsl(var(--clr-sec--0))', + 50: 'hsl(var(--clr-sec--50))', + 100: 'hsl(var(--clr-sec--100))', + 200: 'hsl(var(--clr-sec--200))', + 300: 'hsl(var(--clr-sec--300))', + 400: 'hsl(var(--clr-sec--400))', + 500: 'hsl(var(--clr-sec--500))', + 600: 'hsl(var(--clr-sec--600))', + 700: 'hsl(var(--clr-sec--700))', + 800: 'hsl(var(--clr-sec--800))', + 900: 'hsl(var(--clr-sec--900))', + 950: 'hsl(var(--clr-sec--950))', + 1000: 'hsl(var(--clr-sec--1000))', + }} +/> +<ColorItem + title="Accent" + colors={{ + 100: 'hsl(var(--clr-acc--100))', + 200: 'hsl(var(--clr-acc--200))', + 300: 'hsl(var(--clr-acc--300))', + 400: 'hsl(var(--clr-acc--400))', + 500: 'hsl(var(--clr-acc--500))', + 600: 'hsl(var(--clr-acc--600))', + 700: 'hsl(var(--clr-acc--700))', + 800: 'hsl(var(--clr-acc--800))', + }} +/> + <ColorItem + title="info" + colors={{ + 100: 'hsl(var(--clr-info--100))', + 200: 'hsl(var(--clr-info--200))', + 300: 'hsl(var(--clr-info--300))', + 400: 'hsl(var(--clr-info--400))', + 500: 'hsl(var(--clr-info--500))', + 600: 'hsl(var(--clr-info--600))', + 700: 'hsl(var(--clr-info--700))', + 800: 'hsl(var(--clr-info--800))', + }} + /> + <ColorItem + title="Success" + colors={{ + 100: 'hsl(var(--clr-succ--100))', + 200: 'hsl(var(--clr-succ--200))', + 300: 'hsl(var(--clr-succ--300))', + 400: 'hsl(var(--clr-succ--400))', + 500: 'hsl(var(--clr-succ--500))', + 600: 'hsl(var(--clr-succ--600))', + 700: 'hsl(var(--clr-succ--700))', + 800: 'hsl(var(--clr-succ--800))', + }} + /> + <ColorItem + title="Warning" + colors={{ + 100: 'hsl(var(--clr-warn--100))', + 200: 'hsl(var(--clr-warn--200))', + 300: 'hsl(var(--clr-warn--300))', + 400: 'hsl(var(--clr-warn--400))', + 500: 'hsl(var(--clr-warn--500))', + 600: 'hsl(var(--clr-warn--600))', + 700: 'hsl(var(--clr-warn--700))', + 800: 'hsl(var(--clr-warn--800))', + }} /> +<ColorItem + title="Danger" + colors={{ + 100: 'hsl(var(--clr-dang--100))', + 200: 'hsl(var(--clr-dang--200))', + 300: 'hsl(var(--clr-dang--300))', + 400: 'hsl(var(--clr-dang--400))', + 500: 'hsl(var(--clr-dang--500))', + 600: 'hsl(var(--clr-dang--600))', + 700: 'hsl(var(--clr-dang--700))', + 800: 'hsl(var(--clr-dang--800))', + }} +/> + +</ColorPalette> + +### DataVis + +## Categorical + +<ColorPalette> + <ColorItem + title="categoricalColors.lightMode" + colors={{ + cat01: 'hsl(var(--clr-orange-60))', + cat02: 'hsl(var(--clr-purple-70))', + cat03: 'hsl(var(--clr-green-70))', + cat04: 'hsl(var(--clr-blue-50))', + cat05: 'hsl(var(--clr-red-50))', + cat06: 'hsl(var(--clr-yellow-50))', + cat07: 'hsl(var(--clr-magenta-50))', + cat08: 'hsl(var(--clr-teal-60))', + cat09: 'hsl(var(--clr-orange-40))', + cat10: 'hsl(var(--clr-red-80))', + cat11: 'hsl(var(--clr-purple-40))', + cat12: 'hsl(var(--clr-blue-80))', + cat13: 'hsl(var(--clr-green-50))', + cat14: 'hsl(var(--clr-magenta-70))', + }} + /> +</ColorPalette> +<ColorPalette> + <ColorItem + title="categoricalColors.darkMode" + colors={{ + cat01: 'hsl(var(--clr-orange-50))', + cat02: 'hsl(var(--clr-purple-50))', + cat03: 'hsl(var(--clr-green-50))', + cat04: 'hsl(var(--clr-blue-30))', + cat05: 'hsl(var(--clr-red-40))', + cat06: 'hsl(var(--clr-yellow-40))', + cat07: 'hsl(var(--clr-magenta-40))', + cat08: 'hsl(var(--clr-teal-50))', + cat09: 'hsl(var(--clr-orange-30))', + cat10: 'hsl(var(--clr-red-70))', + cat11: 'hsl(var(--clr-purple-40))', + cat12: 'hsl(var(--clr-blue-60))', + cat13: 'hsl(var(--clr-green-30))', + cat14: 'hsl(var(--clr-magenta-60))', + }} + /> +</ColorPalette> + +## Sequential + +<ColorPalette> +<ColorItem + title="Orange" + subtitle="dataColors.sequential.orange" + colors={{ + 5: 'hsl(var(--clr-orange-5))', + 10: 'hsl(var(--clr-orange-10))', + 20: 'hsl(var(--clr-orange-20))', + 30: 'hsl(var(--clr-orange-30))', + 40: 'hsl(var(--clr-orange-40))', + 50: 'hsl(var(--clr-orange-50))', + 60: 'hsl(var(--clr-orange-60))', + 70: 'hsl(var(--clr-orange-70))', + 80: 'hsl(var(--clr-orange-80))', + 90: 'hsl(var(--clr-orange-90))', + 95: 'hsl(var(--clr-orange-95))', + 100: 'hsl(var(--clr-orange-100))', + }} +/> +<ColorItem + title="Blue" + subtitle="dataColors.sequential.blue" + colors={{ + 5: 'hsl(var(--clr-blue-5))', + 10: 'hsl(var(--clr-blue-10))', + 20: 'hsl(var(--clr-blue-20))', + 30: 'hsl(var(--clr-blue-30))', + 40: 'hsl(var(--clr-blue-40))', + 50: 'hsl(var(--clr-blue-50))', + 60: 'hsl(var(--clr-blue-60))', + 70: 'hsl(var(--clr-blue-70))', + 80: 'hsl(var(--clr-blue-80))', + 90: 'hsl(var(--clr-blue-90))', + 95: 'hsl(var(--clr-blue-95))', + 100: 'hsl(var(--clr-blue-100))', + }} +/> +<ColorItem + title="Red" + subtitle="dataColors.sequential.red" + colors={{ + 5: 'hsl(var(--clr-red-5))', + 10: 'hsl(var(--clr-red-10))', + 20: 'hsl(var(--clr-red-20))', + 30: 'hsl(var(--clr-red-30))', + 40: 'hsl(var(--clr-red-40))', + 50: 'hsl(var(--clr-red-50))', + 60: 'hsl(var(--clr-red-60))', + 70: 'hsl(var(--clr-red-70))', + 80: 'hsl(var(--clr-red-80))', + 90: 'hsl(var(--clr-red-90))', + 95: 'hsl(var(--clr-red-95))', + 100: 'hsl(var(--clr-red-100))', + }} +/> +<ColorItem + title="Gray" + subtitle="dataColors.sequential.gray" + colors={{ + 5: 'hsl(var(--clr-gray-5))', + 10: 'hsl(var(--clr-gray-10))', + 20: 'hsl(var(--clr-gray-20))', + 30: 'hsl(var(--clr-gray-30))', + 40: 'hsl(var(--clr-gray-40))', + 50: 'hsl(var(--clr-gray-50))', + 60: 'hsl(var(--clr-gray-60))', + 70: 'hsl(var(--clr-gray-70))', + 80: 'hsl(var(--clr-gray-80))', + 90: 'hsl(var(--clr-gray-90))', + 95: 'hsl(var(--clr-gray-95))', + 100: 'hsl(var(--clr-gray-100))', +}} +/> + +<ColorItem + title="Magenta" + subtitle="dataColors.sequential.magenta" + colors={{ + 5: 'hsl(var(--clr-magenta-5))', + 10: 'hsl(var(--clr-magenta-10))', + 20: 'hsl(var(--clr-magenta-20))', + 30: 'hsl(var(--clr-magenta-30))', + 40: 'hsl(var(--clr-magenta-40))', + 50: 'hsl(var(--clr-magenta-50))', + 60: 'hsl(var(--clr-magenta-60))', + 70: 'hsl(var(--clr-magenta-70))', + 80: 'hsl(var(--clr-magenta-80))', + 90: 'hsl(var(--clr-magenta-90))', + 95: 'hsl(var(--clr-magenta-95))', + 100: 'hsl(var(--clr-magenta-100))', + }} +/> +<ColorItem + title="Teal" + subtitle="dataColors.sequential.teal" + colors={{ + 5: 'hsl(var(--clr-teal-5))', + 10: 'hsl(var(--clr-teal-10))', + 20: 'hsl(var(--clr-teal-20))', + 30: 'hsl(var(--clr-teal-30))', + 40: 'hsl(var(--clr-teal-40))', + 50: 'hsl(var(--clr-teal-50))', + 60: 'hsl(var(--clr-teal-60))', + 70: 'hsl(var(--clr-teal-70))', + 80: 'hsl(var(--clr-teal-80))', + 90: 'hsl(var(--clr-teal-90))', + 95: 'hsl(var(--clr-teal-95))', + 100: 'hsl(var(--clr-teal-100))', + }} +/> +<ColorItem + title="Cyan" + subtitle="dataColors.sequential.cyan" + colors={{ + 5: 'hsl(var(--clr-cyan-5))', + 10: 'hsl(var(--clr-cyan-10))', + 20: 'hsl(var(--clr-cyan-20))', + 30: 'hsl(var(--clr-cyan-30))', + 40: 'hsl(var(--clr-cyan-40))', + 50: 'hsl(var(--clr-cyan-50))', + 60: 'hsl(var(--clr-cyan-60))', + 70: 'hsl(var(--clr-cyan-70))', + 80: 'hsl(var(--clr-cyan-80))', + 90: 'hsl(var(--clr-cyan-90))', + 95: 'hsl(var(--clr-cyan-95))', + 100: 'hsl(var(--clr-cyan-100))', + }} +/> +<ColorItem + title="Neutral" + subtitle="dataColors.sequential.neutral" + colors={{ + 5: 'hsl(var(--clr-neutral-5))', + 10: 'hsl(var(--clr-neutral-10))', + 20: 'hsl(var(--clr-neutral-20))', + 30: 'hsl(var(--clr-neutral-30))', + 40: 'hsl(var(--clr-neutral-40))', + 50: 'hsl(var(--clr-neutral-50))', + 60: 'hsl(var(--clr-neutral-60))', + 70: 'hsl(var(--clr-neutral-70))', + 80: 'hsl(var(--clr-neutral-80))', + 90: 'hsl(var(--clr-neutral-90))', + 95: 'hsl(var(--clr-neutral-95))', + 100: 'hsl(var(--clr-neutral-100))', + }} +/> +<ColorItem + title="Purple" + subtitle="dataColors.sequential.purple" + colors={{ + 5: 'hsl(var(--clr-purple-5))', + 10: 'hsl(var(--clr-purple-10))', + 20: 'hsl(var(--clr-purple-20))', + 30: 'hsl(var(--clr-purple-30))', + 40: 'hsl(var(--clr-purple-40))', + 50: 'hsl(var(--clr-purple-50))', + 60: 'hsl(var(--clr-purple-60))', + 70: 'hsl(var(--clr-purple-70))', + 80: 'hsl(var(--clr-purple-80))', + 90: 'hsl(var(--clr-purple-90))', + 95: 'hsl(var(--clr-purple-95))', + 100: 'hsl(var(--clr-purple-100))', + }} +/> +<ColorItem + title="Yellow" + subtitle="dataColors.sequential.yellow" + colors={{ + 5: 'hsl(var(--clr-yellow-5))', + 10: 'hsl(var(--clr-yellow-10))', + 20: 'hsl(var(--clr-yellow-20))', + 30: 'hsl(var(--clr-yellow-30))', + 40: 'hsl(var(--clr-yellow-40))', + 50: 'hsl(var(--clr-yellow-50))', + 60: 'hsl(var(--clr-yellow-60))', + 70: 'hsl(var(--clr-yellow-70))', + 80: 'hsl(var(--clr-yellow-80))', + 90: 'hsl(var(--clr-yellow-90))', + 95: 'hsl(var(--clr-yellow-95))', + 100: 'hsl(var(--clr-yellow-100))', + }} +/> + +</ColorPalette> + +## Divergence + +<ColorPalette> + <ColorItem + title="blueRed" + subtitle="dataColors.divergence.blueRed" + colors={{ + 1: 'hsl(220 80% 23%)', + 2: 'hsl(220 86% 36%)', + 3: 'hsl(220 79% 49%)', + 4: 'hsl(220 84% 58%)', + 5: 'hsl(220 92% 67%)', + 6: 'hsl(220 94% 75%)', + 7: 'hsl(220 92% 85%)', + 8: 'hsl(220 95% 92%)', + 9: 'hsl(220 71% 96%)', + 10: 'hsl(220 80% 98%)', + 11: 'hsl(0 100% 99%)', + 12: 'hsl(0 100% 97%)', + 13: 'hsl(0 100% 92%)', + 14: 'hsl(0 100% 85%)', + 15: 'hsl(0 98% 76%)', + 16: 'hsl(0 95% 68%)', + 17: 'hsl(0 89% 58%)', + 18: 'hsl(0 76% 48%)', + 19: 'hsl(0 82% 35%)', + 20: 'hsl(0 85% 24%)', + }} + /> + +<ColorItem + title="blueRedMiddle" + subtitle="dataColors.divergence.blueRedMiddle" + colors={{ + 1: 'hsl(220 80% 23%)', + 2: 'hsl(220 86% 36%)', + 3: 'hsl(220 79% 49%)', + 4: 'hsl(220 84% 58%)', + 5: 'hsl(220 92% 67%)', + 6: 'hsl(220 94% 75%)', + 7: 'hsl(220 92% 85%)', + 8: 'hsl(220 95% 92%)', + 9: 'hsl(220 71% 96%)', + 10: 'hsl(220 80% 98%)', + 11: 'hsl(0 0% 98%)', + 12: 'hsl(0 100% 99%)', + 13: 'hsl(0 100% 97%)', + 14: 'hsl(0 100% 92%)', + 15: 'hsl(0 100% 85%)', + 16: 'hsl(0 98% 76%)', + 17: 'hsl(0 95% 68%)', + 18: 'hsl(0 89% 58%)', + 19: 'hsl(0 76% 48%)', + 20: 'hsl(0 82% 35%)', + 21: 'hsl(0 85% 24%)', + }} +/> + +<ColorItem + title="magentaGreen" + subtitle="dataColors.divergence.magentaGreen" + colors={{ + 1: 'hsl(334 91% 21%)', + 2: 'hsl(334 75% 32%)', + 3: 'hsl(334 73% 44%)', + 4: 'hsl(334 75% 54%)', + 5: 'hsl(334 84% 64%)', + 6: 'hsl(334 98% 74%)', + 7: 'hsl(334 100% 84%)', + 8: 'hsl(334 100% 92%)', + 9: 'hsl(334 100% 97%)', + 10: 'hsl(334 100% 99%)', + 11: 'hsl(122 80% 98%)', + 12: 'hsl(122 80% 94%)', + 13: 'hsl(122 73% 87%)', + 14: 'hsl(122 59% 76%)', + 15: 'hsl(122 54% 63%)', + 16: 'hsl(122 50% 51%)', + 17: 'hsl(122 57% 40%)', + 18: 'hsl(122 58% 31%)', + 19: 'hsl(122 66% 22%)', + 20: 'hsl(122 82% 15%)', + }} +/> + +<ColorItem + title="magentaGreenMiddle" + subtitle="dataColors.divergence.magentaGreenMiddle" + colors={{ + 1: 'hsl(334 91% 21%)', + 2: 'hsl(334 75% 32%)', + 3: 'hsl(334 73% 44%)', + 4: 'hsl(334 75% 54%)', + 5: 'hsl(334 84% 64%)', + 6: 'hsl(334 98% 74%)', + 7: 'hsl(334 100% 84%)', + 8: 'hsl(334 100% 92%)', + 9: 'hsl(334 100% 97%)', + 10: 'hsl(334 100% 99%)', + 11: 'hsl(0 0% 98%)', + 12: 'hsl(122 80% 98%)', + 13: 'hsl(122 80% 94%)', + 14: 'hsl(122 73% 87%)', + 15: 'hsl(122 59% 76%)', + 16: 'hsl(122 54% 63%)', + 17: 'hsl(122 50% 51%)', + 18: 'hsl(122 57% 40%)', + 19: 'hsl(122 58% 31%)', + 20: 'hsl(122 66% 22%)', + 21: 'hsl(122 82% 15%)', + }} +/> + +<ColorItem + title="orangePurple" + subtitle="dataColors.divergence.orangePurple" + colors={{ + 1: 'hsl(25 100% 17%)', + 2: 'hsl(26 96% 27%)', + 3: 'hsl(25 100% 35%)', + 4: 'hsl(28 100% 43%)', + 5: 'hsl(29 100% 50%)', + 6: 'hsl(29 96% 60%)', + 7: 'hsl(29 100% 76%)', + 8: 'hsl(29 100% 87%)', + 9: 'hsl(29 100% 95%)', + 10: 'hsl(29 100% 98%)', + 11: 'hsl(263 100% 99%)', + 12: 'hsl(263 100% 97%)', + 13: 'hsl(263 100% 93%)', + 14: 'hsl(263 97% 87%)', + 15: 'hsl(263 96% 78%)', + 16: 'hsl(263 96% 71%)', + 17: 'hsl(263 80% 61%)', + 18: 'hsl(263 65% 52%)', + 19: 'hsl(263 64% 38%)', + 20: 'hsl(263 67% 25%)', + }} +/> + +<ColorItem + title="orangePurpleMiddle" + subtitle="dataColors.divergence.orangePurpleMiddle" + colors={{ + 1: 'hsl(25 100% 17%)', + 2: 'hsl(26 96% 27%)', + 3: 'hsl(25 100% 35%)', + 4: 'hsl(28 100% 43%)', + 5: 'hsl(29 100% 50%)', + 6: 'hsl(29 96% 60%)', + 7: 'hsl(29 100% 76%)', + 8: 'hsl(29 100% 87%)', + 9: 'hsl(29 100% 95%)', + 10: 'hsl(29 100% 98%)', + 11: 'hsl(0 0% 98%)', + 12: 'hsl(263 100% 99%)', + 13: 'hsl(263 100% 97%)', + 14: 'hsl(263 100% 93%)', + 15: 'hsl(263 97% 87%)', + 16: 'hsl(263 96% 78%)', + 17: 'hsl(263 96% 71%)', + 18: 'hsl(263 80% 61%)', + 19: 'hsl(263 65% 52%)', + 20: 'hsl(263 64% 38%)', + 21: 'hsl(263 67% 25%)', + }} +/> + +<ColorItem + title="yellowTeal" + subtitle="dataColors.divergence.yellowTeal" + colors={{ + 1: 'hsl(49 91% 13%)', + 2: 'hsl(49 86% 19%)', + 3: 'hsl(49 89% 26%)', + 4: 'hsl(49 100% 31%)', + 5: 'hsl(49 100% 38%)', + 6: 'hsl(49 100% 44%)', + 7: 'hsl(49 95% 56%)', + 8: 'hsl(49 97% 76%)', + 9: 'hsl(49 86% 91%)', + 10: 'hsl(49 82% 98%)', + 11: 'hsl(180 80% 98%)', + 12: 'hsl(180 83% 93%)', + 13: 'hsl(180 79% 85%)', + 14: 'hsl(180 72% 71%)', + 15: 'hsl(180 62% 57%)', + 16: 'hsl(180 93% 40%)', + 17: 'hsl(180 93% 33%)', + 18: 'hsl(180 89% 26%)', + 19: 'hsl(180 71% 20%)', + 20: 'hsl(180 79% 13%)', + }} +/> + +<ColorItem + title="yellowTealMiddle" + subtitle="dataColors.divergence.yellowTealMiddle" + colors={{ + 1: 'hsl(49 91% 13%)', + 2: 'hsl(49 86% 19%)', + 3: 'hsl(49 89% 26%)', + 4: 'hsl(49 100% 31%)', + 5: 'hsl(49 100% 38%)', + 6: 'hsl(49 100% 44%)', + 7: 'hsl(49 95% 56%)', + 8: 'hsl(49 97% 76%)', + 9: 'hsl(49 86% 91%)', + 10: 'hsl(49 82% 98%)', + 11: 'hsl(0 0% 98%)', + 12: 'hsl(180 80% 98%)', + 13: 'hsl(180 83% 93%)', + 14: 'hsl(180 79% 85%)', + 15: 'hsl(180 72% 71%)', + 16: 'hsl(180 62% 57%)', + 17: 'hsl(180 93% 40%)', + 18: 'hsl(180 93% 33%)', + 19: 'hsl(180 89% 26%)', + 20: 'hsl(180 71% 20%)', + 21: 'hsl(180 79% 13%)', + }} +/> + +</ColorPalette> + +--- + +## Typography + +<Unstyled> + +<div style={{ display: 'flex', flexDirection:"column" , justifyContent: 'center'}}> + + <div style={{ marginRight: '10px' }}> + <table border="1"> + <tbody className="align-top"> + <tr> + <th className="min-w-[8rem]">font-family</th> + <td> + <div className="font-sans"> + font-sans(default) + [Inter font](https://fonts.google.com/specimen/Inter) + Alternatives: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, "Helvetica Neue", Arial, "NotoSans", sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" + </div> + <div className="font-mono"> + font-mono + [Robot mono](https://fonts.google.com/specimen/Roboto+Mono) Alternatives: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; + </div> + <div className="font-mono bg-secondary-100 p-3"> + font-mono: Useful to display and compare numbers: + <span className="font-mono">11230</span> + <span className="font-mono">1230</span> + </div> + </td> + </tr> + <tr> + <th>Font Weight</th> + <td> + <div className="flex flex-col"> + <span className="font-normal">font-normal</span> + <span className="font-semibold">font-semibold</span> + <span className="font-bold">font-bold</span> + </div> + </td> + </tr> + <tr> + <th>Font Size</th> + <td> + <div className="flex flex-col"> + <span className="text-3xs">text-3xs</span> + <span className="text-2xs">text-2xs</span> + <span className="text-xs">text-xs</span> + <span className="text-sm">text-sm</span> + <span className="text-base">text-base</span> + <span className="text-lg">text-lg</span> + <span className="text-xl">text-xl</span> + <span className="text-2xl">text-2xl</span> + <span className="text-3xl">text-3xl</span> + <span className="text-4xl">text-4xl</span> + <span className="text-5xl">text-5xl</span> + </div> + </td> + </tr> + </tbody> + </table> + + </div> + +</div> + +</Unstyled> + +## Icons + +GraphPolaris uses [Material UI](https://mui.com/material-ui/material-icons/) through an Icon component, where the Material UI icon is specified along its size: + +<div className="w-32 m-5 mx-auto"> + <Icon name="ArrowBack" size={32} /> +</div> + +```jsx +<Icon name="ArrowBack" size={32} /> +``` + +There are 5 types of Icons in Material UI: + +<IconGallery> + <IconItem name="Filled-Default"> + <DeleteIcon /> + </IconItem> + <IconItem name="Outlined"> + <DeleteOutlinedIcon /> + </IconItem> + <IconItem name="Rounded"> + <DeleteRoundedIcon /> + </IconItem> + <IconItem name="TwoTone"> + <DeleteTwoToneIcon /> + </IconItem> + <IconItem name="Sharp"> + <DeleteSharpIcon /> + </IconItem> +</IconGallery> + +GP, mainly, use <b>Filled</b> and <b>Outlined</b>. + +We ditinguish two usage scenarios: + +- General app view. Elements already in use: + +<IconGallery> + <IconItem name="Settings"> + <SettingsIcon /> + </IconItem> + <IconItem name="PhotoCamera"> + <PhotoCameraIcon /> + </IconItem> + <IconItem name="Add"> + <AddIcon /> + </IconItem> + <IconItem name="Fullscreen"> + <FullscreenIcon /> + </IconItem> + <IconItem name="FullscreenExit"> + <FullscreenExitIcon /> + </IconItem> + <IconItem name="Content Copy"> + <ContentCopyIcon /> + </IconItem> + <IconItem name="Cached"> + <CachedIcon /> + </IconItem> + <IconItem name="Info"> + <InfoIcon /> + </IconItem> + <IconItem name="Error Outline"> + <ErrorOutlineIcon /> + </IconItem> + <IconItem name="CheckCircleOutline"> + <CheckCircleOutlineIcon /> + </IconItem> + <IconItem name="Search"> + <SearchIcon /> + </IconItem> +<IconItem name="ArrowDropDown"> + <ArrowDropDownIcon /> +</IconItem> +<IconItem name="ArrowDropUp"> + <ArrowDropUpIcon /> +</IconItem> +<IconItem name="ArrowBack"> + <ArrowBackIcon /> +</IconItem> +<IconItem name="ArrowForward"> + <ArrowForwardIcon /> +</IconItem> +<IconItem name="Close"> + <CloseIcon /> +</IconItem> +<IconItem name="Cancel"> + <CancelIcon /> +</IconItem> +<IconItem name="KeyboardArrowDown"> + <KeyboardArrowDownIcon /> +</IconItem> +<IconItem name="KeyboardArrowLeft"> + <KeyboardArrowLeftIcon /> +</IconItem> +<IconItem name="KeyboardArrowRight"> + <KeyboardArrowRightIcon /> +</IconItem> +<IconItem name="KeyboardArrowUp"> + <KeyboardArrowUpIcon /> +</IconItem> + +</IconGallery> + +- Per component, Button component can have an icon (for more information go to Button's document): + +<div className="w-32 m-5 mx-auto"> + <Button + type="primary" + label="Next" + iconName="ArrowForward" + iconPosition="leading" + onClick={() => { + // Add your code to handle the button click event here + console.log('Button clicked!'); + // You can perform any desired actions or state updates here + }} + /> +</div> + +## Border radius + +<div style={{ display: 'flex', justifyContent: 'center' }}> + <table> + <thead> + <tr> + <th>Class</th> + <th>borderRadius-None</th> + <th>borderRadius-sm</th> + <th>borderRadius-Default</th> + <th>borderRadius-md</th> + <th>borderRadius-lg</th> + <th>borderRadius-xl</th> + <th>borderRadius-2xl</th> + <th>borderRadius-3xl</th> + <th>borderRadius-full</th> + </tr> + </thead> + <tbody> + <tr> + <td>Value</td> + <td align="center">0</td> + <td align="center">0.125rem</td> + <td align="center">0.25rem</td> + <td align="center">0.375rem</td> + <td align="center">0.5rem</td> + <td align="center">0.75rem</td> + <td align="center">1rem</td> + <td align="center">1.5rem</td> + <td align="center">9999px</td> + </tr> + <tr> + <td>Example</td> + <td align="center"> + <div className="p-11 border-2 border-black bg-primary-light rounded-none"></div> + </td> + <td align="center"> + <div className="p-11 border-2 border-black bg-primary-light rounded-sm"></div> + </td> + <td align="center"> + <div className="p-11 border-2 border-black bg-primary-light rounded"></div> + </td> + <td align="center"> + <div className="p-11 border-2 border-black bg-primary-light rounded-md"></div> + </td> + <td align="center"> + <div className="p-11 border-2 border-black bg-primary-light rounded-lg"></div> + </td> + <td align="center"> + <div className="p-11 border-2 border-black bg-primary-light rounded-xl"></div> + </td> + <td align="center"> + <div className="p-11 border-2 border-black bg-primary-light rounded-2xl"></div> + </td> + <td align="center"> + <div className="p-11 border-2 border-black bg-primary-light rounded-3xl"></div> + </td> + <td align="center"> + <div className="p-11 border-2 border-black bg-primary-light rounded-full"></div> + </td> + </tr> + </tbody> + </table> +</div> + +## Spacing + +GP uses default Tailwind spacing. + +```jsx +<div className="space-x-2"></div> +``` + +<table style={{ width: '100%' }}> + <thead> + <tr> + <th style={{ width: '10%' }}>Class</th> + <th style={{ width: '30%' }}>Value</th> + <th style={{ width: '60%' }}>Example</th> + </tr> + </thead> + <tbody> + <tr> + <td align="center">4</td> + <td align="center">0.4rem</td> + <td align="center"> + <div className="pb-4 w-160 bg-secondary-light border-2 border-black-500"></div> + </td> + </tr> + <tr> + <td align="center">8</td> + <td align="center">0.8rem</td> + <td align="center"> + <div className="pb-8 w-160 bg-secondary-light border-2 border-black-500"></div> + </td> + </tr> + <tr> + <td align="center">12</td> + <td align="center">1.2rem</td> + <td align="center"> + <div className="pb-12 w-160 bg-secondary-light border-2 border-black-500"></div> + </td> + </tr> + <tr> + <td align="center">16</td> + <td align="center">1.6rem</td> + <td align="center"> + <div className="pb-16 w-160 bg-secondary-light border-2 border-black-500"></div> + </td> + </tr> + <tr> + <td align="center">24</td> + <td align="center">2.4rem</td> + <td align="center"> + <div className="pb-24 w-160 bg-secondary-light border-2 border-black-500"></div> + </td> + </tr> + <tr> + <td align="center">32</td> + <td align="center">3.2rem</td> + <td align="center"> + <div className="pb-32 w-160 bg-secondary-light border-2 border-black-500"></div> + </td> + </tr> + </tbody> +</table> + +## Shadows + +The general guide is to not use shadows in the components. However in inputs and dropdowns are helpful e.g.: + +<div style={{ display: 'flex', justifyContent: 'center' }}> + +<div className="w-52 m-5"> + <Text + label="Username" + placeholder="Enter your username" + value="" + required={true} + errorText="Username is required" + validate={() => console.log('d')} + onChange={() => console.log('d')} + type="text"/> +</div> +</div> + +## Develop components + +To add a new component, include the following files: + +- "index.tsx": component's code. +- "componentName.stories.tsx": Story's definition. On tag Meta defines the location of the component in the Storybook dashboard e.g.: title: 'Components/Button' +- "overview.mdx": Component's documentation. diff --git a/libs/shared/lib/components/Legend.tsx b/libs/shared/lib/components/Legend.tsx index 9b3f4a7ef04e9ae1bcb41f403ed33f324924743b..33aedb864e05ab64e098dddf47c45aa9d381ccc8 100644 --- a/libs/shared/lib/components/Legend.tsx +++ b/libs/shared/lib/components/Legend.tsx @@ -12,12 +12,12 @@ type Props = { const DEFAULT_TITLE = 'Legend'; const DEFAULT_SCREEN_POSITION = 'top-2 right-2'; -export const Legend =({ title = DEFAULT_TITLE, screenPosition = DEFAULT_SCREEN_POSITION, open, setOpen, children }: Props) => { +export const Legend = ({ title = DEFAULT_TITLE, screenPosition = DEFAULT_SCREEN_POSITION, open, setOpen, children }: Props) => { const [expand, setExpand] = React.useState<boolean>(true); return ( open && ( - <div className={`absolute ${screenPosition} z-20 bg-white w-60`}> + <div className={`absolute ${screenPosition} z-20 bg-light w-60`}> <div className="px-4 py-3 flex justify-between align-middle"> <h4 className="font-bold">{title}</h4> <div> @@ -33,4 +33,4 @@ export const Legend =({ title = DEFAULT_TITLE, screenPosition = DEFAULT_SCREEN_P </div> ) ); -} +}; diff --git a/libs/shared/lib/components/LoadingSpinner.tsx b/libs/shared/lib/components/LoadingSpinner.tsx index 32094a661309468b6a89ad20d986c1c9d2b15c77..69b32fccc72d0c267028e430045c583c76ddeddf 100644 --- a/libs/shared/lib/components/LoadingSpinner.tsx +++ b/libs/shared/lib/components/LoadingSpinner.tsx @@ -5,7 +5,7 @@ export const LoadingSpinner = (props: PropsWithChildren) => { <div className="text-sm"> <svg aria-hidden="true" - className="inline w-6 h-6 mr-2 text-gray-200 animate-spin fill-entity-600" + className="inline w-6 h-6 mr-2 text-secondary-200 animate-spin fill-accent-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg" diff --git a/libs/shared/lib/components/Popup.tsx b/libs/shared/lib/components/Popup.tsx index 9066287af25880719faf27c5f053a41fb81d1d8e..5f62f11ccfa9d8747e65dc040b6dfa42b06a2e14 100644 --- a/libs/shared/lib/components/Popup.tsx +++ b/libs/shared/lib/components/Popup.tsx @@ -15,7 +15,7 @@ export const Popup = (props: { <div ref={ref} className={ - 'absolute z-50 max-w-[20rem] bg-white flex flex-col gap-2 animate-openmenu p-0 m-0 ' + (props.className ? props.className : '') + 'absolute z-50 max-w-[20rem] bg-light flex flex-col gap-2 animate-openmenu p-0 m-0 ' + (props.className ? props.className : '') } style={props.hAnchor === 'right' ? { left: props.offset || 0 } : { right: props.offset || 0 }} > diff --git a/libs/shared/lib/components/buttons/button.stories.tsx b/libs/shared/lib/components/buttons/button.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e250be5faca1a680f9475e88d04c88e543b16abe --- /dev/null +++ b/libs/shared/lib/components/buttons/button.stories.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from '.'; + +const meta: Meta<typeof Button> = { + title: 'Components/Button', + component: Button, + decorators: [(Story) => <div className="p-5">{Story()}</div>], + argTypes: { + type: { + control: 'select', + options: ['primary', 'secondary', 'danger'], + }, + variant: { + control: 'select', + options: ['solid', 'outline', 'ghost'], + }, + size: { + control: 'select', + options: ['xs', 'sm', 'md', 'lg'], + }, + label: { + control: 'text', + }, + disabled: { + control: 'boolean', + }, + block: { + control: 'boolean', + }, + rounded: { + control: 'boolean', + }, + iconName: { + control: 'select', + options: ['ArrowBack', 'DeleteOutline', 'KeyboardArrowLeft', 'Settings'], + }, + iconPosition: { + control: 'select', + options: ['leading', 'trailing'], + }, + }, +} as Meta; + +export default meta; +type Story = StoryObj<typeof Button>; + +const BaseStory: Story = { + args: { + type: 'primary', + variant: 'solid', + label: 'Click me', + size: 'md', + disabled: false, + block: false, + rounded: false, + }, +}; + +export const Primary = { ...BaseStory }; + +export const Secondary: Story = { + args: { + ...BaseStory.args, + type: 'secondary', + }, +}; + +export const Danger: Story = { + args: { + ...BaseStory.args, + type: 'danger', + }, +}; + +export const Solid: Story = { + args: { + ...BaseStory.args, + variant: 'solid', + }, +}; + +export const Outline: Story = { + args: { + ...BaseStory.args, + variant: 'outline', + }, +}; + +export const Ghost: Story = { + args: { + ...BaseStory.args, + variant: 'ghost', + }, +}; + +export const IconButton: Story = { + args: { + type: 'primary', + variant: 'outline', + iconName: 'ArrowBack', + rounded: true, + }, +}; diff --git a/libs/shared/lib/components/buttons/buttons.module.scss b/libs/shared/lib/components/buttons/buttons.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..eae142ad98aa19373590136969c78712dc33f0d8 --- /dev/null +++ b/libs/shared/lib/components/buttons/buttons.module.scss @@ -0,0 +1,130 @@ +.btn { + --btn-color: var(--btn-secondary); + --btn-color-light-hover: var(--btn-secondary--dark); + --btn-text: var(--clr-light); + --btn-text-hover: var(--clr-light); + --btn-bg-color: hsl(var(--btn-color)); + --btn-bg: var(--btn-color) / var(--btn-bg-opacity); + --btn-border: var(--btn-color); + + color: hsl(var(--btn-text)); + border-color: hsl(var(--btn-border)); + background-color: hsl(var(--btn-bg)); + &:hover { + color: hsl(var(--btn-text)); + border-color: hsl(var(--btn-border)); + background-color: hsl(var(--btn-bg)); + } + &:active { + opacity: 0.88; + } + padding: 0.25em 0.75em; + span { + overflow: hidden; + text-overflow: ellipsis; + } + svg { + width: 1em; + height: 1em; + fill: currentColor; + } + + @apply font-semibold cursor-pointer duration-100 ease-in-out transition rounded border flex flex-row items-center justify-center whitespace-nowrap; + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 4px hsl(var(--focus-shadow-clr) / 20%); + } + + &:disabled { + --btn-color: var(--clr-sec--300) !important; + --btn-text: var(--clr-sec--800) !important; + @apply cursor-not-allowed; + } +} + +.btn-lg { + @apply text-lg h-10 gap-1.5; + &.btn-icon-only { + @apply w-10; + } +} +.btn-md { + @apply text-base h-9 gap-1; + &.btn-icon-only { + @apply w-9; + } +} +.btn-sm { + @apply text-sm h-8 gap-1; + &.btn-icon-only { + @apply w-8; + } +} +.btn-xs { + @apply text-xs h-6 gap-0.5; + &.btn-icon-only { + @apply w-6; + } +} + +.btn-primary { + --btn-color: var(--clr-pri--600); + &:hover { + --btn-color: var(--clr-pri--700); + --btn-color-light-hover: var(--clr-pri--600); + } +} + +.btn-secondary { + --btn-color: var(--clr-sec--700); + &:hover { + --btn-color: var(--clr-sec--800); + --btn-color-light-hover: var(--clr-sec--700); + } +} +.btn-danger { + --btn-color: var(--clr-dang--600); + &:hover { + --btn-color: var(--clr-dang--700); + --btn-color-light-hover: var(--clr-dang--600); + } +} +.btn-solid { + --btn-text: var(--clr-light); + --btn-bg: var(--btn-color); + &:hover { + --btn-text: var(--clr-light); + --btn-bg: var(--btn-color); + } + &:disabled { + opacity: 0.5 !important; + --btn-text: var(--clr-sec--800) !important; + } +} + +.btn-outline, +.btn-ghost { + --btn-bg: var(--btn-color) / 0; + --btn-text: var(--btn-color); + &:hover { + --btn-bg: var(--btn-color-light-hover) / 0.1; + --btn-text: var(--btn-color); + } + &:disabled { + --btn-text: var(--clr-sec--300) !important; + --btn-bg: var(--btn-color) / 0; + } +} + +.btn-ghost { + --btn-border: var(--btn-color) / 0; +} + +.btn-block { + @apply w-full; +} + +.btn-rounded { + @apply rounded-full; +} diff --git a/libs/shared/lib/components/buttons/buttons.module.scss.d.ts b/libs/shared/lib/components/buttons/buttons.module.scss.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..3165183bcc43cd371b85d32e546d2904b856a67e --- /dev/null +++ b/libs/shared/lib/components/buttons/buttons.module.scss.d.ts @@ -0,0 +1,18 @@ +declare const classNames: { + readonly btn: 'btn'; + readonly 'btn-lg': 'btn-lg'; + readonly '5': '5'; + readonly 'btn-icon-only': 'btn-icon-only'; + readonly 'btn-md': 'btn-md'; + readonly 'btn-sm': 'btn-sm'; + readonly 'btn-xs': 'btn-xs'; + readonly 'btn-primary': 'btn-primary'; + readonly 'btn-secondary': 'btn-secondary'; + readonly 'btn-danger': 'btn-danger'; + readonly 'btn-solid': 'btn-solid'; + readonly 'btn-outline': 'btn-outline'; + readonly 'btn-ghost': 'btn-ghost'; + readonly 'btn-block': 'btn-block'; + readonly 'btn-rounded': 'btn-rounded'; +}; +export = classNames; diff --git a/libs/shared/lib/components/buttons/index.tsx b/libs/shared/lib/components/buttons/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2100b11f8e3a6d19732da62854700d28282e88cd --- /dev/null +++ b/libs/shared/lib/components/buttons/index.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import styles from './buttons.module.scss'; +import { Icon, Sizes } from '../icon'; + +type ButtonProps = { + type?: 'primary' | 'secondary' | 'danger'; + variant?: 'solid' | 'outline' | 'ghost'; + size?: 'xs' | 'sm' | 'md' | 'lg'; + label?: string; + rounded?: boolean; + disabled?: boolean; + block?: boolean; + onClick: (e: any) => void; + iconName?: string; + iconPosition?: 'leading' | 'trailing'; + ariaLabel?: string; + children?: React.ReactNode; +}; + +export function Button({ + type = 'secondary', + variant = 'solid', + label, + size = 'md', + rounded = false, + disabled = false, + onClick, + block = false, + iconName, + iconPosition = 'leading', + ariaLabel, + children, + ...props +}: ButtonProps & React.HTMLAttributes<HTMLButtonElement>) { + let typeClass = ''; + let variantClass = ''; + let sizeClass = ''; + const blockClass = block ? styles['btn-block'] : ''; + const roundedClass = rounded ? styles['btn-rounded'] : ''; + + switch (type) { + case 'primary': + typeClass = styles['btn-primary']; + break; + case 'secondary': + typeClass = styles['btn-secondary']; + break; + case 'danger': + typeClass = styles['btn-danger']; + break; + default: + return null; + } + + switch (variant) { + case 'solid': + variantClass = styles['btn-solid']; + break; + case 'outline': + variantClass = styles['btn-outline']; + break; + case 'ghost': + variantClass = styles['btn-ghost']; + break; + default: + return null; + } + + let iconSize: Sizes = 24; + + switch (size) { + case 'xs': + sizeClass = styles['btn-xs']; + iconSize = 16; + break; + case 'sm': + sizeClass = styles['btn-sm']; + iconSize = 20; + break; + case 'md': + sizeClass = styles['btn-md']; + iconSize = 24; + break; + case 'lg': + sizeClass = styles['btn-lg']; + iconSize = 28; + break; + default: + return null; + } + + const iconComponent = iconName ? <Icon name={iconName} size={iconSize} /> : null; + + const iconOnlyClass = iconName && !label && !children ? styles['btn-icon-only'] : ''; + + return ( + <button + className={`${styles.btn} ${typeClass} ${variantClass} ${sizeClass} ${blockClass} ${roundedClass} ${iconOnlyClass}`} + onClick={onClick} + disabled={disabled} + aria-label={ariaLabel} + {...props} + > + {iconPosition === 'leading' && iconComponent} + {label && <span>{label}</span>} + {children && <span>{children}</span>} + {iconPosition === 'trailing' && iconComponent} + </button> + ); +} + +type ButtonGroupProps = { + children: React.ReactNode; +}; + +export function ButtonGroup({ children }: ButtonGroupProps) { + return <div>{children}</div>; +} + +type ButtonItemProps = { + icon: React.ReactNode; + onClick: () => void; +}; + +export function ButtonGroupItem({ icon, onClick }: ButtonItemProps) { + return ( + <div onClick={onClick} className="rounded-sm bg-secondary-50 p-2"> + <span>{icon}</span> + </div> + ); +} diff --git a/libs/shared/lib/components/buttons/overview.mdx b/libs/shared/lib/components/buttons/overview.mdx new file mode 100644 index 0000000000000000000000000000000000000000..2739cf127245fb97d27209ae8c89c08282178e55 --- /dev/null +++ b/libs/shared/lib/components/buttons/overview.mdx @@ -0,0 +1,75 @@ +import { Canvas, Meta, Story } from '@storybook/blocks'; + +import * as ButtonStories from './button.stories'; +import { Button } from '.'; + +<Meta title="Components/Button" component={Button} /> + +# Button + +A button is used a lot in GP. + +[//]: # '<Canvas of={ButtonStories.Primary} />' + +<Canvas> + <Story id="components-button--primary" name="Primary Button"> + {ButtonStories.Primary.args} + </Story> + <Story id="components-button--secondary" name="Secondary Button"> + {ButtonStories.Secondary.args} + </Story> + <Story id="components-button--danger" name="Danger Button"> + {ButtonStories.Danger.args} + </Story> +</Canvas> + +<Canvas> + <Story id="components-button--solid" name="Solid Button"> + {ButtonStories.Solid.args} + </Story> + <Story id="components-button--outline" name="Outline Button"> + {ButtonStories.Outline.args} + </Story> + <Story id="components-button--ghost" name="Ghost Button"> + {ButtonStories.Ghost.args} + </Story> +</Canvas> + +<Canvas> + <Story id="components-button--icon-button" name="Icon Button"> + {ButtonStories.IconButton.args} + </Story> +</Canvas> + +<div className="grid grid-cols-2 gap-4"> + + <div className="flex flex-col gap-3"> + <Button type="primary" variant="solid" label="Click me" onClick={() => alert('Button clicked')} /> + <Button type="primary" variant="outline" label="Click me" onClick={() => alert('Button clicked')} /> + <Button type="primary" variant="ghost" label="Click me" onClick={() => alert('Button clicked')} /> + </div> + <div className="flex flex-col gap-3"> + <Button type="secondary" variant="solid" label="Click me" onClick={() => alert('Button clicked')} /> + <Button type="secondary" variant="outline" label="Click me" onClick={() => alert('Button clicked')} /> + <Button type="secondary" variant="ghost" label="Click me" onClick={() => alert('Button clicked')} /> + </div> + <div className="flex flex-col gap-3"> + <Button type="danger" variant="solid" label="Click me" onClick={() => alert('Button clicked')} /> + <Button type="danger" variant="outline" label="Click me" onClick={() => alert('Button clicked')} /> + <Button type="danger" variant="ghost" label="Click me" onClick={() => alert('Button clicked')} /> + </div> + <div className="flex flex-col gap-3"> + <Button type="primary" variant="solid" label="Click me" onClick={() => alert('Button clicked')} disabled /> + <Button type="secondary" variant="outline" label="Click me" onClick={() => alert('Button clicked')} disabled /> + <Button type="danger" variant="ghost" label="Click me" onClick={() => alert('Button clicked')} disabled /> + </div> + <div className="flex flex-row gap-3 col-span-2"> + <Button type="primary" size="lg" label="Click me" iconName="NavigateBefore" onClick={() => alert('Button clicked')} /> + <Button type="primary" size="md" label="Click me" iconName="ArrowForward" iconPlacement="trailing" onClick={() => alert('Button clicked')} /> + <Button type="danger" size="sm" label="Click me" iconName="DeleteOutline" onClick={() => alert('Button clicked')} /> + <Button type="primary" size="xs" label="Click me" rounded onClick={() => alert('Button clicked')} /> + </div> + <div className="col-span-2"> + <Button block type="primary" size="md" label="Click me" onClick={() => alert('Button clicked')} /> + </div> +</div> diff --git a/libs/shared/lib/components/charts/barplot/barplot.stories.tsx b/libs/shared/lib/components/charts/barplot/barplot.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..947810b4d6611c652f8e5dc0868fd9c83f0add8e --- /dev/null +++ b/libs/shared/lib/components/charts/barplot/barplot.stories.tsx @@ -0,0 +1,38 @@ +// BarPlot.stories.tsx +import React from 'react'; +import { Meta, Story } from '@storybook/react'; +import BarPlot, { BarPlotProps } from '.'; + +const Component: Meta<typeof BarPlot> = { + title: 'Visual charts/Charts/BarPlot', + component: BarPlot, + decorators: [(story) => <div style={{ width: '100%', height: '100vh' }}>{story()}</div>], +}; + +export default Component; + +export const CategoricalData = { + args: { + data: [ + { category: 'Category A', count: 10 }, + { category: 'Category B', count: 20 }, + { category: 'Category C', count: 15 }, + ], + numBins: 5, + typeBarPlot: 'categorical', + }, +}; + +export const NumericalData = { + args: { + data: [ + { category: 'Data Point 1', count: 3 }, + { category: 'Data Point 2', count: 7 }, + { category: 'Data Point 3', count: 2 }, + { category: 'Data Point 4', count: 5 }, + { category: 'Data Point 5', count: 8 }, + ], + numBins: 5, + typeBarPlot: 'numerical', + }, +}; diff --git a/libs/shared/lib/vis/table_vis/components/BarPlot.tsx b/libs/shared/lib/components/charts/barplot/index.tsx similarity index 87% rename from libs/shared/lib/vis/table_vis/components/BarPlot.tsx rename to libs/shared/lib/components/charts/barplot/index.tsx index ade23d96320b730a5cf7129cc4ea1663529a33c4..54fe22869f1bb84271834e14291f8dc02070f24d 100644 --- a/libs/shared/lib/vis/table_vis/components/BarPlot.tsx +++ b/libs/shared/lib/components/charts/barplot/index.tsx @@ -1,7 +1,7 @@ import React, { LegacyRef, useEffect, useRef, useState } from 'react'; import * as d3 from 'd3'; -type BarPlotProps = { +export type BarPlotProps = { data: { category: string; count: number }[]; numBins: number; typeBarPlot: 'numerical' | 'categorical'; @@ -9,6 +9,7 @@ type BarPlotProps = { export const BarPlot = ({ typeBarPlot: typeBarplot, numBins, data }: BarPlotProps) => { const svgRef = useRef<SVGSVGElement | null>(null); + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); useEffect(() => { if (!svgRef.current) return; @@ -16,6 +17,8 @@ export const BarPlot = ({ typeBarPlot: typeBarplot, numBins, data }: BarPlotProp const widthSVG: number = +svgRef.current.clientWidth; const heightSVG: number = +svgRef.current.clientHeight; + setDimensions({ width: widthSVG, height: heightSVG }); + const marginPercentage = { top: 0.15, right: 0.15, bottom: 0.15, left: 0.15 }; const margin = { top: marginPercentage.top * heightSVG, @@ -44,7 +47,7 @@ export const BarPlot = ({ typeBarPlot: typeBarplot, numBins, data }: BarPlotProp .domain( dataSorted.map(function (d) { return d.category; - }) + }), ) .padding(0.2); @@ -59,7 +62,7 @@ export const BarPlot = ({ typeBarPlot: typeBarplot, numBins, data }: BarPlotProp .attr('y', (d) => yScale(d.count)) .attr('width', xScale.bandwidth()) .attr('height', (d) => heightSVGwithinMargin - yScale(d.count)) - .attr('class', 'fill-primary stroke-white'); + .attr('class', 'fill-primary stroke-light'); const yAxis = d3.axisLeft(yScale).ticks(5); groupMargin.append('g').call(yAxis); @@ -104,7 +107,7 @@ export const BarPlot = ({ typeBarPlot: typeBarplot, numBins, data }: BarPlotProp .enter() .append('rect') .attr('x', 1) - .attr('class', 'fill-primary stroke-white') + .attr('class', 'fill-primary stroke-light') .attr('transform', (d) => 'translate(' + xScale(d.x0 || 0) + ',' + yScale(d.length) + ')') .attr('width', (d) => xScale(d.x1 || 0) - xScale(d.x0 || 0) - 1) .attr('height', (d) => heightSVGwithinMargin - yScale(d.length)); @@ -117,7 +120,7 @@ export const BarPlot = ({ typeBarPlot: typeBarplot, numBins, data }: BarPlotProp .call(xAxis); } - svgPlot.selectAll('.tick text').attr('class', 'font-inter text-primary font-semibold').style('stroke', 'none'); + svgPlot.selectAll('.tick text').attr('class', 'font-sans text-primary font-semibold').style('stroke', 'none'); groupMargin .append('rect') @@ -142,7 +145,14 @@ export const BarPlot = ({ typeBarPlot: typeBarplot, numBins, data }: BarPlotProp return ( <div className="svg"> - <svg ref={svgRef} className="container" width="100%" height="100%"></svg> + <svg + ref={svgRef} + className="container" + width="100%" + height="100%" + preserveAspectRatio="xMidYMid meet" + viewBox={`0 0 ${dimensions.width} ${dimensions.height}`} + ></svg> </div> ); }; diff --git a/libs/shared/lib/components/charts/colorLegendCat/colorLegendCat.stories.tsx b/libs/shared/lib/components/charts/colorLegendCat/colorLegendCat.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4a643a82043b67655bbd7585ed7681b71ad0a73e --- /dev/null +++ b/libs/shared/lib/components/charts/colorLegendCat/colorLegendCat.stories.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Meta } from '@storybook/react'; +import ColorLegendCat, { LegendProps } from '.'; + +export default { + title: 'Visual charts/Charts/ColorLegendCat', + component: ColorLegendCat, + decorators: [(story) => <div style={{ margin: '0 auto', width: '200px', height: '300px' }}>{story()}</div>], +} as Meta<typeof ColorLegendCat>; + +export const Default = { + args: { + items: [ + { text: 'Category A', color: 'red' }, + { text: 'Category B', color: 'blue' }, + { text: 'Category C', color: 'green' }, + ], + }, +}; diff --git a/libs/shared/lib/components/charts/colorLegendCat/index.tsx b/libs/shared/lib/components/charts/colorLegendCat/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..868d5fed5e81745a0948995c9d6bd0b0972955cd --- /dev/null +++ b/libs/shared/lib/components/charts/colorLegendCat/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +interface LegendItem { + text: string; + color: string; +} + +export interface LegendProps { + items: LegendItem[]; +} + +const ColorLegendCat: React.FC<LegendProps> = ({ items }) => { + return ( + <div className="flex flex-col items-start space-y-2 font-sans bg-light p-4 rounded-lg shadow-md overflow-x-hidden truncate"> + {items.map((item, index) => ( + <div key={index} className="flex items-center space-x-2"> + <div className="w-4 h-4 rounded-full" style={{ backgroundColor: item.color }}></div> + <span>{item.text}</span> + </div> + ))} + </div> + ); +}; + +export default ColorLegendCat; diff --git a/libs/shared/lib/components/charts/colorLegendSeqDiv/colorLegendSeqDiv.stories.tsx b/libs/shared/lib/components/charts/colorLegendSeqDiv/colorLegendSeqDiv.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d4f151be57be68bc9e90a7c46450d6802781eecf --- /dev/null +++ b/libs/shared/lib/components/charts/colorLegendSeqDiv/colorLegendSeqDiv.stories.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Meta } from '@storybook/react'; + +import ColorLegend, { ColorLegendProps } from '.'; + +export default { + title: 'Visual charts/Charts/ColorLegend', + component: ColorLegend, + decorators: [(story) => <div style={{ margin: '0 auto', width: '600px', height: '300px' }}>{story()}</div>], +} as Meta<typeof ColorLegend>; + +export const Default = { + args: { + colors: [ + '220deg 80% 98%', + '220deg 71% 96%', + '220deg 95% 92%', + '220deg 92% 85%', + '220deg 94% 75%', + '220deg 92% 67%', + '220deg 84% 58%', + '220deg 79% 49%', + '220deg 86% 36%', + '220deg 80% 23%', + '220deg 84% 17%', + '220deg 61% 13%', + ], + data: { min: 0, max: 100 }, + tickCount: 5, + name: 'seq:blue', + }, +}; + +/* +export const SequentialBlues = Template.bind({}); +Default.args = { + colors: divergenceColors.blueRed, + data: { min: 0, max: 100 }, + tickCount: 5, + name: 'div:blue', +}; + +export const CustomColors = Template.bind({}); +CustomColors.args = { + colors: divergenceColors.blueRed, + data: { min: 0, max: 100 }, + tickCount: 5, + name: 'custom-colors', +}; +*/ diff --git a/libs/shared/lib/components/charts/colorLegendSeqDiv/index.tsx b/libs/shared/lib/components/charts/colorLegendSeqDiv/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4fd1f9cbe20a831514122a58ddbfda826a4e7a36 --- /dev/null +++ b/libs/shared/lib/components/charts/colorLegendSeqDiv/index.tsx @@ -0,0 +1,93 @@ +import React, { useEffect, useRef } from 'react'; +import * as d3 from 'd3'; +//import { tailwindColors, dataColors, divergenceColors, categoricalColors } from './../../../../../config/src/colors.js'; +//import { tailwindColors, dataColors, divergenceColors, categoricalColors } from '@graphpolaris/config/colors.js'; + +export type ColorLegendProps = { + colors: string[], + data: { min: number; max: number }, + name: string, + tickCount?: number // Optional prop for specifying tick count +} + +export const ColorLegend = ({ colors, data, tickCount = 5, name }: ColorLegendProps) => { + const svgRef = useRef<SVGSVGElement | null>(null); + useEffect(() => { + if (!svgRef.current) return; + + console.log(colors); + //console.log(divergenceColors.blueRed); + + const widthSVG: number = +svgRef.current.clientWidth; + const heightSVG: number = +svgRef.current.clientHeight; + + // Parse HSL strings + const hslValues = colors.map((hslString) => { + const matches = hslString.match(/(\d+)deg (\d+)% (\d+)%/); + if (matches && matches.length === 4) { + const [h, s, l] = matches.slice(1).map(Number); + return `hsl(${h}, ${s}%, ${l}%)`; + } else { + return null; + } + }); + + // Set up SVG container + const svg = d3.select(svgRef.current); + svg.selectAll('*').remove(); // Clear previous content + + const marginPercentage = { top: 0.15, right: 0.1, bottom: 0.15, left: 0.1 }; + const margin = { + top: marginPercentage.top * heightSVG, + right: marginPercentage.right * widthSVG, + bottom: marginPercentage.bottom * heightSVG, + left: marginPercentage.left * widthSVG, + }; + + const groupMargin = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`); + const widthSVGwithinMargin: number = widthSVG - margin.left - margin.right; + const heightSVGwithinMargin: number = heightSVG - margin.top - margin.bottom; + + // Create a gradient + const gradient = groupMargin + .append('defs') + .append('linearGradient') + .attr('id', `clrGradient_${name}`) + .attr('x1', '0%') + .attr('y1', '0%') + .attr('x2', '100%') + .attr('y2', '0%'); + + // Add color stops to the gradient + for (let i = 0; i < hslValues.length; i++) { + gradient + .append('stop') + .attr('offset', `${(i / (hslValues.length - 1)) * 100}%`) + .style('stop-color', hslValues[i] as string); + } + + groupMargin + .append('rect') + .attr('width', widthSVGwithinMargin) + .attr('height', heightSVGwithinMargin) + .style('stroke', 'black') + .style('fill', `url(#clrGradient_${name})`); + + const xScale = d3.scaleLinear().domain([data.min, data.max]).range([0, widthSVGwithinMargin]); + + const xAxis = d3.axisBottom(xScale).ticks(tickCount); + + groupMargin.append('g').attr('class', 'x-axis').attr('transform', `translate(0, ${heightSVGwithinMargin})`).call(xAxis); + + svg.selectAll('.tick text').attr('class', 'font-sans text-primary font-semibold').style('stroke', 'none'); + }, [colors, data, tickCount]); + + //return <svg ref={svgRef} className="container" width="100%" height="100%" />; + return ( + <div className="svg"> + <svg ref={svgRef} className="container" width="100%" height="100%"></svg> + </div> + ); +}; + +export default ColorLegend; diff --git a/libs/shared/lib/components/charts/scatterplot/index.tsx b/libs/shared/lib/components/charts/scatterplot/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d65989f00d9587ce754f6586754c2f6875b02ff1 --- /dev/null +++ b/libs/shared/lib/components/charts/scatterplot/index.tsx @@ -0,0 +1,35 @@ +import React, { useEffect, useRef } from 'react'; +import * as d3 from 'd3'; + +export interface ScatterplotProps { + data: { x: number; y: number }[]; +} + +const Scatterplot: React.FC<ScatterplotProps> = ({ data }) => { + const svgRef = useRef<SVGSVGElement | null>(null); + + useEffect(() => { + if (svgRef.current) { + const svg = d3.select(svgRef.current); + + svg + .selectAll('circle') + .data(data) + .enter() + .append('circle') + .attr('cx', (d) => d.x * 30) + .attr('cy', (d) => 100 - d.y * 10) + .attr('r', 5) + .attr('fill', 'red'); + } + }, [data]); + + return ( + <div> + <h2>Scatterplot</h2> + <svg ref={svgRef} width={300} height={120}></svg> + </div> + ); +}; + +export default Scatterplot; diff --git a/libs/shared/lib/components/charts/scatterplot/scatterplot.stories.tsx b/libs/shared/lib/components/charts/scatterplot/scatterplot.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..271cda53db411ca9bd4d97ee5b06bd7e7ea120d3 --- /dev/null +++ b/libs/shared/lib/components/charts/scatterplot/scatterplot.stories.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Meta } from '@storybook/react'; +import Scatterplot, { ScatterplotProps } from '.'; + +const Component: Meta<typeof Scatterplot> = { + title: 'Visual charts/Charts/Scatterplot', + //tags: ['autodocs'], + component: Scatterplot, +}; + +export default Component; + +export const ScatterplotInput = { + args: { + data: [ + { x: 1, y: 3 }, + { x: 2, y: 7 }, + { x: 3, y: 2 }, + { x: 4, y: 5 }, + { x: 5, y: 8 }, + ], + }, +}; diff --git a/libs/shared/lib/components/controls/index.tsx b/libs/shared/lib/components/controls/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..92fc2cc4cf01d8e129b00c63ea672f92d01ff587 --- /dev/null +++ b/libs/shared/lib/components/controls/index.tsx @@ -0,0 +1,9 @@ +import React, { ReactNode } from 'react'; + +type Props = { + children: ReactNode; +}; + +export default function ControlContainer({ children }: Props) { + return <div className="top-4 right-4 flex flex-row-reverse justify-between z-50">{children}</div>; +} diff --git a/libs/shared/lib/components/dropdowns/dropdowns.module.scss b/libs/shared/lib/components/dropdowns/dropdowns.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..a564d157f1e0764f25d045dabec6dced46c7f625 --- /dev/null +++ b/libs/shared/lib/components/dropdowns/dropdowns.module.scss @@ -0,0 +1,19 @@ +.dropdown { +} +.dropdown-item { + //todo: color + //@apply cursor-pointer block px-4 py-2 text-sm bg-secondary-200; + @apply cursor-pointer block px-4 py-2 text-sm; +} +.dropdown-container { + width: inherit; + max-width: 100%; + @apply absolute z-10 mt-2 origin-top-right rounded-sm shadow-lg border border-secondary-200; + ul { + //todo: color + @apply divide-y; + //@apply divide-y divide-secondary-100; + li { + } + } +} diff --git a/libs/shared/lib/components/dropdowns/dropdowns.module.scss.d.ts b/libs/shared/lib/components/dropdowns/dropdowns.module.scss.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b29e98054e04c92420158f26b5e0ca2e74eb81a --- /dev/null +++ b/libs/shared/lib/components/dropdowns/dropdowns.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly 'dropdown-item': 'dropdown-item'; + readonly 'dropdown-container': 'dropdown-container'; +}; +export = classNames; diff --git a/libs/shared/lib/components/dropdowns/index.tsx b/libs/shared/lib/components/dropdowns/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4ce20ad797d0c3eb1d839900f836b42311994e64 --- /dev/null +++ b/libs/shared/lib/components/dropdowns/index.tsx @@ -0,0 +1,150 @@ +import React, { useState, useEffect, useRef, ReactNode } from 'react'; +import styles from './dropdowns.module.scss'; + +type DropdownContainerProps = { + children: ReactNode; + className?: string; +}; + +export const DropdownContainer = React.forwardRef<HTMLDivElement, DropdownContainerProps>( + ({ children, className }, ref: React.ForwardedRef<HTMLDivElement>) => { + return ( + <div className={`border-1 border-secondary-800 relative inline-block text-left ${className && className}`} ref={ref}> + {children} + </div> + ); + } +); + +type DropdownButtonProps = { + title: string | ReactNode; + onClick: (e: React.MouseEvent<HTMLButtonElement>) => void; + size?: 'xs' | 'sm' | 'md' | 'xl'; +}; + +export function DropdownButton({ title, onClick, size }: DropdownButtonProps) { + return ( + <> + <button + className="inline-flex w-full justify-between items-center gap-x-1.5 rounded bg-light px-3 py-2 text-secondary-900 shadow-sm ring-1 ring-inset ring-secondary-300 hover:bg-secondary-50" + onClick={onClick} + > + <span className={`text-${size}`}>{title}</span> + <svg className="-mr-1 h-5 w-5 text-secondary-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path + fillRule="evenodd" + d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" + clipRule="evenodd" + /> + </svg> + </button> + </> + ); +} + +type DropdownItemContainerProps = { + align: string; + className?: string; + children: ReactNode; +}; + +export function DropdownItemContainer({ align, className, children }: DropdownItemContainerProps) { + return ( + <div + className={`${styles['dropdown-container']} ${align} ${className} bg-light`} + role="menu" + aria-orientation="vertical" + aria-labelledby="menu-button" + tabIndex={-1} + > + <ul role="none">{children}</ul> + </div> + ); +} + +type DropdownItemProps = { + value: string; + disabled?: boolean; + className?: string; + onClick?: (value: string) => void; + submenu?: React.ReactNode; +}; + +export function DropdownItem({ value, disabled, className, onClick, submenu }: DropdownItemProps) { + const itemRef = useRef(null); + const submenuRef = useRef(null); + const [isSubmenuOpen, setIsSubmenuOpen] = useState(false); + + return ( + <li + ref={itemRef} + className={`${styles['dropdown-item']} ${className && className} hover:bg-primary-100`} + onClick={() => { + !disabled && onClick && onClick(value); + }} + onMouseEnter={() => setIsSubmenuOpen(true)} + onMouseLeave={() => setIsSubmenuOpen(false)} + > + {value} + {submenu && isSubmenuOpen && <DropdownSubmenuContainer ref={submenuRef}>{submenu}</DropdownSubmenuContainer>} + </li> + ); +} + +type DropdownSubmenuContainerProps = { + children: ReactNode; +}; + +export const DropdownSubmenuContainer = React.forwardRef<HTMLDivElement, DropdownSubmenuContainerProps>(({ children }, ref) => { + return ( + <div ref={ref} className="z-10 absolute bg-light rounded shadow w-44 left-[-80%] -translate-y-7"> + <ul className="text-sm text-secondary-700">{children}</ul> + </div> + ); +}); + +type DropdownProps = { + title: string; + options: Record<string, () => void>; + align: 'left-0' | 'right-0'; + closeOnClick?: boolean; + size?: 'xs' | 'sm' | 'md' | 'xl'; +}; + +export function MenuDropdown({ title, options, align = 'left-0', closeOnClick = true, size = 'sm' }: DropdownProps) { + const dropdownRef = useRef<HTMLDivElement>(null); + const [isDropdownOpen, setIsDropdownOpen] = useState<boolean>(false); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsDropdownOpen(false); + } + }; + if (isDropdownOpen) document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isDropdownOpen]); + + return ( + <DropdownContainer ref={dropdownRef}> + <DropdownButton title={title} onClick={(e) => setIsDropdownOpen(!isDropdownOpen)} size={size} /> + {isDropdownOpen && ( + <DropdownItemContainer align={align}> + {options && + Object.keys(options).map((key, index) => ( + <DropdownItem + key={index} + value={key} + onClick={() => { + options[key](); + closeOnClick && setIsDropdownOpen(false); + }} + /> + ))} + </DropdownItemContainer> + )} + </DropdownContainer> + ); +} diff --git a/libs/shared/lib/components/dropdowns/menudropdown.stories.tsx b/libs/shared/lib/components/dropdowns/menudropdown.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5f51fd829691947b73341762425425b120dc02c9 --- /dev/null +++ b/libs/shared/lib/components/dropdowns/menudropdown.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { MenuDropdown } from '.'; + +const Component: Meta<typeof MenuDropdown> = { + title: 'Components/Menu', + component: MenuDropdown, + decorators: [(Story) => <div className="m-5">{Story()}</div>], +}; + +export default Component; +type Story = StoryObj<typeof Component>; + +// export const Dropdown: Story = { +// args: { +// // type: 'select', +// // label: 'Select component', +// // options: ['Option 1', 'Option 2'], +// // onChange: (value) => {}, +// }, +// }; diff --git a/libs/shared/lib/components/forms/index.tsx b/libs/shared/lib/components/forms/index.tsx index 561852353bc47599c1c5b1d46e553e401ebd8393..ffab26661b9bd889aa63515ef78decfbb410846b 100644 --- a/libs/shared/lib/components/forms/index.tsx +++ b/libs/shared/lib/components/forms/index.tsx @@ -1,12 +1,65 @@ -import React, { PropsWithChildren, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import CloseIcon from '@mui/icons-material/Close'; +import React, { PropsWithChildren } from 'react'; +import { Button } from '../buttons'; export const FormDiv = React.forwardRef<HTMLDivElement, PropsWithChildren<{ className?: string; hAnchor?: string; offset?: string }>>( (props, ref) => { + const dialogRef = React.useRef<HTMLDivElement>(null); + const isClicked = React.useRef<boolean>(false); + const initialPosition = React.useRef<{ x: number; y: number }>({ x: 0, y: 0 }); + + React.useEffect(() => { + if (!dialogRef.current) return; + + const dialog = dialogRef.current; + const container = document.body; + + const onMouseDown = (e: MouseEvent) => { + isClicked.current = true; + initialPosition.current = { + x: e.clientX - dialog.getBoundingClientRect().left, + y: e.clientY - dialog.getBoundingClientRect().top, + }; + }; + + const onMouseUp = () => { + isClicked.current = false; + }; + + const onMouseMove = (e: MouseEvent) => { + if (!isClicked.current) return; + const newX = e.clientX - initialPosition.current.x; + const newY = e.clientY - initialPosition.current.y; + dialog.style.top = `${newY}px`; + dialog.style.left = `${newX}px`; + }; + + dialog.addEventListener('mousedown', onMouseDown); + dialog.addEventListener('mouseup', onMouseUp); + container.addEventListener('mousemove', onMouseMove); + container.addEventListener('mouseleave', onMouseUp); + + return () => { + dialog.removeEventListener('mousedown', onMouseDown); + dialog.removeEventListener('mouseup', onMouseUp); + container.removeEventListener('mousemove', onMouseMove); + container.removeEventListener('mouseleave', onMouseUp); + }; + }, []); + return ( <div - className={'absolute opacity-100 transition-opacity group-hover:opacity-100 z-50 ' + (props.className ? props.className : '')} - ref={ref} + className={ + 'absolute opacity-100 transition-opacity group-hover:opacity-100 z-50 w-fit cursor-move' + + (props.className ? props.className : '') + } + ref={(node) => { + dialogRef.current = node; + if (typeof ref === 'function') { + ref(node); + } else if (ref) { + ref.current = node; + } + }} style={props.hAnchor === 'left' ? { left: props.offset || 0 } : { right: props.offset || '5rem' }} > {props.children} @@ -15,7 +68,7 @@ export const FormDiv = React.forwardRef<HTMLDivElement, PropsWithChildren<{ clas } ); export const FormCard = (props: PropsWithChildren<{ className?: string }>) => ( - <div className={'card card-bordered bg-white rounded-none ' + (props.className ? props.className : '')}>{props.children}</div> + <div className={'card card-bordered bg-light rounded-none ' + (props.className ? props.className : '')}>{props.children}</div> ); export const FormBody = ({ children, @@ -25,27 +78,27 @@ export const FormBody = ({ {children} </form> ); -export const FormTitle = ({ children, title, onClose }: PropsWithChildren<{ title: string; onClose: () => void }>) => ( - <div className="card-title p-5 py-0 flex w-full"> - <h2 className="w-full">{title}</h2> - <button className="btn btn-circle btn-sm btn-ghost" onClick={() => onClose()}> - <CloseIcon fontSize="small" /> - </button> - </div> -); +export const FormTitle = ({ children, title, onClose }: PropsWithChildren<{ title: string; onClose: () => void }>) => { + return ( + <div className="card-title p-5 py-0 flex w-full"> + <h2 className="w-full">{title}</h2> + <Button rounded variant="ghost" iconName="Close" onClick={() => onClose()} /> + </div> + ); +}; export const FormHBar = () => <div className="divider m-0"></div>; export const FormControl = ({ children }: PropsWithChildren) => <div className="form-control px-5">{children}</div>; export const FormActions = (props: { onClose: () => void }) => ( - <div className="card-actions mt-1 w-full px-5 flex flex-row"> - <button - className="btn btn-secondary flex-grow" + <div className="grid grid-cols-2 px-5 gap-2"> + <Button + type="secondary" + variant="outline" + label="Cancel" onClick={(e) => { e.preventDefault(); props.onClose(); }} - > - Cancel - </button> - <button className="btn btn-primary flex-grow">Apply</button> + /> + <Button type="primary" label="Apply" onClick={() => {}} /> </div> ); diff --git a/libs/shared/lib/components/forms/requiredinput.tsx b/libs/shared/lib/components/forms/requiredinput.tsx deleted file mode 100644 index 2c83f22297eb2e90869d98a9273835931bce966e..0000000000000000000000000000000000000000 --- a/libs/shared/lib/components/forms/requiredinput.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -export const RequiredInput = (props: { - value: any; - label: string; - placeHolder: string; - type: string; - errorText: string; - validate: (value: any) => boolean; - onChange: (value: any) => void; -}) => { - const [isValid, setIsValid] = React.useState<boolean>(true); - - return ( - <div className="form-control w-full "> - <label className="label"> - <span className="label-text">{props.label}</span> - {isValid ? null : <span className="label-text-alt text-error">{props.errorText}</span>} - </label> - <input - type={props.type} - placeholder={props.placeHolder} - className={`input input-bordered w-full ${isValid ? '' : 'input-error'}`} - value={props.value} - onChange={(event) => { - setIsValid(props.validate(event.target.value)); - props.onChange(event.target.value); - }} - required - /> - </div> - ); -}; diff --git a/libs/shared/lib/components/icon/icon.stories.tsx b/libs/shared/lib/components/icon/icon.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6eef104db2b3f5cc2775e7108223748245128fbb --- /dev/null +++ b/libs/shared/lib/components/icon/icon.stories.tsx @@ -0,0 +1,28 @@ +import { StoryObj, Meta } from '@storybook/react'; +import Icon from '../icon'; + +export default { + title: 'Components/Icon', + component: Icon, + decorators: [(Story) => <div className="p-5">{Story()}</div>], + argTypes: { + name: { + control: 'select', + options: ['ArrowBack', 'DeleteOutline', 'KeyboardArrowLeft', 'Settings'], + }, + size: { + control: 'radio', + options: [16, 20, 24, 28, 32, 40], + }, + }, +} as Meta; +type Story = StoryObj<typeof Icon>; + +const BaseIcon: Story = { + args: { + name: 'ArrowBack', + size: 24, + }, +}; + +export const Default = { ...BaseIcon }; diff --git a/libs/shared/lib/components/icon/index.tsx b/libs/shared/lib/components/icon/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fb2a905f167adf03813febb0d59592d433e4c54c --- /dev/null +++ b/libs/shared/lib/components/icon/index.tsx @@ -0,0 +1,22 @@ +import * as Icons from '@mui/icons-material'; +import { ElementType, SVGProps } from 'react'; + +export type Sizes = 16 | 20 | 24 | 28 | 32 | 40; + +export type IconProps = SVGProps<SVGSVGElement> & { + name: string; + size?: Sizes; +}; + +export const Icon: React.FC<IconProps> = ({ name, size = 24, ...props }) => { + const IconComponent = (Icons as { [index: string]: ElementType })[name]; + + if (!IconComponent) { + console.error(`No icon found for name: ${name}`); + return null; + } + + return <IconComponent style={{ fontSize: size }} width={size} height={size} {...props} />; +}; + +export default Icon; diff --git a/libs/shared/lib/components/icon/overview.mdx b/libs/shared/lib/components/icon/overview.mdx new file mode 100644 index 0000000000000000000000000000000000000000..dc7299e9e2fd0769da3a27e3f6f5bdac33a22adc --- /dev/null +++ b/libs/shared/lib/components/icon/overview.mdx @@ -0,0 +1,18 @@ +import { Canvas, Meta, Story } from '@storybook/blocks'; +import * as IconStories from './icon.stories'; +import Icon from '.'; + +<Meta title="Components/Icon" component={Icon} /> + +# Icon Component + +The `Icon` component is used to display icons in the application in one of the following sizes: +16px, 20px, 24px, 28px, 32px, 40px + +## Usage + +<Canvas> + <Story id="components-icon--default" name="Default Icon"> + {IconStories.Default.args} + </Story> +</Canvas> diff --git a/libs/shared/lib/components/inputs/checkbox.stories.tsx b/libs/shared/lib/components/inputs/checkbox.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4ef5a1b18af809abeb55077ef5869b07241ce3cd --- /dev/null +++ b/libs/shared/lib/components/inputs/checkbox.stories.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Checkbox } from '.'; + +const Component: Meta<typeof Checkbox> = { + title: 'Components/Inputs', + component: Checkbox, + argTypes: { onChange: {} }, + decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], +}; + +export default Component; +type Story = StoryObj<typeof Component>; + +export const CheckboxInput: Story = { + args: { + type: 'checkbox', + label: 'Checkbox component', + options: ['Option 1', 'Option 2'], + onChange: (value) => {}, + }, +}; diff --git a/libs/shared/lib/components/inputs/dropdown.stories.tsx b/libs/shared/lib/components/inputs/dropdown.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0c7aa3d5d631cb67df8f024e6208cdb5e4e3e85a --- /dev/null +++ b/libs/shared/lib/components/inputs/dropdown.stories.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { DropDown } from '.'; + +const Component: Meta<typeof DropDown> = { + title: 'Components/Inputs', + component: DropDown, + argTypes: { onChange: {} }, + decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], +}; + +export default Component; +type Story = StoryObj<typeof Component>; + +export const DropdownInput: Story = { + args: { + type: 'dropdown', + label: 'Select component', + options: ['Option 1', 'Option 2'], + onChange: (value) => {}, + }, +}; diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1681881cafecf3a63491ea7d756e74d5a5d09e06 --- /dev/null +++ b/libs/shared/lib/components/inputs/index.tsx @@ -0,0 +1,249 @@ +import React from 'react'; +import styles from './inputs.module.scss'; +import { DropdownButton, DropdownContainer, DropdownItem, DropdownItemContainer } from '../dropdowns'; + +type BaseProps = { + type: 'slider' | 'text' | 'checkbox' | 'dropdown' | 'radio'; +}; + +type SliderProps = BaseProps & { + label: string; + value: number; + min: number; + max: number; + step: number; + showValue?: boolean; + unit?: string; + onChange: (value: number) => void; +}; + +type TextProps = BaseProps & { + label: string; + placeholder?: string; + value: string; + required?: boolean; + errorText?: string; + visible?: boolean; + disabled?: boolean; + validate?: (value: any) => boolean; + onChange: (value: string) => void; +}; + +type CheckboxProps = BaseProps & { + label?: string; + options: Array<string>; + value: Array<string>; + onChange: (value: Array<string>) => void; +}; + +type RadioProps = BaseProps & { + label?: string; + options: Array<string>; + value: string; + onChange: (value: string) => void; +}; + +type DropdownProps = BaseProps & { + label?: string; + value: string; + options: Array<string>; + onChange: (value: string) => void; + required?: boolean; +}; + +type InputProps = SliderProps | TextProps | CheckboxProps | DropdownProps | RadioProps; + +const Input: React.FC<InputProps> = (props) => { + switch (props.type) { + case 'slider': + return <Slider {...(props as SliderProps)} />; + case 'text': + return <Text {...(props as TextProps)} />; + case 'checkbox': + return <Checkbox {...(props as CheckboxProps)} />; + case 'dropdown': + return <DropDown {...(props as DropdownProps)} />; + case 'radio': + return <Radio {...(props as RadioProps)} />; + default: + return null; + } +}; + +export const Slider = ({ label, value, min, max, step, unit, showValue = true, onChange }: SliderProps) => { + return ( + <div className={styles['slider']}> + <label className="label flex flex-row justify-between items-end"> + <span className="label-text">{label}</span> + {showValue ? ( + <div className="label-text"> + {value} + {unit} + </div> + ) : null} + </label> + + <input + type="range" + min={min} + max={max} + step={step} + value={value} + onChange={(e) => { + onChange(parseFloat(e.target.value)); + }} + aria-labelledby="input-slider" + /> + </div> + ); +}; + +export const Text = ({ + label, + placeholder, + value = '', + required = false, + visible = true, + errorText, + validate, + disabled = false, + onChange, +}: TextProps) => { + const [isValid, setIsValid] = React.useState<boolean>(true); + + return ( + <div className="form-control w-full"> + <label className="label"> + <span className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`}> + {label} + </span> + {required && isValid ? null : <span className="label-text-alt text-error">{errorText}</span>} + </label> + <input + type={visible ? 'text' : 'password'} + placeholder={placeholder} + className={`px-3 py-2 bg-light border border-secondary-300 placeholder-secondary-400 focus:outline-none block w-full sm:text-sm focus:ring-1 ${ + isValid ? '' : 'input-error' + }`} + value={value} + onChange={(e) => { + if (required && validate) { + setIsValid(validate(e.target.value)); + } + onChange(e.target.value); + }} + required={required} + disabled={disabled} + /> + </div> + ); +}; + +export const Radio = ({ label, value, options, onChange }: RadioProps) => { + return ( + <div> + <label className="label"> + <span className="label-text">{label}</span> + </label> + {options.map((option, index) => ( + <label key={index} className="label cursor-pointer w-fit gap-2 px-0 py-1"> + <input + type="radio" + name={option} + checked={value === option} + onChange={() => { + onChange(option); + }} + className="radio radio-xs radio-primary" + /> + <span className="label-text">{option}</span> + </label> + ))} + </div> + ); +}; + +export const Checkbox = ({ label, value, options, onChange }: CheckboxProps) => { + return ( + <div> + {label && ( + <label className="label"> + <span className="label-text">{label}</span> + </label> + )} + {options.map((option, index) => ( + <label key={index} className="label cursor-pointer w-fit gap-2 px-0 py-1"> + <input + type="checkbox" + name={option} + checked={Array.isArray(value) && value.includes(option)} + onChange={(event) => { + const updatedValue = event.target.checked ? [...value, option] : value.filter((val) => val !== option); + onChange(updatedValue); + }} + className="checkbox checkbox-xs" + /> + <span className="label-text">{option}</span> + </label> + ))} + </div> + ); +}; + +export const DropDown = ({ label, value, options, onChange, required = false }: DropdownProps) => { + const dropdownRef = React.useRef<HTMLDivElement>(null); + const [isDropdownOpen, setIsDropdownOpen] = React.useState<boolean>(false); + + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsDropdownOpen(false); + } + }; + if (isDropdownOpen) document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isDropdownOpen]); + + return ( + <div className="w-full"> + {label && ( + <label className="label"> + <span + className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} + > + {label} + </span> + </label> + )} + <DropdownContainer className="w-full" ref={dropdownRef}> + <DropdownButton + title={value} + onClick={(e) => { + e.stopPropagation(); + e.preventDefault(); + setIsDropdownOpen(!isDropdownOpen); + }} + /> + {isDropdownOpen && ( + <DropdownItemContainer align="left-0"> + {options && + options.map((item, index) => ( + <DropdownItem + key={index} + value={item} + onClick={() => { + onChange(item); + setIsDropdownOpen(false); + }} + /> + ))} + </DropdownItemContainer> + )} + </DropdownContainer> + </div> + ); +}; + +export default Input; diff --git a/libs/shared/lib/components/inputs/inputs.module.scss b/libs/shared/lib/components/inputs/inputs.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..32f8e811dc08b054d619c2cb78845ad0ecf90cb8 --- /dev/null +++ b/libs/shared/lib/components/inputs/inputs.module.scss @@ -0,0 +1,23 @@ +.slider { + input[type='range'] { + -webkit-appearance: none !important; + @apply bg-primary rounded-full w-full; + height: 4px; + border: none; + outline: none; + } + input[type='range']::-webkit-slider-thumb { + -webkit-appearance: none !important; + @apply hover:shadow cursor-pointer rounded-full; + border-style: solid; + border-width: 1px; + height: 20px; + width: 20px; + background: hsl(var(--clr-sec--50)); + border-color: hsl(var(--clr-sec--300)); + &:hover { + background: hsl(var(--clr-sec--100)); + border-color: hsl(var(--clr-sec--400)); + } + } +} diff --git a/libs/shared/lib/components/inputs/inputs.module.scss.d.ts b/libs/shared/lib/components/inputs/inputs.module.scss.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7907e61e52e19fff7e5ff9b9d08a1d1756bc1ae --- /dev/null +++ b/libs/shared/lib/components/inputs/inputs.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly slider: 'slider'; +}; +export = classNames; diff --git a/libs/shared/lib/components/inputs/overview.mdx b/libs/shared/lib/components/inputs/overview.mdx new file mode 100644 index 0000000000000000000000000000000000000000..2682f0b10af242b24b9bcd72c1dd71c7a41a7147 --- /dev/null +++ b/libs/shared/lib/components/inputs/overview.mdx @@ -0,0 +1,88 @@ +import { Meta } from '@storybook/blocks'; +import Input from '.'; // Adjust the import path as needed + +export const components = { + Input, +}; + +<Meta title="Components/inputs" component={Input} /> + +# Pagination + +A pagination component for navigating through pages. + +# Form Input Demo + +## Slider Input + +<div className="w-52 m-5"> + <Input type="slider" label="Slider Label" value={50} unit={'%'} min={0} max={100} step={1} onChange={(value) => console.log(value)} /> +</div> + +```jsx +<Input type="slider" label="Slider Label" value={50} unit={'%'} min={0} max={100} step={1} onChange={(value) => console.log(value)} /> +``` + +## Text Input + +<div className="w-52 m-5"> + <Input type="text" label="Text Label" value="" onChange={(value) => console.log(value)} /> +</div> + +```jsx +<Input type="text" label="Text Label" value="" onChange={(value) => console.log(value)} /> +``` + +## Radio Input + +<div className="w-52 m-5"> + <Input type="radio" label="Radio Label" options={['Option 1', 'Option 2']} value="Option 1" onChange={(value) => console.log(value)} /> +</div> + +```jsx +<Input type="radio" label="Radio Label" options={['Option 1', 'Option 2']} value="Option 1" onChange={(value) => console.log(value)} /> +``` + +## Checkbox Input + +<div className="w-52 m-5"> + <Input + type="checkbox" + label="Checkbox Label" + options={['Option 1', 'Option 2']} + value={['Option 1']} + onChange={(value) => console.log(value)} + /> +</div> + +```jsx +<Input + type="checkbox" + label="Checkbox Label" + options={['Option 1', 'Option 2']} + value={['Option 1']} + onChange={(value) => console.log(value)} +/> +``` + +## Dropdown Input + +<div className="w-52 m-5"> + <Input + type="dropdown" + label="Dropdown Label" + value="Option 1" + options={['Option 1', 'Option 2']} + onChange={(value) => console.log(value)} + /> +</div> + +```jsx +<Input + type="dropdown" + label="Dropdown Label" + value="Option 1" + options={['Option 1', 'Option 2']} + onChange={(value) => console.log(value)} +/> +``` diff --git a/libs/shared/lib/components/inputs/radio.stories.tsx b/libs/shared/lib/components/inputs/radio.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e1d3be1c3e832acf54191430c7dd493fcb3a75c6 --- /dev/null +++ b/libs/shared/lib/components/inputs/radio.stories.tsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Radio } from '.'; + +const Component: Meta<typeof Radio> = { + title: 'Components/Inputs', + component: Radio, + argTypes: { onChange: { action: 'changed' } }, + decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], +}; + +export default Component; +type Story = StoryObj<typeof Component>; + +export const RadioInput: Story = (args: any) => { + const [value, setValue] = useState<string>(''); + return <Radio {...args} value={value} onChange={setValue} />; +}; + +RadioInput.args = { + type: 'radio', + label: 'Radio component', + options: ['Option 1', 'Option 2'], +}; diff --git a/libs/shared/lib/components/inputs/slider.stories.tsx b/libs/shared/lib/components/inputs/slider.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..12c65ffb82548246cdd58e69f8850e4caa1b9247 --- /dev/null +++ b/libs/shared/lib/components/inputs/slider.stories.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Slider } from '.'; + +const Component: Meta<typeof Slider> = { + title: 'Components/Inputs', + component: Slider, + argTypes: { onChange: {}, unit: {}, showValue: { control: 'boolean' } }, + decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], +}; + +export default Component; +type Story = StoryObj<typeof Component>; + +export const SliderInput: Story = { + args: { + type: 'slider', + label: 'Slider component', + min: 0, + max: 1, + step: 0.1, + unit: 'px', + value: 50, + showValue: true, + onChange: (value) => {}, + }, +}; diff --git a/libs/shared/lib/components/inputs/text.stories.tsx b/libs/shared/lib/components/inputs/text.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c76d075cd6762bfe5f89da42f8ee51ae6fc0c6fd --- /dev/null +++ b/libs/shared/lib/components/inputs/text.stories.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Text } from '.'; + +const Component: Meta<typeof Text> = { + title: 'Components/Inputs', + component: Text, + argTypes: { onChange: {} }, + decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], +}; + +export default Component; +type Story = StoryObj<typeof Component>; + +export const TextInput: Story = { + args: { + type: 'text', + label: 'Text input', + placeholder: 'Put here some text', + required: true, + onChange: (value) => {}, + }, +}; + +export const RequiredTextInput: Story = { + args: { + type: 'text', + label: 'Text input', + placeholder: 'Put here some text', + errorText: 'This field is required', + validate: (value) => { + return value.length > 0; + }, + onChange: (value) => {}, + }, +}; diff --git a/libs/shared/lib/components/pagination/index.tsx b/libs/shared/lib/components/pagination/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2de28ec3153c6ad4b512f299e42de8e5229aeeb0 --- /dev/null +++ b/libs/shared/lib/components/pagination/index.tsx @@ -0,0 +1,66 @@ +import React, { useRef } from 'react'; +import { Button } from '../buttons'; + +export type PaginationProps = { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + itemsPerPageInput: number; + numItemsArrayReal: number; + totalItems: number; +}; + +export const Pagination: React.FC<PaginationProps> = ({ + currentPage, + totalPages, + onPageChange, + itemsPerPageInput, + numItemsArrayReal, + totalItems, +}) => { + const pageNumbers = Array.from({ length: totalPages }, (_, index) => index + 1); + + const firstItem = (currentPage - 1) * itemsPerPageInput + 1; + const lastItem = Math.min(currentPage * itemsPerPageInput, totalPages); + + const goToPreviousPage = () => { + if (currentPage > 1) { + onPageChange(currentPage - 1); + } + }; + + const goToNextPage = () => { + if (currentPage < totalPages) { + onPageChange(currentPage + 1); + } + }; + + return ( + <div className="table-pagination flex flex-col items-center py-2 gap-1.5"> + <div className="inline-block text-sm"> + <span className="font-semibold">{`${firstItem} - ${numItemsArrayReal}`}</span> of {totalItems} + </div> + <div className="grid grid-cols-2 gap-2"> + <Button + size={'sm'} + label="Previous" + variant="outline" + iconName="ArrowBack" + onClick={goToPreviousPage} + disabled={currentPage === 1} + /> + <Button + size={'sm'} + label="Next" + variant="outline" + iconName="ArrowForward" + iconPosition="trailing" + onClick={goToNextPage} + disabled={currentPage === totalPages} + /> + </div> + </div> + ); +}; + +export default Pagination; diff --git a/libs/shared/lib/components/pagination/overview.mdx b/libs/shared/lib/components/pagination/overview.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9401f95bee68ed93d548279336a3b8a266ce6ad3 --- /dev/null +++ b/libs/shared/lib/components/pagination/overview.mdx @@ -0,0 +1,41 @@ +import { Canvas, Meta, Story } from '@storybook/blocks'; +import Pagination from '.'; +import * as PaginationStories from './pagination.stories'; + +<Meta title="Components/pagination" component={Pagination} /> + +# Pagination + +A pagination component for navigating through pages. + +## Default Pagination + +<div style={{ display: 'flex', justifyContent: 'center' }}> + <Pagination + totalPages={10} + itemsPerPageInput={10} + numItemsArrayReal={100} + totalItems={1000} + currentPage={1} + onPageChange={() => alert('Button clicked')} + /> +</div> + +Code: + +```jsx +const [currentPage, setCurrentPage] = useState(1); + +const handlePageChange = (page) => { + setCurrentPage(page); +}; + +<Pagination + totalPages={10} + itemsPerPageInput={10} + numItemsArrayReal={100} + totalItems={1000} + currentPage={1} + onPageChange={handlePageChange} +/>; +``` diff --git a/libs/shared/lib/components/pagination/pagination.stories.tsx b/libs/shared/lib/components/pagination/pagination.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9dac5d343918fe8d97856d0c20b5242d6eb8f630 --- /dev/null +++ b/libs/shared/lib/components/pagination/pagination.stories.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import Pagination, { PaginationProps } from '.'; + +const metaPagination: Meta<typeof Pagination> = { + component: Pagination, + title: 'Components/Pagination', +}; + +export default metaPagination; +type Story = StoryObj<typeof Pagination>; + +export const mainStory: Story = { + render: (args) => { + const [currentPage, setCurrentPage] = useState(1); + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + return <Pagination {...args} currentPage={currentPage} onPageChange={handlePageChange} />; + }, + args: { + currentPage: 1, + totalPages: 10, + onPageChange: (page: number) => console.log(`Go to page ${page}`), + itemsPerPageInput: 10, + numItemsArrayReal: 100, + totalItems: 1000, + }, +}; diff --git a/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx b/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx index f67aa55e4caded4ca8264172184c17706fbdf53d..336b6af78b5dc8ea757a80f515c4f0d77edafcbb 100644 --- a/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx +++ b/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx @@ -106,7 +106,7 @@ export const DashboardAlerts = (props: { timer?: number }) => { className: 'alert-error', }, undefined, - 'error' + 'error', ); dispatch(removeLastError()); } @@ -124,7 +124,7 @@ export const DashboardAlerts = (props: { timer?: number }) => { className: 'alert-warning', }, undefined, - 'warning' + 'warning', ); dispatch(removeLastWarning()); } @@ -159,7 +159,7 @@ export const DashboardAlerts = (props: { timer?: number }) => { ref.current.onShowTimeout(m.key); }} > - <span className="flex flex-row content-center gap-3 text-white">{m.message.message}</span> + <span className="flex flex-row content-center gap-3 text-light">{m.message.message}</span> {/* {!message && (m.data?.status ? m.data.status : m.routingKey)} */} </div> ); diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts index c6da6ad308ab98f04b0e82e1891d5c62817d94f0..da9e43a2c896e14594dffa596f640481f647ad30 100644 --- a/libs/shared/lib/data-access/store/hooks.ts +++ b/libs/shared/lib/data-access/store/hooks.ts @@ -12,7 +12,7 @@ import { sessionCacheState } from './sessionSlice'; import { authState } from './authSlice'; import { visualizationState } from './visualizationSlice'; import { allMLEnabled, selectML } from './mlSlice'; -import { searchResultState, searchResultData, searchResultSchema, searchResultQB } from './searchResultSlice'; +import { searchResultState, searchResultData, searchResultSchema, searchResultQB, recentSearches } from './searchResultSlice'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch; @@ -46,6 +46,7 @@ export const useSearchResult = () => useAppSelector(searchResultState); export const useSearchResultData = () => useAppSelector(searchResultData); export const useSearchResultSchema = () => useAppSelector(searchResultSchema); export const useSearchResultQB = () => useAppSelector(searchResultQB); +export const useRecentSearches = () => useAppSelector(recentSearches); // Visualization Slices export const useVisualizationState = () => useAppSelector(visualizationState); diff --git a/libs/shared/lib/data-access/store/index.ts b/libs/shared/lib/data-access/store/index.ts index eb2b9cc7a19c31fee9376036a79c5443e19aaac9..d5ba3f4cbfc749511ab82d1bea8dbd14db6237a0 100644 --- a/libs/shared/lib/data-access/store/index.ts +++ b/libs/shared/lib/data-access/store/index.ts @@ -12,6 +12,7 @@ export { graphQueryResultSlice, } from './graphQueryResultSlice'; export { mlSlice } from './mlSlice'; +export { searchResultSlice } from './searchResultSlice'; // Exported types export type { Node, Edge, GraphQueryResult } from './graphQueryResultSlice'; diff --git a/libs/shared/lib/data-access/store/schemaSlice.ts b/libs/shared/lib/data-access/store/schemaSlice.ts index fd7244e55d713dba671fc0ee916087a6117701a5..691cb708805dc9db9aefd5013ddf9f183b6a3683 100644 --- a/libs/shared/lib/data-access/store/schemaSlice.ts +++ b/libs/shared/lib/data-access/store/schemaSlice.ts @@ -30,6 +30,7 @@ export const schemaSlice = createSlice({ initialState, reducers: { setSchema: (state, action: PayloadAction<SchemaGraph>) => { + if (action.payload === undefined) throw new Error('Schema is undefined'); state.graph = action.payload; }, clearSchema: (state) => { diff --git a/libs/shared/lib/data-access/store/searchResultSlice.ts b/libs/shared/lib/data-access/store/searchResultSlice.ts index 32c58a255e69ca30510f5d974bb2d57b734dff75..7f449947e8abd0b46d541a7f23e66047a19bb758 100644 --- a/libs/shared/lib/data-access/store/searchResultSlice.ts +++ b/libs/shared/lib/data-access/store/searchResultSlice.ts @@ -3,22 +3,22 @@ import type { RootState } from './store'; export type CATEGORY_KEYS = 'data' | 'schema' | 'querybuilder'; -// Define the initial state using that type -export const initialState: { - [key in CATEGORY_KEYS]: { nodes: Record<string, any>[]; edges: Record<string, any>[] }; -} = { - data: { - nodes: [], - edges: [], - }, - schema: { - nodes: [], - edges: [], - }, - querybuilder: { - nodes: [], - edges: [], +type CategoryDataI = { nodes: Record<string, any>[]; edges: Record<string, any>[] }; + +type InitialState = { + categories: { + [key in CATEGORY_KEYS]: CategoryDataI; + }; + recentSearches: string[]; +}; + +const initialState: InitialState = { + categories: { + data: { nodes: [], edges: [] }, + schema: { nodes: [], edges: [] }, + querybuilder: { nodes: [], edges: [] }, }, + recentSearches: [], }; export const searchResultSlice = createSlice({ @@ -26,13 +26,13 @@ export const searchResultSlice = createSlice({ initialState, reducers: { addSearchResultData: (state, action: PayloadAction<{ nodes: Record<string, any>[]; edges: Record<string, any>[] }>) => { - state.data = action.payload; + state.categories.data = action.payload; }, addSearchResultSchema: (state, action: PayloadAction<{ nodes: Record<string, any>[]; edges: Record<string, any>[] }>) => { - state.schema = action.payload; + state.categories.schema = action.payload; }, addSearchResultQueryBuilder: (state, action: PayloadAction<{ nodes: Record<string, any>[]; edges: Record<string, any>[] }>) => { - state.querybuilder = action.payload; + state.categories.querybuilder = action.payload; }, addSearchResultSelected: ( state, @@ -41,35 +41,48 @@ export const searchResultSlice = createSlice({ value: { nodes: Record<string, any>[]; edges: Record<string, any>[] }; }> ) => { - state.data = { nodes: [], edges: [] }; - state.schema = { nodes: [], edges: [] }; - state.querybuilder = { nodes: [], edges: [] }; + state.categories.data = { nodes: [], edges: [] }; + state.categories.schema = { nodes: [], edges: [] }; + state.categories.querybuilder = { nodes: [], edges: [] }; const { category, value } = action.payload; - state[category] = value; + state.categories[category] = value; }, resetSearchResults: (state) => { return { ...state, - data: initialState.data, - schema: initialState.schema, - querybuilder: initialState.querybuilder, + data: initialState.categories.data, + schema: initialState.categories.schema, + querybuilder: initialState.categories.querybuilder, }; }, + addRecentSearch: (state, action: PayloadAction<string>) => { + if (!state.recentSearches.includes(action.payload)) { + state.recentSearches.unshift(action.payload); + state.recentSearches = state.recentSearches.slice(0, 10); + } + }, }, }); -export const { addSearchResultData, addSearchResultSchema, addSearchResultQueryBuilder, addSearchResultSelected, resetSearchResults } = - searchResultSlice.actions; +export const { + addSearchResultData, + addSearchResultSchema, + addSearchResultQueryBuilder, + addSearchResultSelected, + resetSearchResults, + addRecentSearch, +} = searchResultSlice.actions; // Other code such as selectors can use the imported `RootState` type -export const searchResultState = (state: RootState) => state.searchResults; -export const searchResultData = (state: RootState) => state.searchResults.data; +export const recentSearches = (state: RootState) => state.searchResults.recentSearches; +export const searchResultState = (state: RootState) => state.searchResults.categories; +export const searchResultData = (state: RootState) => state.searchResults?.categories.data; export const searchResultSchema = (state: RootState) => { - const nodes = state.searchResults.schema.nodes.map((node) => node.key); - const edges = state.searchResults.schema.edges.map((edge) => edge.key); + const nodes = state.searchResults?.categories?.schema?.nodes?.map((node) => node.key) || []; + const edges = state.searchResults?.categories?.schema?.edges?.map((edge) => edge.key) || []; return [...nodes, ...edges]; }; -export const searchResultQB = (state: RootState) => state.searchResults.querybuilder; +export const searchResultQB = (state: RootState) => state.searchResults.categories.querybuilder; export default searchResultSlice.reducer; diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index c0ca3d3aa4641d59659b2f8c57a74debd76ba413..a671ece20dbaf4e5b574effb22780a4c55d97f65 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -9,8 +9,6 @@ import { import ReactFlow, { Background, Connection, - ControlButton, - Controls, Edge, HandleType, Node, @@ -20,32 +18,16 @@ import ReactFlow, { OnEdgesChange, ReactFlowInstance, ReactFlowProvider, - XYPosition, isNode, useReactFlow, } from 'reactflow'; import styles from './querybuilder.module.scss'; import { clearQB, setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; -import { Cached as CachedIcon, Delete as DeleteIcon, ImportExport as ExportIcon, Settings as SettingsIcon } from '@mui/icons-material'; import { useDispatch } from 'react-redux'; -import { - AllLogicDescriptions, - AllLogicMap, - Handles, - NodeAttribute, - QueryElementTypes, - QueryGraphNodes, - createReactFlowElements, - isLogicHandle, - toHandleData, -} from '../model'; -import { InputNodeType } from '../model/logic/general'; +import { AllLogicMap, QueryElementTypes, createReactFlowElements, isLogicHandle, toHandleData } from '../model'; import { ConnectionDragLine, ConnectionLine, EntityFlowElement, RelationPill } from '../pills'; import LogicPill from '../pills/customFlowPills/logicpill/logicpill'; import { dragPillStarted, movePillTo } from '../pills/dragging/dragPill'; -import DifferenceIcon from '@mui/icons-material/Difference'; -import LightbulbIcon from '@mui/icons-material/Lightbulb'; -import CameraAltIcon from '@mui/icons-material/CameraAlt'; import { Dialog } from '../../components/Dialog'; import { QueryBuilderLogicPillsPanel } from './querysidepanel/queryBuilderLogicPillsPanel'; import { QueryMLDialog } from './querysidepanel/queryMLDialog'; @@ -55,6 +37,8 @@ import { LayoutFactory } from '../../graph-layout'; import { ConnectingNodeDataI } from './utils/connectorDrop'; import { QueryBuilderRelatedNodesPanel } from './querysidepanel/queryBuilderRelatedNodesPanel'; import { addError } from '../../data-access/store/configSlice'; +import ControlContainer from '../../components/controls'; +import { Button } from '../../components/buttons'; export type QueryBuilderProps = { onRunQuery?: () => void; @@ -76,7 +60,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { relation: RelationPill, logic: LogicPill, }), - [] + [], ); var edgeTypes = useMemo(() => ({ connection: ConnectionLine, attribute_connection: ConnectionLine }), []); @@ -94,6 +78,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { const graphologyGraph = useMemo(() => toQuerybuilderGraphology(graph), [graph]); const elements = useMemo(() => createReactFlowElements(graphologyGraph), [graph]); const searchResults = useSearchResultQB(); + const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null); useEffect(() => { const searchResultKeys = new Set([...searchResults.nodes.map((node) => node.key), ...searchResults.edges.map((edge) => edge.key)]); @@ -201,7 +186,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { name: dragData.name, schemaKey: dragData.name, }, - schema.getNodeAttribute(dragData.name, 'attributes') + schema.getNodeAttribute(dragData.name, 'attributes'), ); dispatch(setQuerybuilderGraphology(graphologyGraph)); @@ -218,7 +203,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { schemaKey: dragData.label, collection: dragData.collection, }, - schema.getEdgeAttribute(dragData.label, 'attributes') + schema.getEdgeAttribute(dragData.label, 'attributes'), ); if (config.autoSendQueries) { @@ -271,7 +256,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { } } }, - [graph] + [graph], ); const onConnectStart = useCallback( @@ -289,7 +274,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { attribute: { handleData: handleData }, }; }, - [graph] + [graph], ); const onConnectEnd = useCallback( @@ -329,7 +314,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { // setToggleSettings('logic'); } }, - [reactFlow.project] + [reactFlow.project], ); const onEdgeUpdateStart = useCallback(() => { @@ -354,7 +339,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { dispatch(setQuerybuilderGraphology(graphologyGraph)); } }, - [graph] + [graph], ); const onEdgesChange = (params: OnEdgesChange) => { @@ -371,7 +356,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { } isEdgeUpdating.current = false; }, - [graph] + [graph], ); const onNodeContextMenu = (event: React.MouseEvent, node: Node) => { @@ -388,6 +373,12 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { } } + const fitView = () => { + if (reactFlowInstanceRef.current) { + reactFlowInstanceRef.current.fitView(); + } + }; + useEffect(() => { applyLayout(); }, [queryBuilderSettings]); @@ -397,6 +388,76 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { <QuerySettingsDialog open={toggleSettings === 'settings'} onClose={() => setToggleSettings(undefined)} /> <QueryMLDialog open={toggleSettings === 'ml'} onClose={() => setToggleSettings(undefined)} /> + <div className="relative flex items-center justify-between z-[2] py-0 px-2 bg-secondary-100 border-b border-secondary-200"> + <h1 className="text-xs font-semibold text-secondary-800">Query builder</h1> + <ControlContainer> + <Button type="secondary" variant="ghost" size="xs" iconName="Fullscreen" onClick={fitView} /> + <Button type="secondary" variant="ghost" size="xs" iconName="Delete" onClick={() => clearAllNodes()} /> + <Button + type="secondary" + variant="ghost" + size="xs" + iconName="CameraAlt" + onClick={(event) => { + event.stopPropagation(); + }} + /> + <Button + type="secondary" + variant="ghost" + size="xs" + iconName="ImportExport" + onClick={(event) => { + event.stopPropagation(); + applyLayout(); + }} + /> + <Button + type="secondary" + variant="ghost" + size="xs" + iconName="Settings" + onClick={(event) => { + event.stopPropagation(); + if (toggleSettings === 'settings') setToggleSettings(undefined); + else setToggleSettings('settings'); + }} + /> + <Button + type="secondary" + variant="ghost" + size="xs" + iconName="Cached" + onClick={(event) => { + event.stopPropagation(); + if (props.onRunQuery) props.onRunQuery(); + }} + /> + <Button + type="secondary" + variant="ghost" + size="xs" + iconName="Difference" + onClick={(event) => { + event.stopPropagation(); + if (toggleSettings === 'logic') setToggleSettings(undefined); + else setToggleSettings('logic'); + }} + /> + <Button + type="secondary" + variant="ghost" + size="xs" + iconName="Lightbulb" + onClick={(event) => { + event.stopPropagation(); + if (toggleSettings === 'ml') setToggleSettings(undefined); + else setToggleSettings('ml'); + }} + /> + </ControlContainer> + </div> + <Dialog open={toggleSettings === 'logic'} onClose={() => { @@ -451,7 +512,10 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { edgeTypes={edgeTypes} connectionLineComponent={ConnectionDragLine} // connectionMode={ConnectionMode.Loose} - onInit={onInit} + onInit={(reactFlowInstance) => { + reactFlowInstanceRef.current = reactFlowInstance; + onInit(reactFlowInstance); + }} onNodesChange={onNodesChange} onDragOver={onDragOver} onConnect={onConnect} @@ -472,74 +536,6 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { proOptions={{ hideAttribution: true }} > <Background gap={10} size={0.7} /> - - <Controls showZoom={false} showInteractive={false} className={`${styles.controls} query-settings`}> - <ControlButton className={styles.buttons} title={'Remove all elements'} onClick={() => clearAllNodes()}> - <DeleteIcon /> - </ControlButton> - <ControlButton - className={styles.buttons} - title={'Export querybuilder'} - onClick={(event) => { - event.stopPropagation(); - }} - > - <CameraAltIcon /> - </ControlButton> - <ControlButton - className={styles.buttons} - title={'Apply Layout'} - onClick={(event) => { - event.stopPropagation(); - applyLayout(); - }} - > - <ExportIcon /> - </ControlButton> - <ControlButton - className={styles.buttons + (toggleSettings === 'settings' ? ' btn-active' : '')} - title={'Other settings'} - onClick={(event) => { - event.stopPropagation(); - if (toggleSettings === 'settings') setToggleSettings(undefined); - else setToggleSettings('settings'); - }} - > - <SettingsIcon /> - </ControlButton> - <ControlButton - className={styles.buttons} - title={'Re-Run Query'} - onClick={(event) => { - event.stopPropagation(); - if (props.onRunQuery) props.onRunQuery(); - }} - > - <CachedIcon /> - </ControlButton> - <ControlButton - className={styles.buttons + (toggleSettings === 'logic' ? ' btn-active' : '')} - title={'Logic Pills'} - onClick={(event) => { - event.stopPropagation(); - if (toggleSettings === 'logic') setToggleSettings(undefined); - else setToggleSettings('logic'); - }} - > - <DifferenceIcon /> - </ControlButton> - <ControlButton - className={styles.buttons + (toggleSettings === 'ml' ? ' btn-active' : '')} - title={'Machine Learning'} - onClick={(event) => { - event.stopPropagation(); - if (toggleSettings === 'ml') setToggleSettings(undefined); - else setToggleSettings('ml'); - }} - > - <LightbulbIcon /> - </ControlButton> - </Controls> </ReactFlow> </div> ); @@ -551,7 +547,6 @@ export const QueryBuilder = (props: QueryBuilderProps) => { <ReactFlowProvider> <QueryBuilderInner {...props} /> </ReactFlowProvider> - {/* <QuerySidePanel title="Query Panel" draggable /> */} </div> ); }; diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx index 5d60445a5d08b8882008f9ae2756aa8f4de8d186..193a1991ffe11fee0bf3f11f07c710abf61a613f 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx @@ -104,7 +104,7 @@ export const QueryBuilderLogicPillsPanel = (props: { graphologyGraph.getNodeAttributes(params.nodeId), graphologyGraph.getNodeAttributes(logicNode.id), { type: 'connection' }, - { sourceHandleName: sourceHandleData.attributeName, targetHandleName: firstLeftLogicInput.name } + { sourceHandleName: sourceHandleData.attributeName, targetHandleName: firstLeftLogicInput.name }, ); } @@ -151,7 +151,7 @@ export const QueryBuilderLogicPillsPanel = (props: { .filter((item) => selectedOp === -1 || item.key.toLowerCase().includes(dataOps?.[selectedOp].title)) .filter((item) => selectedType === -1 || item.key.toLowerCase().includes(dataTypes?.[selectedType].title)) .map((item, index) => ( - <li key={item.key + item.description} className="h-fit bg-base-200 "> + <li key={item.key + item.description} className="h-fit bg-secondary-200 "> <span data-tip={item.description} className="flex before:w-[10rem] before:text-center tooltip tooltip-bottom text-start " diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx index 6b7c032cdcf700b6755e3ee55a650dc767437b88..cae5133bc4ff58211cd2720c9ab54a0d3a223161 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx @@ -1,10 +1,10 @@ -import { PropsWithChildren, useEffect, useRef } from 'react'; -import { Dialog, DialogProps } from '../../../components/Dialog'; +import { useEffect } from 'react'; +import { DialogProps } from '../../../components/Dialog'; import React from 'react'; import { useAppDispatch, useQuerybuilderSettings } from '../../../data-access'; import { QueryBuilderSettings, setQuerybuilderSettings } from '../../../data-access/store/querybuilderSlice'; import { addWarning } from '../../../data-access/store/configSlice'; -import { FormBody, FormCard, FormDiv, FormHBar, FormTitle } from '../../../components/forms'; +import { FormActions, FormBody, FormCard, FormDiv, FormHBar, FormTitle } from '../../../components/forms'; import { Layouts } from '@graphpolaris/shared/lib/graph-layout'; type QuerySettingsDialogProps = DialogProps; @@ -34,7 +34,7 @@ export const QuerySettingsDialog = React.forwardRef<HTMLDivElement, QuerySetting return ( <> {props.open && ( - <FormDiv ref={ref} className="" hAnchor="right"> + <FormDiv hAnchor="right" ref={ref}> <FormCard> <FormBody onSubmit={(e) => { @@ -120,18 +120,8 @@ export const QuerySettingsDialog = React.forwardRef<HTMLDivElement, QuerySetting </select> </div> <FormHBar /> - <div className="card-actions mt-1 w-full px-5 flex flex-row"> - <button - className="btn btn-secondary flex-grow" - onClick={(e) => { - e.preventDefault(); - props.onClose(); - }} - > - Cancel - </button> - <button className="btn btn-primary flex-grow">Apply</button> - </div> + + <FormActions onClose={props.onClose} /> </FormBody> </FormCard> </FormDiv> diff --git a/libs/shared/lib/querybuilder/pills/customFlowLines/connection.scss b/libs/shared/lib/querybuilder/pills/customFlowLines/connection.scss index 4e133c2c64d948d2a26b8079022581886cc8bde7..1b9e88a5e7e5ffbe9dfd220639fc4a480ac8a99e 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowLines/connection.scss +++ b/libs/shared/lib/querybuilder/pills/customFlowLines/connection.scss @@ -1,3 +1,3 @@ g { - @apply stroke-line-300; + @apply stroke-secondary-300; } diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx index 22698240b93e394ec7fff9e9cc855da97f088171..139c807ac695900aa2754a921c2eb9c365a4d517 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx @@ -1,5 +1,10 @@ import React from 'react'; -import { querybuilderSlice, setQuerybuilderNodes } from '@graphpolaris/shared/lib/data-access/store'; +import { + querybuilderSlice, + schemaSlice, + setQuerybuilderNodes, + searchResultSlice, +} from '@graphpolaris/shared/lib/data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; @@ -7,26 +12,30 @@ import { Provider } from 'react-redux'; import { QueryBuilder } from '../../../panel'; import { QueryMultiGraphology } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils'; import { QueryElementTypes } from '../../../model'; +import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils'; const Component: Meta<typeof QueryBuilder> = { component: QueryBuilder, title: 'Querybuilder/Pills/EntityPill', - decorators: [(story) => <Provider store={mockStore}>{story()}</Provider>], + decorators: [(story) => <Provider store={Mockstore}>{story()}</Provider>], }; -// Mock palette store -const mockStore = configureStore({ +const Mockstore = configureStore({ reducer: { querybuilder: querybuilderSlice.reducer, + schema: schemaSlice.reducer, + searchResults: searchResultSlice.reducer, }, }); -const graph = new QueryMultiGraphology(); -graph.addPill2Graphology({ id: '2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Entity Pill' }); -console.log(graph.export()); - -mockStore.dispatch(setQuerybuilderNodes(graph.export())); export const Flow = { + play: async () => { + const dispatch = Mockstore.dispatch; + + const graph = new QueryMultiGraphology(); + graph.addPill2Graphology({ id: '2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Entity Pill' }); + dispatch(setQuerybuilderNodes(graph.export())); + }, args: {}, }; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.module.scss b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.module.scss deleted file mode 100644 index df212f7f699a6e6787fa338c63b3a3286f8a767f..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.module.scss +++ /dev/null @@ -1 +0,0 @@ -@import '../../querypills.module.scss'; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.module.scss.d.ts b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.module.scss.d.ts deleted file mode 100644 index e478e565f42129a12498bfde643731c7c978d6fc..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.module.scss.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -declare const classNames: { - readonly handle: 'handle'; - readonly handle_logic: 'handle_logic'; - readonly handle_logic_duration: 'handle_logic_duration'; - readonly handle_logic_datetime: 'handle_logic_datetime'; - readonly handle_logic_time: 'handle_logic_time'; - readonly handle_logic_date: 'handle_logic_date'; - readonly handle_logic_bool: 'handle_logic_bool'; - readonly handle_logic_float: 'handle_logic_float'; - readonly handle_logic_int: 'handle_logic_int'; - readonly handle_logic_string: 'handle_logic_string'; - readonly 'react-flow__node': 'react-flow__node'; - readonly selected: 'selected'; - readonly entityWrapper: 'entityWrapper'; - readonly hidden: 'hidden'; - readonly 'react-flow__edges': 'react-flow__edges'; - readonly 'react-flow__edge-default': 'react-flow__edge-default'; - readonly handleConnectedFill: 'handleConnectedFill'; - readonly handleConnectedBorderRight: 'handleConnectedBorderRight'; - readonly handleConnectedBorderLeft: 'handleConnectedBorderLeft'; - readonly handleFunction: 'handleFunction'; -}; -export = classNames; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.stories.tsx index 7de4cbc2af54dc602612809808b5ebbbc0d6937e..4d38dd88341c00f002df61815ed2301b0edd5917 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.stories.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.stories.tsx @@ -4,15 +4,11 @@ import EntityFlowElement from './entitypill'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; -import { querybuilderSlice, schemaSlice } from '@graphpolaris/shared/lib/data-access/store'; +import { querybuilderSlice, schemaSlice, searchResultSlice } from '@graphpolaris/shared/lib/data-access/store'; import { ReactFlowProvider } from 'reactflow'; -import { EntityData } from '../../../model'; +import { EntityData, Handles, QueryElementTypes } from '../../../model'; const Component: Meta<typeof EntityFlowElement> = { - /* 👇 The title prop is optional. - * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading - * to learn how to generate automatic titles - */ title: 'Querybuilder/Pills/EntityPill', component: EntityFlowElement, decorators: [ @@ -26,28 +22,21 @@ const Component: Meta<typeof EntityFlowElement> = { export default Component; -// A super-simple mock of a redux store const Mockstore = configureStore({ reducer: { querybuilder: querybuilderSlice.reducer, - // schema: schemaSlice.reducer, + schema: schemaSlice.reducer, + searchResults: searchResultSlice.reducer, }, }); -// const Template = (args: any) => <EntityRFPill {...args} />; - export const Default: StoryObj<{ data: EntityData }> = { args: { data: { name: 'TestEntity', + leftRelationHandleId: { nodeId: '1', nodeName: 'string', nodeType: QueryElementTypes.Entity, handleType: Handles.EntityLeft }, + rightRelationHandleId: { nodeId: '2', nodeName: 'string2', nodeType: QueryElementTypes.Entity, handleType: Handles.EntityRight }, + selected: false, }, }, }; - -// Default.decorators = [ -// (story) => ( -// <Provider store={Mockstore}> -// {story()} -// </Provider> -// ), -// ]; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx index 6138a6aab807249c4b3132c3713bc46c39a86ac8..56777be2c128e07f35cb9c8d64aa43c5ca810558 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx @@ -1,10 +1,8 @@ // import { handles } from '@graphpolaris/shared/lib/querybuilder/usecases'; import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; import React, { useMemo, useState } from 'react'; -import { Handle, Position } from 'reactflow'; +import { Position } from 'reactflow'; import { NodeAttribute, SchemaReactflowEntityNode } from '../../../model'; -import { styleHandleMap } from '../../utils'; -import styles from './entitypill.module.scss'; import { PillDropdown } from '../../pilldropdown/pilldropdown'; import { FilterHandle } from '../../FilterHandle'; @@ -20,7 +18,7 @@ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => const graph = useQuerybuilderGraph(); const attributeEdges = useMemo( () => graph.edges.filter((edge) => edge.source === node.id && !!edge?.attributes?.sourceHandleData.attributeType), - [graph] + [graph], ); const [hovered, setHovered] = useState(false); @@ -50,36 +48,32 @@ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => return ( <div className="p-3 bg-transparent" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> - <div - className={`border-l-[3px] border-solid ${ - data.selected ? 'bg-slate-400' : 'bg-entity-50' - } border-l-entity-600 font-bold text-xs min-w-[8rem] query_builder-entity`} - > - <div> + <div className={`rounded-sm shadow min-w-[8rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#FFA952] to-[#D66700]`}> + <div className={`py-1 ${data.selected ? 'bg-secondary-400' : 'bg-secondary-50'}`}> <FilterHandle handle={data.leftRelationHandleId} type="target" position={Position.Left} - className={styles.handle + ' !top-6 !left-2 !bg-entity-800 !rounded-none'} + className={'!top-8 !left-2 !bg-accent-700 !rounded-none w-2 h-2'} /> <FilterHandle handle={data.rightRelationHandleId} type="source" position={Position.Right} - className={styles.handle + ' !top-6 !right-2 !bg-entity-800 !rounded-none'} + className={'!top-8 !right-2 !bg-accent-700 !rounded-none w-2 h-2'} /> <div className="text-center py-1">{data.name}</div> + {data?.attributes && ( + <PillDropdown + node={node} + attributes={data.attributes} + attributeEdges={attributeEdges.map((edge) => edge?.attributes)} + hovered={hovered} + handleBeingDraggedIdx={handleBeingDragged} + onHandleMouseDown={onHandleMouseDown} + /> + )} </div> - {data?.attributes && ( - <PillDropdown - node={node} - attributes={data.attributes} - attributeEdges={attributeEdges.map((edge) => edge?.attributes)} - hovered={hovered} - handleBeingDraggedIdx={handleBeingDragged} - onHandleMouseDown={onHandleMouseDown} - /> - )} </div> </div> ); diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicInput.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicInput.tsx index 7a1ae28b68a94e3e499ca479cc7b4e00e72122fc..e0087c7ff65730d855706d27fbf9861be8f63171 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicInput.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicInput.tsx @@ -15,7 +15,7 @@ export const LogicInput = (props: { value: string; type: string; onChange(value: return ( <input ref={ref} - className="px-0.5 m-2 mt-0 h-5 border-logic-600 rounded-sm border-[1px]" + className="px-0.5 m-2 mt-0 h-5 rounded-sm border-[1px]" style={{ width: props.type === 'string' ? '9rem' : '4rem' }} placeholder="empty" value={props.value} diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.module.scss b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.module.scss deleted file mode 100644 index 6a1592bfa3ba7da3ac008f16139ce96c00e0590f..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.module.scss +++ /dev/null @@ -1,12 +0,0 @@ -@import '../../querypills.module.scss'; - -.logic { - text-align: center; - font-weight: bold; - border-left: 3px solid; - @apply border-l-logic-600; - @apply bg-logic-100; - font-size: 13px; - display: flex; - min-width: 5rem; -} diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.module.scss.d.ts b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.module.scss.d.ts deleted file mode 100644 index 9fa2784a18c05ec92d88e5bf2147a3b37d389a1d..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.module.scss.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -declare const classNames: { - readonly handle: 'handle'; - readonly handle_logic: 'handle_logic'; - readonly handle_logic_duration: 'handle_logic_duration'; - readonly handle_logic_datetime: 'handle_logic_datetime'; - readonly handle_logic_time: 'handle_logic_time'; - readonly handle_logic_date: 'handle_logic_date'; - readonly handle_logic_bool: 'handle_logic_bool'; - readonly handle_logic_float: 'handle_logic_float'; - readonly handle_logic_int: 'handle_logic_int'; - readonly handle_logic_string: 'handle_logic_string'; - readonly 'react-flow__node': 'react-flow__node'; - readonly selected: 'selected'; - readonly entityWrapper: 'entityWrapper'; - readonly hidden: 'hidden'; - readonly 'react-flow__edges': 'react-flow__edges'; - readonly 'react-flow__edge-default': 'react-flow__edge-default'; - readonly handleConnectedFill: 'handleConnectedFill'; - readonly handleConnectedBorderRight: 'handleConnectedBorderRight'; - readonly handleConnectedBorderLeft: 'handleConnectedBorderLeft'; - readonly handleFunction: 'handleFunction'; - readonly logic: 'logic'; -}; -export = classNames; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx index 032c8b291e0f71f307c59c524aa240b53847e5f8..0a5e0b8843e13ee7835c3a1f77796c3ab2bf096d 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx @@ -9,12 +9,11 @@ * We do not test components/renderfunctions/styling files. * See testing plan for more details.*/ import { useAppDispatch, useQuerybuilderGraph, useQuerybuilderHash } from '@graphpolaris/shared/lib/data-access'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Handle, HandleType, Position } from 'reactflow'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { Handle, Position } from 'reactflow'; import { Handles, LogicNodeAttributes, SchemaReactflowLogicNode, toHandleId } from '../../../model'; import { InputNode, InputNodeTypeTypes } from '../../../model/logic/general'; import { styleHandleMap } from '../../utils'; -import styles from './logicpill.module.scss'; import { setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { LogicInput } from './logicInput'; @@ -36,7 +35,7 @@ export default function LogicPill(node: SchemaReactflowLogicNode) { const connectionsToRight = useMemo(() => graph.edges.filter((edge) => edge.source === node.id), [graph]); const graphologyNodeAttributes = useMemo<LogicNodeAttributes | undefined>( () => (graphologyGraph.hasNode(node.id) ? { ...(graphologyGraph.getNodeAttributes(node.id) as LogicNodeAttributes) } : undefined), - [node.id] + [node.id], ); const [localInputCache, setLocalInputCache] = useState<Record<string, InputNodeTypeTypes>>({ ...graphologyNodeAttributes?.inputs }); @@ -57,29 +56,16 @@ export default function LogicPill(node: SchemaReactflowLogicNode) { logicNode.inputs = logicNodeInputs; graphologyGraph.setNodeAttribute<any>(node.id, 'inputs', logicNodeInputs); // FIXME: I'm not sure why TS requires <any> to work here dispatch(setQuerybuilderGraphology(graphologyGraph)); - // console.log('updated input', input.name, 'from', data.inputs[input.name], 'to', value); } }; - // const createLeftHandles = useCallback( - // (sideInputs: InputNode[]) => { - // return sideInputs.filter((input, i) => { - // return !connectionsToLeft.some( - // (edge) => edge?.attributes?.targetHandleData.nodeId === data.id && edge?.attributes?.targetHandleData.attributeName === input.name - // ); - // }).length; - // }, - // [node] - // ); - // const leftInputsNumber = createLeftHandles(node.data.logic.inputs); - useEffect(() => { if (inputReference?.current) inputReference.current.focus(); }, [node.id]); return ( - <div className={styles.logic + ' w-fit h-min-[3rem]'}> - <div className="h-fit"> + <div className="rounded-sm shadow h-min-[3rem] text-[13px] bg-gradient-to-r pt-1 from-[#DEB68E] to-[#543719]"> + <div className={`py-1 h-fit ${data.selected ? 'bg-secondary-400' : 'bg-secondary-50'}`}> { <div className="m-1 mx-2 text-left"> {connectionsToLeft.map((e) => e?.attributes?.sourceHandleData.attributeName)}.{output.name} @@ -91,7 +77,8 @@ export default function LogicPill(node: SchemaReactflowLogicNode) { <div className="w-full flex"> {!connectionsToLeft.some( (edge) => - edge?.attributes?.targetHandleData.nodeId === data.id && edge?.attributes?.targetHandleData.attributeName === input.name + edge?.attributes?.targetHandleData.nodeId === data.id && + edge?.attributes?.targetHandleData.attributeName === input.name, ) && ( <LogicInput value={localInputCache?.[input.name] as string} diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx index 0d7b64dad8a2fe4776d14cd4e9052a20b6e1e7d1..b0a877a187598fb0242bdd555256290054da8e94 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import { querybuilderSlice, setQuerybuilderNodes } from '@graphpolaris/shared/lib/data-access/store'; +import { querybuilderSlice, schemaSlice, setQuerybuilderNodes, searchResultSlice } from '@graphpolaris/shared/lib/data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; -import { MultiGraph } from 'graphology'; import { QueryBuilder } from '../../../panel'; import { QueryElementTypes, QueryMultiGraphology } from '../../../model'; @@ -18,6 +17,8 @@ const Component: Meta<typeof QueryBuilder> = { const mockStore = configureStore({ reducer: { querybuilder: querybuilderSlice.reducer, + schema: schemaSlice.reducer, + searchResults: searchResultSlice.reducer, }, }); const graph = new QueryMultiGraphology(); diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx index 272abc33bf974c13f1fef06d6f756132e0a916d3..56fcf6848d47447818fca5537fb7b8bd5e099705 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx @@ -1,5 +1,5 @@ import { Handle, HandleType, Position } from 'reactflow'; -import { QueryGraphEdgeHandle, toHandleId } from '../../..'; +import { QueryGraphEdgeHandle } from '../../..'; import { tailwindColors } from 'config'; import { FilterHandle } from '../../FilterHandle'; @@ -23,7 +23,7 @@ export const LeftHandle = (props: Props) => { handle={props.handle} type={props.type} position={Position.Left} - className="!top-2.5" + className="!top-4" style={{ transform: `translate(${offset}px, -3px)` }} onDoubleClickCapture={(e) => { e.preventDefault(); @@ -46,7 +46,7 @@ export const RightHandle = (props: Props) => { handle={props.handle} type={props.type} position={Position.Right} - className="!top-2.5" + className="!top-4" style={{ transform: `translate(${offset}px, -3px)` }} onDoubleClickCapture={(e) => { e.preventDefault(); diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx index 6c67d686933392357762594925ec20bf0ffea63a..3fbbdd1c485dc77ef7d9b8c82772c605c4593be0 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx @@ -4,7 +4,7 @@ import RelationPill from './relationpill'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; -import { querybuilderSlice, schemaSlice } from '@graphpolaris/shared/lib/data-access/store'; +import { querybuilderSlice, schemaSlice, searchResultSlice } from '@graphpolaris/shared/lib/data-access/store'; import { ReactFlowProvider } from 'reactflow'; import { RelationData } from '../../../model'; @@ -30,12 +30,11 @@ export default Component; const Mockstore = configureStore({ reducer: { querybuilder: querybuilderSlice.reducer, - // schema: schemaSlice.reducer, + schema: schemaSlice.reducer, + searchResults: searchResultSlice.reducer, }, }); -// const Template = (args: any) => <EntityRFPill {...args} />; - export const Default: StoryObj<{ data: RelationData }> = { args: { data: { diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx index 24e1528ce599c5026189c1dfaeae407474ace646..b1ec0d735508ac99a31fa9b7b869397f5d5e3ec3 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx @@ -1,16 +1,8 @@ -import { memo, useRef, useState, useMemo, useEffect } from 'react'; -import { Handle, Position } from 'reactflow'; +import { memo, useState, useMemo, useEffect } from 'react'; import { NodeAttribute, RelationNodeAttributes, SchemaReactflowRelationNode } from '../../../model'; -import { - setQuerybuilderNodes, - useAppDispatch, - useConfig, - useQuerybuilderGraph, - useQuerybuilderSettings, -} from '@graphpolaris/shared/lib/data-access'; +import { useAppDispatch, useQuerybuilderGraph, useQuerybuilderSettings } from '@graphpolaris/shared/lib/data-access'; import { addWarning } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; -import graphology from 'graphology'; import { LeftHandle, RelationshipHandleArrowType, RightHandle } from './relation-handles'; import { PillDropdown } from '../../pilldropdown/pilldropdown'; @@ -26,11 +18,11 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { const dispatch = useAppDispatch(); const graphologyNodeAttributes = useMemo<RelationNodeAttributes | undefined>( () => (graphologyGraph.hasNode(node.id) ? { ...(graphologyGraph.getNodeAttributes(node.id) as RelationNodeAttributes) } : undefined), - [node.id] + [node.id], ); const attributeEdges = useMemo( () => graph.edges.filter((edge) => edge.source === node.id && !!edge?.attributes?.sourceHandleData.attributeType), - [graph] + [graph], ); const [hovered, setHovered] = useState(false); const [handleBeingDragged, setHandleBeingDragged] = useState(-1); @@ -94,13 +86,11 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { return ( <div - className={`text-center font-bold ${ - data.selected ? 'bg-slate-400' : 'bg-relation-50' - } border-l-relation-600 border-l-[3px] text-[13px] min-w-[200px]`} + className={`rounded-sm shadow min-w-[200px] text-[13px] bg-gradient-to-r pt-1 from-[#4893D4] to-[#1A476E]`} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} > - <div> + <div className={`py-1 ${data.selected ? 'bg-secondary-400' : 'bg-secondary-50'}`}> <span> {data.rightEntityHandleId && ( <RightHandle handle={data.rightEntityHandleId} type="source" point={direction} onDoubleClick={onChangeDirection} /> @@ -112,7 +102,8 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { <span>[</span> <input className={ - 'bg-inherit text-center appearance-none mx-0.5 rounded-sm ' + (depth.min < 0 || depth.min > depth.max ? ' bg-red-400 ' : '') + 'bg-inherit text-center appearance-none mx-0.5 rounded-sm ' + + (depth.min < 0 || depth.min > depth.max ? ' bg-danger-400 ' : '') } style={{ maxWidth: calcWidth(depth.min) }} type="number" @@ -135,7 +126,7 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { <input className={ 'bg-inherit text-center appearance-none mx-0.5 rounded-sm ' + - (depth.max > 99 || depth.min > depth.max ? ' bg-red-400 ' : '') + (depth.max > 99 || depth.min > depth.max ? ' bg-danger-400 ' : '') } style={{ maxWidth: calcWidth(depth.max) }} type="number" @@ -159,7 +150,7 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { </div> <span> {data.leftEntityHandleId && ( - <LeftHandle handle={data.leftEntityHandleId} type="target" point={direction} onDoubleClick={onChangeDirection} /> + <LeftHandle handle={data.leftEntityHandleId} type="target" point={'left'} onDoubleClick={onChangeDirection} /> )} </span> {data?.attributes && ( diff --git a/libs/shared/lib/querybuilder/pills/querypills.module.scss b/libs/shared/lib/querybuilder/pills/querypills.module.scss index 14cb45860084bd792203e4f7851edc074568a982..8a704a7b1de63d3386c8515a55d84d1f99b04599 100644 --- a/libs/shared/lib/querybuilder/pills/querypills.module.scss +++ b/libs/shared/lib/querybuilder/pills/querypills.module.scss @@ -13,14 +13,14 @@ } .react-flow__edges { - zindex: '3'; + z-index: 3; } .react-flow__nodes { } .react-flow__pane { } .react-flow__edge-default .selected { - stroke: 'gray !important'; + stroke: gray !important; } // This is used to override the previous color of the handle, for that to work it has to be on the bottom of the file diff --git a/libs/shared/lib/schema/panel/schema.tsx b/libs/shared/lib/schema/panel/schema.tsx index 26e8cc5127891474026b8c12be2d72afb2e043a6..fa644223691ead5c18c376216433f336139984c4 100644 --- a/libs/shared/lib/schema/panel/schema.tsx +++ b/libs/shared/lib/schema/panel/schema.tsx @@ -1,33 +1,13 @@ import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '@graphpolaris/shared/lib/graph-layout'; import { schemaGraphology2Reactflow, schemaExpandRelation } from '@graphpolaris/shared/lib/schema/schema-utils'; -import { - useSchemaGraph, - useSchemaLayout, - useSchemaSettings, - useSearchResultSchema, - useSessionCache, -} from '@graphpolaris/shared/lib/data-access/store'; +import { useSchemaGraph, useSchemaSettings, useSearchResultSchema, useSessionCache } from '@graphpolaris/shared/lib/data-access/store'; import { SmartBezierEdge, SmartStepEdge, SmartStraightEdge } from '@tisoap/react-flow-smart-edge'; import { useEffect, useMemo, useRef, useState } from 'react'; -import ReactFlow, { - ControlButton, - Controls, - Node, - Edge, - ReactFlowProvider, - useNodesState, - useEdgesState, - ReactFlowInstance, - useReactFlow, -} from 'reactflow'; -import CachedIcon from '@mui/icons-material/Cached'; -import SettingsIcon from '@mui/icons-material/Settings'; +import ReactFlow, { Node, Edge, ReactFlowProvider, useNodesState, useEdgesState, ReactFlowInstance } from 'reactflow'; import 'reactflow/dist/style.css'; -import styles from './schema.module.scss'; - import { ConnectionDragLine, ConnectionLine } from '@graphpolaris/shared/lib/querybuilder/pills'; import { EntityNode } from '../pills/nodes/entity/entity-node'; import { RelationNode } from '../pills/nodes/relation/relation-node'; @@ -36,6 +16,8 @@ import SelfEdge from '../pills/edges/self-edge'; import { useSchemaAPI } from '../../data-access'; import { SchemaDialog } from './schemaDialog'; import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; +import { Button } from '../../components/buttons'; +import ControlContainer from '../../components/controls'; interface Props { content?: string; @@ -49,18 +31,10 @@ const onInit = (reactFlowInstance: ReactFlowInstance) => { const nodeTypes = { entity: EntityNode, relation: RelationNode, - // nodeQualityEntityPopup: NodeQualityEntityPopupNode, - // nodeQualityRelationPopup: NodeQualityRelationPopupNode, - // attributeAnalyticsPopupMenu: AttributeAnalyticsPopupMenu, - - // entity: EntityRFPill, - // relation: RelationRFPill, - // attribute: AttributeRFPill, }; const edgeTypes = { nodeEdge: NodeEdge, selfEdge: SelfEdge, - // connection: ConnectionLine, bezier: SmartBezierEdge, connection: ConnectionLine, straight: SmartStraightEdge, @@ -71,16 +45,14 @@ export const Schema = (props: Props) => { const api_schema = useSchemaAPI(); const session = useSessionCache(); const settings = useSchemaSettings(); - const searchResults = useSearchResultSchema(); - const [toggleSchemaSettings, setToggleSchemaSettings] = useState(false); const [nodes, setNodes, onNodeChanged] = useNodesState([] as Node[]); const [edges, setEdges, onEdgeChanged] = useEdgesState([] as Edge[]); const [firstUserConnection, setFirstUserConnection] = useState<boolean>(true); const [auth, setAuth] = useState(props.auth); - const settingsIconRef = useRef<SVGSVGElement>(null); - const dialogRef = useRef<HTMLDivElement>(null); + + const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null); // In case the schema is updated const schemaGraph = useSchemaGraph(); @@ -92,6 +64,12 @@ export const Schema = (props: Props) => { layout.current = layoutFactory.createLayout(settings.layoutName); } + const fitView = () => { + if (reactFlowInstanceRef.current) { + reactFlowInstanceRef.current.fitView(); + } + }; + useEffect(() => { updateLayout(); sessionStorage.setItem('firstUserConnection', firstUserConnection.toString()); @@ -111,31 +89,9 @@ export const Schema = (props: Props) => { updateLayout(); const expandedSchema = schemaExpandRelation(schemaGraphology); layout.current?.layout(expandedSchema); - const schemaFlow = schemaGraphology2Reactflow(expandedSchema, settings.connectionType); - - // schemaFlow.nodes.forEach((n) => { - // n.data.toggleNodeQualityPopup = toggleNodeQualityPopup; - // n.data.toggleAttributeAnalyticsPopupMenu = - // toggleAttributeAnalyticsPopupMenu; - // }); - // console.log(edges); - - // console.log( - // 'schema Layout', - // schemaLayout, - // 'order', - // expandedSchema, - // schemaFlow - // ); setNodes(schemaFlow.nodes); setEdges(schemaFlow.edges); - // console.log( - // 'update schema useEffect', - // schemaGraphology, - // schemaGraphology.order, - // schemaFlow - // ); }, [schemaGraph, settings]); useEffect(() => { @@ -143,22 +99,38 @@ export const Schema = (props: Props) => { nds.map((node) => ({ ...node, selected: searchResults.includes(node.id) || searchResults.includes(node.data.label), - })) + })), ); }, [searchResults]); - useEffect(() => { - if (dialogRef.current && settingsIconRef.current) { - dialogRef.current.style.top = `${settingsIconRef.current.getBoundingClientRect().top}px`; - dialogRef.current.style.left = `${settingsIconRef.current.getBoundingClientRect().left + 30}px`; - } - }, [settingsIconRef, dialogRef, toggleSchemaSettings]); - return ( <div className="schema-panel w-full h-full"> - <SchemaDialog open={toggleSchemaSettings} onClose={() => setToggleSchemaSettings(false)} ref={dialogRef} /> - <div className="flex flex-col h-[1rem]"> - <h1>Schema</h1> + <SchemaDialog open={toggleSchemaSettings} onClose={() => setToggleSchemaSettings(false)} /> + <div className="relative flex items-center justify-between z-[2] py-0 px-2 bg-secondary-100 border-b border-secondary-200"> + <h1 className="text-xs font-semibold text-secondary-800">Schema</h1> + <ControlContainer> + <Button type="secondary" variant="ghost" size="xs" iconName="Fullscreen" onClick={fitView} /> + <Button + type="secondary" + variant="ghost" + size="xs" + iconName="Cached" + onClick={(e) => { + e.stopPropagation(); + api_schema.RequestSchema(session.currentDatabase); + }} + /> + <Button + type="secondary" + variant="ghost" + size="xs" + iconName="Settings" + onClick={(e) => { + e.stopPropagation(); + setToggleSchemaSettings(!toggleSchemaSettings); + }} + /> + </ControlContainer> </div> {nodes.length === 0 ? ( <p className="text-sm">No Elements</p> @@ -177,45 +149,12 @@ export const Schema = (props: Props) => { onEdgesChange={onEdgeChanged} nodes={nodes} edges={edges} - onInit={onInit} + onInit={(reactFlowInstance) => { + reactFlowInstanceRef.current = reactFlowInstance; + onInit(reactFlowInstance); + }} proOptions={{ hideAttribution: true }} - > - <Controls showInteractive={false} showZoom={false} showFitView={true} className={`${styles.controls} schema-settings`}> - {/* <ControlButton - className={styles.exportButton} - title={'Export graph schema'} - onClick={(event) => { - event.stopPropagation(); - // this.setState({ - // ...this.state, - // exportMenuAnchor: event.currentTarget, - // }); - }} - > - <img src={exportIcon} width={21}></img> - </ControlButton> */} - <ControlButton - className={styles.exportButton} - title={'Refresh graph schema'} - onClick={(event) => { - event.stopPropagation(); - api_schema.RequestSchema(session.currentDatabase); - }} - > - <CachedIcon /> - </ControlButton> - <ControlButton - className={styles.exportButton} - title={'Open Settings'} - onClick={(event) => { - event.stopPropagation(); - setToggleSchemaSettings(!toggleSchemaSettings); - }} - > - <SettingsIcon ref={settingsIconRef} /> - </ControlButton> - </Controls> - </ReactFlow> + ></ReactFlow> </div> </ReactFlowProvider> )} diff --git a/libs/shared/lib/schema/panel/schemaDialog.tsx b/libs/shared/lib/schema/panel/schemaDialog.tsx index 8dddeb6645de2b0a6e4e4d4a602b57eecb15a573..54b8ad354804cd3db8ee758521418e2af3183858 100644 --- a/libs/shared/lib/schema/panel/schemaDialog.tsx +++ b/libs/shared/lib/schema/panel/schemaDialog.tsx @@ -1,16 +1,18 @@ -import { PropsWithChildren, useEffect, useRef } from 'react'; +import { useEffect, useState } from 'react'; import { Dialog, DialogProps } from '../../components/Dialog'; import React from 'react'; -import CloseIcon from '@mui/icons-material/Close'; import { useAppDispatch, useSchemaSettings } from '../../data-access'; import { SchemaSettings, setSchemaSettings } from '../../data-access/store/schemaSlice'; import { FormActions, FormBody, FormCard, FormControl, FormHBar, FormTitle, FormDiv } from '../../components/forms'; import { Layouts } from '../../graph-layout'; +import Input from '../../components/inputs'; -export const SchemaDialog = React.forwardRef<HTMLDivElement, DialogProps>((props, ref) => { +export const SchemaDialog = (props: DialogProps) => { const settings = useSchemaSettings(); const dispatch = useAppDispatch(); const [state, setState] = React.useState<SchemaSettings>(settings); + const [display, setDisplay] = useState<string[]>([]); + const [opacity, setOpacity] = useState<number>(0); useEffect(() => { setState(settings); @@ -18,15 +20,14 @@ export const SchemaDialog = React.forwardRef<HTMLDivElement, DialogProps>((props function submit() { dispatch(setSchemaSettings(state)); - props.onClose(); } return ( <> {props.open && ( - <FormDiv ref={ref}> - <FormCard className="w-fit"> + <FormDiv hAnchor="left"> + <FormCard> <FormBody onSubmit={(e) => { e.preventDefault(); @@ -36,25 +37,27 @@ export const SchemaDialog = React.forwardRef<HTMLDivElement, DialogProps>((props <FormTitle title="Quick Settings" onClose={props.onClose} /> <FormHBar /> <FormControl> - <label className="label cursor-pointer w-fit gap-2 px-0 py-1"> - <input type="checkbox" checked={true} onChange={(e) => {}} className="checkbox checkbox-xs" /> - <span className="label-text">Points</span> - </label> - <label className="label cursor-pointer w-fit gap-2 px-0 py-1"> - <input type="checkbox" checked={true} onChange={(e) => {}} className="checkbox checkbox-xs" /> - <span className="label-text">Line</span> - </label> - <label className="label cursor-pointer w-fit gap-2 px-0 py-1"> - <input type="checkbox" checked={true} onChange={(e) => {}} className="checkbox checkbox-xs" /> - <span className="label-text">Line</span> - </label> + <Input + type="checkbox" + value={display} + options={['Points', 'Line', 'Box']} + onChange={(value: string[]) => { + setDisplay(value); + }} + /> </FormControl> <FormHBar /> <FormControl> - <label className="label"> - <span className="label-text">Opacity</span> - </label> - <input type="range" min={0} max="100" value="40" onChange={(e) => {}} className="range range-sm" /> + <Input + type="slider" + label="Opacity" + unit={'%'} + value={opacity} + min={0} + max={100} + step={1} + onChange={(value: number) => setOpacity(value)} + /> </FormControl> <FormHBar /> <FormControl> @@ -65,48 +68,27 @@ export const SchemaDialog = React.forwardRef<HTMLDivElement, DialogProps>((props </FormControl> <FormHBar /> <FormControl> - <label className="label"> - <span className="label-text">Type of Connection</span> - </label> - <select - className="select select-primary select-sm " + <Input + type="dropdown" + label="Type of Connection" value={state.connectionType} - onChange={(e) => { - setState({ ...state, connectionType: e.target.value as any }); + options={['Default', 'Step', 'Straight', 'Bezier']} + onChange={(value: string) => { + setState({ ...state, connectionType: value as any }); }} - > - <option className="option" value="connection"> - Default - </option> - <option className="option" value="step"> - Step - </option> - <option className="option" value="straight"> - Straight - </option> - <option className="option" value="bezier"> - Bezier - </option> - </select> + /> </FormControl> <FormHBar /> <FormControl> - <label className="label"> - <span className="label-text">Layout Type</span> - </label> - <select - className="select select-primary select-sm " + <Input + type="dropdown" + label="Layout Type" value={state.layoutName} - onChange={(e) => { - setState({ ...state, layoutName: e.target.value as any }); + options={Object.values(Layouts)} + onChange={(value: string) => { + setState({ ...state, layoutName: value as any }); }} - > - {Object.entries(Layouts).map(([k, v]) => ( - <option className="option" value={v} key={v}> - {k} - </option> - ))} - </select> + /> </FormControl> <FormHBar /> @@ -117,4 +99,4 @@ export const SchemaDialog = React.forwardRef<HTMLDivElement, DialogProps>((props )} </> ); -}); +}; diff --git a/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx b/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx index 1c96996cd23ba117c4bd2a3578953ee9f225cf56..3e9b26c705f2cf6d335caa3f2ec7e535831325ce 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx +++ b/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx @@ -29,7 +29,7 @@ export const SchemaEntityPopup = (props: SchemaEntityPopupProps) => { <span>Nodes</span> <span className="float-right">TBD</span> </span> - <div className="h-[1px] w-full bg-offwhite-300"></div> + <div className="h-[1px] w-full bg-secondary-200"></div> <div className="px-2.5 text-[0.8rem]"> <p> Null Values: <span className="float-right">TBD</span> @@ -38,17 +38,7 @@ export const SchemaEntityPopup = (props: SchemaEntityPopupProps) => { Not connected: <span className="float-right">TBD</span> </p> </div> - <div className="h-[1px] w-full bg-offwhite-300"></div> - {/* <span>Attributes:</span> - <div className="text-xs"> - {data.attributes.map((attribute) => { - return ( - <div className="flex flex-row" key={attribute.name}> - <span>{attribute.name}</span> - </div> - ); - })} - </div> */} + <div className="h-[1px] w-full bg-secondary-200"></div> <button className="btn btn-outline btn-accent border-0 btn-sm p-0 m-0 text-[0.8rem] mb-2 mx-2.5 min-h-0 h-5" onClick={() => props.onClose()} diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx index f5ce8680031079d797a1736ff412908ad99b26f9..5192cb2d04dc7306c9090cdcd5da3ca1df3cad19 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx @@ -62,7 +62,7 @@ export const EntityNode = React.memo(({ id, selected, data }: NodeProps<SchemaRe </Popup> )} <div - className={`border-l-2 border-l-entity-600 min-w-[8rem] text-[0.8rem] ${selected ? 'bg-slate-400' : 'bg-offwhite-200'}`} + className={`rounded-sm hover:shadow-xl transition-all duration-150 shadow min-w-[8rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#FFA952] to-[#D66700]`} onDragStart={(event) => onDragStart(event)} onDragStartCapture={(event) => onDragStart(event)} onMouseDownCapture={(event) => { @@ -73,111 +73,24 @@ export const EntityNode = React.memo(({ id, selected, data }: NodeProps<SchemaRe }} draggable > - {/* <div - className={styles.entityNodeAttributesBox} - onClick={() => onClickToggleAttributeAnalyticsPopupMenu()} - style={{ - borderBottomColor: theme.palette.custom.elements.entityBase[0], - width: calcWidthEntityNodeBox(data.attributes.length, data.nodeCount) + 'ch', - background: `linear-gradient(-90deg, - ${theme.palette.custom.nodesBase} 0%, - ${theme.palette.custom.nodesBase} ${calculateAttributeQuality(data)}%, - ${theme.palette.custom.elements.entitySecond[0]} ${calculateAttributeQuality(data)}%)`, - }} - > - <span - style={{ - paddingLeft: '5px', - float: 'left', - }} - > - A - </span> - <span className={styles.nodeSpan}>{data.attributes.length}</span> - </div> */} - {/* <div - className={styles.entityNodeNodesBox} - onClick={() => onClickToggleNodeQualityPopup()} - style={{ - borderBottomColor: theme.palette.custom.elements.entityBase[0], - width: calcWidthEntityNodeBox(data.attributes.length, data.nodeCount) + 'ch', - background: `linear-gradient(-90deg, - ${theme.palette.custom.nodesBase} 0%, - ${theme.palette.custom.nodesBase} ${calculateEntityQuality(data)}%, - ${theme.palette.custom.elements.entitySecond[0]} ${calculateEntityQuality(data)}%)`, - }} - > - <span - style={{ - paddingLeft: '5px', - float: 'left', - }} - > - N - </span> - <span className={styles.nodeSpan}>{data.nodeCount}</span> - </div> */} - {/* <Handle - style={{ pointerEvents: 'none' }} - id="entitySourceLeft" - position={Position.Left} - className={styles.handleTriangleLeft} - type="source" - // hidden={Array.from(data.handles).includes('entitySourceLeft') ? false : true} - ></Handle> */} - <Handle - style={{ pointerEvents: 'none' }} - id="entityTargetLeft" - position={Position.Left} - className={styles.handleTriangleLeft} - type="target" - // hidden={Array.from(data.handles).includes('entityTargetLeft') ? false : true} - ></Handle> - <Handle - style={{ pointerEvents: 'none' }} - id="entitySourceRight" - position={Position.Right} - className={styles.handleTriangleRight} - type="source" - // hidden={Array.from(data.handles).includes('entitySourceRight') ? false : true} - ></Handle> - {/* <Handle - style={{ pointerEvents: 'none' }} - id="entityTargetRight" - position={Position.Right} - type="target" - // hidden={Array.from(data.handles).includes('entityTargetRight') ? false : true} - ></Handle> */} - {/* <Handle - style={{ pointerEvents: 'none' }} - id="entitySourceTop" - position={Position.Top} - type="source" - // hidden={Array.from(data.handles).includes('entitySourceTop') ? false : true} - ></Handle> - <Handle - style={{ pointerEvents: 'none' }} - id="entityTargetTop" - position={Position.Top} - type="target" - // hidden={Array.from(data.handles).includes('entityTargetTop') ? false : true} - ></Handle> */} - {/* <Handle - style={{ pointerEvents: 'none' }} - id="entitySourceBottom" - position={Position.Bottom} - type="source" - // hidden={Array.from(data.handles).includes('entitySourceBottom') ? false : true} - ></Handle> - <Handle - style={{ pointerEvents: 'none' }} - id="entityTargetBottom" - position={Position.Bottom} - type="target" - // hidden={Array.from(data.handles).includes('entityTargetBottom') ? false : true} - ></Handle> */} - <div className="p-2 py-1"> - <span className="">{id}</span> + <div className={`py-1 ${selected ? 'bg-secondary-400' : 'bg-secondary-50'}`}> + <Handle + style={{ pointerEvents: 'none' }} + id="entityTargetLeft" + position={Position.Left} + className={styles.handleTriangleLeft} + type="target" + ></Handle> + <Handle + style={{ pointerEvents: 'none' }} + id="entitySourceRight" + position={Position.Right} + className={styles.handleTriangleRight} + type="source" + ></Handle> + <div className="p-2 py-1 text-center"> + <span className="">{id}</span> + </div> </div> </div> </> diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity.module.scss b/libs/shared/lib/schema/pills/nodes/entity/entity.module.scss index ec0ccd5e85d6c7a7610dad8b0f9200438cb05afb..c15fbed8f6aefeeced231f1ea0da0720b4852843 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/entity.module.scss +++ b/libs/shared/lib/schema/pills/nodes/entity/entity.module.scss @@ -19,7 +19,8 @@ border-left: 5px solid transparent !important; border-right: 5px solid transparent !important; border-bottom: 8px solid !important; - @apply border-b-base-300 #{!important}; + //todo: color + //@apply border-b-secondary-300; } .handleTriangleLeft { @@ -30,5 +31,5 @@ .handleTriangleRight { @extend .handleTriangle; transform: rotate(90deg) translate(-50%, -45%) scale(0.65) !important; - @apply border-b-entity-300 #{!important}; + @apply border-b-entity-300; } diff --git a/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationshipPopup.tsx b/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationshipPopup.tsx index b61517f3618382df8ebfdd14b9ae32d269faaefd..d8716992682d02dbc060d46d61ec150a36e141eb 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationshipPopup.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationshipPopup.tsx @@ -31,7 +31,7 @@ export const SchemaRelationshipPopup = (props: SchemaRelationshipPopupProps) => <span>Relationships</span> <span className="float-right">TBD</span> </span> - <div className="h-[1px] w-full bg-offwhite-300"></div> + <div className="h-[1px] w-full bg-secondary-200"></div> <div className="px-2.5 text-[0.8rem]"> <p> Null Values: <span className="float-right">TBD</span> @@ -40,17 +40,7 @@ export const SchemaRelationshipPopup = (props: SchemaRelationshipPopupProps) => Not connected: <span className="float-right">TBD</span> </p> </div> - <div className="h-[1px] w-full bg-offwhite-300"></div> - {/* <span>Attributes:</span> - <div className="text-xs"> - {data.attributes.map((attribute) => { - return ( - <div className="flex flex-row" key={attribute.name}> - <span>{attribute.name}</span> - </div> - ); - })} - </div> */} + <div className="h-[1px] w-full bg-secondary-200"></div> <button className="btn btn-outline btn-primary border-0 btn-sm p-0 m-0 text-[0.8rem] mb-2 mx-2.5 min-h-0 h-5" onClick={() => props.onClose()} diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx index e36cd36ffa6b98d86cb8b1d2ec059106efd86d7c..41196ba18f85c0c1cc70ab4a37b44f71ed920c32 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx @@ -76,74 +76,32 @@ export const RelationNode = React.memo(({ id, selected, data }: NodeProps<Schema setOpenPopup(!openPopup); }} draggable - // style={{ width: 100, height: 100 }} > <div - className="text-[0.8rem] border-l-2 border-l-relation-600 min-w-[8rem]" + className={`rounded-sm hover:shadow-xl transition-all duration-150 shadow min-w-[8rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#4893D4] to-[#1A476E]`} style={{ backgroundColor: selected ? '#97a2b6' : '#f4f6f7', }} > - <Handle - style={{ pointerEvents: 'none' }} - className={styles.handleTriangleTop} - id="entitySourceLeft" - position={Position.Top} - type="target" - ></Handle> - {/* <div - className={styles.relationNodeAttributesBox} - onClick={() => onClickToggleAttributeAnalyticsPopupMenu()} - style={{ - width: widthExternalBoxes + 'px', - backgroundImage: `linear-gradient(-90deg, - ${theme.palette.custom.nodesBase} 0%, - ${theme.palette.custom.nodesBase} ${calculateAttributeQuality(data)}%, - ${theme.palette.custom.elements.relationSecond[0]} ${calculateAttributeQuality(data)}%)`, - }} - > - <span - style={{ - paddingLeft: '25px', - float: 'left', - }} - > - A - </span> - <span className={styles.nodeSpan}>{data.attributes.length}</span> - </div> */} - {/* <div - className={styles.relationNodeNodesBox} - onClick={() => onClickToggleNodeQualityPopup()} - style={{ - width: widthExternalBoxes + 'px', - backgroundImage: `linear-gradient(-90deg, - ${theme.palette.custom.nodesBase} 0%, - ${theme.palette.custom.nodesBase} ${calculateRelationQuality(data)}%, - ${theme.palette.custom.elements.relationSecond[0]} ${calculateRelationQuality(data)}%)`, - }} - > - <span - style={{ - paddingLeft: '25px', - float: 'left', - }} - > - N - </span> - <span className={styles.nodeSpan}>{data.nodeCount}</span> - </div> */} + <div className={`py-1 ${selected ? 'bg-secondary-400' : 'bg-secondary-50'}`}> + <Handle + style={{ pointerEvents: 'none' }} + className={styles.handleTriangleTop} + id="entitySourceLeft" + position={Position.Top} + type="target" + ></Handle> + <div className="p-2 py-1 text-center"> + <span className="">{data.collection}</span> + </div> - <div className="p-2 py-1"> - <span className="">{data.collection}</span> + <Handle + className={styles.handleTriangleBottom} + style={{ pointerEvents: 'none' }} + position={Position.Bottom} + type="source" + ></Handle> </div> - - <Handle - className={styles.handleTriangleBottom} - style={{ pointerEvents: 'none' }} - position={Position.Bottom} - type="source" - ></Handle> </div> </div> </> diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation.module.scss b/libs/shared/lib/schema/pills/nodes/relation/relation.module.scss index 3e8bd9bd72e207a19e6641d8b845dd9a1b34fc2e..cc085e42685fd796b64ea96cfdabf531ae6f5b42 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/relation.module.scss +++ b/libs/shared/lib/schema/pills/nodes/relation/relation.module.scss @@ -10,7 +10,8 @@ $width: 145; border-left: 4px solid transparent !important; border-right: 4px solid transparent !important; border-bottom: 6px solid !important; - @apply border-b-base-200 #{!important}; + //todo: color + //@apply border-b-secondary-200; } .handleTriangleTop { @@ -21,7 +22,7 @@ $width: 145; .handleTriangleBottom { @extend .handleTriangle; transform: rotate(-180deg) translate(0, -40%) !important; - @apply border-b-relation-700 #{!important}; + @apply border-b-relation-700; } .controls { diff --git a/libs/shared/lib/vis/mapvis/components/FilterMenu.tsx b/libs/shared/lib/vis/mapvis/components/FilterMenu.tsx index 42ca2b30666d843d151b7131aab7d94ea3d7df47..6cb870b9d2eafdabdfb36190e3be96d050371dc8 100644 --- a/libs/shared/lib/vis/mapvis/components/FilterMenu.tsx +++ b/libs/shared/lib/vis/mapvis/components/FilterMenu.tsx @@ -18,7 +18,7 @@ export default function FilterMenu({ graph, setShowFilter }: Props) { const edgeAttributes = graph.getGraphInfo().edgeAttributes; return ( - <div className="absolute z-10 bg-white w-11/12 bottom-5 p-2.5 left-2/4 -translate-x-1/2 shadow-sm"> + <div className="absolute z-10 bg-light w-11/12 bottom-5 p-2.5 left-2/4 -translate-x-1/2 shadow-sm"> <div className="flex justify-between items-center"> <div> <PlayArrow /> diff --git a/libs/shared/lib/vis/mapvis/components/LayerPanel.tsx b/libs/shared/lib/vis/mapvis/components/LayerPanel.tsx index 66fdea7c2e1cffc8201a3a0b67c5b3c1b1512eeb..af8342ed084def1b0a966cd0729bcbd2d889f02a 100644 --- a/libs/shared/lib/vis/mapvis/components/LayerPanel.tsx +++ b/libs/shared/lib/vis/mapvis/components/LayerPanel.tsx @@ -28,7 +28,7 @@ export function LayerPanel({ layers, setLayers, graphInfo, setShowFilter }: Prop }; return ( - <div className="w-3/10 bg-white flex flex-col h-full"> + <div className="w-3/10 bg-light flex flex-col h-full"> <div className="p-3 flex justify-center font-bold"> <h4>Layer Configurations</h4> </div> @@ -62,7 +62,7 @@ export function LayerPanel({ layers, setLayers, graphInfo, setShowFilter }: Prop ) : ( <> {layers.map((layer) => ( - <div className="collapse bg-base-200"> + <div className="collapse bg-secondary-200"> <input type="radio" name="my-accordion-2" /> <div className="collapse-title text-xl font-medium">{`${layer.type.type} layer`}</div> <div className="collapse-content">{layer.type.generateLayerOptions(layer, updateLayer, graphInfo, deleteLayer)}</div> diff --git a/libs/shared/lib/vis/mapvis/components/SecondaryMenu.tsx b/libs/shared/lib/vis/mapvis/components/SecondaryMenu.tsx index b3426b6886058e6c40b92851e11a26a9ec780775..870e6a50d8fe999fc9e042d41964b3848d7a32d5 100644 --- a/libs/shared/lib/vis/mapvis/components/SecondaryMenu.tsx +++ b/libs/shared/lib/vis/mapvis/components/SecondaryMenu.tsx @@ -42,12 +42,12 @@ export default function SecondaryMenu({ <div title="Full screen" onClick={() => setMapSize(!mapSize)} - className="bg-white p-1 mb-1 cursor-pointer shadow-sm flex justify-center items-center hover:bg-secondary" + className="bg-light p-1 mb-1 cursor-pointer shadow-sm flex justify-center items-center hover:bg-secondary" > <FitScreenOutlined /> </div> <div - className="bg-white p-1 mb-1 cursor-pointer shadow-sm flex justify-center items-center hover:bg-secondary" + className="bg-light p-1 mb-1 cursor-pointer shadow-sm flex justify-center items-center hover:bg-secondary" title="Search for location" > <SearchOutlined onClick={() => setSearchOpen(!searchOpen)} /> @@ -58,7 +58,7 @@ export default function SecondaryMenu({ )} </div> <div - className="bg-white p-1 mb-1 cursor-pointer shadow-sm flex justify-center items-center hover:bg-secondary" + className="bg-light p-1 mb-1 cursor-pointer shadow-sm flex justify-center items-center hover:bg-secondary" title="Select objects in an area" > <HighlightAltOutlined onClick={() => setSelectingRectangle(!selectingRectangle)} /> @@ -73,7 +73,7 @@ export default function SecondaryMenu({ transitionInterpolator: new FlyToInterpolator(), })) } - className="bg-white p-1 mb-1 cursor-pointer shadow-sm flex justify-center items-center hover:bg-secondary" + className="bg-light p-1 mb-1 cursor-pointer shadow-sm flex justify-center items-center hover:bg-secondary" > <Add /> </div> @@ -87,7 +87,7 @@ export default function SecondaryMenu({ transitionInterpolator: new FlyToInterpolator(), })) } - className="bg-white p-1 mb-1 cursor-pointer shadow-sm flex justify-center items-center hover:bg-secondary" + className="bg-light p-1 mb-1 cursor-pointer shadow-sm flex justify-center items-center hover:bg-secondary" > <Remove /> </div> diff --git a/libs/shared/lib/vis/mapvis/components/SelectedMenu.tsx b/libs/shared/lib/vis/mapvis/components/SelectedMenu.tsx index 98000c2d573169c7944773a5ce1c47ebf5f8aaf9..8de310c810f52e14314fafba6f28859d016d38a8 100644 --- a/libs/shared/lib/vis/mapvis/components/SelectedMenu.tsx +++ b/libs/shared/lib/vis/mapvis/components/SelectedMenu.tsx @@ -12,7 +12,7 @@ export default function SelectedMenu({ selected }: Props) { return ( <div> {selected.length > 0 && !exploreEntities && ( - <div className="bg-white p-2.5 cursor-pointer absolute top-0 right-0 hover:bg-secondary" onClick={() => setExploreEntities(true)}> + <div className="bg-light p-2.5 cursor-pointer absolute top-0 right-0 hover:bg-secondary" onClick={() => setExploreEntities(true)}> <ArrowBack /> </div> )} @@ -20,11 +20,11 @@ export default function SelectedMenu({ selected }: Props) { {selected.length > 0 && exploreEntities && ( <div className="absolute flex justify-between items-start z-10 top-0 right-0"> <div className="flex flex-col"> - <div className="bg-white p-2.5 cursor-pointer hover:bg-secondary" onClick={() => setExploreEntities(false)}> + <div className="bg-light p-2.5 cursor-pointer hover:bg-secondary" onClick={() => setExploreEntities(false)}> <ArrowForward /> </div> </div> - <div className="p-3 bg-white max-h-52 overflow-y-auto min-w-[100px] min-h-[18px]"> + <div className="p-3 bg-light max-h-52 overflow-y-auto min-w-[100px] min-h-[18px]"> <ReactJSONView src={selected} name={'Selected'} diff --git a/libs/shared/lib/vis/mapvis/mapvis.stories.tsx b/libs/shared/lib/vis/mapvis/mapvis.stories.tsx index 00d31ad5a440469596afad455dfe87f548bdcb02..195aebdf7988e798fc19e5cd2444948c2fc52fe5 100644 --- a/libs/shared/lib/vis/mapvis/mapvis.stories.tsx +++ b/libs/shared/lib/vis/mapvis/mapvis.stories.tsx @@ -12,7 +12,7 @@ const Mockstore = configureStore({ }); export default { - title: 'Components/Visualizations/MapVis', + title: 'Visualizations/MapVis', component: MapVis, decorators: [ (story: any) => ( diff --git a/libs/shared/lib/vis/mapvis/mapvis.tsx b/libs/shared/lib/vis/mapvis/mapvis.tsx index dc8ddc763793a6231221d6a6373b4a0392ed0465..e2a5810ffc99fbad5fad4dd877d4700db7ad14cd 100644 --- a/libs/shared/lib/vis/mapvis/mapvis.tsx +++ b/libs/shared/lib/vis/mapvis/mapvis.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { MapPanel, LayerPanel } from './components'; -import { useGraphQueryResult } from '../../data-access/store'; +import {MapPanel, LayerPanel} from './components'; +import {useGraphQueryResult} from '../../data-access/store'; import GraphModel from './graphModel'; -import { GraphType, Layer } from './Types'; +import {GraphType, Layer} from './Types'; export default function MapVis() { const [layers, setLayers] = React.useState<Layer[]>([]); @@ -23,9 +23,9 @@ export default function MapVis() { } return ( - <div className="flex flex-row justify-between overflow-hidden w-screen h-screen font-inter"> - <MapPanel graph={graph} layers={layers} showFilter={showFilter} setShowFilter={setShowFilter} /> - <LayerPanel layers={layers} setLayers={setLayers} graphInfo={graph.getGraphInfo()} setShowFilter={setShowFilter} /> + <div className="flex flex-row justify-between overflow-hidden w-screen h-screen font-sans"> + <MapPanel graph={graph} layers={layers} showFilter={showFilter} setShowFilter={setShowFilter}/> + <LayerPanel layers={layers} setLayers={setLayers} graphInfo={graph.getGraphInfo()} setShowFilter={setShowFilter}/> </div> ); } diff --git a/libs/shared/lib/vis/mapvis/shared/ColorPicker.tsx b/libs/shared/lib/vis/mapvis/shared/ColorPicker.tsx index aeafdeb14516bd82889dc64774c4e7b6fe390a2e..b9322f11681af831748dc6170656e78bdcc96be5 100644 --- a/libs/shared/lib/vis/mapvis/shared/ColorPicker.tsx +++ b/libs/shared/lib/vis/mapvis/shared/ColorPicker.tsx @@ -11,7 +11,7 @@ export default function ColorPicker({ value, updateValue }: Props) { return ( <> - <div className="p-1 inline-block cursor-pointer bg-white shadow-sm" onClick={() => setOpen(!open)}> + <div className="p-1 inline-block cursor-pointer bg-light shadow-sm" onClick={() => setOpen(!open)}> <div className="rounded-sm w-9 h-3.5" style={{ diff --git a/libs/shared/lib/vis/nodelink/components/NLPopup.tsx b/libs/shared/lib/vis/nodelink/components/NLPopup.tsx index cab6d5b29d129197bd484c404aeb85679a742b56..8e413b31aaad72c5fc7ac9512e306b6b32be87a3 100644 --- a/libs/shared/lib/vis/nodelink/components/NLPopup.tsx +++ b/libs/shared/lib/vis/nodelink/components/NLPopup.tsx @@ -11,7 +11,7 @@ export const NLPopup = (props: NodelinkPopupProps) => { return ( <div - className="absolute card card-bordered z-50 bg-white rounded-none text-[0.9rem] min-w-[10rem]" + className="absolute card card-bordered z-50 bg-light rounded-none text-[0.9rem] min-w-[10rem]" // style={{ top: props.data.pos.y + 10, left: props.data.pos.x + 10 }} style={{ transform: 'translate(' + (props.data.pos.x + 20) + 'px, ' + (props.data.pos.y + 10) + 'px)' }} > @@ -20,7 +20,7 @@ export const NLPopup = (props: NodelinkPopupProps) => { <span>Node</span> <span className="float-right">{node.id}</span> </span> - <div className="h-[1px] w-full bg-offwhite-300"></div> + <div className="h-[1px] w-full bg-secondary-200"></div> <div className="px-2.5 text-[0.8rem]"> {node.attributes && Object.entries(node.attributes).map(([k, v], i) => { diff --git a/libs/shared/lib/vis/nodelink/nodelinkvis.stories.tsx b/libs/shared/lib/vis/nodelink/nodelinkvis.stories.tsx index 15f2c742039dc0aa9377ff2304e438992d6853a4..83479342bf3d5191b569939266b7588a7a8e118e 100644 --- a/libs/shared/lib/vis/nodelink/nodelinkvis.stories.tsx +++ b/libs/shared/lib/vis/nodelink/nodelinkvis.stories.tsx @@ -12,7 +12,7 @@ const Component: Meta<typeof NodeLinkVis> = { * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading * to learn how to generate automatic titles */ - title: 'Components/Visualizations/NodeLinkVis', + title: 'Visualizations/NodeLinkVis', component: NodeLinkVis, decorators: [ (story) => ( @@ -104,21 +104,4 @@ export const TestWithLargeQueryResult = { }, }; -// export const Loading = Template.bind({}); -// Loading.args = { -// loading: true, -// }; -// -// export const Empty = Template.bind({}); -// Empty.args = { -// // Shaping the stories through args composition. -// // Inherited data coming from the Loading story. -// ...Loading.args, -// loading: false, -// }; -// Empty.play = async () => { -// const dispatch = store.dispatch; -// dispatch(resetGraphQueryResults()); -// }; - export default Component; diff --git a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx index 58f9d6568446f0cbd89193fdc8664205075e40ea..640c11db90d57bd083bbb5e20c5eb8c34a42f04e 100644 --- a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx +++ b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx @@ -1,26 +1,17 @@ -import { GraphQueryResult, useAppDispatch, useGraphQueryResult, useML } from '../../data-access/store'; -import React, { LegacyRef, useEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; +import { useAppDispatch, useGraphQueryResult, useML } from '../../data-access/store'; +import React, { useEffect, useRef, useState } from 'react'; import * as PIXI from 'pixi.js'; import { GraphType, LinkType, NodeType } from './Types'; import { NLPixi } from './components/NLPixi'; -import { getRelatedLinks } from './components/utils'; import { parseQueryResult } from './components/query2NL'; -import { processML } from './components/NLMachineLearning'; import { useImmer } from 'use-immer'; import { ML, setShortestPathSource, setShortestPathTarget } from '../../data-access/store/mlSlice'; -import { NLPopup } from './components/NLPopup'; interface Props { loading?: boolean; // currentColours: any; } -const Div = styled.div` - background-color: red; - font: 'Arial'; -`; - /** Return default radius */ function Radius() { return 1; @@ -59,7 +50,7 @@ export const NodeLinkVis = React.memo((props: Props) => { parseQueryResult(graphQueryResult, ml, { defaultX: (ref.current?.clientWidth || 1000) / 2, defaultY: (ref.current?.clientHeight || 1000) / 2, - }) + }), ); } }, [graphQueryResult, ml]); diff --git a/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.scss b/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.scss index 0db6bf0a10cbc3afa8eec5852b666dfe1581025b..28ee8cad8935512abc486e9364c397a2502782ef 100644 --- a/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.scss +++ b/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.scss @@ -23,9 +23,3 @@ font-size: 17px; color: inherit; } - -#makeButton { - height: 40px; - margin-right: 5em; - font-weight: bold; -} diff --git a/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.tsx b/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.tsx index 7c152154363f2a9871b0ea0eddc1672415fa978b..049efdee8177bbd312095f6032be9c8751ee51e6 100644 --- a/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.tsx +++ b/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.tsx @@ -28,6 +28,7 @@ import { calculateAttributesAndRelations, calculateAttributesFromRelation } from import calcEntitiesFromQueryResult from '../utils/CalcEntitiesFromQueryResult'; import { isNodeLinkResult } from '../../shared/ResultNodeLinkParserUseCase'; import { select } from 'd3'; +import { Button } from '../../../components/buttons'; /** The typing for the props of the Paohvis menu */ type MakePaohvisMenuProps = { @@ -37,7 +38,7 @@ type MakePaohvisMenuProps = { relationName: string, isEntityFromRelationFrom: boolean, chosenAttribute: Attribute, - nodeOrder: PaohvisNodeOrder + nodeOrder: PaohvisNodeOrder, ) => void; }; @@ -324,7 +325,7 @@ export const MakePaohvisMenu = (props: MakePaohvisMenuProps) => { { orderBy: state.sortOrder, isReverseOrder: state.isReverseOrder, - } + }, ); } else { setState((draft) => { @@ -554,9 +555,7 @@ export const MakePaohvisMenu = (props: MakePaohvisMenuProps) => { {reverseIcon} </button> - <button id="makeButton" className="btn btn-outline" disabled={!state.isButtonEnabled} onClick={onClickMakeButton}> - <span>Make</span> - </button> + <Button label="Make" type="primary" variant="solid" disabled={!state.isButtonEnabled} onClick={onClickMakeButton} /> </div> </div> ); diff --git a/libs/shared/lib/vis/paohvis/paohvis.stories.tsx b/libs/shared/lib/vis/paohvis/paohvis.stories.tsx index 36815e5862ef55d031a75c234f3f420bee22caf0..e1178e8fa98f2a9b964c94dae3e140fe18bb2a62 100644 --- a/libs/shared/lib/vis/paohvis/paohvis.stories.tsx +++ b/libs/shared/lib/vis/paohvis/paohvis.stories.tsx @@ -13,7 +13,7 @@ const Component: Meta<typeof Paohvis> = { * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading * to learn how to generate automatic titles */ - title: 'Components/Visualizations/Paohvis', + title: 'Visualizations/Paohvis', component: Paohvis, decorators: [ (story) => ( diff --git a/libs/shared/lib/vis/rawjsonvis/rawjsonvis.stories.tsx b/libs/shared/lib/vis/rawjsonvis/rawjsonvis.stories.tsx index 8bf071d2e2ee9b0268faf0c4265a17a1dff99a1b..ec51b4483a92561fd291837252cdb15fb9edf7f8 100644 --- a/libs/shared/lib/vis/rawjsonvis/rawjsonvis.stories.tsx +++ b/libs/shared/lib/vis/rawjsonvis/rawjsonvis.stories.tsx @@ -12,7 +12,7 @@ const Component: Meta<typeof RawJSONVis> = { * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading * to learn how to generate automatic titles */ - title: 'Components/Visualizations/RawJSONVIS', + title: 'Visualizations/RawJSONVIS', component: RawJSONVis, decorators: [(story) => <Provider store={Mockstore}>{story()}</Provider>], }; diff --git a/libs/shared/lib/vis/rawjsonvis/rawjsonvis.tsx b/libs/shared/lib/vis/rawjsonvis/rawjsonvis.tsx index a90933c95371769db19dd568d2976c43606e7d20..51c2ab50018eb5b743a35938cc0fba2c9275064d 100644 --- a/libs/shared/lib/vis/rawjsonvis/rawjsonvis.tsx +++ b/libs/shared/lib/vis/rawjsonvis/rawjsonvis.tsx @@ -27,11 +27,11 @@ export const RawJSONVis = React.memo((props: RawJSONVisProps) => { const loading = props.loading; return ( - <div className="overflow-scroll"> + <div> {loading && ( <div style={{ - marginTop: '40px', + marginTop: '50px', paddingLeft: '30px', }} > @@ -45,9 +45,10 @@ export const RawJSONVis = React.memo((props: RawJSONVisProps) => { <div style={{ overflowY: 'auto' }}> <div style={{ - marginTop: '40px', + marginTop: '50px', paddingLeft: '30px', }} + className="font-mono text-sm" > <ReactJSONView src={graphQueryResult} collapsed={1} quotesOnKeys={false} displayDataTypes={false} /> </div> diff --git a/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.stories.tsx b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.stories.tsx index a984c344dab304010b73cc2553453d5fa084bf28..7e1351364757adb810df330fa44399a420619c93 100644 --- a/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.stories.tsx +++ b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.stories.tsx @@ -13,7 +13,7 @@ const Component: Meta<typeof SemanticSubstrates> = { * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading * to learn how to generate automatic titles */ - title: 'Components/Visualizations/SemanticSubstrates', + title: 'Visualizations/SemanticSubstrates', component: SemanticSubstrates, decorators: [(story) => <Provider store={Mockstore}>{story()}</Provider>], }; diff --git a/libs/shared/lib/vis/table_vis/components/Pagination.tsx b/libs/shared/lib/vis/table_vis/components/Pagination.tsx deleted file mode 100644 index 8d55c2cbf40752594d808e2eb9515fbce27fd2c3..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/table_vis/components/Pagination.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useRef } from 'react'; -import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; -import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; - -interface PaginationProps { - currentPage: number; - totalPages: number; - onPageChange: (page: number) => void; - itemsPerPageInput: number; - numItemsArrayReal: number; - totalItems: number; -} - -const Pagination: React.FC<PaginationProps> = ({ - currentPage, - totalPages, - onPageChange, - itemsPerPageInput, - numItemsArrayReal, - totalItems, -}) => { - const pageNumbers = Array.from({ length: totalPages }, (_, index) => index + 1); - - const firstItem = (currentPage - 1) * itemsPerPageInput + 1; - const lastItem = Math.min(currentPage * itemsPerPageInput, totalPages); - - const goToPreviousPage = () => { - if (currentPage > 1) { - onPageChange(currentPage - 1); - } - }; - - const goToNextPage = () => { - if (currentPage < totalPages) { - onPageChange(currentPage + 1); - } - }; - - return ( - <div className="table-pagination h-full flex flex-col items-center"> - <span className="inline-block m-2 max-w-32 min-w-32">{`${firstItem}-${numItemsArrayReal} of ${totalItems}`}</span> - <div className="join"> - <button className="m-1 btn btn-outline" onClick={goToPreviousPage} disabled={currentPage === 1}> - <ArrowBackIosIcon /> Previous - </button> - <button className="m-1 btn btn-outline" onClick={goToNextPage} disabled={currentPage === totalPages}> - <ArrowForwardIosIcon /> Next - </button> - </div> - </div> - ); -}; - -export default Pagination; diff --git a/libs/shared/lib/vis/table_vis/components/Table.tsx b/libs/shared/lib/vis/table_vis/components/Table.tsx index 1e56ce8578b0dfd68b5c5ec6988b0024e08baf38..d677c45be00548f03a5f9a25b8445c6a78b3263e 100644 --- a/libs/shared/lib/vis/table_vis/components/Table.tsx +++ b/libs/shared/lib/vis/table_vis/components/Table.tsx @@ -1,9 +1,13 @@ -import React, { useState, useEffect, useRef, useMemo } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import * as d3 from 'd3'; -import Pagination from './Pagination'; -import BarPlot from './BarPlot'; +import Pagination from '@graphpolaris/shared/lib/components/pagination'; +import Icon from '@graphpolaris/shared/lib/components/icon'; + +import BarPlot from '@graphpolaris/shared/lib/components/charts/barplot'; + import { NodeAttributes } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; import { SchemaAttributeTypes } from '@graphpolaris/shared/lib/schema'; +import styles from './table.module.scss'; export type AugmentedNodeAttributes = { attribute: NodeAttributes; type: Record<string, SchemaAttributeTypes> }; @@ -21,7 +25,8 @@ type Data2RenderI = { }; export const Table = ({ data, itemsPerPage, showBarPlot }: TableProps) => { - const maxUniqueValues = 50; + console.debug('show; ', showBarPlot); + const maxUniqueValues = 350; const barPlotNumBins = 10; const [sortedData, setSortedData] = useState<AugmentedNodeAttributes[]>(data); @@ -134,7 +139,6 @@ export const Table = ({ data, itemsPerPage, showBarPlot }: TableProps) => { count: obj.attribute[dataColumn] as number, // fill values of data })); - console.log('number', categoryCounts); newData2Render.numElements = categoryCounts.length; newData2Render.data = categoryCounts; } else { @@ -156,57 +160,71 @@ export const Table = ({ data, itemsPerPage, showBarPlot }: TableProps) => { }, [currentPage, data, sortedData]); return ( - <div className="text-center font-inter text-primary"> + <> {currentPage && currentPage?.currentData?.length > 0 && data2Render?.length > 0 && ( <> - <table className="text-center my-2 mx-auto table-fixed w-11/12"> - <thead className="thead"> - <tr className="bg-white text-center p-0 pl-2 border-0 h-2 font-weight: 700"> - {dataColumns.map((item, index) => ( - <th className="th cursor-pointer select-none" key={index + item} onClick={() => toggleSort(item)}> - {item} {sortColumn === item && <span>{sortOrder === 'asc' ? '▲' : '▼'}</span>} - </th> - ))} - </tr> - <tr className="svggs align-top"> - {dataColumns.map((item, index) => ( - <th className="th" key={index + item}> - <div className="h-full w-full overflow-hidden"> - {showBarPlot && - data2Render[index].showBarPlot && - (data2Render[index]?.typeData === 'int' || data2Render[index]?.typeData === 'float') ? ( - <BarPlot typeBarPlot="numerical" numBins={barPlotNumBins} data={data2Render[index].data} /> - ) : !data2Render[index]?.typeData || !showBarPlot || !data2Render[index].showBarPlot ? ( - <div className="h-full text-xs font-normal flex flex-row items-center justify-center gap-1"> - <span>Unique values:</span> - <span className="text-sm">{data2Render[index]?.numElements}</span> - </div> - ) : ( - <BarPlot typeBarPlot="categorical" numBins={barPlotNumBins} data={data2Render[index].data} /> - )} - </div> - </th> - ))} - </tr> - </thead> - <tbody className="border-l-2 border-t-2 border-r-2 border-b-2 border-white w-20"> - {currentPage.currentData.map((item, index) => ( - <tr key={index} className={index % 2 === 0 ? 'bg-base-200' : 'bg-base-100'}> - {dataColumns.map((col, index) => ( - <td - className={`${ - item.type[col] === 'string' ? 'text-left' : 'text-center' - } px-1 py-0.5 border border-white m-0 overflow-x-hidden truncate`} - key={index} + <div className={styles['table-container']}> + <table className={`${styles.table} bg-secondary-100`}> + <thead className="thead"> + <tr> + {dataColumns.map((item, index) => ( + <th + className="border-light group hover:bg-secondary-300 bg-secondary-200 text-left overflow-x-hidden truncate capitalize cursor-pointer" + key={index + item} + onClick={() => toggleSort(item)} > - {item.attribute[col] as string} - </td> + <div className="flex flex-row max-w-full gap-1 justify-between"> + <span className="shrink overflow-hidden text-ellipsis">{item}</span> + <div + className={ + sortColumn === item ? 'opacity-100 text-primary' : 'opacity-0 group-hover:opacity-100 text-secondary-400' + } + > + <Icon + name={sortColumn === item ? (sortOrder === 'asc' ? 'ArrowDownward' : 'ArrowUpward') : 'Sort'} + size={20} + className={`${sortColumn === item && sortOrder ? 'text-secondary-800' : 'text-secondary-500'}`} + /> + </div> + </div> + </th> ))} </tr> - ))} - </tbody> - </table> - + <tr> + {dataColumns.map((item, index) => ( + <th className="border-light bg-light"> + <div className="th" key={index + item}> + <div className="h-full w-full overflow-hidden"> + {data2Render[index].showBarPlot && + (data2Render[index]?.typeData === 'int' || data2Render[index]?.typeData === 'float') ? ( + <BarPlot typeBarPlot="numerical" numBins={barPlotNumBins} data={data2Render[index].data} /> + ) : !data2Render[index]?.typeData || !data2Render[index].showBarPlot ? ( + <div className="font-normal mx-auto flex flex-row items-start justify-center w-full gap-1 text-center text-secondary-700"> + <span className="text-2xs overflow-x-hidden truncate">Unique values:</span> + <span className="text-xs shrink-0">{data2Render[index]?.numElements}</span> + </div> + ) : ( + <BarPlot typeBarPlot="categorical" numBins={barPlotNumBins} data={data2Render[index].data} /> + )} + </div> + </div> + </th> + ))} + </tr> + </thead> + <tbody> + {currentPage.currentData.map((item, index) => ( + <tr key={index}> + {dataColumns.map((col, index) => ( + <td className="border-light" data-datatype={item.type[col]} key={index}> + {item.attribute[col] as string} + </td> + ))} + </tr> + ))} + </tbody> + </table> + </div> <Pagination currentPage={currentPage.page} totalPages={totalPages} @@ -217,7 +235,7 @@ export const Table = ({ data, itemsPerPage, showBarPlot }: TableProps) => { /> </> )} - </div> + </> ); }; diff --git a/libs/shared/lib/vis/table_vis/components/table.module.scss b/libs/shared/lib/vis/table_vis/components/table.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..3c837c6367667bb967424608fd3799375757f286 --- /dev/null +++ b/libs/shared/lib/vis/table_vis/components/table.module.scss @@ -0,0 +1,49 @@ +.table-container { + @apply w-full relative overflow-x-auto; +} +.table { + @apply mx-auto table-fixed text-sm; + thead { + tr { + @apply p-0 border-0; + th { + @apply px-1.5 py-1.5 font-semibold; + } + } + } + tbody { + tr { + @apply border-b; + border-color: hsl(var(--clr-sec--200)); + &:hover { + background-color: hsl(var(--clr-sec--200)); + } + } + td { + @apply px-1.5 py-1.5 m-0 overflow-x-hidden truncate; + } + } + td, + th { + max-width: 20rem; + @apply border-r-2 text-left; + } + td[data-datatype='int'], + td[data-datatype='float'] { + @apply font-mono text-right; + } + td[data-datatype='bool'] { + @apply font-mono text-center; + } + td[data-datatype='string'] { + @apply text-left overflow-ellipsis overflow-hidden; + } + td[data-datatype='date'], + td[data-datatype='datetime'], + td[data-datatype='time'] { + @apply font-mono; + } + td[data-datatype='duration'] { + @apply font-mono; + } +} diff --git a/libs/shared/lib/vis/table_vis/components/table.module.scss.d.ts b/libs/shared/lib/vis/table_vis/components/table.module.scss.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..0dc7b2cfe2feff56721fa723aee89e4d10e2f30d --- /dev/null +++ b/libs/shared/lib/vis/table_vis/components/table.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly 'table-container': 'table-container'; + readonly table: 'table'; + readonly '5': '5'; +}; +export = classNames; diff --git a/libs/shared/lib/vis/table_vis/tableVis.tsx b/libs/shared/lib/vis/table_vis/tableVis.tsx index d9c8a89dc38b3c67c2b22edcd206d672ab275476..b15ade80aa926928d00e643cef44607d7bb119fa 100644 --- a/libs/shared/lib/vis/table_vis/tableVis.tsx +++ b/libs/shared/lib/vis/table_vis/tableVis.tsx @@ -1,14 +1,12 @@ import { useGraphQueryResult, useSchemaGraph } from '../../data-access/store'; import React, { useMemo, useRef } from 'react'; -import {Table, AugmentedNodeAttributes} from './components/Table'; -import { NodeAttributes } from '../../data-access/store/graphQueryResultSlice'; +import { Table, AugmentedNodeAttributes } from './components/Table'; import { SchemaAttribute } from '../../schema'; export const TableVis = React.memo(({ showBarplot }: { showBarplot: boolean }) => { const ref = useRef<HTMLDivElement>(null); const graphQueryResult = useGraphQueryResult(); const schema = useSchemaGraph(); - console.log(schema); const attributesArray = useMemo<AugmentedNodeAttributes[]>( () => @@ -17,18 +15,18 @@ export const TableVis = React.memo(({ showBarplot }: { showBarplot: boolean }) = schema.nodes.find((n) => n.key === node.label)?.attributes?.attributes ?? schema.edges.find((r) => r.key === node.label)?.attributes?.attributes ?? []; - + return { attribute: node.attributes, - type: Object.fromEntries(types.map((t) => ([t.name, t.type]))), + type: Object.fromEntries(types.map((t) => [t.name, t.type])), }; }), - [graphQueryResult.nodes] + [graphQueryResult.nodes], ); return ( <> - <div className="h-full w-full overflow-auto" ref={ref}> + <div className="h-full w-full" ref={ref}> {attributesArray.length > 0 && <Table data={attributesArray} itemsPerPage={10} showBarPlot={showBarplot} />} </div> </> diff --git a/libs/shared/lib/vis/table_vis/tablevis.stories.tsx b/libs/shared/lib/vis/table_vis/tablevis.stories.tsx index 0edeea9fcc34bb6847c572ca5c991828da2dd538..0e68173d56f39b81bb9a69cbcc314c537ab63042 100644 --- a/libs/shared/lib/vis/table_vis/tablevis.stories.tsx +++ b/libs/shared/lib/vis/table_vis/tablevis.stories.tsx @@ -2,17 +2,25 @@ import React from 'react'; import { Meta } from '@storybook/react'; import { TableVis } from './tableVis'; -import { assignNewGraphQueryResult, graphQueryResultSlice, resetGraphQueryResults, store } from '../../data-access/store'; +import { + assignNewGraphQueryResult, + graphQueryResultSlice, + schemaSlice, + setSchema, +} from '../../data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; -import { big2ndChamberQueryResult, smallFlightsQueryResults, mockLargeQueryResults, bigMockQueryResults } from '../../mock-data'; +import { smallFlightsQueryResults, simpleSchemaRaw, bigMockQueryResults } from '../../mock-data'; + +import { SchemaUtils } from '../../schema/schema-utils'; +import { simpleSchemaAirportRaw } from '../../mock-data/schema/simpleAirportRaw'; const Component: Meta<typeof TableVis> = { /* 👇 The title prop is optional. * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading * to learn how to generate automatic titles */ - title: 'Components/Visualizations/SimpleTableVis', + title: 'Visualizations/SimpleTableVis', component: TableVis, decorators: [ (story) => ( @@ -32,16 +40,46 @@ const Component: Meta<typeof TableVis> = { const Mockstore = configureStore({ reducer: { + schema: schemaSlice.reducer, graphQueryResult: graphQueryResultSlice.reducer, }, }); +/* export const TestWithBig2ndChamber = { args: { loading: false }, play: async () => { const dispatch = Mockstore.dispatch; + dispatch(assignNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: big2ndChamberQueryResult } })); }, }; +*/ + +export const TestWithAirport = { + args: { + loading: false, + }, + play: async () => { + const dispatch = Mockstore.dispatch; + const schema = SchemaUtils.schemaBackend2Graphology(simpleSchemaAirportRaw); + + dispatch(setSchema(schema.export())); + dispatch(assignNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: bigMockQueryResults } })); + }, +}; export default Component; + +export const TestWithAirportSimple = { + args: { + loading: false, + }, + play: async () => { + const dispatch = Mockstore.dispatch; + const schema = SchemaUtils.schemaBackend2Graphology(simpleSchemaRaw); + + dispatch(setSchema(schema.export())); + dispatch(assignNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: smallFlightsQueryResults } })); + }, +}; diff --git a/libs/storybook/.storybook/main.ts b/libs/storybook/.storybook/main.ts index 8fa1245cb6b48b7fcd9d4e4ee19ea81592251c6e..bbf042bfbbcfb9a791aab270d4f3fac0020e4b43 100644 --- a/libs/storybook/.storybook/main.ts +++ b/libs/storybook/.storybook/main.ts @@ -1,3 +1,4 @@ +import { dirname, join } from 'path'; import type { StorybookConfig } from '@storybook/react-vite'; const { mergeConfig } = require('vite'); const { default: tsconfigPaths } = require('vite-tsconfig-paths'); @@ -8,46 +9,30 @@ const config: StorybookConfig = { // "../src/**/*.stories.@(js|jsx|ts|tsx)", // "../node_modules/@graphpolaris/**/*.stories.@(js|jsx|ts|tsx)" '../node_modules/@graphpolaris/shared/lib/**/*.stories.@(js|jsx|ts|tsx)', + '../node_modules/@graphpolaris/shared/lib/**/*.mdx', + '../src/**/*.mdx', '../node_modules/web/src/**/*.stories.@(js|jsx|ts|tsx)', // "../../../apps/web/src/**/*.stories.@(js|jsx|ts|tsx)", ], addons: [ - '@storybook/addon-links', - '@storybook/addon-essentials', - '@storybook/addon-interactions', - { - name: '@storybook/addon-styling', - options: { - // sass: { - // // Require your Sass preprocessor here - // implementation: require('sass'), - // }, - // postCss: { - // implementation: require.resolve('postcss'), - // }, - }, - }, - { - name: '@storybook/preset-scss', - options: { - cssLoaderOptions: { - modules: true, - }, - }, - }, + getAbsolutePath('@storybook/addon-links'), + getAbsolutePath('@storybook/addon-essentials'), + getAbsolutePath('@storybook/addon-onboarding'), + getAbsolutePath('@storybook/addon-interactions'), ], framework: { - name: '@storybook/react-vite', + name: getAbsolutePath('@storybook/react-vite'), options: {}, }, docs: { autodocs: 'tag', + defaultName: 'Overview', }, - viteFinal(config, { configType }) { - return mergeConfig(config, { - plugins: [tsconfigPaths()], - }); - }, + // viteFinal(config, { configType }) { + // return mergeConfig(config, { + // plugins: [tsconfigPaths()], + // }); + // }, }; // config.module.rules.push({ @@ -56,3 +41,7 @@ const config: StorybookConfig = { // include: path.resolve(__dirname, "../"), }); export default config; + +function getAbsolutePath(value: string): any { + return dirname(require.resolve(join(value, 'package.json'))); +} diff --git a/libs/storybook/.storybook/preview-head.html b/libs/storybook/.storybook/preview-head.html index 05da1e9dfbfe10ccb66bee0d690f7643d4aff7e2..d0ea408cfbd44f401ccfde1e57afa484db6d96f0 100644 --- a/libs/storybook/.storybook/preview-head.html +++ b/libs/storybook/.storybook/preview-head.html @@ -1,3 +1,5 @@ +<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Roboto+Mono&display=swap" rel="stylesheet" /> + <script> window.global = window; -</script> \ No newline at end of file +</script> diff --git a/libs/storybook/.storybook/preview.ts b/libs/storybook/.storybook/preview.ts index d8ff918d3b8fe6c80918b4613691b7d206d936f4..a4379b8e329e28faa649fe0032efb3f6e26f37b0 100644 --- a/libs/storybook/.storybook/preview.ts +++ b/libs/storybook/.storybook/preview.ts @@ -1,5 +1,6 @@ import type { Preview } from '@storybook/react'; -import '../src/tailwind.css'; +import '../../../apps/web/src/main.css'; + import { withThemeByDataAttribute } from '@storybook/addon-styling'; const preview: Preview = { @@ -27,4 +28,8 @@ export const decorators = [ }), ]; +export const parameters = { + actions: { argTypesRegex: '^(on.*)' }, +}; + export default preview; diff --git a/libs/storybook/package.json b/libs/storybook/package.json index 58ff42d2bf66f996193179e556801c4da667d711..8a257e9d2c6ac3a0158148081ce6806eff0c3012 100644 --- a/libs/storybook/package.json +++ b/libs/storybook/package.json @@ -19,6 +19,7 @@ "@storybook/addon-essentials": "7.2.1", "@storybook/addon-interactions": "7.2.1", "@storybook/addon-links": "^7.2.1", + "@storybook/addon-onboarding": "^1.0.9", "@storybook/addon-styling": "^1.3.5", "@storybook/blocks": "^7.2.1", "@storybook/preset-scss": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 045005697cba4f71f0f787fc6f8445ba720fe64b..176c04faf1c1006e16683e7814389662f7d9fbf1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -576,6 +576,9 @@ importers: '@storybook/addon-links': specifier: ^7.2.1 version: 7.2.1(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-onboarding': + specifier: ^1.0.9 + version: 1.0.9(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-styling': specifier: ^1.3.5 version: 1.3.5(less@4.1.3)(postcss@8.4.27)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.2)(webpack@5.77.0) @@ -650,7 +653,7 @@ importers: version: 4.4.8(@types/node@20.4.6)(less@4.1.3)(sass@1.64.2) vite-plugin-sass-dts: specifier: ^1.3.9 - version: 1.3.9(postcss@8.4.27)(prettier@3.0.3)(sass@1.64.2)(vite@4.4.8) + version: 1.3.9(postcss@8.4.27)(prettier@2.8.8)(sass@1.64.2)(vite@4.4.8) vite-tsconfig-paths: specifier: ^4.2.0 version: 4.2.0(typescript@5.1.6)(vite@4.4.8) @@ -6219,6 +6222,21 @@ packages: - supports-color dev: true + /@storybook/addon-onboarding@1.0.9(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HlHm05Py18XOf4g7abiWkvb2WteoHcRNk1PY3Wtsmjuu5aAAjBmp4mVEg59xEeA2HAMICZ2fb72NIpFlBvDN+g==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@storybook/telemetry': 7.2.1 + react: 18.2.0 + react-confetti: 6.1.0(react@18.2.0) + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - encoding + - supports-color + dev: true + /@storybook/addon-outline@7.2.1(@types/react-dom@18.2.7)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-v2dYDhfSzV8Nsi1pmjcLEOHGJLlUnpnSXlQymb338YJEFKP2G5ylHzKAHG16MmzKeZZd3rthTB0246SFCyf0hg==} peerDependencies: @@ -6979,8 +6997,8 @@ packages: type-fest: 2.19.0 dev: true - /@storybook/csf@0.1.1-next.0: - resolution: {integrity: sha512-2M8E4CZOVW77P9lrgZZc2rcwxhNKVVykpzbcAauc3bots7xvDJMG60EasMRB/Y+cfqnSu6aaSUEVmKHTKsVJ3A==} + /@storybook/csf@0.1.2-next.0: + resolution: {integrity: sha512-Kf9XPAnO9VBxxYWNiW3pc1t7lDZ178VFfTCVWOm9r6Fdzm94j3zUNT06WiOojblVVaKsJktk8cT9b27Fmcxs5Q==} dependencies: type-fest: 2.19.0 dev: true @@ -7171,7 +7189,7 @@ packages: '@storybook/channels': 7.0.0-rc.5 '@storybook/client-logger': 7.0.0-rc.5 '@storybook/core-events': 7.0.0-rc.5 - '@storybook/csf': 0.1.1-next.0 + '@storybook/csf': 0.1.2-next.0 '@storybook/global': 5.0.0 '@storybook/types': 7.0.0-rc.5 '@types/qs': 6.9.7 @@ -16897,6 +16915,16 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /react-confetti@6.1.0(react@18.2.0): + resolution: {integrity: sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==} + engines: {node: '>=10.18'} + peerDependencies: + react: ^16.3.0 || ^17.0.1 || ^18.0.0 + dependencies: + react: 18.2.0 + tween-functions: 1.2.0 + dev: true + /react-cookie@4.1.1(react@18.2.0): resolution: {integrity: sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==} peerDependencies: @@ -19426,6 +19454,10 @@ packages: resolution: {integrity: sha512-Ja03QIJlPuHt4IQ2FfGex4F4JAr8m3jpaHbFbQrgwr7s7L6U8ocrHiF3J1+wf9jzhGKxvDeaCAnGDot8OjGFyA==} dev: false + /tween-functions@1.2.0: + resolution: {integrity: sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==} + dev: true + /tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} dev: false @@ -20158,7 +20190,7 @@ packages: vite: 4.2.1(@types/node@17.0.12)(sass@1.64.2) dev: true - /vite-plugin-sass-dts@1.3.9(postcss@8.4.27)(prettier@3.0.3)(sass@1.64.2)(vite@4.4.8): + /vite-plugin-sass-dts@1.3.9(postcss@8.4.27)(prettier@2.8.8)(sass@1.64.2)(vite@4.4.8): resolution: {integrity: sha512-v8+LJ8yeN+TexjWiJjv6XOIvT382ZEcFi9gY7OB1ydGNv50B+RE5I8/Xx5XJDkNXsDfhdatNMq02Zgu9ue/wXw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -20169,7 +20201,7 @@ packages: dependencies: postcss: 8.4.27 postcss-js: 4.0.1(postcss@8.4.27) - prettier: 3.0.3 + prettier: 2.8.8 sass: 1.64.2 vite: 4.4.8(@types/node@20.4.6)(less@4.1.3)(sass@1.64.2) dev: true