diff --git a/frontend/svalyn-studio-app/src/app/App.tsx b/frontend/svalyn-studio-app/src/app/App.tsx
index d29cdc6..95aa61f 100644
--- a/frontend/svalyn-studio-app/src/app/App.tsx
+++ b/frontend/svalyn-studio-app/src/app/App.tsx
@@ -20,6 +20,7 @@
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider } from '@mui/material/styles';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
+import { PaletteProvider } from '../palette/PaletteProvider';
import { ChangeProposalView } from '../views/changeproposal/ChangeProposalView';
import { DomainView } from '../views/domain/DomainView';
import { DomainsView } from '../views/domains/DomainsView';
@@ -49,37 +50,39 @@ export const App = () => {
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
diff --git a/frontend/svalyn-studio-app/src/navbars/Navbar.tsx b/frontend/svalyn-studio-app/src/navbars/Navbar.tsx
index 12377f1..77d270e 100644
--- a/frontend/svalyn-studio-app/src/navbars/Navbar.tsx
+++ b/frontend/svalyn-studio-app/src/navbars/Navbar.tsx
@@ -24,11 +24,13 @@ import LogoutIcon from '@mui/icons-material/Logout';
import MailOutlineIcon from '@mui/icons-material/MailOutline';
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
import PersonIcon from '@mui/icons-material/Person';
+import SearchIcon from '@mui/icons-material/Search';
import SettingsIcon from '@mui/icons-material/Settings';
import AppBar from '@mui/material/AppBar';
import Avatar from '@mui/material/Avatar';
import Badge from '@mui/material/Badge';
import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import ListItem from '@mui/material/ListItem';
@@ -37,10 +39,12 @@ import ListItemText from '@mui/material/ListItemText';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Toolbar from '@mui/material/Toolbar';
-import { useEffect, useState } from 'react';
+import { useContext, useEffect, useState } from 'react';
import { Navigate, Link as RouterLink } from 'react-router-dom';
import { getCookie } from '../cookies/getCookie';
import { Svalyn } from '../icons/Svalyn';
+import { PaletteContext } from '../palette/PaletteContext';
+import { PaletteContextValue } from '../palette/PaletteContext.types';
import { ErrorSnackbar } from '../snackbar/ErrorSnackbar';
import { GetViewerData, GetViewerVariables, NavbarProps, NavbarState } from './Navbar.types';
const { VITE_BACKEND_URL } = import.meta.env;
@@ -79,6 +83,10 @@ export const Navbar = ({ children }: NavbarProps) => {
const handleCloseSnackbar = () => setState((prevState) => ({ ...prevState, message: null }));
+ const { openPalette }: PaletteContextValue = useContext(PaletteContext);
+
+ const handleOnSearchClick: React.MouseEventHandler = () => openPalette();
+
const handleOpenUserMenu: React.MouseEventHandler = (event) => {
const { currentTarget } = event;
setState((prevState) => ({ ...prevState, anchorElement: currentTarget }));
@@ -104,6 +112,8 @@ export const Navbar = ({ children }: NavbarProps) => {
return ;
}
+ var isApple = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
+
return (
<>
@@ -123,6 +133,32 @@ export const Navbar = ({ children }: NavbarProps) => {
marginLeft: 'auto',
}}
>
+
diff --git a/frontend/svalyn-studio-app/src/palette/Palette.tsx b/frontend/svalyn-studio-app/src/palette/Palette.tsx
new file mode 100644
index 0000000..464a5d1
--- /dev/null
+++ b/frontend/svalyn-studio-app/src/palette/Palette.tsx
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2023 Stéphane Bégaudeau.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
+ * portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
+ * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import CorporateFareIcon from '@mui/icons-material/CorporateFare';
+import HomeIcon from '@mui/icons-material/Home';
+import HubIcon from '@mui/icons-material/Hub';
+import SearchIcon from '@mui/icons-material/Search';
+import Box from '@mui/material/Box';
+import Dialog from '@mui/material/Dialog';
+import Divider from '@mui/material/Divider';
+import FormControl from '@mui/material/FormControl';
+import Input from '@mui/material/Input';
+import InputAdornment from '@mui/material/InputAdornment';
+import List from '@mui/material/List';
+import ListItem from '@mui/material/ListItem';
+import ListItemButton from '@mui/material/ListItemButton';
+import ListItemIcon from '@mui/material/ListItemIcon';
+import ListItemText from '@mui/material/ListItemText';
+import { useRef, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { PaletteAction, PaletteProps, PaletteState } from './Palette.types';
+
+export const Palette = ({ open, onClose }: PaletteProps) => {
+ const inputRef = useRef(null);
+ const listRef = useRef(null);
+
+ const navigate = useNavigate();
+
+ const goToHome: PaletteAction = {
+ id: 'go-to-home',
+ icon: ,
+ label: 'Home',
+ handle: () => navigate(`/`),
+ };
+ const goToDomains: PaletteAction = {
+ id: 'go-to-domains',
+ icon: ,
+ label: 'Domains',
+ handle: () => navigate(`/domains`),
+ };
+ const goToNewOrganization: PaletteAction = {
+ id: 'go-to-new-organization',
+ icon: ,
+ label: 'New organization',
+ handle: () => navigate(`/new/organization`),
+ };
+
+ const defaultPaletteActions: PaletteAction[] = [goToHome, goToDomains, goToNewOrganization];
+
+ const [state, setState] = useState({
+ query: '',
+ actions: defaultPaletteActions,
+ selectedActionId: null,
+ });
+
+ const handleChange: React.ChangeEventHandler = (event) => {
+ const {
+ target: { value },
+ } = event;
+ setState((prevState) => ({ ...prevState, query: value }));
+ };
+
+ const handleKeyDown: React.KeyboardEventHandler = (event) => {
+ if (event.key === 'Enter') {
+ navigate(`/search?q=${encodeURIComponent(state.query)}`);
+ onClose();
+ } else if (event.key === 'ArrowDown' && listRef.current) {
+ const firstListItem = listRef.current.childNodes[0];
+ if (firstListItem instanceof HTMLElement) {
+ const firstListItemButton = firstListItem.childNodes[0];
+ if (firstListItemButton instanceof HTMLElement) {
+ firstListItemButton.focus();
+ }
+ }
+ }
+ };
+
+ const handleListItemKeyDown: React.KeyboardEventHandler = (event) => {
+ if (event.key === 'ArrowDown') {
+ const nextListItemButton = event.currentTarget.parentElement?.nextSibling?.childNodes[0];
+ if (nextListItemButton instanceof HTMLElement) {
+ nextListItemButton.focus();
+ }
+ } else if (event.key === 'ArrowUp') {
+ const previousListItemButton = event.currentTarget.parentElement?.previousSibling?.childNodes[0];
+ if (previousListItemButton instanceof HTMLElement) {
+ previousListItemButton.focus();
+ } else {
+ const input = inputRef.current?.querySelector('input');
+ if (input) {
+ input.focus();
+ }
+ }
+ }
+ };
+
+ const handleOnActionClick = (action: PaletteAction) => {
+ action.handle();
+ onClose();
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/svalyn-studio-app/src/palette/Palette.types.ts b/frontend/svalyn-studio-app/src/palette/Palette.types.ts
new file mode 100644
index 0000000..91f2b38
--- /dev/null
+++ b/frontend/svalyn-studio-app/src/palette/Palette.types.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 Stéphane Bégaudeau.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
+ * portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
+ * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+export interface PaletteProps {
+ open: boolean;
+ onClose: () => void;
+}
+
+export interface PaletteState {
+ query: string;
+ actions: PaletteAction[];
+ selectedActionId: string | null;
+}
+
+export interface PaletteAction {
+ id: string;
+ icon: JSX.Element;
+ label: string;
+ handle: () => void;
+}
diff --git a/frontend/svalyn-studio-app/src/palette/PaletteContext.tsx b/frontend/svalyn-studio-app/src/palette/PaletteContext.tsx
new file mode 100644
index 0000000..dc05e3e
--- /dev/null
+++ b/frontend/svalyn-studio-app/src/palette/PaletteContext.tsx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 Stéphane Bégaudeau.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
+ * portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
+ * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import React from 'react';
+import { PaletteContextValue } from './PaletteContext.types';
+
+export const PaletteContext = React.createContext({
+ openPalette: () => {},
+});
diff --git a/frontend/svalyn-studio-app/src/palette/PaletteContext.types.ts b/frontend/svalyn-studio-app/src/palette/PaletteContext.types.ts
new file mode 100644
index 0000000..67ff743
--- /dev/null
+++ b/frontend/svalyn-studio-app/src/palette/PaletteContext.types.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2023 Stéphane Bégaudeau.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
+ * portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
+ * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+export interface PaletteContextValue {
+ openPalette: () => void;
+}
diff --git a/frontend/svalyn-studio-app/src/palette/PaletteProvider.tsx b/frontend/svalyn-studio-app/src/palette/PaletteProvider.tsx
new file mode 100644
index 0000000..5cdfa37
--- /dev/null
+++ b/frontend/svalyn-studio-app/src/palette/PaletteProvider.tsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2023 Stéphane Bégaudeau.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
+ * portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
+ * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import { useEffect, useState } from 'react';
+import { Palette } from './Palette';
+import { PaletteContext } from './PaletteContext';
+import { PaletteContextValue } from './PaletteContext.types';
+import { PaletteProviderProps, PaletteProviderState } from './PaletteProvider.types';
+
+export const PaletteProvider = ({ children }: PaletteProviderProps) => {
+ const [state, setState] = useState({ open: false });
+
+ useEffect(() => {
+ const keyDownEventListener = (event: KeyboardEvent) => {
+ if (event.key === 'k' && (event.ctrlKey || event.metaKey)) {
+ setState((prevState) => ({ ...prevState, open: !prevState.open }));
+ } else if (event.key === 'Esc') {
+ setState((prevState) => ({ ...prevState, open: false }));
+ }
+ };
+
+ document.addEventListener('keydown', keyDownEventListener);
+ return () => document.removeEventListener('keydown', keyDownEventListener);
+ }, []);
+
+ const handleClose = () => setState((prevState) => ({ ...prevState, open: false }));
+
+ const paletteContextValue: PaletteContextValue = {
+ openPalette: () => setState((prevState) => ({ ...prevState, open: true })),
+ };
+
+ return (
+
+ {children}
+ {state.open ? : null}
+
+ );
+};
diff --git a/frontend/svalyn-studio-app/src/palette/PaletteProvider.types.ts b/frontend/svalyn-studio-app/src/palette/PaletteProvider.types.ts
new file mode 100644
index 0000000..ee86f76
--- /dev/null
+++ b/frontend/svalyn-studio-app/src/palette/PaletteProvider.types.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2023 Stéphane Bégaudeau.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
+ * portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
+ * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+export interface PaletteProviderProps {
+ children?: React.ReactNode;
+}
+
+export interface PaletteProviderState {
+ open: boolean;
+}