From 600f7b29e3f1a33765bab5cdab63ca607c593655 Mon Sep 17 00:00:00 2001 From: Dennis Collaris <d.a.c.collaris@uu.nl> Date: Fri, 7 Feb 2025 16:50:12 +0000 Subject: [PATCH] feat: tooltip, popover, and context menu style improvements --- .storybook/main.ts | 8 +- .storybook/preview.tsx | 14 +- public/fonts/inter/inter.css | 514 +++++++++++++----- public/site.webmanifest | 2 +- src/app/navbar/navbar.tsx | 14 +- src/config/styles.css | 4 +- src/config/styling/fonts.css | 10 +- .../VisualizationTooltip.tsx | 23 - .../components/VisualizationTooltip/index.tsx | 1 - src/lib/components/charts/barplot/index.tsx | 2 +- src/lib/components/charts/heatmap1D/index.tsx | 2 +- .../charts/indicatorGraph/index.tsx | 2 +- src/lib/components/color-mode/index.tsx | 2 +- src/lib/components/dropdowns/index.tsx | 6 +- src/lib/components/forms/index.tsx | 66 +-- src/lib/components/index.ts | 2 + src/lib/components/inputs/DropdownInput.tsx | 8 +- src/lib/components/inputs/NumberInput.tsx | 10 +- src/lib/components/inputs/TextInput.tsx | 10 +- .../NodeDetails.stories.tsx} | 23 +- .../components/nodeDetails/NodeDetails.tsx | 21 + src/lib/components/nodeDetails/index.tsx | 1 + src/lib/components/popover/Popover.tsx | 331 +++++++++++ .../components/popover/PopoverProvider.tsx | 77 +++ src/lib/components/popover/index.tsx | 2 + .../components/popover/popover.stories.tsx | 174 ++++++ src/lib/components/tabs/Tab.tsx | 23 +- src/lib/components/tooltip/Overview.mdx | 99 +++- src/lib/components/tooltip/Tooltip.tsx | 101 ++-- src/lib/components/tooltip/index.tsx | 21 - .../components/tooltip/tooltip.stories.tsx | 93 +++- .../data-access/store/visualizationSlice.ts | 11 +- src/lib/management/database/Databases.tsx | 4 +- .../querybuilder/panel/ManualQueryDialog.tsx | 2 +- src/lib/querybuilder/panel/QueryBuilder.tsx | 4 +- ...xtMenu.tsx => QueryBuilderContextMenu.tsx} | 14 +- .../querybuilder/panel/QueryBuilderNav.tsx | 42 +- .../QueryBuilderLogicPillsPanel.tsx | 4 +- .../panel/querysidepanel/QueryMLDialog.tsx | 136 ++--- .../panel/querysidepanel/QuerySettings.tsx | 4 +- .../pillattributes/PillAttributesItem.tsx | 2 +- src/lib/schema/model/reactflow.tsx | 4 +- .../iconsLayout/GridIcon.tsx | 18 +- src/lib/schema/panel/Schema.tsx | 158 +++--- src/lib/schema/panel/SchemaList.tsx | 14 +- src/lib/schema/panel/SchemaSettings.tsx | 6 +- .../pills/nodes/SchemaPopUp/SchemaPopUp.tsx | 39 +- .../pills/nodes/entity/SchemaEntityPill.tsx | 52 +- .../nodes/entity/SchemaListEntityPill.tsx | 52 +- .../node-quality-relation-popup.stories.tsx | 2 - .../node-quality-entity-popup.stories.tsx | 2 - .../nodes/relation/SchemaListRelationPill.tsx | 62 +-- .../nodes/relation/SchemaRelationPill.tsx | 62 +-- src/lib/sidebar/index.tsx | 2 +- src/lib/sidebar/search/SearchBar.tsx | 4 +- src/lib/vis/components/VisualizationPanel.tsx | 5 +- .../vis/components/VisualizationTabBar.tsx | 38 +- .../config/VisualizationSettings.tsx | 2 +- .../arcplotvis/arcplotvis.stories.tsx | 3 +- .../{MapTooltip.tsx => MapDataInspector.tsx} | 19 +- .../visualizations/mapvis/components/index.ts | 2 +- .../visualizations/mapvis/mapvis.stories.tsx | 3 +- src/lib/vis/visualizations/mapvis/mapvis.tsx | 40 +- .../matrixvis/matrix.stories.tsx | 3 +- .../nodelinkvis/components/NLPixi.tsx | 36 +- .../nodelinkvis/components/NLPopup.tsx | 2 +- .../nodelinkvis/nodelinkvis.stories.tsx | 3 +- .../paohvis/components/HyperRangeBlock.tsx | 2 +- .../paohvis/components/RowLabels.tsx | 2 +- .../paohvis/paohvis.stories.tsx | 3 +- .../vis1D/components/CustomChartPlotly.tsx | 11 +- 71 files changed, 1734 insertions(+), 806 deletions(-) delete mode 100644 src/lib/components/VisualizationTooltip/VisualizationTooltip.tsx delete mode 100644 src/lib/components/VisualizationTooltip/index.tsx rename src/lib/components/{VisualizationTooltip/VisualizationTooltip.stories.tsx => nodeDetails/NodeDetails.stories.tsx} (80%) create mode 100644 src/lib/components/nodeDetails/NodeDetails.tsx create mode 100644 src/lib/components/nodeDetails/index.tsx create mode 100644 src/lib/components/popover/Popover.tsx create mode 100644 src/lib/components/popover/PopoverProvider.tsx create mode 100644 src/lib/components/popover/index.tsx create mode 100644 src/lib/components/popover/popover.stories.tsx rename src/lib/querybuilder/panel/{ContextMenu.tsx => QueryBuilderContextMenu.tsx} (93%) rename src/lib/vis/visualizations/mapvis/components/{MapTooltip.tsx => MapDataInspector.tsx} (87%) diff --git a/.storybook/main.ts b/.storybook/main.ts index d82729eb3..c341d8154 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -3,13 +3,7 @@ import type { StorybookConfig } from '@storybook/react-vite'; const config: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: [ - '@storybook/addon-onboarding', - // '@chromatic-com/storybook', - '@storybook/addon-essentials', - '@storybook/addon-interactions', - '@storybook/addon-links', - ], + addons: ['@storybook/addon-onboarding', '@storybook/addon-essentials', '@storybook/addon-interactions', '@storybook/addon-links'], framework: { name: '@storybook/react-vite', options: {}, diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 71958ab8f..a6b7249b0 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,12 +1,12 @@ -import React from 'react'; import type { Preview } from '@storybook/react'; import '../src/main.css'; const preview: Preview = { decorators: [ (Story, context) => { - if (document?.body?.className !== undefined) document.body.className += ' ' + 'light-mode'; - + if (document?.body?.className !== undefined) { + document.body.className += ' light-mode'; + } return <Story />; }, ], @@ -17,13 +17,9 @@ const preview: Preview = { date: /Date$/i, }, }, + actions: {}, + docs: {}, }, }; -export const decorators = []; - -export const parameters = { - actions: {}, -}; - export default preview; diff --git a/public/fonts/inter/inter.css b/public/fonts/inter/inter.css index 614406530..95a861662 100644 --- a/public/fonts/inter/inter.css +++ b/public/fonts/inter/inter.css @@ -8,141 +8,411 @@ font-style: normal; font-weight: 100 900; font-display: swap; - src: url("InterVariable.woff2") format("woff2"); + src: url('InterVariable.woff2') format('woff2'); } @font-face { font-family: InterVariable; font-style: italic; font-weight: 100 900; font-display: swap; - src: url("InterVariable-Italic.woff2") format("woff2"); + src: url('InterVariable-Italic.woff2') format('woff2'); } /* static fonts */ -@font-face { font-family: "Inter"; font-style: normal; font-weight: 100; font-display: swap; src: url("Inter-Thin.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: italic; font-weight: 100; font-display: swap; src: url("Inter-ThinItalic.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: normal; font-weight: 200; font-display: swap; src: url("Inter-ExtraLight.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: italic; font-weight: 200; font-display: swap; src: url("Inter-ExtraLightItalic.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: normal; font-weight: 300; font-display: swap; src: url("Inter-Light.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: italic; font-weight: 300; font-display: swap; src: url("Inter-LightItalic.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("Inter-Regular.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: italic; font-weight: 400; font-display: swap; src: url("Inter-Italic.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap; src: url("Inter-Medium.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: italic; font-weight: 500; font-display: swap; src: url("Inter-MediumItalic.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("Inter-SemiBold.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: italic; font-weight: 600; font-display: swap; src: url("Inter-SemiBoldItalic.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap; src: url("Inter-Bold.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: italic; font-weight: 700; font-display: swap; src: url("Inter-BoldItalic.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: normal; font-weight: 800; font-display: swap; src: url("Inter-ExtraBold.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: italic; font-weight: 800; font-display: swap; src: url("Inter-ExtraBoldItalic.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: normal; font-weight: 900; font-display: swap; src: url("Inter-Black.woff2") format("woff2"); } -@font-face { font-family: "Inter"; font-style: italic; font-weight: 900; font-display: swap; src: url("Inter-BlackItalic.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 100; font-display: swap; src: url("InterDisplay-Thin.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 100; font-display: swap; src: url("InterDisplay-ThinItalic.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 200; font-display: swap; src: url("InterDisplay-ExtraLight.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 200; font-display: swap; src: url("InterDisplay-ExtraLightItalic.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 300; font-display: swap; src: url("InterDisplay-Light.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 300; font-display: swap; src: url("InterDisplay-LightItalic.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 400; font-display: swap; src: url("InterDisplay-Regular.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 400; font-display: swap; src: url("InterDisplay-Italic.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 500; font-display: swap; src: url("InterDisplay-Medium.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 500; font-display: swap; src: url("InterDisplay-MediumItalic.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 600; font-display: swap; src: url("InterDisplay-SemiBold.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 600; font-display: swap; src: url("InterDisplay-SemiBoldItalic.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 700; font-display: swap; src: url("InterDisplay-Bold.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 700; font-display: swap; src: url("InterDisplay-BoldItalic.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 800; font-display: swap; src: url("InterDisplay-ExtraBold.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 800; font-display: swap; src: url("InterDisplay-ExtraBoldItalic.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 900; font-display: swap; src: url("InterDisplay-Black.woff2") format("woff2"); } -@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 900; font-display: swap; src: url("InterDisplay-BlackItalic.woff2") format("woff2"); } +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url('Inter-Thin.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url('Inter-ThinItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url('Inter-ExtraLight.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 200; + font-display: swap; + src: url('Inter-ExtraLightItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url('Inter-Light.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 300; + font-display: swap; + src: url('Inter-LightItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('Inter-Regular.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('Inter-Italic.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('Inter-Medium.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url('Inter-MediumItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('Inter-SemiBold.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 600; + font-display: swap; + src: url('Inter-SemiBoldItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('Inter-Bold.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url('Inter-BoldItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 800; + font-display: swap; + src: url('Inter-ExtraBold.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 800; + font-display: swap; + src: url('Inter-ExtraBoldItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url('Inter-Black.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 900; + font-display: swap; + src: url('Inter-BlackItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url('InterDisplay-Thin.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url('InterDisplay-ThinItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url('InterDisplay-ExtraLight.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: italic; + font-weight: 200; + font-display: swap; + src: url('InterDisplay-ExtraLightItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url('InterDisplay-Light.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: italic; + font-weight: 300; + font-display: swap; + src: url('InterDisplay-LightItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('InterDisplay-Regular.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('InterDisplay-Italic.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('InterDisplay-Medium.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url('InterDisplay-MediumItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('InterDisplay-SemiBold.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: italic; + font-weight: 600; + font-display: swap; + src: url('InterDisplay-SemiBoldItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('InterDisplay-Bold.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url('InterDisplay-BoldItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: normal; + font-weight: 800; + font-display: swap; + src: url('InterDisplay-ExtraBold.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: italic; + font-weight: 800; + font-display: swap; + src: url('InterDisplay-ExtraBoldItalic.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url('InterDisplay-Black.woff2') format('woff2'); +} +@font-face { + font-family: 'InterDisplay'; + font-style: italic; + font-weight: 900; + font-display: swap; + src: url('InterDisplay-BlackItalic.woff2') format('woff2'); +} @font-feature-values InterVariable { - @character-variant { - cv01: 1; cv02: 2; cv03: 3; cv04: 4; cv05: 5; cv06: 6; cv07: 7; cv08: 8; - cv09: 9; cv10: 10; cv11: 11; cv12: 12; cv13: 13; - alt-1: 1; /* Alternate one */ - alt-3: 9; /* Flat-top three */ - open-4: 2; /* Open four */ - open-6: 3; /* Open six */ - open-9: 4; /* Open nine */ - lc-l-with-tail: 5; /* Lower-case L with tail */ - simplified-u: 6; /* Simplified u */ - alt-double-s: 7; /* Alternate German double s */ - uc-i-with-serif: 8; /* Upper-case i with serif */ - uc-g-with-spur: 10; /* Capital G with spur */ - single-story-a: 11; /* Single-story a */ - compact-lc-f: 12; /* Compact f */ - compact-lc-t: 13; /* Compact t */ - } - @styleset { - ss01: 1; ss02: 2; ss03: 3; ss04: 4; ss05: 5; ss06: 6; ss07: 7; ss08: 8; - open-digits: 1; /* Open digits */ - disambiguation: 2; /* Disambiguation (with zero) */ - disambiguation-except-zero: 4; /* Disambiguation (no zero) */ - round-quotes-and-commas: 3; /* Round quotes & commas */ - square-punctuation: 7; /* Square punctuation */ - square-quotes: 8; /* Square quotes */ - circled-characters: 5; /* Circled characters */ - squared-characters: 6; /* Squared characters */ - } + @character-variant { + cv01: 1; + cv02: 2; + cv03: 3; + cv04: 4; + cv05: 5; + cv06: 6; + cv07: 7; + cv08: 8; + cv09: 9; + cv10: 10; + cv11: 11; + cv12: 12; + cv13: 13; + alt-1: 1; /* Alternate one */ + alt-3: 9; /* Flat-top three */ + open-4: 2; /* Open four */ + open-6: 3; /* Open six */ + open-9: 4; /* Open nine */ + lc-l-with-tail: 5; /* Lower-case L with tail */ + simplified-u: 6; /* Simplified u */ + alt-double-s: 7; /* Alternate German double s */ + uc-i-with-serif: 8; /* Upper-case i with serif */ + uc-g-with-spur: 10; /* Capital G with spur */ + single-story-a: 11; /* Single-story a */ + compact-lc-f: 12; /* Compact f */ + compact-lc-t: 13; /* Compact t */ + } + @styleset { + ss01: 1; + ss02: 2; + ss03: 3; + ss04: 4; + ss05: 5; + ss06: 6; + ss07: 7; + ss08: 8; + open-digits: 1; /* Open digits */ + disambiguation: 2; /* Disambiguation (with zero) */ + disambiguation-except-zero: 4; /* Disambiguation (no zero) */ + round-quotes-and-commas: 3; /* Round quotes & commas */ + square-punctuation: 7; /* Square punctuation */ + square-quotes: 8; /* Square quotes */ + circled-characters: 5; /* Circled characters */ + squared-characters: 6; /* Squared characters */ + } } @font-feature-values Inter { - @character-variant { - cv01: 1; cv02: 2; cv03: 3; cv04: 4; cv05: 5; cv06: 6; cv07: 7; cv08: 8; - cv09: 9; cv10: 10; cv11: 11; cv12: 12; cv13: 13; - alt-1: 1; /* Alternate one */ - alt-3: 9; /* Flat-top three */ - open-4: 2; /* Open four */ - open-6: 3; /* Open six */ - open-9: 4; /* Open nine */ - lc-l-with-tail: 5; /* Lower-case L with tail */ - simplified-u: 6; /* Simplified u */ - alt-double-s: 7; /* Alternate German double s */ - uc-i-with-serif: 8; /* Upper-case i with serif */ - uc-g-with-spur: 10; /* Capital G with spur */ - single-story-a: 11; /* Single-story a */ - compact-lc-f: 12; /* Compact f */ - compact-lc-t: 13; /* Compact t */ - } - @styleset { - ss01: 1; ss02: 2; ss03: 3; ss04: 4; ss05: 5; ss06: 6; ss07: 7; ss08: 8; - open-digits: 1; /* Open digits */ - disambiguation: 2; /* Disambiguation (with zero) */ - disambiguation-except-zero: 4; /* Disambiguation (no zero) */ - round-quotes-and-commas: 3; /* Round quotes & commas */ - square-punctuation: 7; /* Square punctuation */ - square-quotes: 8; /* Square quotes */ - circled-characters: 5; /* Circled characters */ - squared-characters: 6; /* Squared characters */ - } + @character-variant { + cv01: 1; + cv02: 2; + cv03: 3; + cv04: 4; + cv05: 5; + cv06: 6; + cv07: 7; + cv08: 8; + cv09: 9; + cv10: 10; + cv11: 11; + cv12: 12; + cv13: 13; + alt-1: 1; /* Alternate one */ + alt-3: 9; /* Flat-top three */ + open-4: 2; /* Open four */ + open-6: 3; /* Open six */ + open-9: 4; /* Open nine */ + lc-l-with-tail: 5; /* Lower-case L with tail */ + simplified-u: 6; /* Simplified u */ + alt-double-s: 7; /* Alternate German double s */ + uc-i-with-serif: 8; /* Upper-case i with serif */ + uc-g-with-spur: 10; /* Capital G with spur */ + single-story-a: 11; /* Single-story a */ + compact-lc-f: 12; /* Compact f */ + compact-lc-t: 13; /* Compact t */ + } + @styleset { + ss01: 1; + ss02: 2; + ss03: 3; + ss04: 4; + ss05: 5; + ss06: 6; + ss07: 7; + ss08: 8; + open-digits: 1; /* Open digits */ + disambiguation: 2; /* Disambiguation (with zero) */ + disambiguation-except-zero: 4; /* Disambiguation (no zero) */ + round-quotes-and-commas: 3; /* Round quotes & commas */ + square-punctuation: 7; /* Square punctuation */ + square-quotes: 8; /* Square quotes */ + circled-characters: 5; /* Circled characters */ + squared-characters: 6; /* Squared characters */ + } } @font-feature-values InterDisplay { - @character-variant { - cv01: 1; cv02: 2; cv03: 3; cv04: 4; cv05: 5; cv06: 6; cv07: 7; cv08: 8; - cv09: 9; cv10: 10; cv11: 11; cv12: 12; cv13: 13; - alt-1: 1; /* Alternate one */ - alt-3: 9; /* Flat-top three */ - open-4: 2; /* Open four */ - open-6: 3; /* Open six */ - open-9: 4; /* Open nine */ - lc-l-with-tail: 5; /* Lower-case L with tail */ - simplified-u: 6; /* Simplified u */ - alt-double-s: 7; /* Alternate German double s */ - uc-i-with-serif: 8; /* Upper-case i with serif */ - uc-g-with-spur: 10; /* Capital G with spur */ - single-story-a: 11; /* Single-story a */ - compact-lc-f: 12; /* Compact f */ - compact-lc-t: 13; /* Compact t */ - } - @styleset { - ss01: 1; ss02: 2; ss03: 3; ss04: 4; ss05: 5; ss06: 6; ss07: 7; ss08: 8; - open-digits: 1; /* Open digits */ - disambiguation: 2; /* Disambiguation (with zero) */ - disambiguation-except-zero: 4; /* Disambiguation (no zero) */ - round-quotes-and-commas: 3; /* Round quotes & commas */ - square-punctuation: 7; /* Square punctuation */ - square-quotes: 8; /* Square quotes */ - circled-characters: 5; /* Circled characters */ - squared-characters: 6; /* Squared characters */ - } + @character-variant { + cv01: 1; + cv02: 2; + cv03: 3; + cv04: 4; + cv05: 5; + cv06: 6; + cv07: 7; + cv08: 8; + cv09: 9; + cv10: 10; + cv11: 11; + cv12: 12; + cv13: 13; + alt-1: 1; /* Alternate one */ + alt-3: 9; /* Flat-top three */ + open-4: 2; /* Open four */ + open-6: 3; /* Open six */ + open-9: 4; /* Open nine */ + lc-l-with-tail: 5; /* Lower-case L with tail */ + simplified-u: 6; /* Simplified u */ + alt-double-s: 7; /* Alternate German double s */ + uc-i-with-serif: 8; /* Upper-case i with serif */ + uc-g-with-spur: 10; /* Capital G with spur */ + single-story-a: 11; /* Single-story a */ + compact-lc-f: 12; /* Compact f */ + compact-lc-t: 13; /* Compact t */ + } + @styleset { + ss01: 1; + ss02: 2; + ss03: 3; + ss04: 4; + ss05: 5; + ss06: 6; + ss07: 7; + ss08: 8; + open-digits: 1; /* Open digits */ + disambiguation: 2; /* Disambiguation (with zero) */ + disambiguation-except-zero: 4; /* Disambiguation (no zero) */ + round-quotes-and-commas: 3; /* Round quotes & commas */ + square-punctuation: 7; /* Square punctuation */ + square-quotes: 8; /* Square quotes */ + circled-characters: 5; /* Circled characters */ + squared-characters: 6; /* Squared characters */ + } } diff --git a/public/site.webmanifest b/public/site.webmanifest index adb99843f..8eb18780a 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -18,4 +18,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} \ No newline at end of file +} diff --git a/src/app/navbar/navbar.tsx b/src/app/navbar/navbar.tsx index cae161513..34a184c70 100644 --- a/src/app/navbar/navbar.tsx +++ b/src/app/navbar/navbar.tsx @@ -15,7 +15,7 @@ import GpLogo from './gp-logo'; import { getEnvVariable } from '@/config'; import { useAuthentication, useAuthCache, useActiveSaveStateAuthorization, Button, DropdownItem, useAppDispatch } from '@/lib'; import { FeatureEnabled } from '@/lib/components/featureFlags'; -import { Popover, PopoverTrigger, PopoverContent } from '@/lib/components/layout/Popover'; +import { Popover, PopoverTrigger, PopoverContent } from '@/lib/components/popover'; import { ManagementViews, ManagementTrigger } from '@/lib/management'; import { addError } from '@/lib/data-access/store/configSlice'; @@ -73,14 +73,14 @@ export const Navbar = () => { </div> </PopoverTrigger> - <PopoverContent className="w-56 z-30 bg-light rounded-sm border-[1px] outline-none"> - <div className="p-2 text-sm border-b"> + <PopoverContent className="w-56 z-30 bg-light divide-y divide-secondary-200"> + <div className="text-sm p-2"> <h2 className="font-bold">user: {authCache.authentication?.username}</h2> <h3 className="text-xs break-words">session: {authCache?.sessionID}</h3> <h3 className="text-xs break-words">license: Creator</h3> </div> {authCache.authentication?.authenticated ? ( - <> + <div className="p-2"> <FeatureEnabled featureFlag="SHARABLE_EXPLORATION"> <DropdownItem value={sharing ? 'Creating Share Link' : 'Share'} @@ -126,7 +126,7 @@ export const Navbar = () => { }} /> )} - </> + </div> ) : ( <> <DropdownItem value="Login" onClick={() => {}} /> @@ -134,11 +134,11 @@ export const Navbar = () => { )} {authCache.authentication?.roomID && ( - <div className="p-2 border-b"> + <div className="p-2"> <h3 className="text-xs break-words">Share ID: {authCache.authentication?.roomID}</h3> </div> )} - <div className="p-2 border-t"> + <div className="p-2"> <h3 className="text-xs">Version: {buildInfo}</h3> </div> </PopoverContent> diff --git a/src/config/styles.css b/src/config/styles.css index 80973a5c1..3cf72f933 100644 --- a/src/config/styles.css +++ b/src/config/styles.css @@ -13,7 +13,7 @@ } /* Hide scrollbar for IE, Edge and Firefox */ .no-scrollbar { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ } } diff --git a/src/config/styling/fonts.css b/src/config/styling/fonts.css index b8edb246c..37724fd45 100644 --- a/src/config/styling/fonts.css +++ b/src/config/styling/fonts.css @@ -1,17 +1,19 @@ :root { - font-family: "Inter", sans-serif; + font-family: 'Inter', sans-serif; } @supports (font-variation-settings: normal) { :root { - font-family: "InterVariable", sans-serif; + font-family: 'InterVariable', sans-serif; font-optical-sizing: auto; } } .font-data { - font-family: "InterVariable", "Inter", sans-serif; + font-family: 'InterVariable', 'Inter', sans-serif; font-optical-sizing: auto; font-variant-numeric: tabular-nums slashed-zero; - font-feature-settings: "tnum" 1, "zero" 1; + font-feature-settings: + 'tnum' 1, + 'zero' 1; } diff --git a/src/lib/components/VisualizationTooltip/VisualizationTooltip.tsx b/src/lib/components/VisualizationTooltip/VisualizationTooltip.tsx deleted file mode 100644 index b718ed68d..000000000 --- a/src/lib/components/VisualizationTooltip/VisualizationTooltip.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { ReactNode } from 'react'; - -export type VisualizationTooltipProps = { - name: string; - colorHeader: string; - children: ReactNode; -}; - -export const VisualizationTooltip: React.FC<VisualizationTooltipProps> = ({ name, colorHeader, children }) => { - return ( - <div className="border-1 border-sec-200 bg-light w-[12rem] -mx-2 -my-2"> - <div className="flex m-0 justify-start items-stretch border-b border-sec-200 relative"> - <div className="left-0 top-0 h-auto w-1.5" style={{ backgroundColor: colorHeader }}></div> - <div className="px-2.5 py-1 truncate flex"> - <div className={'flex max-w-full'}> - <span className="text-base font-semibold line-clamp-1">{name}</span> - </div> - </div> - </div> - {children} - </div> - ); -}; diff --git a/src/lib/components/VisualizationTooltip/index.tsx b/src/lib/components/VisualizationTooltip/index.tsx deleted file mode 100644 index 868ce59de..000000000 --- a/src/lib/components/VisualizationTooltip/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './VisualizationTooltip'; diff --git a/src/lib/components/charts/barplot/index.tsx b/src/lib/components/charts/barplot/index.tsx index 5f703dfae..6b846816d 100644 --- a/src/lib/components/charts/barplot/index.tsx +++ b/src/lib/components/charts/barplot/index.tsx @@ -156,7 +156,7 @@ export const BarPlot = ({ typeBarPlot, numBins, data, marginPercentage, classNam }, [data, dimensions, marginPercentage, numBins, typeBarPlot, axis, name]); return ( - <TooltipProvider delayDuration={300}> + <TooltipProvider delay={300}> <div className={className || ''}> <svg ref={svgRef} width="100%" height="100%"> <g ref={groupMarginRef}> diff --git a/src/lib/components/charts/heatmap1D/index.tsx b/src/lib/components/charts/heatmap1D/index.tsx index 7e6eb999c..3b85046b8 100644 --- a/src/lib/components/charts/heatmap1D/index.tsx +++ b/src/lib/components/charts/heatmap1D/index.tsx @@ -110,7 +110,7 @@ export const Heatmap1D = ({ numBins, maxValue, data, marginPercentage, className }, [data, dimensions, marginPercentage, numBins, axis, name]); return ( - <TooltipProvider delayDuration={300}> + <TooltipProvider delay={300}> <div className={className || ''}> <svg ref={svgRef} width="100%" height="100%"> <g ref={groupMarginRef}> diff --git a/src/lib/components/charts/indicatorGraph/index.tsx b/src/lib/components/charts/indicatorGraph/index.tsx index f6d0f80c2..8f01f43d6 100644 --- a/src/lib/components/charts/indicatorGraph/index.tsx +++ b/src/lib/components/charts/indicatorGraph/index.tsx @@ -79,7 +79,7 @@ export const IndicatorGraph = ({ maxValue, data, marginPercentage, className, ax }, [data, dimensions, marginPercentage, axis, name]); return ( - <TooltipProvider delayDuration={300}> + <TooltipProvider delay={300}> <div className={className || ''}> <svg ref={svgRef} width="100%" height="100%"> <g ref={groupMarginRef}> diff --git a/src/lib/components/color-mode/index.tsx b/src/lib/components/color-mode/index.tsx index 1a104d8b5..4895b2627 100644 --- a/src/lib/components/color-mode/index.tsx +++ b/src/lib/components/color-mode/index.tsx @@ -35,7 +35,7 @@ const ColorMode = () => { : 'icon-[ic--baseline-auto-mode]'; return ( - <TooltipProvider delayDuration={0}> + <TooltipProvider delay={0}> <Tooltip placement={'right'}> <TooltipTrigger asChild> <Button variant="ghost" size="sm" iconComponent={iconComponent} onClick={toggleTheme} /> diff --git a/src/lib/components/dropdowns/index.tsx b/src/lib/components/dropdowns/index.tsx index 56f9d478f..06bc3fe63 100644 --- a/src/lib/components/dropdowns/index.tsx +++ b/src/lib/components/dropdowns/index.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, ReactNode } from 'react'; import { Icon } from '../icon'; -import { PopoverContent, PopoverTrigger, Popover, PopoverOptions } from '../layout/Popover'; +import { PopoverContent, PopoverTrigger, Popover, PopoverOptions } from '@/lib/components/popover'; export const DropdownContainer = ({ children, ...props }: { children: React.ReactNode } & PopoverOptions) => { return ( @@ -127,8 +127,8 @@ export function DropdownItem({ value, label, disabled, className, onClick, subme <li ref={itemRef} style={{ border: 0, listStyleType: 'none' }} - className={`flex w-full grow cursor-pointer divide-y origin-top-right whitespace-nowrap - rounded-sm items-center justify-between text-sm px-2 pe-1 py-1 hover:bg-secondary-200 ${ + className={`flex w-full grow cursor-pointer divide-y origin-top-right whitespace-nowrap + rounded items-center justify-between text-sm gap-1.5 px-1.5 py-1 hover:bg-secondary-100 ${ className && className } ${selected ? 'bg-secondary-400 text-white hover:text-black' : ''}`} onClick={() => { diff --git a/src/lib/components/forms/index.tsx b/src/lib/components/forms/index.tsx index 149e485f0..78ab61026 100644 --- a/src/lib/components/forms/index.tsx +++ b/src/lib/components/forms/index.tsx @@ -69,58 +69,60 @@ export const FormDiv = React.forwardRef<HTMLDivElement, PropsWithChildren<{ clas }, ); export const FormCard = (props: PropsWithChildren<{ className?: string }>) => ( - <div className={'card card-bordered bg-light rounded-none ' + (props.className ? props.className : '')}>{props.children}</div> + <div className={'border border-secondary-200 bg-light rounded-none p-2' + (props.className ? props.className : '')}>{props.children}</div> ); export const FormBody = ({ children, ...props }: PropsWithChildren<React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>>) => ( - <form className="px-0 w-72 py-2" {...props}> - {children} - </form> + <form {...props}>{children}</form> ); -export const FormTitle = ({ children, title, onClose }: PropsWithChildren<{ title: string; onClose?: () => void }>) => { +export const FormTitle = ({ + children, + title, + onClose, + className = '', + ...props +}: PropsWithChildren<{ + title: string; + onClose?: () => void; + className?: string; +}>) => { return ( - <div className="font-semibold p-5 py-0 flex w-full"> - <h2 className="w-full">{title}</h2> - {onClose && <Button rounded variant="ghost" iconComponent="icon-[ic--baseline-close]" onClick={() => onClose()} />} + <div className={`font-semibold font-sm flex flex-row justify-between w-full p-2 ${className}`} {...props}> + <h2>{title}</h2> + {onClose && <Button rounded variant="ghost" iconComponent="icon-[ic--baseline-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; onApply?: () => void }) => ( +export const FormHBar = () => <hr className="border-px w-full my-1"></hr>; +export const FormControl = ({ children }: PropsWithChildren) => <div className="form-control">{children}</div>; +export const FormActions = ({ + onClose, + onApply, + size = 'md', +}: { + onClose?: () => void; + onApply?: () => void; + size?: 'sm' | 'md' | 'lg'; +}) => ( <> - {props.onClose && ( - <div className="grid grid-cols-2 px-5 gap-2 mb-2"> + {onClose ? ( + <div className="grid grid-cols-2 gap-2"> <Button variantType="secondary" variant="outline" label="Cancel" + size={size} onClick={e => { e.preventDefault(); - if (props.onClose) props.onClose(); + onClose(); }} /> - <Button - variantType="primary" - label="Apply" - onClick={() => { - if (props.onApply) props.onApply(); - }} - className="flex-grow" - /> + <Button variantType="primary" label="Apply" size={size} onClick={onApply} className="flex-grow" /> </div> - )} - {!props.onClose && ( - <Button - variantType="primary" - label="Apply" - onClick={() => { - if (props.onApply) props.onApply(); - }} - className="w-full" - /> + ) : ( + <Button variantType="primary" label="Apply" size={size} onClick={onApply} className="w-full" /> )} </> ); diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index b5769a607..ed9def1ff 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -12,7 +12,9 @@ export * from './pagination'; export * from './tooltip'; export * from './Legend'; export * from './LoadingSpinner'; +export * from './nodeDetails'; export * from './Popup'; +export * from './popover'; export * from './layout'; export * from './pills'; export * from './tabs'; diff --git a/src/lib/components/inputs/DropdownInput.tsx b/src/lib/components/inputs/DropdownInput.tsx index f85718699..83c913676 100644 --- a/src/lib/components/inputs/DropdownInput.tsx +++ b/src/lib/components/inputs/DropdownInput.tsx @@ -76,7 +76,7 @@ export const DropdownInput = ({ const [filteredOptions, setFilteredOptions] = React.useState<(string | number | { [key: string]: string })[]>(options); useEffect(() => { - setFilteredOptions(options); + setFilteredOptions(options); }, [options]); const handleInputChange = (e: string) => { @@ -94,11 +94,11 @@ export const DropdownInput = ({ return ( <Tooltip> - <TooltipTrigger className={'w-full' + (inline ? ' grid grid-cols-2 items-center gap-0.5' : '') + ' ' + className}> + <TooltipTrigger className={'w-full' + (inline ? ' flex flex-row items-center gap-0.5 justify-between' : '') + ' ' + className}> {label && ( - <label className="label p-0"> + <label className="label p-0 flex-1 min-w-0"> <span - className={`text-${size} text-left truncate font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} + className={`text-${size} text-left font-medium text-secondary-700 line-clamp-2 leading-snug ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} > {label} </span> diff --git a/src/lib/components/inputs/NumberInput.tsx b/src/lib/components/inputs/NumberInput.tsx index 996dfddfc..4ce219f79 100644 --- a/src/lib/components/inputs/NumberInput.tsx +++ b/src/lib/components/inputs/NumberInput.tsx @@ -38,13 +38,15 @@ export const NumberInput = ({ if (!tooltip && inline && label) tooltip = label; return ( <div className={styles['input'] + `${containerClassName ? ` ${containerClassName}` : ''}`}> - <TooltipProvider delayDuration={50}> + <TooltipProvider delay={50}> <Tooltip> - <TooltipTrigger className={'form-control w-full' + (inline && label ? ' grid grid-cols-2 items-center gap-0.5' : '')}> + <TooltipTrigger + className={'form-control w-full' + (inline && label ? ' flex flex-row items-center gap-0.5 justify-between' : '')} + > {label && ( - <label className="label p-0"> + <label className="label p-0 flex-1 min-w-0"> <span - className={`text-sm text-left truncate font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} + className={`text-${size} text-left font-medium text-secondary-700 line-clamp-2 leading-snug ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} > {label} </span> diff --git a/src/lib/components/inputs/TextInput.tsx b/src/lib/components/inputs/TextInput.tsx index a773c6c45..32e073369 100644 --- a/src/lib/components/inputs/TextInput.tsx +++ b/src/lib/components/inputs/TextInput.tsx @@ -37,11 +37,13 @@ export const TextInput = ({ if (!tooltip && inline) tooltip = label; return ( <Tooltip> - <TooltipTrigger className={styles['input'] + ' form-control w-full' + (inline ? ' grid grid-cols-2 items-center gap-0.5' : '')}> + <TooltipTrigger + className={styles['input'] + ' form-control w-full' + (inline ? ' flex flex-row items-center gap-0.5 justify-between' : '')} + > {label && ( - <label className="label p-0"> + <label className="label p-0 flex-1 min-w-0"> <span - className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} + className={`text-${size} font-medium text-secondary-700 line-clamp-2 leading-snug ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} > {label} </span> @@ -53,7 +55,7 @@ export const TextInput = ({ type={visible ? 'text' : 'password'} placeholder={placeholder} className={ - `${size} bg-light border border-secondary-200 placeholder-secondary-400 focus:outline-none block w-full focus:ring-1 ${ + `${size} bg-light border border-secondary-200 placeholder-secondary-400 w-auto min-w-6 max-w-[65%] shrink-0 grow-1 focus:outline-none block focus:ring-1 ${ isValid ? '' : 'input-error' }` + (className ? ` ${className}` : '') } diff --git a/src/lib/components/VisualizationTooltip/VisualizationTooltip.stories.tsx b/src/lib/components/nodeDetails/NodeDetails.stories.tsx similarity index 80% rename from src/lib/components/VisualizationTooltip/VisualizationTooltip.stories.tsx rename to src/lib/components/nodeDetails/NodeDetails.stories.tsx index cd2b769f2..9ebc14c60 100644 --- a/src/lib/components/VisualizationTooltip/VisualizationTooltip.stories.tsx +++ b/src/lib/components/nodeDetails/NodeDetails.stories.tsx @@ -1,17 +1,16 @@ -import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { VisualizationTooltip, VisualizationTooltipProps } from '@/lib/components/VisualizationTooltip'; +import { NodeDetails, NodeDetailsProps } from '@/lib/components/nodeDetails'; import { SchemaPopUp, SchemaPopUpProps } from '@/lib/schema/pills/nodes/SchemaPopUp/SchemaPopUp'; import { NLPopUp, NLPopUpProps } from '@/lib/vis/visualizations/nodelinkvis/components/NLPopup'; -const meta: Meta<typeof VisualizationTooltip> = { - component: VisualizationTooltip, - title: 'Components/VisualizationTooltip', +const meta: Meta<typeof NodeDetails> = { + component: NodeDetails, + title: 'Components/NodeDetails', }; export default meta; -type CombinedProps = VisualizationTooltipProps & SchemaPopUpProps & NLPopUpProps & { attributes: { name: string; type: string }[] }; +type CombinedProps = NodeDetailsProps & SchemaPopUpProps & NLPopUpProps & { attributes: { name: string; type: string }[] }; type Story = StoryObj<CombinedProps>; @@ -30,9 +29,9 @@ export const SchemaNode: Story = { return ( <div className="w-1/4 my-10 m-auto flex items-center justify-center"> - <VisualizationTooltip name={name} colorHeader={colorHeader}> + <NodeDetails name={name} colorHeader={colorHeader}> <SchemaPopUp data={data} numberOfElements={numberOfElements} /> - </VisualizationTooltip> + </NodeDetails> </div> ); }, @@ -66,9 +65,9 @@ export const SchemaRelationship: Story = { return ( <div className="w-1/4 my-10 m-auto flex items-center justify-center"> - <VisualizationTooltip name={name} colorHeader={colorHeader}> + <NodeDetails name={name} colorHeader={colorHeader}> <SchemaPopUp data={data} numberOfElements={numberOfElements} connections={connections} /> - </VisualizationTooltip> + </NodeDetails> </div> ); }, @@ -92,9 +91,9 @@ export const NodeLinkPopUp: Story = { const { name, data, colorHeader } = args; return ( <div className="w-1/4 my-10 m-auto flex items-center justify-center"> - <VisualizationTooltip name={name} colorHeader={colorHeader}> + <NodeDetails name={name} colorHeader={colorHeader}> <NLPopUp data={data} /> - </VisualizationTooltip> + </NodeDetails> </div> ); }, diff --git a/src/lib/components/nodeDetails/NodeDetails.tsx b/src/lib/components/nodeDetails/NodeDetails.tsx new file mode 100644 index 000000000..ec98753ac --- /dev/null +++ b/src/lib/components/nodeDetails/NodeDetails.tsx @@ -0,0 +1,21 @@ +import React, { ReactNode } from 'react'; + +export type NodeDetailsProps = { + name: string; + colorHeader: string; + children: ReactNode; +}; + +export const NodeDetails: React.FC<NodeDetailsProps> = ({ name, colorHeader, children }) => { + return ( + <div className="w-[12rem] divide-y divider-secondary-200"> + <div className="px-2 relative w-full"> + <div className="left-0 top-0 h-full w-1 shrink-0 absolute" style={{ backgroundColor: colorHeader }}></div> + <div className="px-1 py-1"> + <div className="text-sm font-semibold truncate overflow-hidden whitespace-nowrap">{name}</div> + </div> + </div> + {children && <div className="text-xs p-1">{children}</div>} + </div> + ); +}; diff --git a/src/lib/components/nodeDetails/index.tsx b/src/lib/components/nodeDetails/index.tsx new file mode 100644 index 000000000..ace73d287 --- /dev/null +++ b/src/lib/components/nodeDetails/index.tsx @@ -0,0 +1 @@ +export * from './NodeDetails'; diff --git a/src/lib/components/popover/Popover.tsx b/src/lib/components/popover/Popover.tsx new file mode 100644 index 000000000..099b7d828 --- /dev/null +++ b/src/lib/components/popover/Popover.tsx @@ -0,0 +1,331 @@ +import React, { useEffect } from 'react'; +import { + useFloating, + autoUpdate, + offset, + flip, + shift, + hide, + arrow, + useClick, + useHover, + useFocus, + useDismiss, + useRole, + useInteractions, + useMergeRefs, + FloatingPortal, + FloatingArrow, +} from '@floating-ui/react'; +import type { Placement, Alignment } from '@floating-ui/react'; +import { Button } from '@/lib'; +import { v4 as uuidv4 } from 'uuid'; +import { usePopoverGlobalContext } from '@/lib'; + +export interface PopoverOptions { + id?: string; + initialOpen?: boolean; + placement?: Placement; + alignment?: Alignment; + autoAlignment?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + boundaryElement?: React.RefObject<HTMLElement> | HTMLElement | null; + showArrow?: boolean; + interactive?: boolean; + delay?: number | { open?: number; close?: number }; + activation?: 'click' | 'hover' | 'focus'; + showCloseButton?: boolean; +} + +interface PopoverInstanceContextProps { + open: boolean; + setOpen: (state: boolean) => void; + closePopover: () => void; + interactions: ReturnType<typeof useInteractions>; + data: ReturnType<typeof useFloating>; + interactive: boolean; + showCloseButton?: boolean; +} +const globalOpenPopovers = new Set<string>(); + +const PopoverInstanceContext = React.createContext<PopoverInstanceContextProps | null>(null); + +// Custom hook to access the instance Popover context +export const usePopoverInstanceContext = (): PopoverInstanceContextProps => { + const context = React.useContext(PopoverInstanceContext); + if (!context) { + throw new Error('Popover components must be wrapped in <Popover>.'); + } + return context; +}; + +// Hook to handle popover state +export function usePopover({ + id = uuidv4(), + initialOpen = false, + placement = 'top', + alignment, + autoAlignment = true, + open: controlledOpen, + onOpenChange, + boundaryElement = null, + showArrow = true, + interactive = true, + delay, + activation = 'click', +}: PopoverOptions = {}) { + // Access the global Popover context if available + const popoverGlobal = usePopoverGlobalContext(); + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen); + + const hasInitialized = React.useRef(false); + // Sync initialOpen when component mounts + useEffect(() => { + if (initialOpen && !hasInitialized.current) { + hasInitialized.current = true; + setUncontrolledOpen(true); + if (popoverGlobal) { + popoverGlobal.openPopover(id); + } else { + globalOpenPopovers.add(id); + } + } + }, [initialOpen, popoverGlobal, id]); + + const isManaged = popoverGlobal !== null; + const open = controlledOpen ?? uncontrolledOpen ?? (isManaged ? popoverGlobal.isOpen(id) : false); + + const setOpen = (state: boolean) => { + if (isManaged && popoverGlobal) { + // Popover zit in een provider + if (state) { + popoverGlobal.openPopover(id); + } else { + popoverGlobal.closePopover(id); + } + } else { + if (state) { + globalOpenPopovers.clear(); + globalOpenPopovers.add(id); + } else { + globalOpenPopovers.delete(id); + } + } + + setUncontrolledOpen(state); + onOpenChange?.(state); + }; + + const arrowRef = React.useRef<SVGSVGElement | null>(null); + const middleware = [ + offset(5), + shift({ padding: 5 }), + flip({ + crossAxis: placement.includes('-'), + flipAlignment: autoAlignment === false ? false : !!alignment, + padding: 5, + }), + ]; + + if (boundaryElement) { + const boundary = boundaryElement instanceof HTMLElement ? boundaryElement : (boundaryElement?.current ?? undefined); + if (boundary) { + middleware.push(hide({ boundary })); + } + } + + if (showArrow) middleware.push(arrow({ element: arrowRef })); + + const adjustedPlacement = alignment ? (`${placement}-${alignment}` as Placement) : placement; + + const data = useFloating({ + placement: adjustedPlacement, + open, + onOpenChange: setOpen, + whileElementsMounted: autoUpdate, + middleware, + }); + + // Attach the arrow ref + (data.refs as any).arrow = arrowRef; + + const click = useClick(data.context, { enabled: activation === 'click' }); + const hover = useHover(data.context, { enabled: activation === 'hover', delay, move: true, restMs: 50 }); + const focus = useFocus(data.context, { enabled: activation === 'focus' }); + const dismiss = useDismiss(data.context, { + enabled: activation === 'click', + + outsidePress: (event: MouseEvent) => { + if (isManaged && popoverGlobal) { + return !(popoverGlobal.isMultiselect() && event.shiftKey); + } + return true; + }, + }); + + const role = useRole(data.context, { role: 'dialog' }); + + const interactions = useInteractions([click, hover, focus, dismiss, role]); + + return React.useMemo( + () => ({ + open, + setOpen, + closePopover: () => setOpen(false), + interactions, + data, + interactive, + }), + [open, setOpen, interactions, data, interactive], + ); +} + +/** + * The `Popover` component displays contextual information or actions in a floating container. + * It supports various configurations like placement, alignment, activation method, and options to show an arrow or close button. + */ +export function Popover({ + children, + autoAlignment = true, + showCloseButton = false, + initialOpen = false, + ...options +}: { + children: React.ReactNode; + showCloseButton?: boolean; +} & PopoverOptions) { + const popover = usePopover({ ...options, initialOpen }); + + return ( + <PopoverInstanceContext.Provider + value={{ + ...popover, + showCloseButton, + }} + > + {children} + </PopoverInstanceContext.Provider> + ); +} + +// PopoverTrigger component +export const PopoverTrigger = React.forwardRef<HTMLElement, React.HTMLProps<HTMLElement> & { asChild?: boolean; x?: number; y?: number }>( + function PopoverTrigger({ children, asChild = false, x = null, y = null, ...props }, propRef) { + const context = usePopoverInstanceContext(); + const ref = useMergeRefs([context.data.refs.setReference, propRef]); + + React.useEffect(() => { + if (x !== null && y !== null && context.data.refs.reference.current != null) { + const element = context.data.refs.reference.current as HTMLElement; + element.style.position = 'absolute'; + const { x: offsetX, y: offsetY } = element.getBoundingClientRect(); + element.getBoundingClientRect = () => { + return { + width: 0, + height: 0, + x: offsetX, + y: offsetY, + top: y + offsetY, + left: x + offsetX, + right: x + offsetX, + bottom: y + offsetY, + } as DOMRect; + }; + context.data.update(); + } + }, [x, y, context.data.refs.reference, context.data.update]); + + // `asChild` allows the user to pass any element as the anchor + if (asChild && React.isValidElement(children)) { + return React.cloneElement( + children, + context.interactions.getReferenceProps({ + ref, + ...props, + ...(children as any).props, + 'data-state': context.open ? 'open' : 'closed', + onClick: (e: React.MouseEvent) => { + e.stopPropagation(); + context.setOpen(!context.open); + }, + // Accessibility attributes + 'aria-expanded': context.open, + 'aria-haspopup': 'dialog', + }), + ); + } + + return ( + <div + ref={ref} + data-state={context.open ? 'open' : 'closed'} + {...context.interactions.getReferenceProps({ + ...props, + onClick: (e: React.MouseEvent) => { + e.stopPropagation(); + context.setOpen(!context.open); + }, + // Accessibility attributes + 'aria-expanded': context.open, + 'aria-haspopup': 'dialog', + })} + > + {children} + </div> + ); + }, +); + +// PopoverContent component +export const PopoverContent = React.forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement>>(function PopoverContent( + { style, className, ...props }, + propRef, +) { + const context = usePopoverInstanceContext(); + const ref = useMergeRefs([context.data.refs.setFloating, propRef]); + + React.useEffect(() => { + if (context.open) { + context.data.update(); + } + }, [context.open, context.data]); + + if (!context.open) return null; + + return ( + <FloatingPortal> + <div + ref={ref} + className={`max-w-72 sm:max-w-lg rounded bg-light border border-secondary-200 shadow-lg animate-in fade-in-0 zoom-in-95 + ${className ? ` ${className}` : ''}`} + style={{ + ...context.data.floatingStyles, + ...style, + display: context.data.middlewareData.hide?.referenceHidden ? 'none' : 'block', + pointerEvents: context.interactive ? 'auto' : 'none', + }} + {...context.interactions.getFloatingProps(props)} + > + {context.showCloseButton && ( + <Button + onClick={() => context.setOpen(false)} + variantType="secondary" + rounded + className="float-right z-10 m-1" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-close]" + aria-label="Close popover" + /> + )} + + {props.children} + + {context.data.middlewareData.arrow ? ( + <FloatingArrow ref={(context.data.refs as any).arrow} context={context.data.context} className="fill-secondary-300" /> + ) : null} + </div> + </FloatingPortal> + ); +}); diff --git a/src/lib/components/popover/PopoverProvider.tsx b/src/lib/components/popover/PopoverProvider.tsx new file mode 100644 index 000000000..b20952dd7 --- /dev/null +++ b/src/lib/components/popover/PopoverProvider.tsx @@ -0,0 +1,77 @@ +import React, { useState, useEffect } from 'react'; + +interface PopoverGlobalContextProps { + openPopover: (id: string) => void; + closePopover: (id: string) => void; + isOpen: (id: string) => boolean; + isMultiselect: () => boolean; +} + +const PopoverGlobalContext = React.createContext<PopoverGlobalContextProps | null>(null); + +// Custom hook to access the global Popover context +export const usePopoverGlobalContext = (): PopoverGlobalContextProps | null => { + return React.useContext(PopoverGlobalContext); +}; + +// PopoverProvider component +interface PopoverProviderProps { + children: React.ReactNode; + multiselect?: boolean; + multiselectKey?: 'Shift' | 'Alt' | 'Control'; +} + +export function PopoverProvider({ children, multiselect = false, multiselectKey = 'Shift' }: PopoverProviderProps) { + const [openPopoverIds, setOpenPopoverIds] = useState<Set<string>>(new Set()); + const [isModifierPressed, setIsModifierPressed] = useState(false); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === multiselectKey) { + setIsModifierPressed(true); + } + }; + const handleKeyUp = (event: KeyboardEvent) => { + if (event.key === multiselectKey) { + setIsModifierPressed(false); + } + }; + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + }; + }, [multiselectKey]); + + const openPopover = (id: string) => { + setOpenPopoverIds(prev => { + const newSet = new Set(prev); + + if (multiselect && isModifierPressed) { + newSet.add(id); // Voeg popover toe zonder anderen te sluiten + } else { + prev.forEach(openId => newSet.delete(openId)); // Sluit alle popovers in deze provider + newSet.add(id); + } + + return newSet; + }); + }; + + const closePopover = (id: string) => { + setOpenPopoverIds(prev => { + const newSet = new Set(prev); + newSet.delete(id); + return newSet; + }); + }; + + const isOpen = (id: string) => openPopoverIds.has(id); + const isMultiselect = () => multiselect; + + return ( + <PopoverGlobalContext.Provider value={{ openPopover, closePopover, isOpen, isMultiselect }}>{children}</PopoverGlobalContext.Provider> + ); +} diff --git a/src/lib/components/popover/index.tsx b/src/lib/components/popover/index.tsx new file mode 100644 index 000000000..45477db8e --- /dev/null +++ b/src/lib/components/popover/index.tsx @@ -0,0 +1,2 @@ +export * from './Popover'; +export * from './PopoverProvider'; diff --git a/src/lib/components/popover/popover.stories.tsx b/src/lib/components/popover/popover.stories.tsx new file mode 100644 index 000000000..3309a6bd1 --- /dev/null +++ b/src/lib/components/popover/popover.stories.tsx @@ -0,0 +1,174 @@ +// Popover.stories.tsx +import { Meta, StoryObj } from '@storybook/react'; +import { Popover, PopoverTrigger, PopoverContent, PopoverProvider } from '@/lib'; + +export default { + title: 'Components/Popover', + component: Popover, + tags: ['autodocs'], + decorators: [Story => <div className="p-10">{Story()}</div>], + argTypes: { + placement: { + control: 'inline-radio', + options: ['top', 'bottom', 'left', 'right'], + description: 'Popover placement relative to the trigger', + }, + alignment: { + control: 'inline-radio', + options: [undefined, 'start', 'end'], + description: 'Popover alignment relative to the trigger', + }, + activation: { + control: 'inline-radio', + options: ['click', 'hover', 'focus'], + description: 'Defines how the popover is triggered', + }, + showArrow: { + control: 'boolean', + description: 'Show an arrow on the popover', + }, + showCloseButton: { + control: 'boolean', + description: 'Show a close button on the popover', + }, + delayOpen: { + control: 'number', + description: 'Delay (hover/focus trigger only) before the popover opens (ms)', + }, + delayClose: { + control: 'number', + description: 'Delay (hover/focus trigger only) before the popover closes (ms)', + }, + id: { + control: { disable: true }, + description: 'Unique identifier to manage multiple popovers, can be set manually else auto-generated', + }, + boundaryElement: { + control: { disable: true }, + description: 'Element to which the popover is constrained', + }, + autoAlignment: { + control: { disable: true }, + description: 'Automatically align the popover based on the available space', + }, + initialOpen: { + control: 'boolean', + description: 'Initial state of the popover', + }, + children: { table: { disable: true } }, + boundaryElement: { table: { disable: true } }, + interactive: { table: { disable: true } }, + delay: { table: { disable: true } }, + onOpenChange: { table: { disable: true } }, + open: { table: { disable: true } }, + initialOpen: { table: { disable: true } }, + }, +} as Meta<typeof Popover>; + +type Story = StoryObj<typeof Popover>; + +// Template Function (Independent Popovers) +const DefaultTemplate = (args: any) => ( + <div className="min-h-52 flex flex-col gap-1 items-center justify-center"> + <PopoverInstance {...args} /> + </div> +); + +// PopoverInstance Component for Independent Story +const PopoverInstance = ({ placement, alignment, activation, showArrow, delayOpen, delayClose, showCloseButton, initialOpen }: any) => ( + <Popover + placement={placement} + activation={activation} + alignment={alignment} + showArrow={showArrow} + showCloseButton={showCloseButton} + initialOpen={initialOpen} + {...(activation === 'hover' && { delay: { open: delayOpen, close: delayClose } })} + > + <PopoverTrigger> + <button className="border px-3 py-2 hover:text-primary rounded"> + {activation === 'click' ? 'Click me' : activation === 'hover' ? 'Hover me' : 'Focus me'} + </button> + </PopoverTrigger> + <PopoverContent> + <h3 className={'p-2 border-b border-secondary-200'}>Popover at {placement}</h3> + <p className="text-xs text-secondary-500 p-2"> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec dui id nunc lacinia eleifend. Cras vestibulum. + </p> + </PopoverContent> + </Popover> +); + +// Default Story: Independent Popovers +/** + * The `Popover` component displays contextual information or actions in a floating container. + * It supports configurations like placement, alignment, activation method, and options to show an arrow or close button. + */ +export const Default: Story = { + render: DefaultTemplate, + args: { + placement: 'top', + alignment: undefined, + activation: 'click', + showArrow: true, + showCloseButton: false, + delayOpen: 500, + delayClose: 150, + initialOpen: false, + }, +}; + +// Template for Managed Story (multiselect) +const ManagedTemplate = (args: any) => { + return ( + <div> + <PopoverProvider multiselect> + <div className="min-h-52 flex flex-col gap-4 items-center justify-center"> + <div className="flex space-x-4"> + <PopoverInstanceManaged {...args} id="popover-1" /> + <PopoverInstanceManaged {...args} id="popover-2" /> + </div> + </div> + </PopoverProvider> + </div> + ); +}; + +// Managed PopoverInstance Component +const PopoverInstanceManaged = ({ id, placement, alignment, activation, showArrow, delayOpen, delayClose, showCloseButton }: any) => ( + <Popover + id={id} + placement={placement} + activation={activation} + alignment={alignment} + showArrow={showArrow} + showCloseButton={showCloseButton} + {...(activation === 'hover' && { delay: { open: delayOpen, close: delayClose } })} + > + <PopoverTrigger> + <button className="border px-3 py-2 hover:text-primary rounded">{`Open Popover ${id.split('-')[1]}`}</button> + </PopoverTrigger> + <PopoverContent> + <h3 className={'p-2 border-b border-secondary-200'}> + Popover {id.split('-')[1]} at {placement} + </h3> + <p className="text-xs text-secondary-500 p-2"> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec dui id nunc lacinia eleifend. Cras vestibulum. + </p> + </PopoverContent> + </Popover> +); + +/** Managed Popovers, a `<PopoverProvider multiselect>` will have options like hold shift to select multiple */ +export const Managed: StoryObj<typeof Popover> = { + render: ManagedTemplate, + name: 'Managed Popovers', + args: { + placement: 'top', + alignment: undefined, + activation: 'click', + showArrow: true, + showCloseButton: false, + delay: { open: 500, close: 150 }, + }, +}; diff --git a/src/lib/components/tabs/Tab.tsx b/src/lib/components/tabs/Tab.tsx index c61d4cd1d..cd2b537ee 100644 --- a/src/lib/components/tabs/Tab.tsx +++ b/src/lib/components/tabs/Tab.tsx @@ -10,11 +10,14 @@ const TabContext = React.createContext<ContextType>({ tabType: 'inline', }); -export const Tabs = forwardRef<HTMLDivElement, { - children: React.ReactNode; - tabType?: TabTypes; - className?: string; -}>((props, ref) => { +export const Tabs = forwardRef< + HTMLDivElement, + { + children: React.ReactNode; + tabType?: TabTypes; + className?: string; + } +>((props, ref) => { const { children, tabType = 'inline', className = '' } = props; let baseClass = ''; if (tabType === 'inline') { @@ -26,7 +29,7 @@ export const Tabs = forwardRef<HTMLDivElement, { } const combinedClass = `${baseClass} ${className}`.trim(); return ( - <TabContext.Provider value={{ tabType : tabType}}> + <TabContext.Provider value={{ tabType: tabType }}> <div ref={ref} className={combinedClass}> {children} </div> @@ -53,16 +56,14 @@ export const Tab = ({ if (context.tabType === 'inline') { className += ` px-2 gap-1 relative h-full max-w-64 flex-nowrap before:content-[''] before:absolute before:left-0 before:bottom-0 before:h-[2px] before:w-full - ${activeTab + ${ + activeTab ? 'before:bg-primary-500 text-dark' : ' text-secondary-600 hover:text-dark hover:bg-secondary-200 hover:bg-opacity-50 before:bg-transparent hover:before:bg-secondary-300' }`; } else if (context.tabType === 'rounded') { className += ` py-1.5 px-3 -mb-px text-sm flex-nowrap text-center border border-secondary-200 rounded-t - ${activeTab - ? 'active z-[2] text-dark bg-light' - : 'bg-secondary-100 hover:text-dark border-secondary-100 text-secondary-600' - }`; + ${activeTab ? 'active z-[2] text-dark bg-light' : 'bg-secondary-100 hover:text-dark border-secondary-100 text-secondary-600'}`; } else if (context.tabType === 'simple') { className += ` px-2 py-1 gap-1 ${activeTab ? 'active text-dark' : 'text-secondary-500 hover:text-dark'}`; } diff --git a/src/lib/components/tooltip/Overview.mdx b/src/lib/components/tooltip/Overview.mdx index 369f13159..b64d3ff58 100644 --- a/src/lib/components/tooltip/Overview.mdx +++ b/src/lib/components/tooltip/Overview.mdx @@ -1,36 +1,85 @@ import { Canvas, Meta, Story } from '@storybook/blocks'; - import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './index'; <Meta title="Components/Tooltip" component={Tooltip} /> ## Usage -<div> - <TooltipProvider delayDuration={0}> - <div className="flex justify-center items-center py-16"> - <Tooltip> - <TooltipTrigger> - <div className="mx-auto">Hover over me</div> - </TooltipTrigger> - <TooltipContent side={'bottom'}> - <p>This is a tooltip</p> - </TooltipContent> - </Tooltip> - </div> - </TooltipProvider> +### ✨ Single Tooltip (With Delay) + +The **Tooltip** component provides contextual information when users hover over an element. It supports the following options: + +- **`placement`**: Position of the tooltip (`top`, `bottom`, `left`, `right`). +- **`showArrow`**: Whether to display an arrow pointing to the trigger. +- **`interactive`**: Allows interaction with the tooltip content. +- **`delay`**: Controls the tooltip's open/close delay (in milliseconds). +- **`boundaryElement`**: Prevents clipping by specifying a boundary element. + +This example shows a **single Tooltip** with a custom delay. + <div className="flex flex-row gap-2"> + <Tooltip delay={{ open: 500, close: 100 }}> + <TooltipTrigger> + <div className="border px-3 py-2 hover:text-primary rounded">Hover over me</div> + </TooltipTrigger> + <TooltipContent side="bottom"> + <p>This is a tooltip</p> + </TooltipContent> + </Tooltip> + </div> + +```jsx +<Tooltip delay={{ open: 500, close: 100 }}> + <TooltipTrigger> + <div className="border px-3 py-2 hover:text-primary rounded">Hover over me</div> + </TooltipTrigger> + <TooltipContent side={"bottom"}> + <p>This is a tooltip</p> + </TooltipContent> +</Tooltip> +``` + +### ✨ Multiple Tooltips (with group delay) +For grouped tooltips, use **`TooltipProvider`** to apply a shared delay across multiple tooltips. + +This example shows **multiple Tooltips** with a group delay. +<div className="flex flex-row gap-2"> +<TooltipProvider delay={{ open: 500, close: 100 }}> + <Tooltip> + <TooltipTrigger> + <div className="border px-3 py-2 hover:text-primary rounded">Hover over me</div> + </TooltipTrigger> + <TooltipContent side="bottom"> + <p>This is a tooltip</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger> + <div className="border px-3 py-2 hover:text-primary rounded">Hover over me</div> + </TooltipTrigger> + <TooltipContent side="bottom"> + <p>This is a tooltip</p> + </TooltipContent> + </Tooltip> +</TooltipProvider> </div> ```jsx - <div className="flex justify-center items-center py-16"> - <Tooltip> - <TooltipTrigger> - <div className="mx-auto">Hover over me</div> - </TooltipTrigger> - <TooltipContent side={"bottom"}> - <p>This is a tooltip</p> - </TooltipContent> - </Tooltip> - </div> - </TooltipProvider> +<TooltipProvider delay={{ open: 500, close: 100 }}> + <Tooltip> + <TooltipTrigger> + <div className="border px-3 py-2 hover:text-primary rounded">Hover over me</div> + </TooltipTrigger> + <TooltipContent side={"bottom"}> + <p>This is a tooltip</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger> + <div className="border px-3 py-2 hover:text-primary rounded">Hover over me</div> + </TooltipTrigger> + <TooltipContent side={"bottom"}> + <p>This is a tooltip</p> + </TooltipContent> + </Tooltip> +</TooltipProvider> ``` diff --git a/src/lib/components/tooltip/Tooltip.tsx b/src/lib/components/tooltip/Tooltip.tsx index 2a2287989..6c512a6d0 100644 --- a/src/lib/components/tooltip/Tooltip.tsx +++ b/src/lib/components/tooltip/Tooltip.tsx @@ -17,7 +17,7 @@ import { FloatingArrow, } from '@floating-ui/react'; import type { Placement } from '@floating-ui/react'; -import { FloatingDelayGroup } from '@floating-ui/react'; +import { useDelayGroup, FloatingDelayGroup } from '@floating-ui/react'; interface TooltipOptions { initialOpen?: boolean; @@ -27,6 +27,7 @@ interface TooltipOptions { boundaryElement?: React.RefObject<HTMLElement> | HTMLElement | null | React.RefObject<HTMLDivElement | null>; showArrow?: boolean; interactive?: boolean; + delay?: number | { open?: number; close?: number }; } export function useTooltip({ @@ -37,6 +38,7 @@ export function useTooltip({ boundaryElement = null, showArrow = false, interactive = true, + delay, }: TooltipOptions = {}): { open: boolean; setOpen: (open: boolean) => void; @@ -50,45 +52,58 @@ export function useTooltip({ const setOpen = setControlledOpen ?? setUncontrolledOpen; const arrowRef = React.useRef<SVGSVGElement | null>(null); - const config = { - placement, - open, - onOpenChange: setOpen, - whileElementsMounted: autoUpdate, - middleware: [ - offset(5), - flip({ - crossAxis: placement.includes('-'), - fallbackAxisSideDirection: 'start', - padding: 5, - }), - shift({ padding: 5 }), - ], - }; - - if (boundaryElement != null) { - const boundary = boundaryElement instanceof HTMLElement ? (boundaryElement ?? undefined) : (boundaryElement?.current ?? undefined); - config.middleware.find(x => x.name == 'flip')!.options[0].boundary = boundary; - config.middleware.find(x => x.name == 'shift')!.options[0].boundary = boundary; - config.middleware.push(hide({ boundary })); + const middleware = [ + offset(5), + flip({ + crossAxis: placement.includes('-'), + fallbackAxisSideDirection: 'start', + padding: 5, + }), + shift({ padding: 5 }), + ]; + + if (boundaryElement) { + const boundary = boundaryElement instanceof HTMLElement ? boundaryElement : (boundaryElement?.current ?? undefined); + + if (boundary) { + const flipMiddleware = middleware.find(m => m.name === 'flip'); + const shiftMiddleware = middleware.find(m => m.name === 'shift'); + + if (flipMiddleware) { + (flipMiddleware as any).options.boundary = boundary; + } + if (shiftMiddleware) { + (shiftMiddleware as any).options.boundary = boundary; + } + middleware.push(hide({ boundary })); + } } if (showArrow) { - config.middleware.push(arrow({ element: arrowRef })); + middleware.push(arrow({ element: arrowRef })); } - const data = useFloating(config); + const data = useFloating({ + placement, + open, + onOpenChange: setOpen, + whileElementsMounted: autoUpdate, + middleware, + }); + (data.refs as any).arrow = arrowRef; const context = data.context; + const { delay: groupDelay } = useDelayGroup(context); + const hover = useHover(context, { move: false, enabled: controlledOpen == null, + delay: delay ?? groupDelay ?? undefined, }); - const focus = useFocus(context, { - enabled: controlledOpen == null, - }); + + const focus = useFocus(context, { enabled: controlledOpen == null }); const dismiss = useDismiss(context); const role = useRole(context, { role: 'tooltip' }); @@ -98,9 +113,9 @@ export function useTooltip({ () => ({ open, setOpen, - interactions: interactions, - data: data, - interactive: interactive, + interactions, + data, + interactive, }), [open, setOpen, interactions, data, interactive], ); @@ -131,15 +146,7 @@ export function Tooltip({ children, ...options }: { children: React.ReactNode } export const TooltipTrigger = React.forwardRef<HTMLElement, React.HTMLProps<HTMLElement> & { asChild?: boolean; x?: number; y?: number }>( function TooltipTrigger({ children, asChild = false, x = null, y = null, ...props }, propRef) { const context = useTooltipContext(); - const childrenRef = React.useMemo(() => { - if (children == null) { - return null; - } else { - return (children as any).ref; - } - }, [children]); - - const ref = useMergeRefs([context.data.refs.setReference, propRef, childrenRef]); + const ref = useMergeRefs([context.data.refs.setReference, propRef]); React.useEffect(() => { if (x && y && context.data.refs.reference.current != null) { @@ -203,8 +210,8 @@ export const TooltipContent = React.forwardRef< <FloatingPortal> <div ref={ref} - className={`max-w-64 rounded bg-light px-2 py-2 shadow text-xs border border-secondary-200 - text-dark animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 + className={`max-w-64 rounded bg-secondary-900 px-2 py-1 shadow text-xs + text-secondary-100 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2${className ? ` ${className}` : ''}`} style={{ @@ -217,13 +224,19 @@ export const TooltipContent = React.forwardRef< > {props.children} {context.data.middlewareData.arrow ? ( - <FloatingArrow ref={(context.data.refs as any).arrow} context={context.data.context} style={{ fill: 'white' }} /> + <FloatingArrow ref={(context.data.refs as any).arrow} context={context.data.context} className={'fill-secondary-900'} /> ) : null} </div> </FloatingPortal> ); }); -export const TooltipProvider = (props: { delayDuration: number; children: React.ReactNode }) => { - return <FloatingDelayGroup delay={props.delayDuration}>{props.children}</FloatingDelayGroup>; +export const TooltipProvider = ({ + delay = { open: 500, close: 100 }, + children, +}: { + delay?: number | { open?: number; close?: number }; + children: React.ReactNode; +}) => { + return <FloatingDelayGroup delay={delay}>{children}</FloatingDelayGroup>; }; diff --git a/src/lib/components/tooltip/index.tsx b/src/lib/components/tooltip/index.tsx index 0e13947ed..7594a8f06 100644 --- a/src/lib/components/tooltip/index.tsx +++ b/src/lib/components/tooltip/index.tsx @@ -1,22 +1 @@ -// https://www.radix-ui.com/primitives/docs/components/tooltip - export * from './Tooltip'; - -/* -export interface BarTooltipProps { - x: number; - y: number; - children: React.ReactNode; -} - -export const BarPlotTooltip = ({ x, y, children }: BarTooltipProps) => { - return ( - <div - className="absolute bg-light border border-secondary text-secondary p-2 pointer-events-none" - style={{ left: `${x}px`, top: `${y}px` }} - > - {children} - </div> - ); -}; -*/ diff --git a/src/lib/components/tooltip/tooltip.stories.tsx b/src/lib/components/tooltip/tooltip.stories.tsx index 5c6822de2..046981d4e 100644 --- a/src/lib/components/tooltip/tooltip.stories.tsx +++ b/src/lib/components/tooltip/tooltip.stories.tsx @@ -1,20 +1,87 @@ -import React from 'react'; -import { Meta } from '@storybook/react'; -import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './index'; +import { Meta, StoryObj } from '@storybook/react'; +import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/lib'; export default { title: 'Components/Tooltip', component: Tooltip, - decorators: [Story => <div className="m-5">{Story()}</div>], + decorators: [ + Story => ( + <div className="p-10"> + <Story /> + </div> + ), + ], + + argTypes: { + placement: { + control: 'inline-radio', + options: ['top', 'bottom', 'left', 'right'], + description: 'Tooltip placement relative to the trigger', + }, + groupDelay: { + control: 'boolean', + description: 'Wrap the tooltip in a TooltipProvider to apply group delay', + }, + delayOpen: { + control: 'number', + description: 'Delay before the tooltip opens (ms)', + }, + delayClose: { + control: 'number', + description: 'Delay before the tooltip closes (ms)', + }, + showArrow: { + control: 'boolean', + description: 'Show an arrow on the tooltip', + }, + + children: { table: { disable: true } }, + boundaryElement: { table: { disable: true } }, + interactive: { table: { disable: true } }, + delay: { table: { disable: true } }, + onOpenChange: { table: { disable: true } }, + open: { table: { disable: true } }, + initialOpen: { table: { disable: true } }, + }, } as Meta; -export const Default = () => ( - <TooltipProvider delayDuration={0}> - <Tooltip> - <TooltipTrigger> - <button>Hover me</button> - </TooltipTrigger> - <TooltipContent>Tooltip content</TooltipContent> - </Tooltip> - </TooltipProvider> +const Template = (args: any) => { + return ( + <div className="p-10 flex flex-col gap-1 items-center justify-center"> + {/* ✅ Conditionally wrap in TooltipProvider */} + {args.groupDelay ? ( + <TooltipProvider delay={{ open: args.delayOpen, close: args.delayClose }}> + <TooltipInstance {...args} /> + <TooltipInstance {...args} /> + <TooltipInstance {...args} /> + </TooltipProvider> + ) : ( + <TooltipInstance {...args} /> + )} + </div> + ); +}; + +const TooltipInstance = ({ placement, showArrow, delayOpen, delayClose, groupDelay }: any) => ( + <Tooltip + placement={placement} + showArrow={showArrow} + {...(!groupDelay && { delay: { open: delayOpen, close: delayClose } })} // ✅ Remove delay if groupDelay is true + > + <TooltipTrigger> + <button className="border px-3 py-2 hover:text-primary rounded">Hover me</button> + </TooltipTrigger> + <TooltipContent>Tooltip at {placement}</TooltipContent> + </Tooltip> ); + +export const Default: StoryObj = { + render: Template, + args: { + placement: 'top', + showArrow: false, + delayOpen: 500, + delayClose: 150, + groupDelay: false, + }, +}; diff --git a/src/lib/data-access/store/visualizationSlice.ts b/src/lib/data-access/store/visualizationSlice.ts index 9a1fa4eb0..ed1138497 100644 --- a/src/lib/data-access/store/visualizationSlice.ts +++ b/src/lib/data-access/store/visualizationSlice.ts @@ -15,13 +15,12 @@ export const initialState: VisState = { openVisualizationArray: [], }; function generateUUID(): string { - if (typeof crypto?.randomUUID === "function") { + if (typeof crypto?.randomUUID === 'function') { return crypto.randomUUID(); } return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; } - export const visualizationSlice = createSlice({ name: 'visualization', initialState, @@ -51,12 +50,10 @@ export const visualizationSlice = createSlice({ setVisualizationState: (state, action: PayloadAction<VisState>) => { if (action.payload.activeVisualizationIndex !== undefined && !isEqual(action.payload, state)) { state.openVisualizationArray = (action.payload.openVisualizationArray || []).map(item => ({ - ...item, uuid: item.uuid || generateUUID(), + ...item, + uuid: item.uuid || generateUUID(), })); - state.activeVisualizationIndex = Math.min( - action.payload.activeVisualizationIndex, - state.openVisualizationArray.length - 1 - ); + state.activeVisualizationIndex = Math.min(action.payload.activeVisualizationIndex, state.openVisualizationArray.length - 1); } }, updateVisualization: (state, action: PayloadAction<{ id: number; settings: VisualizationSettingsType }>) => { diff --git a/src/lib/management/database/Databases.tsx b/src/lib/management/database/Databases.tsx index 2c9c94e4f..0e01b3a32 100644 --- a/src/lib/management/database/Databases.tsx +++ b/src/lib/management/database/Databases.tsx @@ -13,7 +13,7 @@ import { } from '../..'; import { clearQB, deleteSaveState, selectSaveState } from '../../data-access/store/sessionSlice'; import { clearSchema } from '../../data-access/store/schemaSlice'; -import { Popover, PopoverContent, PopoverTrigger } from '../../components/layout/Popover'; +import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover'; import { useHandleDatabase } from './useHandleDatabase'; import { SaveState } from 'ts-common'; @@ -272,7 +272,7 @@ export function Databases({ onClose, saveStates, changeActive, setSelectedSaveSt <PopoverTrigger> <Button iconComponent="icon-[mi--options-vertical]" variant="ghost" /> </PopoverTrigger> - <PopoverContent className="w-56 z-50 bg-light rounded-sm border-[1px] outline-none"> + <PopoverContent className="p-1 min-w-24"> <DropdownItem value="Open" onClick={() => { diff --git a/src/lib/querybuilder/panel/ManualQueryDialog.tsx b/src/lib/querybuilder/panel/ManualQueryDialog.tsx index 0b0c16ef5..8467e08f0 100644 --- a/src/lib/querybuilder/panel/ManualQueryDialog.tsx +++ b/src/lib/querybuilder/panel/ManualQueryDialog.tsx @@ -16,7 +16,7 @@ export const ManualQueryDialog = ({ onSubmit }: ManualQueryDialogProps) => { }; return ( - <div className="flex flex-col w-full gap-2 px-4 py-2"> + <div className="flex flex-col w-full gap-2 p-2"> <label className="text-xs font-bold">Manual Query</label> <textarea value={query} diff --git a/src/lib/querybuilder/panel/QueryBuilder.tsx b/src/lib/querybuilder/panel/QueryBuilder.tsx index 0a806685e..8867c776b 100644 --- a/src/lib/querybuilder/panel/QueryBuilder.tsx +++ b/src/lib/querybuilder/panel/QueryBuilder.tsx @@ -38,7 +38,7 @@ import { ConnectingNodeDataI } from './utils/connectorDrop'; import { QueryBuilderDispatcherContext } from './QueryBuilderDispatcher'; import { QueryBuilderNav, QueryBuilderToggleSettings } from './QueryBuilderNav'; import html2canvas from 'html2canvas'; -import { ContextMenu } from './ContextMenu'; +import { QueryBuilderContextMenu } from './QueryBuilderContextMenu'; import { QueryElementTypes, AllLogicMap, isLogicHandle, defaultGraph } from 'ts-common'; import { setQuerybuilderGraphology, toQuerybuilderGraphology } from '@/lib/data-access/store/sessionSlice'; @@ -550,7 +550,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { }, }} > - <ContextMenu + <QueryBuilderContextMenu open={contextMenuOpen.open} node={contextMenuOpen.node} position={contextMenuOpen.position} diff --git a/src/lib/querybuilder/panel/ContextMenu.tsx b/src/lib/querybuilder/panel/QueryBuilderContextMenu.tsx similarity index 93% rename from src/lib/querybuilder/panel/ContextMenu.tsx rename to src/lib/querybuilder/panel/QueryBuilderContextMenu.tsx index 27df0fb08..5af7cc5df 100644 --- a/src/lib/querybuilder/panel/ContextMenu.tsx +++ b/src/lib/querybuilder/panel/QueryBuilderContextMenu.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react'; -import { Tooltip, TooltipTrigger, TooltipContent, DropdownItem, Icon, Input } from '../../components'; +import { Popover, PopoverTrigger, PopoverContent, DropdownItem, Input, Icon } from '@/lib/components'; import { ReactFlowInstance, Node } from 'reactflow'; import { useActiveQuery, useActiveSaveState, useAppDispatch, useQuerybuilderHash } from '../..'; import { isEqual } from 'lodash-es'; @@ -12,7 +12,7 @@ import { toQuerybuilderGraphology, } from '@/lib/data-access/store/sessionSlice'; -export const ContextMenu = (props: { +export const QueryBuilderContextMenu = (props: { open: boolean; position?: { x: number; y: number }; node?: Node; @@ -83,9 +83,9 @@ export const ContextMenu = (props: { } return ( - <Tooltip open={props.open && state !== undefined} interactive={true} showArrow={false} placement="bottom-start"> - <TooltipTrigger x={state ? state.x : 0} y={state ? state.y : 0} /> - <TooltipContent> + <Popover open={props.open && state !== undefined} interactive={true} showArrow={false} placement="bottom-start"> + <PopoverTrigger x={state ? state.x : 0} y={state ? state.y : 0} /> + <PopoverContent> {props.node && props?.node.type !== 'logic' && ( <> <DropdownItem @@ -133,7 +133,7 @@ export const ContextMenu = (props: { </> )} <DropdownItem value="Remove" className="text-danger" onClick={e => removeNode()} /> - </TooltipContent> - </Tooltip> + </PopoverContent> + </Popover> ); }; diff --git a/src/lib/querybuilder/panel/QueryBuilderNav.tsx b/src/lib/querybuilder/panel/QueryBuilderNav.tsx index f0be3bff9..ae7c9afab 100644 --- a/src/lib/querybuilder/panel/QueryBuilderNav.tsx +++ b/src/lib/querybuilder/panel/QueryBuilderNav.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { ControlContainer, TooltipProvider, Tooltip, TooltipTrigger, Button, TooltipContent, Input } from '../../components'; -import { Popover, PopoverTrigger, PopoverContent } from '../../components/layout/Popover'; +import { Popover, PopoverTrigger, PopoverContent } from '@/lib/components/popover'; import { useActiveQuery, useActiveSaveState, @@ -188,7 +188,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { </Tabs> <div className="sticky right-0 px-0.5 ml-auto items-center flex truncate"> <ControlContainer> - <TooltipProvider delayDuration={0}> + <TooltipProvider> <Tooltip> <TooltipTrigger> <Button @@ -291,11 +291,10 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { <TooltipTrigger> <Button variantType={mlEnabled ? 'primary' : 'secondary'} - variant="ghost" + variant={mlEnabled ? 'outline' : 'ghost'} size="xs" disabled={!saveStateAuthorization.query.W} iconComponent="icon-[ic--baseline-lightbulb]" - className={mlEnabled ? 'border-primary-600' : ''} /> </TooltipTrigger> <TooltipContent disabled={props.toggleSettings === 'ml'}> @@ -349,45 +348,42 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { <QueryMLDialog /> </PopoverContent> </Popover> - <Popover> + <Popover placement="bottom"> <PopoverTrigger> <Tooltip> <TooltipTrigger> <Button variantType={activeQuery.settings.limit <= resultSize ? 'primary' : 'secondary'} - variant="ghost" + variant={activeQuery.settings.limit <= resultSize ? 'outline' : 'ghost'} size="xs" disabled={!saveStateAuthorization.query.W} iconComponent="icon-[ic--baseline-filter-alt]" - className={activeQuery.settings.limit <= resultSize ? 'border-primary-600' : ''} /> </TooltipTrigger> <TooltipContent disabled={props.toggleSettings === 'ml'}> <p className="font-bold text-base">Limit</p> <p>Limits the number of edges retrieved from the database.</p> <p>Required to manage performance.</p> - <p className={`font-semibold${activeQuery.settings.limit <= resultSize ? ' text-primary-800' : ''}`}> + <p className={`font-semibold${activeQuery.settings.limit <= resultSize ? ' text-primary-200' : ''}`}> Fetched {resultSize} of a maximum of {activeQuery.settings.limit} edges </p> </TooltipContent> </Tooltip> </PopoverTrigger> <PopoverContent> - <div className="flex flex-col w-full gap-2 px-4 py-2"> - <span className="text-xs font-bold">Limit</span> - <Input - type="number" - size="xs" - value={activeQuery.settings.limit} - lazy - label="" - onChange={e => { - dispatch(setQuerybuilderSettings({ ...activeQuery.settings, limit: Number(e) })); - }} - className={`w-24${activeQuery.settings.limit <= resultSize ? ' border-danger-600' : ''}`} - containerClassName="" - /> - </div> + <Input + type="number" + size="sm" + label="Limit" + value={activeQuery.settings.limit} + lazy + label="" + onChange={e => { + dispatch(setQuerybuilderSettings({ ...activeQuery.settings, limit: Number(e) })); + }} + className={`w-24${activeQuery.settings.limit <= resultSize ? ' border-danger-600' : ''}`} + containerClassName="p-2" + /> </PopoverContent> </Popover> <Popover> diff --git a/src/lib/querybuilder/panel/querysidepanel/QueryBuilderLogicPillsPanel.tsx b/src/lib/querybuilder/panel/querysidepanel/QueryBuilderLogicPillsPanel.tsx index a94f560e6..746313ca8 100644 --- a/src/lib/querybuilder/panel/querysidepanel/QueryBuilderLogicPillsPanel.tsx +++ b/src/lib/querybuilder/panel/querysidepanel/QueryBuilderLogicPillsPanel.tsx @@ -171,7 +171,7 @@ export const QueryBuilderLogicPillsPanel = (props: { <div className={props.className + ' card'}> {props.title && <h1 className="card-title mb-2 mx-auto">{props.title}</h1>} <div className="gap-1 flex justify-center"> - <TooltipProvider delayDuration={50}> + <TooltipProvider delay={50}> {dataOps.map((item, index) => ( <Tooltip key={'QB_Type_' + item.title}> <TooltipContent>{item.description}</TooltipContent> @@ -212,7 +212,7 @@ export const QueryBuilderLogicPillsPanel = (props: { </TooltipProvider> </div> <div className="mt-1 overflow-x-hidden h-[80vh]"> - <TooltipProvider delayDuration={50}> + <TooltipProvider delay={50}> <ul className="pb-10 gap-1 flex flex-col w-full h-full"> {AllLogicMapAvailable.map((item, index) => ( <Tooltip key={'QB_Options_' + index}> diff --git a/src/lib/querybuilder/panel/querysidepanel/QueryMLDialog.tsx b/src/lib/querybuilder/panel/querysidepanel/QueryMLDialog.tsx index aebf595a5..c82f1dc26 100644 --- a/src/lib/querybuilder/panel/querysidepanel/QueryMLDialog.tsx +++ b/src/lib/querybuilder/panel/querysidepanel/QueryMLDialog.tsx @@ -6,7 +6,7 @@ import { setLinkPredictionEnabled, setShortestPathEnabled, } from '@/lib/data-access/store/mlSlice'; -import { FormCard, FormBody, FormTitle, FormHBar } from '@/lib/components/forms'; +import { FormBody, FormTitle } from '@/lib/components/forms'; import { Input } from '@/lib/components/inputs'; import { FeatureEnabled } from '@/lib/components/featureFlags'; @@ -15,73 +15,81 @@ export const QueryMLDialog = () => { const ml = useML(); return ( - <div> - <FormCard> - <FormBody - onSubmit={e => { - e.preventDefault(); - }} - > - <FormTitle title="Machine Learning Options" /> - <FormHBar /> + <FormBody + onSubmit={e => { + e.preventDefault(); + }} + className="divide-y divide-secondary-200" + > + <FormTitle title="Machine Learning Options" className="p-1 text-sm" /> - <div className="px-5"> - <FeatureEnabled featureFlag="LINK_PREDICTION"> - <Input - type="boolean" - label="Link Prediction" - value={ml.linkPrediction.enabled} - onChange={() => dispatch(setLinkPredictionEnabled(!ml.linkPrediction.enabled))} - /> - {ml.linkPrediction.enabled && ml.linkPrediction.result && <span># of predictions: {ml.linkPrediction.result.length}</span>} - {ml.linkPrediction.enabled && !ml.linkPrediction.result && <span>Loading...</span>} - </FeatureEnabled> + <div className="flex flex-col p-1 text-sm divide-y divider-secondary-200"> + <FeatureEnabled featureFlag="LINK_PREDICTION"> + <div className="p-1"> + <Input + size="sm" + type="boolean" + label="Link Prediction" + value={ml.linkPrediction.enabled} + onChange={() => dispatch(setLinkPredictionEnabled(!ml.linkPrediction.enabled))} + /> + {ml.linkPrediction.enabled && ml.linkPrediction.result && <span># of predictions: {ml.linkPrediction.result.length}</span>} + {ml.linkPrediction.enabled && !ml.linkPrediction.result && <span>Loading...</span>} + </div> + </FeatureEnabled> - <FeatureEnabled featureFlag="CENTRALITY"> - <Input - type="boolean" - label="Centrality" - value={ml.centrality.enabled} - onChange={() => dispatch(setCentralityEnabled(!ml.centrality.enabled))} - /> - {ml.centrality.enabled && Object.values(ml.centrality.result).length > 0 && ( - <span> - sum of centers: - {Object.values(ml.centrality.result) - .reduce((a, b) => b + a) - .toFixed(2)} - </span> - )} - {ml.centrality.enabled && Object.values(ml.centrality.result).length === 0 && <span>No Centers Found</span>} - </FeatureEnabled> + <FeatureEnabled featureFlag="CENTRALITY"> + <div className="p-1"> + <Input + size="sm" + type="boolean" + label="Centrality" + value={ml.centrality.enabled} + onChange={() => dispatch(setCentralityEnabled(!ml.centrality.enabled))} + /> + {ml.centrality.enabled && Object.values(ml.centrality.result).length > 0 && ( + <span> + sum of centers: + {Object.values(ml.centrality.result) + .reduce((a, b) => b + a) + .toFixed(2)} + </span> + )} + {ml.centrality.enabled && Object.values(ml.centrality.result).length === 0 && <span>No Centers Found</span>} + </div> + </FeatureEnabled> - <FeatureEnabled featureFlag="COMMUNITY_DETECTION"> - <Input - type="boolean" - label="Community detection" - value={ml.communityDetection.enabled} - onChange={() => dispatch(setCommunityDetectionEnabled(!ml.communityDetection.enabled))} - /> - {ml.communityDetection.enabled && ml.communityDetection.result && ( - <span># of communities: {ml.communityDetection.result.length}</span> - )} - {ml.communityDetection.enabled && !ml.communityDetection.result && <span>Loading...</span>} - </FeatureEnabled> + <FeatureEnabled featureFlag="COMMUNITY_DETECTION"> + <div className="p-1"> + <Input + size="sm" + type="boolean" + label="Community detection" + value={ml.communityDetection.enabled} + onChange={() => dispatch(setCommunityDetectionEnabled(!ml.communityDetection.enabled))} + /> + {ml.communityDetection.enabled && ml.communityDetection.result && ( + <span># of communities: {ml.communityDetection.result.length}</span> + )} + {ml.communityDetection.enabled && !ml.communityDetection.result && <span>Loading...</span>} + </div> + </FeatureEnabled> - <FeatureEnabled featureFlag="SHORTEST_PATH"> - <Input - type="boolean" - label="Shortest path" - value={ml.shortestPath.enabled} - onChange={() => dispatch(setShortestPathEnabled(!ml.shortestPath.enabled))} - /> - {ml.shortestPath.enabled && ml.shortestPath.result?.length > 0 && <span># of hops: {ml.shortestPath.result.length}</span>} - {ml.shortestPath.enabled && !ml.shortestPath.srcNode && <span>Please select source node</span>} - {ml.shortestPath.enabled && ml.shortestPath.srcNode && !ml.shortestPath.trtNode && <span>Please select target node</span>} - </FeatureEnabled> + <FeatureEnabled featureFlag="SHORTEST_PATH"> + <div className="p-1"> + <Input + size="sm" + type="boolean" + label="Shortest path" + value={ml.shortestPath.enabled} + onChange={() => dispatch(setShortestPathEnabled(!ml.shortestPath.enabled))} + /> + {ml.shortestPath.enabled && ml.shortestPath.result?.length > 0 && <span># of hops: {ml.shortestPath.result.length}</span>} + {ml.shortestPath.enabled && !ml.shortestPath.srcNode && <span>Please select source node</span>} + {ml.shortestPath.enabled && ml.shortestPath.srcNode && !ml.shortestPath.trtNode && <span>Please select target node</span>} </div> - </FormBody> - </FormCard> - </div> + </FeatureEnabled> + </div> + </FormBody> ); }; diff --git a/src/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx b/src/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx index c293641cf..b8c1104f2 100644 --- a/src/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx +++ b/src/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx @@ -31,9 +31,10 @@ export const QuerySettings = React.forwardRef<HTMLDivElement, object>((props, re } return ( - <div className="flex flex-col w-full gap-2 px-4 py-2"> + <div className="flex flex-col w-full gap-2 p-2"> <span className="text-xs font-bold">Query Settings</span> <Input + size="sm" type="boolean" value={state.autocompleteRelation} label="Autocomplete Edges" @@ -85,6 +86,7 @@ export const QuerySettings = React.forwardRef<HTMLDivElement, object>((props, re /> <FormActions + size={'sm'} onApply={() => { submit(); }} diff --git a/src/lib/querybuilder/pills/pillattributes/PillAttributesItem.tsx b/src/lib/querybuilder/pills/pillattributes/PillAttributesItem.tsx index dad095294..62ef62985 100644 --- a/src/lib/querybuilder/pills/pillattributes/PillAttributesItem.tsx +++ b/src/lib/querybuilder/pills/pillattributes/PillAttributesItem.tsx @@ -38,7 +38,7 @@ export const PillAttributesItem = (props: PillAttributesItemProps) => { } return ( - <div className="px-2 py-1 bg-secondary-100 flex justify-between items-center"> + <div className="px-1.5 py-1 bg-secondary-100 flex justify-between items-center"> <p className="truncate w-[90%]">{props.attribute.handleData.attributeName}</p> <Button variantType="secondary" diff --git a/src/lib/schema/model/reactflow.tsx b/src/lib/schema/model/reactflow.tsx index 70649ba36..c80a44aa2 100644 --- a/src/lib/schema/model/reactflow.tsx +++ b/src/lib/schema/model/reactflow.tsx @@ -37,7 +37,7 @@ export type SchemaReactflowEntity = SchemaReactflowData & { x: number; y: number; reactFlowRef: any; - tooltipClose: boolean; + popoverClose: boolean; }; export type SchemaReactflowRelation = SchemaReactflowData & { @@ -49,7 +49,7 @@ export type SchemaReactflowRelation = SchemaReactflowData & { x: number; y: number; reactFlowRef: any; - tooltipClose: boolean; + popoverClose: boolean; }; export type SchemaReactflowNodeWithFunctions = SchemaReactflowEntity & { diff --git a/src/lib/schema/panel/LayoutDescription/iconsLayout/GridIcon.tsx b/src/lib/schema/panel/LayoutDescription/iconsLayout/GridIcon.tsx index 79c5c08ec..a2c28b70b 100644 --- a/src/lib/schema/panel/LayoutDescription/iconsLayout/GridIcon.tsx +++ b/src/lib/schema/panel/LayoutDescription/iconsLayout/GridIcon.tsx @@ -14,22 +14,8 @@ export const GridIcon: React.FC<IconProps> = ({ className = '' }) => ( preserveAspectRatio="xMidYMid meet" fill="none" > - <rect - fill="hsl(var(--clr-node))" - id="rect1-97" - width="7.3312178" - height="2.7723093" - x="-7.0483686e-10" - y="-1.7570567e-09" - /> - <rect - fill="hsl(var(--clr-node))" - id="rect1-97-4" - width="7.3312178" - height="2.7723093" - x="-7.0483686e-10" - y="6.3607378" - /> + <rect fill="hsl(var(--clr-node))" id="rect1-97" width="7.3312178" height="2.7723093" x="-7.0483686e-10" y="-1.7570567e-09" /> + <rect fill="hsl(var(--clr-node))" id="rect1-97-4" width="7.3312178" height="2.7723093" x="-7.0483686e-10" y="6.3607378" /> <rect fill="hsl(var(--clr-relation))" width="7.3312178" height="2.7723093" x="-7.0483686e-10" y="12.721476" /> <rect fill="hsl(var(--clr-relation))" width="7.3312178" height="2.7723093" x="8.6687822" y="-1.7570567e-09" /> <rect fill="hsl(var(--clr-relation))" width="7.3312178" height="2.7723093" x="8.6687822" y="6.3607378" /> diff --git a/src/lib/schema/panel/Schema.tsx b/src/lib/schema/panel/Schema.tsx index 84cb4feef..320393687 100644 --- a/src/lib/schema/panel/Schema.tsx +++ b/src/lib/schema/panel/Schema.tsx @@ -3,9 +3,9 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import ReactFlow, { Edge, MiniMap, Node, ReactFlowInstance, ReactFlowProvider, useEdgesState, useNodesState } from 'reactflow'; import 'reactflow/dist/style.css'; -import { Icon, Panel } from '../../components'; +import { Icon, Panel, TooltipProvider } from '../../components'; import { Button } from '../../components/buttons'; -import { Popover, PopoverContent, PopoverTrigger } from '../../components/layout/Popover'; +import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover'; import { Tooltip, TooltipContent, TooltipTrigger } from '../../components/tooltip/Tooltip'; import { useSchema, useSchemaSettings, useSearchResultSchema } from '../../data-access'; import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; @@ -103,14 +103,14 @@ export const Schema = (props: Props) => { nodesWithRef = schemaFlow.nodes.map(node => { return { ...node, - data: { ...node.data, reactFlowRef, tooltipClose: false }, + data: { ...node.data, reactFlowRef, popoverClose: false }, }; }); edgesWithRef = schemaFlow.edges.map(edge => { return { ...edge, - data: { ...edge.data, reactFlowRef, tooltipClose: false }, + data: { ...edge.data, reactFlowRef, popoverClose: false }, }; }); @@ -156,14 +156,14 @@ export const Schema = (props: Props) => { nodesWithRef = schemaFlow.nodes.map(node => { return { ...node, - data: { ...node.data, reactFlowRef, tooltipClose: false }, + data: { ...node.data, reactFlowRef, popoverClose: false }, }; }); edgesWithRef = schemaFlow.edges.map(edge => { return { ...edge, - data: { ...edge.data, reactFlowRef, tooltipClose: false }, + data: { ...edge.data, reactFlowRef, popoverClose: false }, }; }); @@ -234,7 +234,7 @@ export const Schema = (props: Props) => { ...node, data: { ...node.data, - tooltipClose: clickedOutsideNode, + popoverClose: clickedOutsideNode, }, })), ); @@ -244,7 +244,7 @@ export const Schema = (props: Props) => { ...edge, data: { ...edge.data, - tooltipClose: clickedOutsideNode, + popoverClose: clickedOutsideNode, }, })), ); @@ -256,76 +256,78 @@ export const Schema = (props: Props) => { className="schema-panel" tooltips={ <> - <Tooltip> - <TooltipTrigger> - <Button - variantType="secondary" - variant="ghost" - size="xs" - iconComponent="icon-[ic--baseline-remove]" - onClick={() => { - if (props.onRemove) props.onRemove(); - }} - /> - </TooltipTrigger> - <TooltipContent> - <p>Hide</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger> - <Button - variantType="secondary" - variant="ghost" - size="xs" - iconComponent="icon-[ic--baseline-content-copy]" - onClick={() => { - // Copy the schema to the clipboard - navigator.clipboard.writeText(JSON.stringify(schema.graph, null, 2)); - }} - /> - </TooltipTrigger> - <TooltipContent> - <p>Copy Schema to Clipboard</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger> - <Button - variantType="secondary" - variant="ghost" - size="xs" - iconComponent="icon-[ic--baseline-fullscreen]" - onClick={() => { - fitView(); - }} - /> - </TooltipTrigger> - <TooltipContent> - <p>Fit to screen</p> - </TooltipContent> - </Tooltip> - <Popover> - <PopoverTrigger> - <Tooltip> - <TooltipTrigger> - <Button - variantType="secondary" - variant="ghost" - size="xs" - iconComponent="icon-[ic--baseline-settings]" - className="schema-settings" - /> - </TooltipTrigger> - <TooltipContent> - <p>Schema Settings</p> - </TooltipContent> - </Tooltip> - </PopoverTrigger> - <PopoverContent> - <SchemaSettings /> - </PopoverContent> - </Popover> + <TooltipProvider> + <Tooltip> + <TooltipTrigger> + <Button + variantType="secondary" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-remove]" + onClick={() => { + if (props.onRemove) props.onRemove(); + }} + /> + </TooltipTrigger> + <TooltipContent> + <p>Hide</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger> + <Button + variantType="secondary" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-content-copy]" + onClick={() => { + // Copy the schema to the clipboard + navigator.clipboard.writeText(JSON.stringify(schema.graph, null, 2)); + }} + /> + </TooltipTrigger> + <TooltipContent> + <p>Copy Schema to Clipboard</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger> + <Button + variantType="secondary" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-fullscreen]" + onClick={() => { + fitView(); + }} + /> + </TooltipTrigger> + <TooltipContent> + <p>Fit to screen</p> + </TooltipContent> + </Tooltip> + <Popover> + <PopoverTrigger> + <Tooltip> + <TooltipTrigger> + <Button + variantType="secondary" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-settings]" + className="schema-settings" + /> + </TooltipTrigger> + <TooltipContent> + <p>Schema Settings</p> + </TooltipContent> + </Tooltip> + </PopoverTrigger> + <PopoverContent> + <SchemaSettings /> + </PopoverContent> + </Popover> + </TooltipProvider> </> } > diff --git a/src/lib/schema/panel/SchemaList.tsx b/src/lib/schema/panel/SchemaList.tsx index 7db15c954..8b3762e35 100644 --- a/src/lib/schema/panel/SchemaList.tsx +++ b/src/lib/schema/panel/SchemaList.tsx @@ -14,9 +14,10 @@ import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '. import { ConnectionLine, ConnectionDragLine } from '../../querybuilder'; import { schemaExpandRelation, schemaGraphology2Reactflow } from '../schema-utils'; import { Panel } from '../../components'; -import { Tooltip, TooltipContent, TooltipTrigger } from '../../components/tooltip/Tooltip'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/lib/components/tooltip'; +import { resultSetFocus } from '../../data-access/store/interactionSlice'; import { useDispatch } from 'react-redux'; -import { Popover, PopoverContent, PopoverTrigger } from '../../components/layout/Popover'; +import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover'; interface Props { content?: string; @@ -102,14 +103,14 @@ export const Schema = (props: Props) => { nodesWithRef = schemaFlow.nodes.map(node => { return { ...node, - data: { ...node.data, reactFlowRef, tooltipClose: false }, + data: { ...node.data, reactFlowRef, popoverClose: false }, }; }); edgesWithRef = schemaFlow.edges.map(edge => { return { ...edge, - data: { ...edge.data, reactFlowRef, tooltipClose: false }, + data: { ...edge.data, reactFlowRef, popoverClose: false }, }; }); @@ -168,7 +169,7 @@ export const Schema = (props: Props) => { ...node, data: { ...node.data, - tooltipClose: clickedOutsideNode, + popoverClose: clickedOutsideNode, }, })), ); @@ -178,7 +179,7 @@ export const Schema = (props: Props) => { ...edge, data: { ...edge.data, - tooltipClose: clickedOutsideNode, + popoverClose: clickedOutsideNode, }, })), ); @@ -277,6 +278,7 @@ export const Schema = (props: Props) => { connectionLineComponent={ConnectionDragLine} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} + onMouseDownCapture={() => dispatch(resultSetFocus({ focusType: 'schema' }))} nodes={nodes} edges={edges} onInit={reactFlowInstance => { diff --git a/src/lib/schema/panel/SchemaSettings.tsx b/src/lib/schema/panel/SchemaSettings.tsx index 1d946b941..36c22e596 100644 --- a/src/lib/schema/panel/SchemaSettings.tsx +++ b/src/lib/schema/panel/SchemaSettings.tsx @@ -14,9 +14,10 @@ export const SchemaSettings = () => { Object.values(SchemaLayoutConfig).find(layout => layout.id === settings.layoutName) || Object.values(SchemaLayoutConfig)[0], ); return ( - <div className="flex flex-col w-full gap-2 px-4 py-2"> + <div className="flex flex-col w-full gap-2 p-2"> <span className="text-xs font-bold">Schema Settings</span> <Input + size="sm" type="boolean" value={settings.animatedEdges} label="Animated Edges" @@ -25,6 +26,7 @@ export const SchemaSettings = () => { }} /> <Input + size="sm" type="boolean" value={settings.showMinimap} label="Show Minimap" @@ -51,7 +53,7 @@ export const SchemaSettings = () => { <span className="text-secondary-700 m-2">Layout types</span> </div> <div className="grid grid-cols-2 gap-4 p-2"> - <TooltipProvider delayDuration={0}> + <TooltipProvider delay={0}> {Object.values(SchemaLayoutConfig).map(thisSchemaLayoutConfig => ( <Tooltip key={thisSchemaLayoutConfig.id}> <TooltipTrigger asChild> diff --git a/src/lib/schema/pills/nodes/SchemaPopUp/SchemaPopUp.tsx b/src/lib/schema/pills/nodes/SchemaPopUp/SchemaPopUp.tsx index 83efed72b..008bf1030 100644 --- a/src/lib/schema/pills/nodes/SchemaPopUp/SchemaPopUp.tsx +++ b/src/lib/schema/pills/nodes/SchemaPopUp/SchemaPopUp.tsx @@ -16,29 +16,27 @@ export type SchemaPopUpProps = { export const SchemaPopUp: React.FC<SchemaPopUpProps> = ({ data, numberOfElements, connections }) => { return ( <> - <div className=""> + <div className="divide-y divide-y-secondary-200"> {numberOfElements != null && numberOfElements != 0 && ( - <div className="border-b border-sec-200"> - <div className="flex flex-row gap-1 items-center justify-between px-3 py-1"> - <Icon component="icon-[ic--baseline-numbers]" size={24} /> - <span className="ml-auto text-right">{formatNumber(numberOfElements)}</span> - </div> + <div className="flex flex-row gap-1 items-center justify-between px-3 py-1"> + <Icon component="icon-[ic--baseline-numbers]" size={24} /> + <span className="ml-auto text-right">{formatNumber(numberOfElements)}</span> </div> )} {connections && ( - <div className="border-b border-sec-200 px-3 py-1"> - <div className="flex flex-row gap-3 items-center justify-between"> - <span className="font-semibold">From</span> - <span className="ml-auto text-right truncate w-[90%]">{connections.from}</span> + <div className="px-1.5 py-1"> + <div className="flex flex-row gap-1 items-center justify-between"> + <span className="font-medium shrink-0">From</span> + <span className="ml-auto text-right truncate grow">{connections.from}</span> </div> <div className="flex flex-row gap-1 items-center justify-between"> - <span className="font-semibold">To</span> - <span className="ml-auto text-right truncate w-[90%]">{connections.to}</span> + <span className="font-medium shrink-0">To</span> + <span className="ml-auto text-right truncate grow">{connections.to}</span> </div> </div> )} - <TooltipProvider delayDuration={300}> - <div className="px-3 py-1"> + <TooltipProvider delay={300}> + <div className="px-1.5 py-1"> {Object.keys(data).length === 0 ? ( <div className="flex justify-center items-center h-full "> <span>No attributes</span> @@ -47,19 +45,14 @@ export const SchemaPopUp: React.FC<SchemaPopUpProps> = ({ data, numberOfElements Object.entries(data).map(([k, v]) => ( <Tooltip key={k}> <div className="flex flex-row gap-1 items-center min-h-6"> - <span className="font-semibold truncate w-[90%]">{k}</span> + <span className="font-medium truncate">{k}</span> <TooltipTrigger asChild> - <span className="ml-auto text-right truncate grow-1 flex items-center"> - <Icon - className="ml-auto text-right flex-shrink-0" - component={<Icon component={getDataTypeIcon(v)} size={24} color={'hsl(var(--clr-sec--500))'} />} - color="hsl(var(--clr-sec--400))" - size={24} - /> + <span className="ml-auto text-right truncate shrink-0 flex items-center text-secondary-400 hover:text-secondary-600"> + <Icon component={<Icon component={getDataTypeIcon(v)} size={24} />} size={24} /> </span> </TooltipTrigger> <TooltipContent side="right"> - <div className="max-w-[18rem] break-all line-clamp-6 mx-1"> + <div className="max-w-[18rem] break-all line-clamp-6"> {v !== undefined && (typeof v !== 'object' || Array.isArray(v)) && v != '' ? v : 'noData'} </div> </TooltipContent> diff --git a/src/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx b/src/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx index 41b288fd2..d57983004 100644 --- a/src/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx +++ b/src/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx @@ -1,6 +1,6 @@ import { EntityPill } from '@/lib/components'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/lib/components/tooltip'; -import { VisualizationTooltip } from '@/lib/components/VisualizationTooltip'; +import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover'; +import { NodeDetails } from '@/lib/components/nodeDetails'; import { useSchemaStats } from '@/lib/data-access'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Handle, NodeProps, Position, useViewport } from 'reactflow'; @@ -29,18 +29,18 @@ export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<Sc }; useEffect(() => { - if (data.tooltipClose === true) { + if (data.popoverClose) { setOpenPopupLocation(null); } - }, [data.tooltipClose]); + }, [data.popoverClose]); - const tooltipX = useMemo(() => { + const popoverX = useMemo(() => { if (ref.current == null || openPopupLocation == null) return -1; const rect = ref.current.getBoundingClientRect(); return rect.x - openPopupLocation.x + rect.width / 2; }, [viewport.x, openPopupLocation]); - const tooltipY = useMemo(() => { + const popoverY = useMemo(() => { if (ref.current == null || openPopupLocation == null) return -1; const rect = ref.current.getBoundingClientRect(); return rect.y - openPopupLocation.y + rect.height / 2; @@ -66,27 +66,25 @@ export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<Sc ref={ref} > {openPopupLocation !== null && ( - <Tooltip key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}> - <TooltipTrigger x={tooltipX} y={tooltipY} /> - <TooltipContent> - <div> - <VisualizationTooltip name={data.name} colorHeader={'hsl(var(--clr-node))'}> - <SchemaPopUp - data={data.attributes.reduce( - (acc, attr) => { - if (attr.name && attr.type) { - acc[attr.name] = attr.type; - } - return acc; - }, - {} as Record<string, string>, - )} - numberOfElements={schemaStats.nodes.stats[data.name]?.count} - /> - </VisualizationTooltip> - </div> - </TooltipContent> - </Tooltip> + <Popover key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}> + <PopoverTrigger x={popoverX} y={popoverY} /> + <PopoverContent> + <NodeDetails name={data.name} colorHeader={'hsl(var(--clr-node))'}> + <SchemaPopUp + data={data.attributes.reduce( + (acc, attr) => { + if (attr.name && attr.type) { + acc[attr.name] = attr.type; + } + return acc; + }, + {} as Record<string, string>, + )} + numberOfElements={schemaStats.nodes.stats[data.name]?.count} + /> + </NodeDetails> + </PopoverContent> + </Popover> )} <EntityPill diff --git a/src/lib/schema/pills/nodes/entity/SchemaListEntityPill.tsx b/src/lib/schema/pills/nodes/entity/SchemaListEntityPill.tsx index 210a45480..f036c7026 100644 --- a/src/lib/schema/pills/nodes/entity/SchemaListEntityPill.tsx +++ b/src/lib/schema/pills/nodes/entity/SchemaListEntityPill.tsx @@ -1,6 +1,6 @@ import { EntityPill } from '@/lib/components'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/lib/components/tooltip'; -import { VisualizationTooltip } from '@/lib/components/VisualizationTooltip'; +import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover'; +import { NodeDetails } from '@/lib/components/nodeDetails'; import { useSchemaStats } from '@/lib/data-access'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Handle, NodeProps, Position, useViewport } from 'reactflow'; @@ -30,18 +30,18 @@ export const SchemaListEntityPill = React.memo(({ id, selected, data }: NodeProp }; useEffect(() => { - if (data.tooltipClose === true) { + if (data.popoverClose) { setOpenPopupLocation(null); } - }, [data.tooltipClose]); + }, [data.popoverClose]); - const tooltipX = useMemo(() => { + const popoverX = useMemo(() => { if (ref.current == null || openPopupLocation == null) return -1; const rect = ref.current.getBoundingClientRect(); return rect.x - openPopupLocation.x + rect.width / 2; }, [viewport.x, openPopupLocation]); - const tooltipY = useMemo(() => { + const popoverY = useMemo(() => { if (ref.current == null || openPopupLocation == null) return -1; const rect = ref.current.getBoundingClientRect(); return rect.y - openPopupLocation.y + rect.height / 2; @@ -67,27 +67,25 @@ export const SchemaListEntityPill = React.memo(({ id, selected, data }: NodeProp ref={ref} > {openPopupLocation !== null && ( - <Tooltip key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}> - <TooltipTrigger x={tooltipX} y={tooltipY} /> - <TooltipContent> - <div> - <VisualizationTooltip name={data.name} colorHeader={'hsl(var(--clr-node))'}> - <SchemaPopUp - data={data.attributes.reduce( - (acc, attr) => { - if (attr.name && attr.type) { - acc[attr.name] = attr.type; - } - return acc; - }, - {} as Record<string, string>, - )} - numberOfElements={schemaStats?.nodes?.stats[data.name]?.count} - /> - </VisualizationTooltip> - </div> - </TooltipContent> - </Tooltip> + <Popover key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}> + <PopoverTrigger x={popoverX} y={popoverY} /> + <PopoverContent> + <NodeDetails name={data.name} colorHeader={'hsl(var(--clr-node))'}> + <SchemaPopUp + data={data.attributes.reduce( + (acc, attr) => { + if (attr.name && attr.type) { + acc[attr.name] = attr.type; + } + return acc; + }, + {} as Record<string, string>, + )} + numberOfElements={schemaStats?.nodes?.stats[data.name]?.count} + /> + </NodeDetails> + </PopoverContent> + </Popover> )} <EntityPill diff --git a/src/lib/schema/pills/nodes/popup/node-quality-relation-popup.stories.tsx b/src/lib/schema/pills/nodes/popup/node-quality-relation-popup.stories.tsx index 351ef7143..95739f5e3 100644 --- a/src/lib/schema/pills/nodes/popup/node-quality-relation-popup.stories.tsx +++ b/src/lib/schema/pills/nodes/popup/node-quality-relation-popup.stories.tsx @@ -3,7 +3,6 @@ import { Meta } from '@storybook/react'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; -import { querybuilderSlice } from '@/lib/data-access/store'; import { ReactFlowProvider } from 'reactflow'; import { NodeQualityRelationPopupNode } from './node-quality-relation-popup'; @@ -28,7 +27,6 @@ export default Component; // A super-simple mock of a redux store const Mockstore = configureStore({ reducer: { - querybuilder: querybuilderSlice.reducer, // schema: schemaSlice.reducer, }, }); diff --git a/src/lib/schema/pills/nodes/popup/popupmenus/node-quality-entity-popup.stories.tsx b/src/lib/schema/pills/nodes/popup/popupmenus/node-quality-entity-popup.stories.tsx index ba24de3bb..0f499192a 100644 --- a/src/lib/schema/pills/nodes/popup/popupmenus/node-quality-entity-popup.stories.tsx +++ b/src/lib/schema/pills/nodes/popup/popupmenus/node-quality-entity-popup.stories.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { Meta } from '@storybook/react'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; -import { querybuilderSlice } from '@/lib/data-access/store'; import { ReactFlowProvider } from 'reactflow'; import { NodeQualityEntityPopupNode } from './node-quality-entity-popup'; @@ -27,7 +26,6 @@ export default Component; // A super-simple mock of a redux store const Mockstore = configureStore({ reducer: { - querybuilder: querybuilderSlice.reducer, // schema: schemaSlice.reducer, }, }); diff --git a/src/lib/schema/pills/nodes/relation/SchemaListRelationPill.tsx b/src/lib/schema/pills/nodes/relation/SchemaListRelationPill.tsx index 57136c8f5..c64e40168 100644 --- a/src/lib/schema/pills/nodes/relation/SchemaListRelationPill.tsx +++ b/src/lib/schema/pills/nodes/relation/SchemaListRelationPill.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Handle, NodeProps, Position, useViewport } from 'reactflow'; import { SchemaReactflowRelationWithFunctions } from '../../../model/reactflow'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/lib/components/tooltip'; -import { VisualizationTooltip } from '@/lib/components/VisualizationTooltip'; +import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover'; +import { NodeDetails } from '@/lib/components/nodeDetails'; import { useSchemaStats } from '@/lib/data-access'; import { SchemaPopUp } from '../SchemaPopUp/SchemaPopUp'; import { SchemaEdge, QueryElementTypes } from 'ts-common'; @@ -37,18 +37,18 @@ export const SchemaListRelationPill = React.memo(({ id, selected, data, ...props }; useEffect(() => { - if (data.tooltipClose === true) { + if (data.popoverClose === true) { setOpenPopupLocation(null); } - }, [data.tooltipClose]); + }, [data.popoverClose]); - const tooltipX = useMemo(() => { + const popoverX = useMemo(() => { if (ref.current == null || openPopupLocation == null) return -1; const rect = ref.current.getBoundingClientRect(); return rect.x - openPopupLocation.x + rect.width / 2; }, [viewport.x, openPopupLocation]); - const tooltipY = useMemo(() => { + const popoverY = useMemo(() => { if (ref.current == null || openPopupLocation == null) return -1; const rect = ref.current.getBoundingClientRect(); return rect.y - openPopupLocation.y + rect.height / 2; @@ -74,32 +74,30 @@ export const SchemaListRelationPill = React.memo(({ id, selected, data, ...props ref={ref} > {openPopupLocation !== null && ( - <Tooltip key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}> - <TooltipTrigger x={tooltipX} y={tooltipY} /> - <TooltipContent> - <div> - <VisualizationTooltip name={data.collection} colorHeader={'hsl(var(--clr-relation))'}> - <SchemaPopUp - data={ - data.attributes.length > 0 - ? data.attributes.reduce( - (acc, attr) => { - if (attr.name && attr.type) { - acc[attr.name] = attr.type; - } - return acc; - }, - {} as Record<string, string>, - ) - : {} - } - connections={{ from: data.from, to: data.to }} - numberOfElements={schemaStats?.edges?.stats[data.collection]?.count} - /> - </VisualizationTooltip> - </div> - </TooltipContent> - </Tooltip> + <Popover key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}> + <PopoverTrigger x={popoverX} y={popoverY} /> + <PopoverContent> + <NodeDetails name={data.collection} colorHeader={'hsl(var(--clr-relation))'}> + <SchemaPopUp + data={ + data.attributes.length > 0 + ? data.attributes.reduce( + (acc, attr) => { + if (attr.name && attr.type) { + acc[attr.name] = attr.type; + } + return acc; + }, + {} as Record<string, string>, + ) + : {} + } + connections={{ from: data.from, to: data.to }} + numberOfElements={schemaStats?.edges?.stats[data.collection]?.count} + /> + </NodeDetails> + </PopoverContent> + </Popover> )} <RelationPill draggable diff --git a/src/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx b/src/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx index bc5b333ec..316c5d5d6 100644 --- a/src/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx +++ b/src/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx @@ -3,8 +3,8 @@ import { Handle, Position, NodeProps, useViewport } from 'reactflow'; import { SchemaReactflowRelationWithFunctions } from '../../../model/reactflow'; import { SchemaEdge, QueryElementTypes } from 'ts-common'; import { RelationPill } from '@/lib/components'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/lib/components/tooltip'; -import { VisualizationTooltip } from '@/lib/components/VisualizationTooltip'; +import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover'; +import { NodeDetails } from '@/lib/components/nodeDetails'; import { SchemaPopUp } from '../SchemaPopUp/SchemaPopUp'; import { useSchemaStats } from '@/lib/data-access'; @@ -36,18 +36,18 @@ export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: }; useEffect(() => { - if (data.tooltipClose === true) { + if (data.popoverClose === true) { setOpenPopupLocation(null); } - }, [data.tooltipClose]); + }, [data.popoverClose]); - const tooltipX = useMemo(() => { + const popoverX = useMemo(() => { if (ref.current == null || openPopupLocation == null) return -1; const rect = ref.current.getBoundingClientRect(); return rect.x - openPopupLocation.x + rect.width / 2; }, [viewport.x, openPopupLocation]); - const tooltipY = useMemo(() => { + const popoverY = useMemo(() => { if (ref.current == null || openPopupLocation == null) return -1; const rect = ref.current.getBoundingClientRect(); return rect.y - openPopupLocation.y + rect.height / 2; @@ -73,32 +73,30 @@ export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: ref={ref} > {openPopupLocation !== null && ( - <Tooltip key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}> - <TooltipTrigger x={tooltipX} y={tooltipY} /> - <TooltipContent> - <div> - <VisualizationTooltip name={data.collection} colorHeader={'hsl(var(--clr-relation))'}> - <SchemaPopUp - data={ - data.attributes.length > 0 - ? data.attributes.reduce( - (acc, attr) => { - if (attr.name && attr.type) { - acc[attr.name] = attr.type; - } - return acc; - }, - {} as Record<string, string>, - ) - : {} - } - connections={{ from: data.from, to: data.to }} - numberOfElements={schemaStats.edges.stats[data.collection]?.count} - /> - </VisualizationTooltip> - </div> - </TooltipContent> - </Tooltip> + <Popover key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}> + <PopoverTrigger x={popoverX} y={popoverY} /> + <PopoverContent> + <NodeDetails name={data.collection} colorHeader={'hsl(var(--clr-relation))'}> + <SchemaPopUp + data={ + data.attributes.length > 0 + ? data.attributes.reduce( + (acc, attr) => { + if (attr.name && attr.type) { + acc[attr.name] = attr.type; + } + return acc; + }, + {} as Record<string, string>, + ) + : {} + } + connections={{ from: data.from, to: data.to }} + numberOfElements={schemaStats.edges.stats[data.collection]?.count} + /> + </NodeDetails> + </PopoverContent> + </Popover> )} <RelationPill draggable diff --git a/src/lib/sidebar/index.tsx b/src/lib/sidebar/index.tsx index cebf79ffe..504212e65 100644 --- a/src/lib/sidebar/index.tsx +++ b/src/lib/sidebar/index.tsx @@ -32,7 +32,7 @@ export function Sidebar({ }) { return ( <div className="side-bar w-fit h-full flex shrink"> - <TooltipProvider delayDuration={100}> + <TooltipProvider> <div className="w-11 flex flex-col items-center"> {tabs.map(t => ( <Tooltip key={t.name} placement={'right'}> diff --git a/src/lib/sidebar/search/SearchBar.tsx b/src/lib/sidebar/search/SearchBar.tsx index ff9dc5583..191d3347e 100644 --- a/src/lib/sidebar/search/SearchBar.tsx +++ b/src/lib/sidebar/search/SearchBar.tsx @@ -112,7 +112,7 @@ export function SearchBar(props: { onRemove?: () => void }) { <Panel title="Search" tooltips={ - <TooltipProvider delayDuration={10}> + <TooltipProvider delay={10}> <Tooltip> <TooltipTrigger asChild> <Button @@ -179,7 +179,7 @@ export function SearchBar(props: { onRemove?: () => void }) { <p className="font-bold text-sm">{results[category].nodes.length + results[category].edges.length} results</p> </div> <div className="h-[1px] w-full bg-secondary-200"></div> - <TooltipProvider delayDuration={300}> + <TooltipProvider delay={300}> {Object.values(Object.values(results[category])) .flat() .map((item, idx) => ( diff --git a/src/lib/vis/components/VisualizationPanel.tsx b/src/lib/vis/components/VisualizationPanel.tsx index 1d56ab818..eaf4a7a17 100644 --- a/src/lib/vis/components/VisualizationPanel.tsx +++ b/src/lib/vis/components/VisualizationPanel.tsx @@ -18,6 +18,7 @@ import { ErrorBoundary } from '../../components/errorBoundary'; import { addError } from '../../data-access/store/configSlice'; import { canViewFeature } from '../../components/featureFlags'; import { NodeQueryResult, EdgeQueryResult } from 'ts-common'; +import { PopoverProvider } from '@/lib'; type PromiseFunc = () => Promise<{ default: VISComponentType<any> }>; export const Visualizations: Record<string, PromiseFunc> = { @@ -132,7 +133,9 @@ export const VisualizationPanel = ({ fullSize }: { fullSize: () => void }) => { </div> )} </div> - <VisualizationTabBar fullSize={fullSize} exportImage={exportImage} handleSelect={handleSelect} /> + <PopoverProvider> + <VisualizationTabBar fullSize={fullSize} exportImage={exportImage} handleSelect={handleSelect} /> + </PopoverProvider> </div> ); }; diff --git a/src/lib/vis/components/VisualizationTabBar.tsx b/src/lib/vis/components/VisualizationTabBar.tsx index be0648e58..c8ecfa51d 100644 --- a/src/lib/vis/components/VisualizationTabBar.tsx +++ b/src/lib/vis/components/VisualizationTabBar.tsx @@ -1,8 +1,8 @@ -import { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Button, DropdownContainer, DropdownItem, DropdownItemContainer, DropdownTrigger } from '../../components'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip'; import { ControlContainer } from '../../components/controls'; -import { Tabs, Tab } from "@/lib/components"; +import { Tabs, Tab } from '@/lib/components'; import { useActiveSaveStateAuthorization, useAppDispatch, useVisualization } from '../../data-access'; import { addVisualization, removeVisualization, reorderVisState, setActiveVisualization } from '../../data-access/store/visualizationSlice'; import { VisualizationsConfig } from './config'; @@ -22,20 +22,16 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor const sortable = new Sortable(tabsRef.current, { animation: 150, - draggable: "[data-type=\"tab\"]", - ghostClass: "bg-secondary-300", - dragClass: "bg-secondary-100", - onEnd: (evt) => { - if ( - evt.oldIndex != null && - evt.newIndex != null && - evt.oldIndex !== evt.newIndex - ) { + draggable: '[data-type="tab"]', + ghostClass: 'bg-secondary-300', + dragClass: 'bg-secondary-100', + onEnd: evt => { + if (evt.oldIndex != null && evt.newIndex != null && evt.oldIndex !== evt.newIndex) { dispatch( reorderVisState({ id: evt.oldIndex, newPosition: evt.newIndex, - }) + }), ); } }, @@ -80,7 +76,7 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor <h1 className="text-xs font-semibold text-secondary-600 truncate">Visualization</h1> </div> <div className="flex items-center px-0.5 gap-1 border-l border-secondary-200"> - <TooltipProvider delayDuration={0}> + <TooltipProvider> <Tooltip> <TooltipTrigger> <DropdownContainer open={open} onOpenChange={setOpen}> @@ -113,13 +109,12 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor </TooltipProvider> </div> - {openVisualizationArray.length > 0 && ( - <Tabs ref={tabsRef} className="-my-px overflow-x-auto overflow-y-hidden no-scrollbar divide-x divide-secondary-200 border-x"> - {openVisualizationArray.map((vis, i) => { - const isActive = activeVisualizationIndex === i; - const config = VisualizationsConfig[vis.id]; - const IconComponent = config?.icons.sm; + <Tabs ref={tabsRef} className="-my-px overflow-x-auto overflow-y-hidden no-scrollbar divide-x divide-secondary-200 border-x"> + {openVisualizationArray.map((vis, i) => { + const isActive = activeVisualizationIndex === i; + const config = VisualizationsConfig[vis.id]; + const IconComponent = config?.icons.sm; return ( <Tab @@ -130,7 +125,6 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor className="group" onClick={() => onSelect(i)} > - {/*{vis.id},{config.id}*/} <Button variantType="secondary" variant="ghost" @@ -139,7 +133,7 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor size="2xs" iconComponent="icon-[ic--baseline-close]" className={!isActive ? 'opacity-50 group-hover:opacity-100 group-focus-within:opacity-100' : ''} - onClick={(e) => { + onClick={e => { e.stopPropagation(); onDelete(i); }} @@ -153,7 +147,7 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor {openVisualizationArray.length > 0 && ( <div className="shrink-0 sticky right-0 px-0.5 ml-auto flex"> <ControlContainer> - <TooltipProvider delayDuration={0}> + <TooltipProvider> <Tooltip> <TooltipTrigger asChild> <Button diff --git a/src/lib/vis/components/config/VisualizationSettings.tsx b/src/lib/vis/components/config/VisualizationSettings.tsx index 237050e87..020918868 100644 --- a/src/lib/vis/components/config/VisualizationSettings.tsx +++ b/src/lib/vis/components/config/VisualizationSettings.tsx @@ -51,7 +51,7 @@ export function VisualizationSettings() { return ( <div className="flex flex-col w-full overflow-auto"> - <div className="text-sm px-4 py-2 flex flex-col gap-1"> + <div className="text-sm p-2 flex flex-col gap-1"> <Input type="text" size="sm" diff --git a/src/lib/vis/visualizations/arcplotvis/arcplotvis.stories.tsx b/src/lib/vis/visualizations/arcplotvis/arcplotvis.stories.tsx index 861588757..50618ed07 100644 --- a/src/lib/vis/visualizations/arcplotvis/arcplotvis.stories.tsx +++ b/src/lib/vis/visualizations/arcplotvis/arcplotvis.stories.tsx @@ -1,7 +1,7 @@ import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; -import { graphQueryResultSlice, querybuilderSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; +import { graphQueryResultSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; import { mockData } from '../../../mock-data'; import ArcPlotComponent from './arcplotvis'; @@ -10,7 +10,6 @@ const Mockstore = configureStore({ schema: schemaSlice.reducer, graphQueryResult: graphQueryResultSlice.reducer, visualize: visualizationSlice.reducer, - querybuilder: querybuilderSlice.reducer, }, }); diff --git a/src/lib/vis/visualizations/mapvis/components/MapTooltip.tsx b/src/lib/vis/visualizations/mapvis/components/MapDataInspector.tsx similarity index 87% rename from src/lib/vis/visualizations/mapvis/components/MapTooltip.tsx rename to src/lib/vis/visualizations/mapvis/components/MapDataInspector.tsx index 280d94194..234665cfc 100644 --- a/src/lib/vis/visualizations/mapvis/components/MapTooltip.tsx +++ b/src/lib/vis/visualizations/mapvis/components/MapDataInspector.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { NodeType } from '../../nodelinkvis/types'; import { GeoJsonType } from '../mapvis.types'; import { SearchResultType } from '../mapvis.types'; -import { TooltipProvider } from '@/lib/components'; +import { PopoverProvider } from '@/lib/components'; export type NodelinkPopupProps = { type: 'node' | 'area' | 'location'; @@ -13,7 +13,7 @@ const isGeoJsonType = (data: NodeType | GeoJsonType | SearchResultType): data is return (data as GeoJsonType).properties !== undefined; }; -const MapTooltipNode = (node: NodeType) => ( +const MapNodeDetails = (node: NodeType) => ( <div> {Object.keys(node.attributes).length === 0 ? ( <div className="flex justify-center items-center h-full"> @@ -41,7 +41,7 @@ const MapTooltipNode = (node: NodeType) => ( )} </div> ); -const MapTooltipChoropleth = (geoJson: GeoJsonType) => ( +const MapChoroplethDetails = (geoJson: GeoJsonType) => ( <div> <div className="flex flex-row gap-3"> <span>Area id: </span> @@ -89,26 +89,25 @@ const renderLocationDetails = (location: SearchResultType) => ( </div> ); -export const MapTooltip = (props: NodelinkPopupProps) => { +export const MapDataInspector = (props: NodelinkPopupProps) => { const { type, data } = props; return ( - <TooltipProvider delayDuration={100}> + <PopoverProvider delay={100}> <div className="text-[0.9rem] min-w-[10rem]"> - <div className="card-body p-0"> - <div className="h-[1px] w-full bg-secondary-200"></div> + <div className="p-0"> <div className="px-2.5 text-[0.8rem]"> {type === 'node' ? data && 'attributes' in data - ? MapTooltipNode(data as NodeType) + ? MapNodeDetails(data as NodeType) : null : data && isGeoJsonType(data) - ? MapTooltipChoropleth(data as GeoJsonType) + ? MapChoroplethDetails(data as GeoJsonType) : renderLocationDetails(data as SearchResultType)} </div> <div className="h-[1px] w-full"></div> </div> </div> - </TooltipProvider> + </PopoverProvider> ); }; diff --git a/src/lib/vis/visualizations/mapvis/components/index.ts b/src/lib/vis/visualizations/mapvis/components/index.ts index 6d9b03270..ca9aded7f 100644 --- a/src/lib/vis/visualizations/mapvis/components/index.ts +++ b/src/lib/vis/visualizations/mapvis/components/index.ts @@ -1,4 +1,4 @@ export * from './ActionBar'; export * from './Attribution'; export * from './MapSettings'; -export * from './MapTooltip'; +export * from './MapDataInspector.tsx'; diff --git a/src/lib/vis/visualizations/mapvis/mapvis.stories.tsx b/src/lib/vis/visualizations/mapvis/mapvis.stories.tsx index 3a8cbab45..f8bf22a81 100644 --- a/src/lib/vis/visualizations/mapvis/mapvis.stories.tsx +++ b/src/lib/vis/visualizations/mapvis/mapvis.stories.tsx @@ -1,7 +1,7 @@ import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; -import { graphQueryResultSlice, querybuilderSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; +import { graphQueryResultSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; import MapComponent from './mapvis'; import { mockData } from '../../../mock-data/query-result/mockData'; @@ -10,7 +10,6 @@ const Mockstore = configureStore({ schema: schemaSlice.reducer, graphQueryResult: graphQueryResultSlice.reducer, visualize: visualizationSlice.reducer, - querybuilder: querybuilderSlice.reducer, }, }); diff --git a/src/lib/vis/visualizations/mapvis/mapvis.tsx b/src/lib/vis/visualizations/mapvis/mapvis.tsx index 86a8d9df0..78a0607f3 100644 --- a/src/lib/vis/visualizations/mapvis/mapvis.tsx +++ b/src/lib/vis/visualizations/mapvis/mapvis.tsx @@ -1,14 +1,14 @@ -import React, { useEffect, useCallback, useState, useRef, forwardRef, useImperativeHandle } from 'react'; +import React, { useEffect, useCallback, useState, useRef, forwardRef, RefObject, useImperativeHandle } from 'react'; import DeckGL from '@deck.gl/react'; import { CompositeLayer, FlyToInterpolator, MapViewState, WebMercatorViewport } from '@deck.gl/core'; import { CompositeLayerType, Coordinate, LayerSettingsType, LocationInfo, SearchResultType } from './mapvis.types'; import { VISComponentType, VisualizationPropTypes } from '../../common'; import { layerTypes, createBaseMap, LayerTypes } from './layers'; import { geoCentroid } from 'd3'; -import { Attribution, ActionBar, MapTooltip, MapSettings } from './components'; +import { Attribution, ActionBar, MapDataInspector, MapSettings } from './components'; import { useSelectionLayer, useCoordinateLookup } from './hooks'; -import { VisualizationTooltip } from '@/lib/components/VisualizationTooltip'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/lib/components/tooltip'; +import { NodeDetails } from '@/lib/components/nodeDetails'; +import { Popover, PopoverContent, PopoverProvider, PopoverTrigger } from '@/lib/components/popover'; import { isGeoJsonType, nodeColorHex, rgbToHex } from './utils'; import { NodeType } from '../nodelinkvis/types'; import { ChoroplethLayer } from './layers/choropleth-layer/ChoroplethLayer'; @@ -303,10 +303,10 @@ export const MapVis = forwardRef((props: VisualizationPropTypes<MapProps>, refEx /> {selected.length > 0 && selected.map((node, index) => ( - <Tooltip key={index} open={true} interactive={false} boundaryElement={ref} showArrow={true}> - <TooltipTrigger x={node.x} y={node.y} /> - <TooltipContent> - <VisualizationTooltip + <Popover key={index} open={true} interactive={false} boundaryElement={ref as RefObject<HTMLElement>} showArrow={true}> + <PopoverTrigger x={node.x} y={node.y} /> + <PopoverContent> + <NodeDetails name={ node.selectedType === 'node' ? (node as NodeType)?._id @@ -324,20 +324,20 @@ export const MapVis = forwardRef((props: VisualizationPropTypes<MapProps>, refEx : 'hsl(var(--clr-node))' } > - <MapTooltip type={node.selectedType} data={{ ...node }} key={node._id} /> - </VisualizationTooltip> - </TooltipContent> - </Tooltip> + <MapDataInspector type={node.selectedType} data={{ ...node }} key={node._id} /> + </NodeDetails> + </PopoverContent> + </Popover> ))} {searchResult && ( - <Tooltip open={true} interactive={false} boundaryElement={ref} showArrow={true}> - <TooltipTrigger x={searchResult.x} y={searchResult.y} /> - <TooltipContent> - <VisualizationTooltip name={searchResult.name} colorHeader="hsl(var(--clr-node))"> - <MapTooltip type="location" data={{ ...searchResult }} key={searchResult.name} /> - </VisualizationTooltip> - </TooltipContent> - </Tooltip> + <Popover open={true} boundaryElement={ref as RefObject<HTMLElement>} showArrow={true}> + <PopoverTrigger x={searchResult.x} y={searchResult.y} /> + <PopoverContent> + <NodeDetails name={searchResult.name} colorHeader="hsl(var(--clr-node))"> + <MapDataInspector type="location" data={{ ...searchResult }} key={searchResult.name} /> + </NodeDetails> + </PopoverContent> + </Popover> )} <Attribution /> </div> diff --git a/src/lib/vis/visualizations/matrixvis/matrix.stories.tsx b/src/lib/vis/visualizations/matrixvis/matrix.stories.tsx index 48ee5407f..4e5e67971 100644 --- a/src/lib/vis/visualizations/matrixvis/matrix.stories.tsx +++ b/src/lib/vis/visualizations/matrixvis/matrix.stories.tsx @@ -2,7 +2,7 @@ import { Meta } from '@storybook/react'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import { mockData } from '../../../mock-data'; -import { graphQueryResultSlice, querybuilderSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; +import { graphQueryResultSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; import MatrixVisComponent from './matrixvis'; import { configSlice } from '@/lib/data-access/store/configSlice'; @@ -30,7 +30,6 @@ const Mockstore: any = configureStore({ schema: schemaSlice.reducer, graphQueryResult: graphQueryResultSlice.reducer, visualize: visualizationSlice.reducer, - querybuilder: querybuilderSlice.reducer, config: configSlice.reducer, }, }); diff --git a/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx index 17b139c18..1660f7ca3 100644 --- a/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx +++ b/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx @@ -1,5 +1,5 @@ -import { Tooltip, TooltipContent, TooltipTrigger } from '@/lib/components/tooltip'; -import { VisualizationTooltip } from '@/lib/components/VisualizationTooltip'; +import { Popover, PopoverContent, PopoverTrigger, PopoverProvider } from '@/lib/components/popover'; +import { NodeDetails } from '@/lib/components/nodeDetails'; import { useConfig } from '@/lib/data-access/store'; import { Theme } from '@/lib/data-access/store/configSlice'; import { dataColors, visualizationColors } from '@/config'; @@ -17,7 +17,7 @@ import { Texture, type StrokeStyle, } from 'pixi.js'; -import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { forwardRef, RefObject, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { useML, useSearchResultData } from '../../../../data-access'; import { AllLayoutAlgorithms, GraphologyForceAtlas2Webworker, LayoutFactory, Layouts, LayoutTypes } from '../../../../graph-layout'; import { NodelinkVisProps } from '../nodelinkvis'; @@ -1064,7 +1064,7 @@ export const NLPixi = forwardRef((props: Props, refExternal) => { updateEdgeLabel(link); }); - // Move tooltips while layouter is running and moving nodes + // Move Popovers while layouter is running and moving nodes imperative.current?.onMoved(viewport.current); } }; @@ -1245,24 +1245,24 @@ export const NLPixi = forwardRef((props: Props, refExternal) => { return ( <> {popups.map(popup => ( - <Tooltip key={popup.node._id} open={true} interactive={!dragging} boundaryElement={ref} showArrow={true}> - <TooltipTrigger x={popup.pos.x} y={popup.pos.y} /> - <TooltipContent> - <VisualizationTooltip name={popup.node._id} colorHeader={nodeColorHex(props.graph.nodes[popup.node._id].type)}> + <Popover key={popup.node._id} open={true} interactive={!dragging} boundaryElement={ref as RefObject<HTMLElement>} showArrow={true}> + <PopoverTrigger x={popup.pos.x} y={popup.pos.y} /> + <PopoverContent> + <NodeDetails name={popup.node._id} colorHeader={nodeColorHex(props.graph.nodes[popup.node._id].type)}> <NLPopUp data={props.graph.nodes[popup.node._id].attributes} /> - </VisualizationTooltip> - </TooltipContent> - </Tooltip> + </NodeDetails> + </PopoverContent> + </Popover> ))} {quickPopup != null && ( - <Tooltip key={quickPopup.node._id} open={true} boundaryElement={ref} showArrow={true}> - <TooltipTrigger x={quickPopup.pos.x} y={quickPopup.pos.y} /> - <TooltipContent> - <VisualizationTooltip name={quickPopup.node._id} colorHeader={nodeColorHex(props.graph.nodes[quickPopup.node._id].type)}> + <Popover key={quickPopup.node._id} open={true} boundaryElement={ref as RefObject<HTMLElement>} showArrow={true}> + <PopoverTrigger x={quickPopup.pos.x} y={quickPopup.pos.y} /> + <PopoverContent> + <NodeDetails name={quickPopup.node._id} colorHeader={nodeColorHex(props.graph.nodes[quickPopup.node._id].type)}> <NLPopUp data={props.graph.nodes[quickPopup.node._id].attributes} /> - </VisualizationTooltip> - </TooltipContent> - </Tooltip> + </NodeDetails> + </PopoverContent> + </Popover> )} <div className="h-full w-full overflow-hidden" diff --git a/src/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx b/src/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx index e4368b7de..0d6cfa8a9 100644 --- a/src/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx +++ b/src/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx @@ -11,7 +11,7 @@ export type NLPopUpProps = { export const NLPopUp: React.FC<NLPopUpProps> = ({ data }) => { return ( - <TooltipProvider delayDuration={100}> + <TooltipProvider delay={100}> <div className={`px-2`}> {Object.keys(data).length === 0 ? ( <div className="flex justify-center items-center h-full"> diff --git a/src/lib/vis/visualizations/nodelinkvis/nodelinkvis.stories.tsx b/src/lib/vis/visualizations/nodelinkvis/nodelinkvis.stories.tsx index d6d072ae4..1249c3f21 100644 --- a/src/lib/vis/visualizations/nodelinkvis/nodelinkvis.stories.tsx +++ b/src/lib/vis/visualizations/nodelinkvis/nodelinkvis.stories.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Meta } from '@storybook/react'; -import { graphQueryResultSlice, querybuilderSlice, schemaSlice, visualizationSlice, searchResultSlice } from '../../../data-access/store'; +import { graphQueryResultSlice, schemaSlice, visualizationSlice, searchResultSlice } from '../../../data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import { mockData } from '../../../mock-data'; @@ -12,7 +12,6 @@ const Mockstore = configureStore({ schema: schemaSlice.reducer, graphQueryResult: graphQueryResultSlice.reducer, visualize: visualizationSlice.reducer, - querybuilder: querybuilderSlice.reducer, config: configSlice.reducer, searchResults: searchResultSlice.reducer, }, diff --git a/src/lib/vis/visualizations/paohvis/components/HyperRangeBlock.tsx b/src/lib/vis/visualizations/paohvis/components/HyperRangeBlock.tsx index f7f8bea3e..8725751a4 100644 --- a/src/lib/vis/visualizations/paohvis/components/HyperRangeBlock.tsx +++ b/src/lib/vis/visualizations/paohvis/components/HyperRangeBlock.tsx @@ -152,7 +152,7 @@ export const HyperEdgeRangesBlock: React.FC<HyperEdgeRangesBlockProps> = ({ className="hyperEdgeInformation text-columns " transform={'translate(' + rowLabelColumnWidth + ',' + widthColumns + ')'} > - <TooltipProvider delayDuration={300}> + <TooltipProvider delay={300}> {columnHeaderInformation[0] && columnHeaderInformation[0].data.map((rowLabel, indexRows) => ( <g diff --git a/src/lib/vis/visualizations/paohvis/components/RowLabels.tsx b/src/lib/vis/visualizations/paohvis/components/RowLabels.tsx index f241069d2..a47cea6bc 100644 --- a/src/lib/vis/visualizations/paohvis/components/RowLabels.tsx +++ b/src/lib/vis/visualizations/paohvis/components/RowLabels.tsx @@ -95,7 +95,7 @@ export const RowLabels = ({ <g key={'parent_' + indexRows}> <g key={'content_' + indexRows} transform={'translate(0,' + (yOffset + indexRows * rowHeight) + ')'}> <g className={'rowsLabel row-' + indexRows} onMouseEnter={onMouseEnterRowLabels} onMouseLeave={onMouseLeaveRowLabels}> - <TooltipProvider delayDuration={300}> + <TooltipProvider delay={300}> {dataRows.map((row, index) => ( <Tooltip key={'gRowTableTooltip-' + index}> <TooltipTrigger asChild> diff --git a/src/lib/vis/visualizations/paohvis/paohvis.stories.tsx b/src/lib/vis/visualizations/paohvis/paohvis.stories.tsx index ee498ea2c..9e54e748c 100644 --- a/src/lib/vis/visualizations/paohvis/paohvis.stories.tsx +++ b/src/lib/vis/visualizations/paohvis/paohvis.stories.tsx @@ -1,5 +1,5 @@ import { Meta } from '@storybook/react'; -import { graphQueryResultSlice, querybuilderSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; +import { graphQueryResultSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import { @@ -16,7 +16,6 @@ const Mockstore = configureStore({ schema: schemaSlice.reducer, graphQueryResult: graphQueryResultSlice.reducer, visualize: visualizationSlice.reducer, - querybuilder: querybuilderSlice.reducer, }, }); diff --git a/src/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx b/src/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx index ef6262009..690b221b1 100644 --- a/src/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx +++ b/src/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx @@ -112,8 +112,8 @@ export const preparePlotData = ( const mainColors = visualizationColors.GPCat.colors[14]; const sharedTickFont = { - family: 'monospace', - size: 12, + family: 'Inter', + size: 11, color: '#374151', // !TODO get GP value }; @@ -537,10 +537,11 @@ export const preparePlotData = ( }, hoverlabel: { bgcolor: 'rgba(255, 255, 255, 0.8)', + className: 'text-dark', bordercolor: 'rgba(0, 0, 0, 0.2)', font: { - family: 'monospace', - size: 14, + family: 'Inter', + size: 13, color: '#374151', }, }, @@ -695,7 +696,7 @@ export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({ onHover={handleHover} onUnhover={handleUnhover} /> - {/* + {/* {hoveredPoint && ( <div> -- GitLab