CTL-214: initial checkin

This commit is contained in:
Tim Chamberlain
2023-06-22 10:04:14 -05:00
parent dd3f126b69
commit 0c32f25d79
11 changed files with 2426 additions and 17723 deletions

19114
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.68",
"@kingsrook/qqq-frontend-core": "1.0.69",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",
@ -18,12 +18,13 @@
"@react-jvectormap/unitedstates": "1.0.1",
"@react-oauth/google": "0.2.8",
"@types/prop-types": "^15.7.5",
"@types/react": "17.0.38",
"@types/react-dom": "17.0.11",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
"@types/react-router-hash-link": "2.4.5",
"ace-builds": "1.12.3",
"chart.js": "3.4.1",
"chroma-js": "2.4.2",
"cmdk": "0.2.0",
"datejs": "1.0.0-rc3",
"downshift": "3.2.10",
"faker": "5.5.3",
@ -33,16 +34,17 @@
"html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6",
"rapidoc": "9.3.4",
"react": "17.0.2",
"react": "18.0.0",
"react-ace": "10.1.0",
"react-chartjs-2": "3.0.4",
"react-cookie": "4.1.1",
"react-dom": "17.0.2",
"react-dom": "18.0.0",
"react-github-btn": "1.2.1",
"react-google-drive-picker": "^1.2.0",
"react-router-dom": "6.2.1",
"react-router-hash-link": "2.4.3",
"react-table": "7.7.0",
"sass": "1.63.4",
"ts-md5": "1.2.11",
"yup": "0.32.11"
},
@ -50,8 +52,8 @@
"build": "react-scripts build",
"clean": "rm -rf node_modules package-lock.json lib",
"eject": "react-scripts eject",
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install",
"npm-install": "npm install",
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps",
"npm-install": "npm install --legacy-peer-deps",
"prepublishOnly": "tsc -p ./ --outDir lib/",
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start",
"test": "react-scripts test",
@ -77,6 +79,7 @@
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@svgr/webpack": "6.5.1",
"@types/chroma-js": "2.1.3",
"@types/faker": "5.5.9",

View File

@ -35,6 +35,7 @@ import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} f
import {useCookies} from "react-cookie";
import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
import {Md5} from "ts-md5/dist/md5";
import CommandMenu from "Command";
import QContext from "QContext";
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
import theme from "qqq/components/legacy/Theme";
@ -64,8 +65,8 @@ export default function App()
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
const [profileRoutes, setProfileRoutes] = useState({});
const [branding, setBranding] = useState({} as QBrandingMetaData);
const [metaData, setMetaData] = useState({} as QInstance);
const [needLicenseKey, setNeedLicenseKey] = useState(true);
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
useEffect(() =>
@ -263,14 +264,14 @@ export default function App()
name: `${app.label}`,
key: app.name,
route: path,
component: <RecordQuery table={table} />,
component: <RecordQuery table={table} key={table.name}/>,
});
routeList.push({
name: `${app.label}`,
key: app.name,
route: `${path}/savedFilter/:id`,
component: <RecordQuery table={table} />,
component: <RecordQuery table={table} key={table.name}/>,
});
routeList.push({
@ -329,14 +330,14 @@ export default function App()
name: process.label,
key: process.name,
route: `${path}/${process.name}`,
component: <RecordQuery table={table} launchProcess={process} />,
component: <RecordQuery table={table} key={`${table.name}-${process.name}`} launchProcess={process} />,
});
routeList.push({
name: process.label,
key: `${app.name}/${process.name}`,
route: `${path}/:id/${process.name}`,
component: <RecordView table={table} launchProcess={process} />,
component: <RecordView table={table} key={`${table.name}-${process.name}`} launchProcess={process} />,
});
});
@ -348,7 +349,7 @@ export default function App()
name: process.label,
key: process.name,
route: `${path}/${process.name}`,
component: <RecordQuery table={table} launchProcess={process} />,
component: <RecordQuery table={table} key={`${table.name}-${process.name}`} launchProcess={process} />,
});
routeList.push({
@ -396,6 +397,7 @@ export default function App()
try
{
const metaData = await Client.getInstance().loadMetaData();
setMetaData(metaData);
if (metaData.branding)
{
setBranding(metaData.branding);
@ -551,17 +553,21 @@ export default function App()
const [pageHeader, setPageHeader] = useState("" as string | JSX.Element);
const [accentColor, setAccentColor] = useState("#0062FF");
const [allowShortcuts, setAllowShortcuts] = useState(true);
return (
appRoutes && (
<QContext.Provider value={{
pageHeader: pageHeader,
accentColor: accentColor,
allowShortcuts: allowShortcuts,
setPageHeader: (header: string | JSX.Element) => setPageHeader(header),
setAccentColor: (accentColor: string) => setAccentColor(accentColor)
setAccentColor: (accentColor: string) => setAccentColor(accentColor),
setAllowShortcuts: (allowShortcuts: boolean) => setAllowShortcuts(allowShortcuts)
}}>
<ThemeProvider theme={theme}>
<CssBaseline />
<CommandMenu metaData={metaData}/>
<Sidenav
color={sidenavColor}
icon={branding.icon}

192
src/Command.tsx Normal file
View File

@ -0,0 +1,192 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QAppMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Icon from "@mui/material/Icon";
import {Command} from "cmdk";
import React, {useContext, useEffect, useReducer, useRef, useState} from "react";
import {useNavigate} from "react-router-dom";
import QContext from "QContext";
import HistoryUtils, {QHistoryEntry} from "qqq/utils/HistoryUtils";
interface Props
{
metaData?: QInstance;
}
const CommandMenu = ({metaData}: Props) =>
{
const [open, setOpen] = useState(false)
const navigate = useNavigate();
const {setAllowShortcuts, allowShortcuts, accentColor} = useContext(QContext);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
function evalueKeyPress(e: KeyboardEvent)
{
forceUpdate();
if (e.key === "." && allowShortcuts)
{
e.preventDefault();
setOpen((open) => !open)
}
}
////////////////////////////////////////
// Toggle the menu when ⌘K is pressed //
////////////////////////////////////////
useEffect(() =>
{
const down = (e: KeyboardEvent) =>
{
evalueKeyPress(e);
}
document.addEventListener("keydown", down)
return () =>
{
document.removeEventListener("keydown", down)
}
}, [allowShortcuts])
/////////////////////////////////////////////////////
// change allowing shortcuts based on open's value //
/////////////////////////////////////////////////////
useEffect(() =>
{
setAllowShortcuts(!open);
}, [open])
function goToItem(path: string)
{
navigate(path, {replace: true});
setOpen(false);
}
function TablesSection()
{
let tableNames : string[]= [];
metaData.tables.forEach((value: QTableMetaData, key: string) =>
{
tableNames.push(value.name);
})
tableNames = tableNames.sort((a: string, b:string) =>
{
return (metaData.tables.get(a).label.localeCompare(metaData.tables.get(b).label));
})
return(
<Command.Group heading="Tables">
{
tableNames.map((tableName: string, index: number) =>
!metaData.tables.get(tableName).isHidden && metaData.getTablePath(metaData.tables.get(tableName)) &&
(
<Command.Item onSelect={() => goToItem(`${metaData.getTablePath(metaData.tables.get(tableName))}`)} key={`${tableName}-${index}`}><Icon sx={{color: accentColor}}>{metaData.tables.get(tableName).iconName ? metaData.tables.get(tableName).iconName : "table_rows"}</Icon>{metaData.tables.get(tableName).label}</Command.Item>
)
)
}
</Command.Group>
);
}
function AppsSection()
{
let appNames: string[] = [];
metaData.apps.forEach((value: QAppMetaData, key: string) =>
{
appNames.push(value.name);
})
appNames = appNames.sort((a: string, b:string) =>
{
return (metaData.apps.get(a).label.localeCompare(metaData.apps.get(b).label));
})
return(
<Command.Group heading="Apps">
{
appNames.map((appName: string, index: number) =>
metaData.getAppPath(metaData.apps.get(appName)) &&
(
<Command.Item onSelect={() => goToItem(`${metaData.getAppPath(metaData.apps.get(appName))}`)} key={`${appName}-${index}`}><Icon sx={{color: accentColor}}>{metaData.apps.get(appName).iconName ? metaData.apps.get(appName).iconName : "apps"}</Icon>{metaData.apps.get(appName).label}</Command.Item>
)
)
}
</Command.Group>
);
}
function RecentlyViewedSection()
{
const history = HistoryUtils.get();
const options = [] as any;
history.entries.reverse().forEach((entry, index) =>
options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName})
)
let appNames: string[] = [];
metaData.apps.forEach((value: QAppMetaData, key: string) =>
{
appNames.push(value.name);
})
appNames = appNames.sort((a: string, b:string) =>
{
return (metaData.apps.get(a).label.localeCompare(metaData.apps.get(b).label));
})
return(
<Command.Group heading="Recently Viewed Records">
{
history.entries.reverse().map((entry: QHistoryEntry, index: number) =>
<Command.Item onSelect={() => goToItem(`${entry.path}`)} key={`${entry.label}-${index}`}><Icon sx={{color: accentColor}}>{entry.iconName}</Icon>{entry.label}</Command.Item>
)
}
</Command.Group>
);
}
const containerElement = useRef(null)
return (
<Box ref={containerElement} className="raycast" sx={{position: "relative", zIndex: 10_000}}>
<Command.Dialog open={open} onOpenChange={setOpen} container={containerElement.current} label="Test Global Command Menu">
<Box sx={{display: "flex"}}>
<Command.Input placeholder="Search for Tables, Actions, or Recently Viewed Items..."/>
<Button onClick={() => setOpen(false)}><Icon>close</Icon></Button>
</Box>
<Command.Loading />
<Command.Separator />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<TablesSection />
<Command.Separator />
<AppsSection />
<Command.Separator />
<RecentlyViewedSection />
</Command.List>
</Command.Dialog>
</Box>
)
}
export default CommandMenu;

View File

@ -19,6 +19,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QAppMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {createContext} from "react";
@ -28,11 +31,17 @@ interface QContext
setPageHeader?: (header: string | JSX.Element) => void;
accentColor: string;
setAccentColor?: (header: string) => void;
qInstance?: QInstance;
appMetaData?: QAppMetaData;
tableMetaData?: QTableMetaData;
allowShortcuts?: boolean;
setAllowShortcuts?: (allowShortcuts: boolean) => void;
}
const defaultState = {
pageHeader: "",
accentColor: "#0062FF"
accentColor: "#0062FF",
allowShortcuts: true
};
const QContext = createContext<QContext>(defaultState);

View File

@ -22,10 +22,12 @@
import {Auth0Provider} from "@auth0/auth0-react";
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
import React from "react";
import {render} from "react-dom";
import {createRoot} from "react-dom/client";
import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom";
import App from "App";
import "qqq/styles/qqq-override-styles.css";
import "qqq/styles/globals.scss";
import "qqq/styles/raycast.scss";
import HandleAuthorizationError from "HandleAuthorizationError";
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
import {MaterialUIControllerProvider} from "qqq/context";
@ -73,6 +75,9 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
}
}
const container = document.getElementById("root");
const root = createRoot(container);
if (authenticationMetaData.type === "AUTH_0")
{
// @ts-ignore
@ -86,9 +91,8 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
if(!domain || !clientId)
{
render(
<div>Error: AUTH0 authenticationMetaData is missing domain [{domain}] and/or clientId [{clientId}].</div>,
document.getElementById("root"),
root.render(
<div>Error: AUTH0 authenticationMetaData is missing domain [{domain}] and/or clientId [{clientId}].</div>
);
return;
}
@ -101,7 +105,7 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
domain = domain.replace(/\/$/, "");
}
render(
root.render(
<BrowserRouter>
<Auth0ProviderWithRedirectCallback
domain={domain}
@ -113,19 +117,18 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
<ProtectedRoute component={App} />
</MaterialUIControllerProvider>
</Auth0ProviderWithRedirectCallback>
</BrowserRouter>,
document.getElementById("root"),
</BrowserRouter>
);
}
else
{
render(
root.render(
<BrowserRouter>
<MaterialUIControllerProvider>
<App />
</MaterialUIControllerProvider>
</BrowserRouter>
, document.getElementById("root"));
);
}
})

View File

@ -23,8 +23,9 @@ import {InputAdornment, InputLabel} from "@mui/material";
import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch";
import {ErrorMessage, Field, useFormikContext} from "formik";
import React, {useState} from "react";
import React, {useContext, useState} from "react";
import AceEditor from "react-ace";
import QContext from "QContext";
import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch";
import MDInput from "qqq/components/legacy/MDInput";
import MDTypography from "qqq/components/legacy/MDTypography";
@ -52,6 +53,7 @@ function QDynamicFormField({
{
const [switchChecked, setSwitchChecked] = useState(false);
const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode);
const {setAllowShortcuts} = useContext(QContext);
const {setFieldValue} = useFormikContext();
@ -125,6 +127,14 @@ function QDynamicFormField({
field = (
<>
<Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="standard" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
onBlur={(e: any) =>
{
setAllowShortcuts(true);
}}
onFocus={(e: any) =>
{
setAllowShortcuts(false);
}}
onKeyPress={(e: any) =>
{
if (e.key === "Enter")

View File

@ -30,8 +30,9 @@ import ListItemIcon from "@mui/material/ListItemIcon";
import Menu from "@mui/material/Menu";
import TextField from "@mui/material/TextField";
import Toolbar from "@mui/material/Toolbar";
import React, {useEffect, useState} from "react";
import React, {useContext, useEffect, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom";
import QContext from "QContext";
import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs";
import {navbar, navbarContainer, navbarIconButton, navbarRow,} from "qqq/components/horseshoe/Styles";
import {setTransparentNavbar, useMaterialUIController,} from "qqq/context";
@ -62,6 +63,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
const [autocompleteValue, setAutocompleteValue] = useState<any>(null);
const route = useLocation().pathname.split("/").slice(1);
const navigate = useNavigate();
const {setAllowShortcuts} = useContext(QContext);
useEffect(() =>
{
@ -93,7 +95,6 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
buildHistoryEntries();
const history = HistoryUtils.get();
setHistory([ {label: "The Godfather", id: 1}, {label: "Pulp Fiction", id: 2}]);
const options = [] as any;
history.entries.reverse().forEach((entry, index) =>
options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName})
@ -119,6 +120,17 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
setHistory(options);
}
function handleHistoryOnOpen()
{
setAllowShortcuts(false);
buildHistoryEntries();
}
function handleHistoryOnClose()
{
setAllowShortcuts(true);
}
const handleOpenMenu = (event: any) => setOpenMenu(event.currentTarget);
const handleCloseMenu = () => setOpenMenu(false);
@ -152,7 +164,8 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
autoHighlight
blurOnSelect
style={{width: "200px"}}
onOpen={buildHistoryEntries}
onOpen={handleHistoryOnOpen}
onClose={handleHistoryOnClose}
onChange={handleAutocompleteOnChange}
PopperComponent={CustomPopper}
isOptionEqualToValue={(option, value) => option.id === value.id}

View File

@ -26,8 +26,9 @@ import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert, Box, Typography} from "@mui/material";
import {Alert, Typography} from "@mui/material";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import Dialog from "@mui/material/Dialog";
@ -79,7 +80,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const location = useLocation();
const navigate = useNavigate();
const {accentColor} = useContext(QContext);
const pathParts = location.pathname.replace(/\/+$/, "").split("/");
const tableName = table.name;
@ -102,7 +102,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const [errorMessage, setErrorMessage] = useState(null as string)
const [successMessage, setSuccessMessage] = useState(null as string);
const [warningMessage, setWarningMessage] = useState(null as string);
const {setPageHeader} = useContext(QContext);
const {accentColor, setPageHeader, allowShortcuts} = useContext(QContext);
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
const [reloadCounter, setReloadCounter] = useState(0);
@ -113,6 +113,8 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null);
const reload = () =>
{
setSuccessMessage(null);
@ -128,6 +130,47 @@ function RecordView({table, launchProcess}: Props): JSX.Element
setShowAudit(false);
};
// Toggle the menu when ⌘K is pressed
useEffect(() =>
{
const down = (e: { key: string; metaKey: any; ctrlKey: any; preventDefault: () => void; }) =>
{
if(allowShortcuts)
{
if (e.key === "e" && table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission)
{
e.preventDefault()
navigate("edit");
}
else if (e.key === "c" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
{
e.preventDefault()
gotoCreate();
}
else if (e.key === "d" && table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission)
{
e.preventDefault()
handleClickDeleteButton();
}
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [allowShortcuts])
const gotoCreate = () =>
{
const path = `${pathParts.slice(0, -1).join("/")}/create`;
navigate(path);
}
const gotoEdit = () =>
{
const path = `${pathParts.slice(0, -1).join("/")}/${record.values.get(table.primaryKeyField)}/edit`;
navigate(path);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// monitor location changes - if we've clicked a link from viewing one record to viewing another, //
// we'll stay in this component, but we'll need to reload all data for the new record. //
@ -506,12 +549,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
let hasEditOrDelete = (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission);
function gotoCreate()
{
const path = `${pathParts.slice(0, -1).join("/")}/create`;
navigate(path);
}
const renderActionsMenu = (
<Menu
anchorEl={actionsMenu}

142
src/qqq/styles/globals.scss Normal file
View File

@ -0,0 +1,142 @@
::selection {
background: hotpink;
color: white;
}
html,
body {
padding: 0;
margin: 0;
font-family: var(--font-sans);
}
body {
background: var(--app-bg);
overflow-x: hidden;
}
button {
background: none;
font-family: var(--font-sans);
padding: 0;
border: 0;
}
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
*,
*::after,
*::before {
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
:root {
--font-sans: 'Inter', --apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans,
Droid Sans, Helvetica Neue, sans-serif;
--app-bg: var(--gray1);
--cmdk-shadow: 0 16px 700px rgb(0 0 0 / 40%);
--lowContrast: #ffffff;
--highContrast: #000000;
--gray1: hsl(0, 0%, 99%);
--gray2: hsl(0, 0%, 97.3%);
--gray3: hsl(0, 0%, 95.1%);
--gray4: hsl(0, 0%, 93%);
--gray5: hsl(0, 0%, 90.9%);
--gray6: hsl(0, 0%, 88.7%);
--gray7: hsl(0, 0%, 85.8%);
--gray8: hsl(0, 0%, 78%);
--gray9: hsl(0, 0%, 56.1%);
--gray10: hsl(0, 0%, 52.3%);
--gray11: hsl(0, 0%, 43.5%);
--gray12: hsl(0, 0%, 9%);
--grayA1: hsla(0, 0%, 0%, 0.012);
--grayA2: hsla(0, 0%, 0%, 0.027);
--grayA3: hsla(0, 0%, 0%, 0.047);
--grayA4: hsla(0, 0%, 0%, 0.071);
--grayA5: hsla(0, 0%, 0%, 0.09);
--grayA6: hsla(0, 0%, 0%, 0.114);
--grayA7: hsla(0, 0%, 0%, 0.141);
--grayA8: hsla(0, 0%, 0%, 0.22);
--grayA9: hsla(0, 0%, 0%, 0.439);
--grayA10: hsla(0, 0%, 0%, 0.478);
--grayA11: hsla(0, 0%, 0%, 0.565);
--grayA12: hsla(0, 0%, 0%, 0.91);
--blue1: hsl(206, 100%, 99.2%);
--blue2: hsl(210, 100%, 98%);
--blue3: hsl(209, 100%, 96.5%);
--blue4: hsl(210, 98.8%, 94%);
--blue5: hsl(209, 95%, 90.1%);
--blue6: hsl(209, 81.2%, 84.5%);
--blue7: hsl(208, 77.5%, 76.9%);
--blue8: hsl(206, 81.9%, 65.3%);
--blue9: hsl(206, 100%, 50%);
--blue10: hsl(208, 100%, 47.3%);
--blue11: hsl(211, 100%, 43.2%);
--blue12: hsl(211, 100%, 15%);
}
.dark {
--app-bg: var(--gray1);
--lowContrast: #000000;
--highContrast: #ffffff;
--gray1: hsl(0, 0%, 8.5%);
--gray2: hsl(0, 0%, 11%);
--gray3: hsl(0, 0%, 13.6%);
--gray4: hsl(0, 0%, 15.8%);
--gray5: hsl(0, 0%, 17.9%);
--gray6: hsl(0, 0%, 20.5%);
--gray7: hsl(0, 0%, 24.3%);
--gray8: hsl(0, 0%, 31.2%);
--gray9: hsl(0, 0%, 43.9%);
--gray10: hsl(0, 0%, 49.4%);
--gray11: hsl(0, 0%, 62.8%);
--gray12: hsl(0, 0%, 93%);
--grayA1: hsla(0, 0%, 100%, 0);
--grayA2: hsla(0, 0%, 100%, 0.026);
--grayA3: hsla(0, 0%, 100%, 0.056);
--grayA4: hsla(0, 0%, 100%, 0.077);
--grayA5: hsla(0, 0%, 100%, 0.103);
--grayA6: hsla(0, 0%, 100%, 0.129);
--grayA7: hsla(0, 0%, 100%, 0.172);
--grayA8: hsla(0, 0%, 100%, 0.249);
--grayA9: hsla(0, 0%, 100%, 0.386);
--grayA10: hsla(0, 0%, 100%, 0.446);
--grayA11: hsla(0, 0%, 100%, 0.592);
--grayA12: hsla(0, 0%, 100%, 0.923);
--blue1: hsl(212, 35%, 9.2%);
--blue2: hsl(216, 50%, 11.8%);
--blue3: hsl(214, 59.4%, 15.3%);
--blue4: hsl(214, 65.8%, 17.9%);
--blue5: hsl(213, 71.2%, 20.2%);
--blue6: hsl(212, 77.4%, 23.1%);
--blue7: hsl(211, 85.1%, 27.4%);
--blue8: hsl(211, 89.7%, 34.1%);
--blue9: hsl(206, 100%, 50%);
--blue10: hsl(209, 100%, 60.6%);
--blue11: hsl(210, 100%, 66.1%);
--blue12: hsl(206, 98%, 95.8%);
}

544
src/qqq/styles/raycast.scss Normal file
View File

@ -0,0 +1,544 @@
/*!
* Copyright © 2022-2023. ColdTrack <contact@coldtrack.com>. All Rights Reserved.
*/
.raycast {
[cmdk-root] {
max-width: 1000px;
width: 650px;
background: var(--gray1);
border-radius: 12px;
padding: 8px 0;
font-family: var(--font-sans);
box-shadow: var(--cmdk-shadow);
border: 1px solid var(--gray6);
position: relative;
.dark & {
background: var(--gray2);
border: 0;
&:after {
content: '';
background: linear-gradient(
to right,
var(--gray6) 20%,
var(--gray6) 40%,
var(--gray10) 50%,
var(--gray10) 55%,
var(--gray6) 70%,
var(--gray6) 100%
);
z-index: -1;
position: absolute;
border-radius: 12px;
top: -1px;
left: -1px;
width: calc(100% + 2px);
height: calc(100% + 2px);
animation: shine 3s ease forwards 0.1s;
background-size: 200% auto;
}
&:before {
content: '';
z-index: -1;
position: absolute;
border-radius: 12px;
top: -1px;
left: -1px;
width: calc(100% + 2px);
height: calc(100% + 2px);
box-shadow: 0 0 0 1px transparent;
animation: border 1s linear forwards 0.5s;
}
}
kbd {
font-family: var(--font-sans);
background: var(--gray3);
color: var(--gray11);
height: 20px;
width: 20px;
border-radius: 4px;
padding: 0 4px;
display: flex;
align-items: center;
justify-content: center;
&:first-of-type {
margin-left: 8px;
}
}
}
[cmdk-input] {
font-family: var(--font-sans);
border: none;
width: 100%;
font-size: 15px;
padding: 8px 16px;
outline: none;
background: var(--bg);
color: var(--gray12);
&::placeholder {
color: var(--gray9);
}
}
[cmdk-raycast-top-shine] {
.dark & {
background: linear-gradient(
90deg,
rgba(56, 189, 248, 0),
var(--gray5) 20%,
var(--gray9) 67.19%,
rgba(236, 72, 153, 0)
);
height: 1px;
position: absolute;
top: -1px;
width: 100%;
z-index: -1;
opacity: 0;
animation: showTopShine 0.1s ease forwards 0.2s;
}
}
[cmdk-raycast-loader] {
--loader-color: var(--gray9);
border: 0;
width: 100%;
width: 100%;
left: 0;
height: 1px;
background: var(--gray6);
position: relative;
overflow: visible;
display: block;
margin-top: 12px;
margin-bottom: 12px;
&:after {
content: '';
width: 50%;
height: 1px;
position: absolute;
background: linear-gradient(90deg, transparent 0%, var(--loader-color) 50%, transparent 100%);
top: -1px;
opacity: 0;
animation-duration: 1.5s;
animation-delay: 1s;
animation-timing-function: ease;
animation-name: loading;
}
}
[cmdk-item] {
content-visibility: auto;
cursor: pointer;
height: 40px;
border-radius: 8px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
color: var(--gray12);
user-select: none;
will-change: background, color;
transition: all 150ms ease;
transition-property: none;
&[data-selected='true'] {
background: var(--gray4);
color: var(--gray12);
}
&[data-disabled='true'] {
color: var(--gray8);
cursor: not-allowed;
}
&:active {
transition-property: background;
background: var(--gray4);
}
&:first-child {
margin-top: 8px;
}
& + [cmdk-item] {
margin-top: 4px;
}
svg {
width: 18px;
height: 18px;
}
}
[cmdk-raycast-meta] {
margin-left: auto;
color: var(--gray11);
font-size: 13px;
}
[cmdk-list] {
padding: 0 8px;
height: 393px;
overflow: auto;
overscroll-behavior: contain;
scroll-padding-block-end: 40px;
transition: 100ms ease;
transition-property: height;
padding-bottom: 40px;
}
[cmdk-raycast-open-trigger],
[cmdk-raycast-subcommand-trigger] {
color: var(--gray11);
padding: 0px 4px 0px 8px;
border-radius: 6px;
font-weight: 500;
font-size: 12px;
height: 28px;
letter-spacing: -0.25px;
}
[cmdk-raycast-clipboard-icon],
[cmdk-raycast-hammer-icon] {
width: 20px;
height: 20px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
svg {
width: 14px;
height: 14px;
}
}
[cmdk-raycast-clipboard-icon] {
background: linear-gradient(to bottom, #f55354, #eb4646);
}
[cmdk-raycast-hammer-icon] {
background: linear-gradient(to bottom, #6cb9a3, #2c6459);
}
[cmdk-raycast-open-trigger] {
display: flex;
align-items: center;
color: var(--gray12);
}
[cmdk-raycast-subcommand-trigger] {
display: flex;
align-items: center;
gap: 4px;
right: 8px;
bottom: 8px;
svg {
width: 14px;
height: 14px;
}
hr {
height: 100%;
background: var(--gray6);
border: 0;
width: 1px;
}
&[aria-expanded='true'],
&:hover {
background: var(--gray4);
kbd {
background: var(--gray7);
}
}
}
[cmdk-separator] {
height: 1px;
width: 100%;
background: var(--gray5);
margin: 4px 0;
}
*:not([hidden]) + [cmdk-group] {
margin-top: 8px;
}
[cmdk-group-heading] {
user-select: none;
font-size: 12px;
color: var(--gray11);
padding: 0 8px;
display: flex;
align-items: center;
}
[cmdk-raycast-footer] {
display: flex;
height: 40px;
align-items: center;
width: 100%;
position: absolute;
background: var(--gray1);
bottom: 0;
padding: 8px;
border-top: 1px solid var(--gray6);
border-radius: 0 0 12px 12px;
svg {
width: 20px;
height: 20px;
filter: grayscale(1);
margin-right: auto;
}
hr {
height: 12px;
width: 1px;
border: 0;
background: var(--gray6);
margin: 0 4px 0px 12px;
}
@media (prefers-color-scheme: dark) {
background: var(--gray2);
}
}
[cmdk-dialog] {
z-index: var(--layer-portal);
position: fixed;
left: 50%;
top: 20%;
transform: translateX(-50%);
[cmdk] {
width: 640px;
transform-origin: center center;
animation: dialogIn var(--transition-fast) forwards;
}
&[data-state='closed'] [cmdk] {
animation: dialogOut var(--transition-fast) forwards;
}
}
[cmdk-empty] {
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
height: 64px;
white-space: pre-wrap;
color: var(--gray11);
}
}
@keyframes loading {
0% {
opacity: 0;
transform: translateX(0);
}
50% {
opacity: 1;
transform: translateX(100%);
}
100% {
opacity: 0;
transform: translateX(0);
}
}
@keyframes shine {
to {
background-position: 200% center;
opacity: 0;
}
}
@keyframes border {
to {
box-shadow: 0 0 0 1px var(--gray6);
}
}
@keyframes showTopShine {
to {
opacity: 1;
}
}
.raycast-submenu {
[cmdk-root] {
display: flex;
flex-direction: column;
width: 320px;
border: 1px solid var(--gray6);
background: var(--gray2);
border-radius: 8px;
}
[cmdk-list] {
padding: 8px;
overflow: auto;
overscroll-behavior: contain;
transition: 100ms ease;
transition-property: height;
}
[cmdk-item] {
height: 40px;
cursor: pointer;
height: 40px;
border-radius: 8px;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
color: var(--gray12);
user-select: none;
will-change: background, color;
transition: all 150ms ease;
transition-property: none;
&[aria-selected='true'] {
background: var(--gray5);
color: var(--gray12);
[cmdk-raycast-submenu-shortcuts] kbd {
background: var(--gray7);
}
}
&[aria-disabled='true'] {
color: var(--gray8);
cursor: not-allowed;
}
svg {
width: 16px;
height: 16px;
}
[cmdk-raycast-submenu-shortcuts] {
display: flex;
margin-left: auto;
gap: 2px;
kbd {
font-family: var(--font-sans);
background: var(--gray5);
color: var(--gray11);
height: 20px;
width: 20px;
border-radius: 4px;
padding: 0 4px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
&:first-of-type {
margin-left: 8px;
}
}
}
}
[cmdk-group-heading] {
text-transform: capitalize;
font-size: 12px;
color: var(--gray11);
font-weight: 500;
margin-bottom: 8px;
margin-top: 8px;
margin-left: 4px;
}
[cmdk-input] {
padding: 12px;
font-family: var(--font-sans);
border: 0;
border-top: 1px solid var(--gray6);
font-size: 13px;
background: transparent;
margin-top: auto;
width: 100%;
outline: 0;
border-radius: 0;
}
animation-duration: 0.2s;
animation-timing-function: ease;
animation-fill-mode: forwards;
transform-origin: var(--radix-popover-content-transform-origin);
&[data-state='open'] {
animation-name: slideIn;
}
&[data-state='closed'] {
animation-name: slideOut;
}
[cmdk-empty] {
display: flex;
align-items: center;
justify-content: center;
height: 64px;
white-space: pre-wrap;
font-size: 14px;
color: var(--gray11);
}
}
@keyframes slideIn {
0% {
opacity: 0;
transform: scale(0.96);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideOut {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.96);
}
}
@media (max-width: 640px) {
.raycast {
[cmdk-input] {
font-size: 16px;
}
}
}