diff --git a/package.json b/package.json index 7ba18543..b3d0db5c 100644 --- a/package.json +++ b/package.json @@ -41,10 +41,10 @@ "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", "@lezer/highlight": "^1.2.0", - "@mantine/core": "^7.12.1", - "@mantine/hooks": "^7.12.1", - "@mantine/modals": "^7.12.1", - "@mantine/notifications": "^7.12.1", + "@mantine/core": "^7.13.3", + "@mantine/hooks": "^7.13.3", + "@mantine/modals": "^7.13.3", + "@mantine/notifications": "^7.13.3", "@mdi/js": "^7.2.96", "@replit/codemirror-indentation-markers": "^6.5.0", "@surrealdb/codemirror": "1.0.0-beta.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71500dd2..bdb2d5ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,17 +68,17 @@ importers: specifier: ^1.2.0 version: 1.2.1 '@mantine/core': - specifier: ^7.12.1 - version: 7.12.1(@mantine/hooks@7.12.1(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.13.3 + version: 7.13.3(@mantine/hooks@7.13.3(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/hooks': - specifier: ^7.12.1 - version: 7.12.1(react@18.3.1) + specifier: ^7.13.3 + version: 7.13.3(react@18.3.1) '@mantine/modals': - specifier: ^7.12.1 - version: 7.12.1(@mantine/core@7.12.1(@mantine/hooks@7.12.1(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.12.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.13.3 + version: 7.13.3(@mantine/core@7.13.3(@mantine/hooks@7.13.3(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.3(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/notifications': - specifier: ^7.12.1 - version: 7.12.1(@mantine/core@7.12.1(@mantine/hooks@7.12.1(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.12.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.13.3 + version: 7.13.3(@mantine/core@7.13.3(@mantine/hooks@7.13.3(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.3(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdi/js': specifier: ^7.2.96 version: 7.4.47 @@ -192,7 +192,7 @@ importers: version: 5.12.2 mantine-contextmenu: specifier: ^7.5.0 - version: 7.11.3(@mantine/core@7.12.1(@mantine/hooks@7.12.1(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.12.1(react@18.3.1))(clsx@2.1.1)(react@18.3.1) + version: 7.11.3(@mantine/core@7.13.3(@mantine/hooks@7.13.3(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.3(react@18.3.1))(clsx@2.1.1)(react@18.3.1) papaparse: specifier: ^5.4.1 version: 5.4.1 @@ -1327,36 +1327,36 @@ packages: '@lezer/rust@1.0.2': resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} - '@mantine/core@7.12.1': - resolution: {integrity: sha512-PXKIDaT1fpNB77dPQIcdFGM2NRnfmsJSVx3uuBccngBQWMIWI0wPyiO1Y26DK4LQrbrypeb+TS+Zxpgx6RoiCA==} + '@mantine/core@7.13.3': + resolution: {integrity: sha512-IV8xSr6rFQefKr2iOEhYYkJ6rZTDEp71qNkAfn90toSNjgT/2bgnqOxXwxqZ3bwo9DyNOAbEDzs1EfdIzln5aA==} peerDependencies: - '@mantine/hooks': 7.12.1 + '@mantine/hooks': 7.13.3 react: ^18.2.0 react-dom: ^18.2.0 - '@mantine/hooks@7.12.1': - resolution: {integrity: sha512-YPA3qiMHJkWID5+YzakBaLvjHtX3Fg3PdPY49iIb/CaWM9+lrJ+77TOVS7bsY7ZTBHXUfzft1/6Woqt3xSuweA==} + '@mantine/hooks@7.13.3': + resolution: {integrity: sha512-r2c+Z8CdvPKFeOwg6mSJmxOp9K/ave5ZFR7eJbgv4wQU8K1CAS5f5ven9K5uUX8Vf9B5dFnSaSgYp9UY3vOWTw==} peerDependencies: react: ^18.2.0 - '@mantine/modals@7.12.1': - resolution: {integrity: sha512-olS07yDcCFLGylLGaQgBiTnKcRrUZVLKqBFBw5glcmc/wZmJf4SDMgx5mxSwBnsbJOwJ2d3aIYwO/qNTNnluSg==} + '@mantine/modals@7.13.3': + resolution: {integrity: sha512-XAx724ZLqQVnsaH72sCoZD7NKcx2haUgAv0G52hq0MbVWWig2rbzN5YBvqGw+kuKgwp20VH+6oLSVvvB+4SMzQ==} peerDependencies: - '@mantine/core': 7.12.1 - '@mantine/hooks': 7.12.1 + '@mantine/core': 7.13.3 + '@mantine/hooks': 7.13.3 react: ^18.2.0 react-dom: ^18.2.0 - '@mantine/notifications@7.12.1': - resolution: {integrity: sha512-YIV2ItCRJzbOjEyXtz5Rjf3qn6kwmcz6CqAGurpd+kecxx6wwNoKuKs6YNlz7tcprFegcH/hCUkW2tVbXHKVBA==} + '@mantine/notifications@7.13.3': + resolution: {integrity: sha512-G01Bf0g6zA+K6ZdBOIxhGIlpi3qITs6W5Z0fYTSQkzLcJSfECdR5KgRvNpzcx2ESTT8BfJJMsLySwh+WTzcoxw==} peerDependencies: - '@mantine/core': 7.12.1 - '@mantine/hooks': 7.12.1 + '@mantine/core': 7.13.3 + '@mantine/hooks': 7.13.3 react: ^18.2.0 react-dom: ^18.2.0 - '@mantine/store@7.12.1': - resolution: {integrity: sha512-zIzYEheEyXchPTNKsm88BJ0CTEZV6ZNwMhMDWHKQE3CzjKLJdKHJdIBcZImRU3Pn4GROZdZdIkQF9HLJ6BjvYw==} + '@mantine/store@7.13.3': + resolution: {integrity: sha512-95nAgH6APhak1OwP2W3ogdWBiWkIDhDSbQEm2G9LTJLIJxzWSm1mLe5uDWluVEPZW2XFx137McuJb58i1A+QhQ==} peerDependencies: react: ^18.2.0 @@ -4073,10 +4073,10 @@ snapshots: '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 - '@mantine/core@7.12.1(@mantine/hooks@7.12.1(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mantine/core@7.13.3(@mantine/hooks@7.13.3(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react': 0.26.23(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mantine/hooks': 7.12.1(react@18.3.1) + '@mantine/hooks': 7.13.3(react@18.3.1) clsx: 2.1.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -4087,27 +4087,27 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@mantine/hooks@7.12.1(react@18.3.1)': + '@mantine/hooks@7.13.3(react@18.3.1)': dependencies: react: 18.3.1 - '@mantine/modals@7.12.1(@mantine/core@7.12.1(@mantine/hooks@7.12.1(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.12.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mantine/modals@7.13.3(@mantine/core@7.13.3(@mantine/hooks@7.13.3(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.3(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@mantine/core': 7.12.1(@mantine/hooks@7.12.1(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mantine/hooks': 7.12.1(react@18.3.1) + '@mantine/core': 7.13.3(@mantine/hooks@7.13.3(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mantine/hooks': 7.13.3(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@mantine/notifications@7.12.1(@mantine/core@7.12.1(@mantine/hooks@7.12.1(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.12.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mantine/notifications@7.13.3(@mantine/core@7.13.3(@mantine/hooks@7.13.3(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.3(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@mantine/core': 7.12.1(@mantine/hooks@7.12.1(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mantine/hooks': 7.12.1(react@18.3.1) - '@mantine/store': 7.12.1(react@18.3.1) + '@mantine/core': 7.13.3(@mantine/hooks@7.13.3(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mantine/hooks': 7.13.3(react@18.3.1) + '@mantine/store': 7.13.3(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mantine/store@7.12.1(react@18.3.1)': + '@mantine/store@7.13.3(react@18.3.1)': dependencies: react: 18.3.1 @@ -5049,10 +5049,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - mantine-contextmenu@7.11.3(@mantine/core@7.12.1(@mantine/hooks@7.12.1(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.12.1(react@18.3.1))(clsx@2.1.1)(react@18.3.1): + mantine-contextmenu@7.11.3(@mantine/core@7.13.3(@mantine/hooks@7.13.3(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.3(react@18.3.1))(clsx@2.1.1)(react@18.3.1): dependencies: - '@mantine/core': 7.12.1(@mantine/hooks@7.12.1(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mantine/hooks': 7.12.1(react@18.3.1) + '@mantine/core': 7.13.3(@mantine/hooks@7.13.3(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mantine/hooks': 7.13.3(react@18.3.1) clsx: 2.1.1 react: 18.3.1 diff --git a/src/adapter/browser.tsx b/src/adapter/browser.tsx index 187fff04..61117bf1 100644 --- a/src/adapter/browser.tsx +++ b/src/adapter/browser.tsx @@ -1,3 +1,4 @@ +import { isFunction, shake } from "radash"; import type { OpenedBinaryFile, OpenedTextFile, @@ -43,6 +44,8 @@ export abstract class BaseBrowserAdapter implements SurrealistAdapter { public async loadConfig() { const localStorageValue = localStorage.getItem(CONFIG_KEY); + + // NOTE legacy local storage config if (localStorageValue) { const config = localStorageValue || "{}"; const parsed = JSON.parse(config); @@ -58,15 +61,7 @@ export abstract class BaseBrowserAdapter implements SurrealistAdapter { } public async saveConfig(config: any) { - const objConfig: any = {}; - - for (const key in config) { - if (typeof config[key] !== "function") { - objConfig[key] = config[key]; - } - } - - await idxdb.setConfig(objConfig); + await idxdb.setConfig(shake(config, isFunction)); localStorage.removeItem(CONFIG_KEY); } diff --git a/src/adapter/desktop.tsx b/src/adapter/desktop.tsx index 0a53899a..6ae24adc 100644 --- a/src/adapter/desktop.tsx +++ b/src/adapter/desktop.tsx @@ -104,6 +104,7 @@ export class DesktopAdapter implements SurrealistAdapter { store: useConfigStore, select: (s) => s.settings.behavior.windowPinned, then: (pinned) => { + console.log("pinned", pinned); getCurrentWindow().setAlwaysOnTop(pinned); }, }); diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 0f20cec0..f821760f 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -8,6 +8,7 @@ import { Scaffold } from "../Scaffold"; import { Globals } from "./globals"; import { ChangelogModal } from "./modals/changelog"; import { CloudExpiredDialog } from "./modals/cloud-expired"; +import { CommandPaletteModal } from "./modals/commands"; import { ConnectionModal } from "./modals/connection"; import { ConnectionsModal } from "./modals/connections"; import { ConsoleDrawer } from "./modals/console"; @@ -19,7 +20,6 @@ import { EmbedderModal } from "./modals/embedder"; import { HighlightToolModal } from "./modals/highlight-tool"; import { KeymapModal } from "./modals/hotkeys"; import { NewsFeedDrawer } from "./modals/newsfeed"; -import { CommandPaletteModal } from "./modals/palette"; import { ProvisioningDialog } from "./modals/provisioning"; import { RegisterUserModal } from "./modals/register"; import { SandboxModal } from "./modals/sandbox"; diff --git a/src/components/App/modals/palette.tsx b/src/components/App/modals/commands.tsx similarity index 74% rename from src/components/App/modals/palette.tsx rename to src/components/App/modals/commands.tsx index fffb1a24..bcb79fd8 100644 --- a/src/components/App/modals/palette.tsx +++ b/src/components/App/modals/commands.tsx @@ -1,13 +1,14 @@ import classes from "../style.module.scss"; import { Box, Divider, Group, Modal, ScrollArea, Stack, Text, TextInput } from "@mantine/core"; - import { useInputState } from "@mantine/hooks"; +import clsx from "clsx"; import posthog from "posthog-js"; -import { useLayoutEffect, useMemo, useRef, useState } from "react"; +import { type KeyboardEvent, useMemo, useRef, useState } from "react"; import { adapter } from "~/adapter"; import { Entry } from "~/components/Entry"; import { Icon } from "~/components/Icon"; +import { PreferenceInput } from "~/components/Inputs/preference"; import { Shortcut } from "~/components/Shortcut"; import { Spacer } from "~/components/Spacer"; import { useBoolean } from "~/hooks/boolean"; @@ -17,19 +18,37 @@ import { useStable } from "~/hooks/stable"; import { dispatchIntent, useIntent } from "~/hooks/url"; import { useConfigStore } from "~/stores/config"; import { type Command, type CommandCategory, computeCommands } from "~/util/commands"; -import { Y_SLIDE_TRANSITION, fuzzyMatch } from "~/util/helpers"; +import { ON_STOP_PROPAGATION, Y_SLIDE_TRANSITION, fuzzyMatch } from "~/util/helpers"; import { iconOpen, iconSearch } from "~/util/icons"; export function CommandPaletteModal() { const { pushCommand } = useConfigStore.getState(); + const searchRef = useRef(null); const [isOpen, openHandle] = useBoolean(); const [search, setSearch] = useInputState(""); const [categories, setCategories] = useState([]); + const handlePreferenceInput = useStable((e: KeyboardEvent) => { + e.stopPropagation(); + + if (e.code === "Tab") { + e.preventDefault(); + return; + } + + if (e.code === "Escape") { + searchRef.current?.focus(); + openHandle.open(); + } + }); + const [filtered, flattened] = useMemo(() => { const filtered = categories.flatMap((cat) => { - if (search && cat.search === false) { + if ( + (cat.visibility === "unsearched" && search) || + (cat.visibility === "searched" && !search) + ) { return []; } @@ -56,16 +75,17 @@ export function CommandPaletteModal() { return [filtered, flattened]; }, [categories, search]); - const activate = (cmd: Command) => { + const activate = useStable((cmd: Command) => { const query = search.trim(); - if (query.length > 0) { - pushCommand(query); - } + posthog.capture("execute_command", { + command: cmd.name, + }); switch (cmd.action.type) { case "insert": { setSearch(cmd.action.content); + searchRef.current?.focus(); break; } case "href": { @@ -83,14 +103,23 @@ export function CommandPaletteModal() { cmd.action.handler(); break; } + case "preference": { + const el = document.querySelector(`[data-navigation-item-id="${cmd.id}"]`); + const input = el?.querySelector(".mantine-InputWrapper-root input"); + const checkbox = el?.querySelector(".mantine-Checkbox-root input"); + + (input ?? checkbox)?.click(); + input?.focus(); + return; + } } - posthog.capture("execute_command", { - command: cmd.name, - }); - }; + if (query.length > 0) { + pushCommand(query); + } + }); - const [handleKeyDown, searchRef] = useKeyNavigation(flattened); + const [handleKeyDown, selected] = useKeyNavigation(flattened, activate); useIntent("open-command-palette", () => { openHandle.open(); @@ -176,6 +205,9 @@ export function CommandPaletteModal() { disabled={cmd.disabled} leftSection={} data-navigation-item-id={cmd.id} + className={clsx( + selected === cmd.id && classes.listingActive, + )} > {cmd.name} {cmd.action.type === "href" && ( @@ -201,6 +233,20 @@ export function CommandPaletteModal() { )} + {cmd.action.type === "preference" && ( + <> + + + + + + )} ))} diff --git a/src/components/App/modals/connections.tsx b/src/components/App/modals/connections.tsx index 65b49b22..b968da96 100644 --- a/src/components/App/modals/connections.tsx +++ b/src/components/App/modals/connections.tsx @@ -1,9 +1,36 @@ import classes from "../style.module.scss"; -import { ActionIcon, Box, Divider, Flex, Group, Menu, Modal, ScrollArea, Stack, Text, TextInput, Tooltip } from "@mantine/core"; +import { + ActionIcon, + Box, + Divider, + Flex, + Group, + Menu, + Modal, + ScrollArea, + Stack, + Text, + TextInput, + Tooltip, +} from "@mantine/core"; + +import { + iconCloud, + iconCopy, + iconDelete, + iconEdit, + iconFolderPlus, + iconHomePlus, + iconPlus, + iconSandbox, + iconServer, +} from "~/util/icons"; + import { useInputState } from "@mantine/hooks"; +import clsx from "clsx"; import { useContextMenu } from "mantine-contextmenu"; -import { group } from "radash"; +import { group, isEmpty } from "radash"; import { type HTMLAttributes, type MouseEvent, type ReactNode, useMemo } from "react"; import { isDesktop } from "~/adapter"; import { EditableText } from "~/components/EditableText"; @@ -14,172 +41,32 @@ import { SANDBOX } from "~/constants"; import { useBoolean } from "~/hooks/boolean"; import { useConnection, useConnections } from "~/hooks/connection"; import { useKeymap } from "~/hooks/keymap"; +import { useKeyNavigation } from "~/hooks/keys"; import { useStable } from "~/hooks/stable"; import { dispatchIntent, useIntent } from "~/hooks/url"; import { useConfigStore } from "~/stores/config"; import type { Connection } from "~/types"; -import { Y_SLIDE_TRANSITION, newId } from "~/util/helpers"; -import { iconCloud, iconCopy, iconDelete, iconEdit, iconFolderPlus, iconHomePlus, iconPlus, iconSandbox, iconSearch, iconServer } from "~/util/icons"; +import { Y_SLIDE_TRANSITION, fuzzyMatch, newId } from "~/util/helpers"; import { USER_ICONS } from "~/util/user-icons"; -const UNGROUPED = Symbol("ungrouped"); - -interface ItemProps extends EntryProps, Omit, 'style' | 'color'> { - connection: Connection; - active: string; - onClose: () => void; -} - -function Item({ - connection, - active, - onClose, - ...other -}: ItemProps) { - const { showContextMenu } = useContextMenu(); - const { setActiveConnection, addConnection, removeConnection } = useConfigStore.getState(); - const isActive = connection.id === active; - - const activate = useStable(() => { - setActiveConnection(connection.id); - onClose(); - }); - - const modify = useStable((e: MouseEvent) => { - e.stopPropagation(); - onClose(); - dispatchIntent("edit-connection", { - id: connection.id - }); - }); - - return ( - - } - rightSection={ - - - - } - onContextMenu={showContextMenu([ - { - key: "edit", - title: "Edit", - icon: , - onClick: modify, - }, - { - key: "duplicate", - title: "Duplicate", - icon: , - onClick: () => addConnection({ - ...connection, - lastNamespace: "", - lastDatabase: "", - id: newId() - }), - }, - { - key: "delete", - title: "Delete connection", - color: "pink.7", - icon: , - onClick: () => removeConnection(connection.id), - } - ])} - {...other} - > - - {connection.name} - - {connection.authentication.mode === "cloud" && ( - - | Surreal Cloud - - - )} - - ); -} - -interface ItemListProps { - title: ReactNode; - connections: Connection[]; - active: string; - className?: string; - onClose: () => void; -} - -function ItemList({ - title, - connections, - active, - className, - onClose -}: ItemListProps) { - const connectionList = useMemo(() => { - return connections.sort((a, b) => a.name.localeCompare(b.name)); - }, [connections]); - - return ( - - - {title} - - {connectionList.length === 0 ? ( - - No connections - - ) : ( - - {connectionList.map((con) => ( - - ))} - - )} - - ); -} +const UNGROUPED = "__ungrouped__"; export function ConnectionsModal() { const [isOpen, openedHandle] = useBoolean(); - const { setActiveConnection, addConnectionGroup, updateConnectionGroup, removeConnectionGroup } = useConfigStore.getState(); + const { + setActiveConnection, + addConnectionGroup, + updateConnectionGroup, + removeConnectionGroup, + } = useConfigStore.getState(); const [search, setSearch] = useInputState(""); const connections = useConnections(); const connection = useConnection(); const groups = useConfigStore((s) => s.connectionGroups); - - const filtered = useMemo(() => { - const needle = search.trim().toLocaleLowerCase(); - - return connections.filter((con) => - con.name.toLowerCase().includes(needle) - || con.authentication.hostname.toLowerCase().includes(needle) - ); - }, [connections, search]); + const sandbox = useConfigStore((s) => s.sandbox); const newConnection = useStable(() => { openedHandle.close(); @@ -203,8 +90,8 @@ export function ConnectionsModal() { access: "", token: "", username, - password - } + password, + }, }); dispatchIntent("new-connection", { template }); @@ -214,27 +101,38 @@ export function ConnectionsModal() { const newGroup = useStable(() => { addConnectionGroup({ id: newId(), - name: `Group ${groups.length + 1}` + name: `Group ${groups.length + 1}`, }); }); - const openSandbox = useStable(() => { - setActiveConnection(SANDBOX); - openedHandle.close(); - }); - const isSandbox = connection?.id === SANDBOX; const groupsList = useMemo(() => { return groups.sort((a, b) => a.name.localeCompare(b.name)); }, [groups]); - const grouped = group(filtered, (con) => con.group ?? UNGROUPED) || {}; - const ungrouped = grouped[UNGROUPED] ?? []; + const connectionsList = useMemo(() => { + return connections.sort((a, b) => a.name.localeCompare(b.name)); + }, [connections]); - useKeymap([ - ["mod+L", openedHandle.open] - ]); + const [grouped, flattened] = useMemo(() => { + const filtered = connectionsList.filter((con) => { + return fuzzyMatch(search, con.name) || fuzzyMatch(search, con.authentication.hostname); + }); + + const grouped = group(filtered, (con) => con.group ?? UNGROUPED) || {}; + + return [grouped, [sandbox, ...filtered]]; + }, [connectionsList, search, sandbox]); + + const activate = useStable((con: Connection) => { + setActiveConnection(con.id); + openedHandle.close(); + }); + + const [handleKeyDown, selected] = useKeyNavigation(flattened, activate, connection?.id); + + useKeymap([["mod+L", openedHandle.open]]); useIntent("open-connections", ({ search }) => { if (search) { @@ -251,6 +149,7 @@ export function ConnectionsModal() { transitionProps={{ transition: Y_SLIDE_TRANSITION }} centered={false} size="lg" + onKeyDown={handleKeyDown} classNames={{ content: classes.listingModal, body: classes.listingBody, @@ -286,7 +185,7 @@ export function ConnectionsModal() { variant="gradient" style={{ backgroundOrigin: "border-box", - border: "1px solid rgba(255, 255, 255, 0.3)" + border: "1px solid rgba(255, 255, 255, 0.3)", }} size={36} radius="md" @@ -303,7 +202,12 @@ export function ConnectionsModal() { {isDesktop && ( } + leftSection={ + + } onClick={newLocalhost} > New local connection @@ -327,39 +231,53 @@ export function ConnectionsModal() { mah="calc(100vh - 200px)" mih={64} > - + - } + onClick={() => activate(sandbox)} + leftSection={} + data-navigation-item-id="sandbox" + className={clsx(selected === "sandbox" && classes.listingActive)} > - - Sandbox - + Sandbox + {!grouped[UNGROUPED] && !groupsList.length && ( + + No connections created yet + + )} + {groupsList.map((group) => ( updateConnectionGroup({ id: group.id, name })} + onChange={(name) => + updateConnectionGroup({ id: group.id, name }) + } c="bright" fz="lg" fw={500} /> - + - + @@ -375,13 +296,19 @@ export function ConnectionsModal() { /> ))} - {(ungrouped.length > 0 || groups.length === 0) && ( + {grouped[UNGROUPED] && ( + Connections } @@ -391,4 +318,149 @@ export function ConnectionsModal() { ); -} \ No newline at end of file +} + +interface ItemListProps { + title: ReactNode; + connections: Connection[]; + active: string; + selected: string; + className?: string; + onClose: () => void; + onActivate: (connection: Connection) => void; +} + +function ItemList({ + title, + connections, + active, + selected, + className, + onClose, + onActivate, +}: ItemListProps) { + return ( + + {title} + {connections.length === 0 ? ( + + No connections + + ) : ( + + {connections.map((con) => ( + + ))} + + )} + + ); +} + +interface ItemProps extends EntryProps, Omit, "style" | "color"> { + connection: Connection; + active: string; + selected: string; + onClose: () => void; + onActivate: (connection: Connection) => void; +} + +function Item({ connection, active, selected, onClose, onActivate, ...other }: ItemProps) { + const { showContextMenu } = useContextMenu(); + const { addConnection, removeConnection } = useConfigStore.getState(); + const isActive = connection.id === active; + + const activate = useStable(() => { + onActivate(connection); + }); + + const modify = useStable((e: MouseEvent) => { + e.stopPropagation(); + onClose(); + dispatchIntent("edit-connection", { + id: connection.id, + }); + }); + + return ( + } + rightSection={ + + + + } + onContextMenu={showContextMenu([ + { + key: "edit", + title: "Edit", + icon: , + onClick: modify, + }, + { + key: "duplicate", + title: "Duplicate", + icon: , + onClick: () => + addConnection({ + ...connection, + lastNamespace: "", + lastDatabase: "", + id: newId(), + }), + }, + { + key: "delete", + title: "Delete connection", + color: "pink.7", + icon: , + onClick: () => removeConnection(connection.id), + }, + ])} + {...other} + > + {connection.name} + {connection.authentication.mode === "cloud" && ( + + | Surreal Cloud + + + )} + + ); +} diff --git a/src/components/App/modals/documentation.tsx b/src/components/App/modals/documentation.tsx index a3a6c414..d22992d2 100644 --- a/src/components/App/modals/documentation.tsx +++ b/src/components/App/modals/documentation.tsx @@ -15,12 +15,15 @@ import { import { useDebouncedValue, useInputState } from "@mantine/hooks"; import { keepPreviousData, useQuery, useQueryClient } from "@tanstack/react-query"; +import clsx from "clsx"; import { adapter } from "~/adapter"; import { Entry } from "~/components/Entry"; import { Icon } from "~/components/Icon"; import { PrimaryTitle } from "~/components/PrimaryTitle"; import { useBoolean } from "~/hooks/boolean"; import { useKeymap } from "~/hooks/keymap"; +import { useKeyNavigation } from "~/hooks/keys"; +import { useStable } from "~/hooks/stable"; import { dispatchIntent, useIntent } from "~/hooks/url"; import { Y_SLIDE_TRANSITION } from "~/util/helpers"; import { iconBook } from "~/util/icons"; @@ -31,6 +34,7 @@ interface Offset { } interface Result { + id: string; url: string; title: string; content: string[]; @@ -61,6 +65,7 @@ export function DocumentationModal() { LET $host = ${hostname}; SELECT + rand::guid() as id, path as url, hostname, title, @@ -112,10 +117,12 @@ export function DocumentationModal() { }, }); - const openDocumentation = (doc: Result) => { + const openDocumentation = useStable((doc: Result) => { openHandle.close(); adapter.openUrl(`https://surrealdb.com${doc.url}`); - }; + }); + + const [handleKeyDown, selected] = useKeyNavigation(data ?? [], openDocumentation); useIntent("open-documentation", ({ search }) => { openHandle.open(); @@ -136,6 +143,7 @@ export function DocumentationModal() { transitionProps={{ transition: Y_SLIDE_TRANSITION }} centered={false} size="lg" + onKeyDown={handleKeyDown} classNames={{ content: classes.listingModal, body: classes.listingBody, @@ -179,7 +187,7 @@ export function DocumentationModal() { display="block" mah="calc(100vh - 200px)" > - {(isEmpty && !search) ? ( + {isEmpty && !search ? ( openDocumentation(doc)} h="unset" ta="start" + data-navigation-item-id={doc.id} + className={clsx(selected === doc.id && classes.listingActive)} > {doc.title} diff --git a/src/components/App/settings/index.tsx b/src/components/App/settings/index.tsx index 75a903e0..82ecbb5e 100644 --- a/src/components/App/settings/index.tsx +++ b/src/components/App/settings/index.tsx @@ -2,15 +2,12 @@ import classes from "./style.module.scss"; import { ActionIcon, - Box, type BoxProps, Center, - Divider, Drawer, Group, Image, Modal, - ScrollArea, Stack, Text, ThemeIcon, @@ -22,37 +19,34 @@ import { iconBalance, iconChevronRight, iconClose, - iconCloud, iconDownload, - iconEye, iconFlag, + iconHelp, iconPlay, iconServer, - iconWrench, + iconTransfer, + iconTune, } from "~/util/icons"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { isDesktop } from "~/adapter"; import { Entry } from "~/components/Entry"; import { Icon } from "~/components/Icon"; import { Spacer } from "~/components/Spacer"; import { useBoolean } from "~/hooks/boolean"; import { useLogoUrl } from "~/hooks/brand"; -import { useVersionCopy } from "~/hooks/debug"; import { useKeymap } from "~/hooks/keymap"; import { useStable } from "~/hooks/stable"; -import { useIsLight } from "~/hooks/theme"; import { useDesktopUpdater } from "~/hooks/updater"; import { useIntent } from "~/hooks/url"; import { useInterfaceStore } from "~/stores/interface"; import type { Assign, FeatureCondition } from "~/types"; -import { isDevelopment, isPreview } from "~/util/environment"; import { useFeatureFlags } from "~/util/feature-flags"; -import { AppearanceTab } from "./tabs/Appearance"; -import { BehaviourTab } from "./tabs/Behaviour"; -import { CloudTab } from "./tabs/Cloud"; +import { AboutTab } from "./tabs/About"; import { FeatureFlagsTab } from "./tabs/FeatureFlags"; import { LicensesTab } from "./tabs/Licenses"; +import { ManageDataTab } from "./tabs/ManageData"; +import { PreferencesTab } from "./tabs/Preferences"; import { ServingTab } from "./tabs/Serving"; import { TemplatesTab } from "./tabs/Templates"; @@ -66,16 +60,10 @@ interface Category { const CATEGORIES: Category[] = [ { - id: "behaviour", - name: "Behavior", - icon: iconWrench, - component: BehaviourTab, - }, - { - id: "appearance", - name: "Appearance", - icon: iconEye, - component: AppearanceTab, + id: "preferences", + name: "Preferences", + icon: iconTune, + component: PreferencesTab, }, { id: "templates", @@ -91,10 +79,10 @@ const CATEGORIES: Category[] = [ disabled: () => !isDesktop, }, { - id: "cloud", - name: "Surreal Cloud", - icon: iconCloud, - component: CloudTab, + id: "manage-data", + name: "Manage Data", + icon: iconTransfer, + component: ManageDataTab, }, { id: "feature-flags", @@ -109,6 +97,12 @@ const CATEGORIES: Category[] = [ icon: iconBalance, component: LicensesTab, }, + { + id: "about", + name: "About", + icon: iconHelp, + component: AboutTab, + }, ]; type OptionalCategory = Assign; @@ -116,62 +110,45 @@ type OptionalCategory = Assign; interface SettingsSidebarProps extends BoxProps { activeTab: string; categories: OptionalCategory[]; + withBorder?: boolean; setActiveTab: (tab: string) => void; } -function SettingsSidebar({ activeTab, categories, setActiveTab, ...other }: SettingsSidebarProps) { - const isLight = useIsLight(); +function SettingsSidebar({ + activeTab, + categories, + withBorder, + setActiveTab, + ...other +}: SettingsSidebarProps) { const logoUrl = useLogoUrl(); const availableUpdate = useInterfaceStore((s) => s.availableUpdate); const { phase, progress, version, startUpdate } = useDesktopUpdater(); - const [copyDebug, clipboard] = useVersionCopy(); const sidebarCategories = categories.filter((c) => !c.disabled || c.id === activeTab); - const versionText = useMemo(() => { - let builder = `Version ${import.meta.env.VERSION}`; - - if (isPreview) { - builder += " (pre)"; - } else if (isDevelopment) { - builder += " (dev)"; - } - - return builder; - }, []); - return ( - -
- -
- - {clipboard.copied ? "Copied to clipboard!" : versionText} - -
+ + (null); + const [activeTab, setActiveTab] = useState("preferences"); const categories: OptionalCategory[] = CATEGORIES.map((c) => ({ ...c, @@ -292,38 +268,32 @@ export function Settings() { opened={open} onClose={openHandle.close} padding={0} - size={960} + size={1200} > - + - - - {Component && } - - + {Component && } diff --git a/src/components/App/settings/style.module.scss b/src/components/App/settings/style.module.scss index 8c5f9f0d..67eb5158 100644 --- a/src/components/App/settings/style.module.scss +++ b/src/components/App/settings/style.module.scss @@ -33,4 +33,9 @@ .update-icon { border: 1px solid rgba(255, 255, 255, 0.3); background-origin: border-box; +} + +.about-value { + user-select: all; + -webkit-user-select: all; } \ No newline at end of file diff --git a/src/components/App/settings/tabs/About.tsx b/src/components/App/settings/tabs/About.tsx new file mode 100644 index 00000000..a2bc45d7 --- /dev/null +++ b/src/components/App/settings/tabs/About.tsx @@ -0,0 +1,80 @@ +import classes from "../style.module.scss"; + +import { Button, Stack, Text } from "@mantine/core"; +import { useMemo } from "react"; +import { Icon } from "~/components/Icon"; +import { LearnMore } from "~/components/LearnMore"; +import { useVersionCopy } from "~/hooks/debug"; +import { isDevelopment, isPreview } from "~/util/environment"; +import { iconCheck, iconWrench } from "~/util/icons"; + +export function AboutTab() { + const [copyDebug, clipboard] = useVersionCopy(); + + const versionText = useMemo(() => { + let builder = import.meta.env.VERSION; + + if (isPreview) { + builder += " (pre)"; + } else if (isDevelopment) { + builder += " (dev)"; + } + + return builder; + }, []); + + const information = useMemo( + () => [ + ["Version", versionText], + ["Build date", import.meta.env.DATE], + ["Build mode", import.meta.env.MODE], + ["Compatibility", import.meta.env.SDB_VERSION], + ], + [versionText], + ); + + return ( + <> + Surrealist © 2024 SurrealDB Ltd + + {information.map(([label, value]) => ( + + {label}:{" "} + + {value} + + + ))} + + + + GitHub Repository + + + Surrealist Documentation + + + + + + + ); +} diff --git a/src/components/App/settings/tabs/Appearance.tsx b/src/components/App/settings/tabs/Appearance.tsx deleted file mode 100644 index b0b6ab65..00000000 --- a/src/components/App/settings/tabs/Appearance.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { - DESIGNER_DIRECTIONS, - DESIGNER_NODE_MODES, - LINE_STYLES, - ORIENTATIONS, - RESULT_FORMATS, - RESULT_MODES, - SIDEBAR_MODES, - THEMES, -} from "~/constants"; - -import { Box, Checkbox, Select, Slider } from "@mantine/core"; -import { isDesktop } from "~/adapter"; -import { Label } from "~/components/Label"; -import { useLineNumberSetting, useSetting } from "~/hooks/config"; -import { useCheckbox } from "~/hooks/events"; -import { useFeatureFlags } from "~/util/feature-flags"; -import { SettingsSection } from "../utilities"; - -const CAT = "appearance"; - -export function AppearanceTab() { - const [flags] = useFeatureFlags(); - - const [colorScheme, setColorScheme] = useSetting(CAT, "colorScheme"); - const [editorScale, setEditorScale] = useSetting(CAT, "editorScale"); - const [windowScale, setWindowScale] = useSetting(CAT, "windowScale"); - // const [resultWordWrap, setResultWordWrap] = useSetting(CAT, "resultWordWrap"); - const [defaultResultMode, setDefaultResultMode] = useSetting(CAT, "defaultResultMode"); - const [queryOrientation, setQueryOrientation] = useSetting(CAT, "queryOrientation"); - const [sidebarMode, setSidebarMode] = useSetting(CAT, "sidebarMode"); - const [lineStyle, setLineStyle] = useSetting(CAT, "lineStyle"); - const [defaultDiagramMode, setDefaultDiagramMode] = useSetting(CAT, "defaultDiagramMode"); - const [defaultDiagramDirection, setDefaultDiagramDirection] = useSetting( - CAT, - "defaultDiagramDirection", - ); - - const [defaultDiagramShowLinks, setDefaultDiagramShowLinks] = useSetting( - CAT, - "defaultDiagramShowLinks", - ); - - const updateDefaultDiagramShowLinks = useCheckbox(setDefaultDiagramShowLinks); - - const [hasLineNumbers, toggleLineNumbers] = useLineNumberSetting(); - - return ( - <> - - {flags.themes && ( - - - - - - - - - {isDesktop && ( - - - - - )} - - - - toggleLineNumbers("query")} - /> - - toggleLineNumbers("inspector")} - /> - - toggleLineNumbers("functions")} - /> - - - - {/* */} - - - - - - - - - - - {cloud_endpoints === "custom" && ( - - setUrlAuthBase(e.target.value)} - /> - - setUrlApiAuthBase(e.target.value)} - /> - - setUrlApiMgmtBase(e.target.value)} - /> - - )} - - ); -} diff --git a/src/components/App/settings/tabs/FeatureFlags.tsx b/src/components/App/settings/tabs/FeatureFlags.tsx index 39737318..23f3c5bc 100644 --- a/src/components/App/settings/tabs/FeatureFlags.tsx +++ b/src/components/App/settings/tabs/FeatureFlags.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, Group, Select, Stack, TextInput } from "@mantine/core"; +import { ActionIcon, Group, ScrollArea, Select, Stack, TextInput } from "@mantine/core"; import { Text } from "@mantine/core"; import { useInputState } from "@mantine/hooks"; import { useMemo } from "react"; @@ -10,10 +10,7 @@ import { iconReset, iconSearch } from "~/util/icons"; export function FeatureFlagsTab() { const [flags, setFlags] = useFeatureFlags(); - const flagNames = useMemo( - () => Object.keys(flags) as (keyof typeof flags)[], - [flags], - ); + const flagNames = useMemo(() => Object.keys(flags) as (keyof typeof flags)[], [flags]); const defaults = featureFlags.initialStore; const [search, setSearch] = useInputState(""); @@ -23,57 +20,71 @@ export function FeatureFlagsTab() { }, [flagNames, search]); return ( - + } - placeholder="Filter feature flags" - variant="unstyled" + leftSection={ + + } + placeholder="Search preferences" value={search} onChange={setSearch} - autoFocus size="xs" - w="100%" mb="sm" /> - {filteredFlags.map((flag) => { - const mapped = Object.fromEntries( - schema[flag].options.map((v) => [v.toString(), v]), - ); - const data = schema[flag].options.map((value) => - value.toString(), - ); + + {filteredFlags.map((flag) => { + const mapped = Object.fromEntries( + schema[flag].options.map((v) => [v.toString(), v]), + ); + const data = schema[flag].options.map((value) => value.toString()); - return ( - - - {flag} - - ({typeof flags[flag]}) - - {defaults[flag] !== flags[flag] && ( - - setFlags({ [flag]: defaults[flag] }) - } - aria-label="Reset to default value" + return ( + + - - - )} - + setFlags({ + [flag]: val ? mapped[val] : defaults[flag], + }) + } + size="xs" + /> + + ); + })} + + ); } diff --git a/src/components/App/settings/tabs/Licenses.tsx b/src/components/App/settings/tabs/Licenses.tsx index 4fbc9b66..cbbe74fa 100644 --- a/src/components/App/settings/tabs/Licenses.tsx +++ b/src/components/App/settings/tabs/Licenses.tsx @@ -1,50 +1,43 @@ -import { ScrollArea, Table } from "@mantine/core"; +import { Box, ScrollArea, Table } from "@mantine/core"; import licenseReport from "~/assets/data/license-report.json"; import { Link } from "~/components/Link"; export function LicensesTab() { return ( - <> - - - - - Package - License - Version - Author - Link - - - - {licenseReport.map((pkg) => { - const link = pkg.link.startsWith("git+") - ? pkg.link.slice(4) - : pkg.link; + +
+ + + Package + License + Version + Author + Link + + + + {licenseReport.map((pkg) => { + const link = pkg.link.startsWith("git+") ? pkg.link.slice(4) : pkg.link; - return ( - - {pkg.name} - {pkg.licenseType} - {pkg.installedVersion} - {pkg.author} - - {link} - - - ); - })} - -
-
- + return ( + + {pkg.name} + {pkg.licenseType} + {pkg.installedVersion} + {pkg.author} + + {link} + + + ); + })} + + + ); } diff --git a/src/components/App/settings/tabs/ManageData.tsx b/src/components/App/settings/tabs/ManageData.tsx new file mode 100644 index 00000000..4182f094 --- /dev/null +++ b/src/components/App/settings/tabs/ManageData.tsx @@ -0,0 +1,12 @@ +import { Alert } from "@mantine/core"; + +export function ManageDataTab() { + return ( + + This functionality will be available soon + + ); +} diff --git a/src/components/App/settings/tabs/Preferences.tsx b/src/components/App/settings/tabs/Preferences.tsx new file mode 100644 index 00000000..607490cd --- /dev/null +++ b/src/components/App/settings/tabs/Preferences.tsx @@ -0,0 +1,81 @@ +import { Box, Divider, Group, Paper, ScrollArea, Stack, Text, TextInput } from "@mantine/core"; +import { useInputState } from "@mantine/hooks"; +import { useMemo } from "react"; +import { Icon } from "~/components/Icon"; +import { PreferenceInput } from "~/components/Inputs/preference"; +import { Spacer } from "~/components/Spacer"; +import { iconSearch } from "~/util/icons"; +import { computePreferences } from "~/util/preferences"; + +export function PreferencesTab() { + const [search, setSearch] = useInputState(""); + + const sections = useMemo(() => { + return computePreferences(); + }, []); + + return ( + + + } + placeholder="Search preferences" + value={search} + onChange={setSearch} + size="xs" + mb="sm" + /> + + {sections.map((section, i) => ( + + + {section.name} + + + {section.preferences.map((preference, j) => ( + <> + + + {preference.name} + {preference.description && ( + + {preference.description} + + )} + + + + + {j < section.preferences.length - 1 && } + + ))} + + + ))} + + + ); +} diff --git a/src/components/App/settings/tabs/Serving.tsx b/src/components/App/settings/tabs/Serving.tsx index 0febd5a7..4bede5b5 100644 --- a/src/components/App/settings/tabs/Serving.tsx +++ b/src/components/App/settings/tabs/Serving.tsx @@ -1,4 +1,4 @@ -import { NumberInput, Select, SimpleGrid, TextInput } from "@mantine/core"; +import { NumberInput, ScrollArea, Select, SimpleGrid, Text, TextInput } from "@mantine/core"; import { useSetting } from "~/hooks/config"; import { useStable } from "~/hooks/stable"; import type { LogLevel, Selection } from "~/types"; @@ -42,8 +42,25 @@ export function ServingTab() { }); return ( - <> - + + + You can use Surrealist Desktop to serve SurrealDB on your local machine. +
+ This page allows you to customize the settings for the database. +
+ + - - - - + - - - setUsername(e.target.value)} - /> + + setUsername(e.target.value)} + /> - setPassword(e.target.value)} - /> - + setPassword(e.target.value)} + /> - - - + + {(driver === "file" || driver === "tikv" || driver === "surrealkv") && ( + setStorage(e.target.value)} /> - - {(driver === "file" || - driver === "tikv" || - driver === "surrealkv") && ( - setStorage(e.target.value)} - /> - )} - + )} - +
); } diff --git a/src/components/App/settings/tabs/Templates.tsx b/src/components/App/settings/tabs/Templates.tsx index 570eeaea..d356bccc 100644 --- a/src/components/App/settings/tabs/Templates.tsx +++ b/src/components/App/settings/tabs/Templates.tsx @@ -91,8 +91,9 @@ export function TemplatesTab() { <> - Templates simplify the process of creating new connections - by pre-filling common connection details. + Connection templates make it easier to create new connections by pre-filling + common connection details.
+ From the connection editor you can choose which template to use.
{templates.map((template) => ( @@ -122,7 +123,10 @@ export function TemplatesTab() { size="lg" >
- + diff --git a/src/components/App/settings/utilities.tsx b/src/components/App/settings/utilities.tsx index 74c3c352..5304c4a3 100644 --- a/src/components/App/settings/utilities.tsx +++ b/src/components/App/settings/utilities.tsx @@ -1,14 +1,26 @@ -import { Stack, Title } from "@mantine/core"; +import { type BoxProps, Stack, Title } from "@mantine/core"; import type { PropsWithChildren, ReactNode } from "react"; +export interface SettingsSectionProps extends BoxProps { + label?: ReactNode; +} + export function SettingsSection({ label, children, -}: PropsWithChildren<{ label?: ReactNode }>) { + ...other +}: PropsWithChildren) { return ( - + {label && ( - + <Title + order={2} + c="bright" + size={18} + > {label} )} diff --git a/src/components/App/style.module.scss b/src/components/App/style.module.scss index 07151456..762c8257 100644 --- a/src/components/App/style.module.scss +++ b/src/components/App/style.module.scss @@ -23,7 +23,9 @@ } .changelog-content { - ul, li { + + ul, + li { margin-bottom: 0; } @@ -133,7 +135,7 @@ .provision-loader::after { border: solid 3px transparent; - background-image: linear-gradient(var(--mantine-color-body), var(--mantine-color-body)), var(--surrealist-gradient); + background-image: linear-gradient(var(--mantine-color-body), var(--mantine-color-body)), var(--surrealist-gradient); background-origin: border-box; background-clip: content-box, border-box; animation-duration: 0.75s; @@ -146,6 +148,14 @@ transform: translateY(1px); } -.docs-scroller :global(.mantine-ScrollArea-viewport) > div { +.docs-scroller :global(.mantine-ScrollArea-viewport)>div { display: block !important; +} + +.listing-active { + background-color: var(--mantine-color-slate-7); + + @include light { + background-color: var(--mantine-color-slate-1); + } } \ No newline at end of file diff --git a/src/components/CodeEditor/index.tsx b/src/components/CodeEditor/index.tsx index 1dbfd22e..8f1723ae 100644 --- a/src/components/CodeEditor/index.tsx +++ b/src/components/CodeEditor/index.tsx @@ -5,9 +5,10 @@ import { EditorView, lineNumbers as renderLineNumbers } from "@codemirror/view"; import { Box, type BoxProps } from "@mantine/core"; import clsx from "clsx"; import { useEffect, useRef } from "react"; -import { colorTheme, editorBase } from "~/editor"; +import { editorBase, editorTheme } from "~/editor"; import { useSetting } from "~/hooks/config"; -import { useIsLight } from "~/hooks/theme"; +import { useTheme } from "~/hooks/theme"; +import { useConfigStore } from "~/stores/config"; import classes from "./style.module.scss"; interface EditorRef { @@ -43,7 +44,8 @@ export function CodeEditor(props: CodeEditorProps) { ...rest } = props; - const isLight = useIsLight(); + const colorScheme = useTheme(); + const syntaxTheme = useConfigStore((s) => s.settings.appearance.syntaxTheme); const ref = useRef(null); const editorRef = useRef(); const [editorScale] = useSetting("appearance", "editorScale"); @@ -71,7 +73,7 @@ export function CodeEditor(props: CodeEditorProps) { editorBase(), readOnlyComp.of(EditorState.readOnly.of(!!readOnly)), historyComp.of(newHistory()), - themeComp.of(colorTheme(isLight)), + themeComp.of(editorTheme(colorScheme, syntaxTheme)), numbersComp.of(lineNumbers ? renderLineNumbers() : []), changeHandler, extensions || [], @@ -159,9 +161,9 @@ export function CodeEditor(props: CodeEditorProps) { const { editor, themeComp } = editorRef.current; editor.dispatch({ - effects: themeComp.reconfigure(colorTheme(isLight)), + effects: themeComp.reconfigure(editorTheme(colorScheme, syntaxTheme)), }); - }, [isLight]); + }, [colorScheme, syntaxTheme]); useEffect(() => { if (!editorRef.current) return; diff --git a/src/components/CodePreview/index.tsx b/src/components/CodePreview/index.tsx index a8579765..8c544680 100644 --- a/src/components/CodePreview/index.tsx +++ b/src/components/CodePreview/index.tsx @@ -6,17 +6,18 @@ import { ActionIcon, Box, CopyButton, Paper, type PaperProps, Text } from "@mant import { surrealql } from "@surrealdb/codemirror"; import clsx from "clsx"; import { type ReactNode, useEffect, useMemo, useRef } from "react"; -import { colorTheme } from "~/editor"; -import { useIsLight } from "~/hooks/theme"; +import { editorTheme } from "~/editor"; +import { useIsLight, useTheme } from "~/hooks/theme"; +import { useConfigStore } from "~/stores/config"; import { dedent } from "~/util/dedent"; import { iconCheck, iconCopy } from "~/util/icons"; import { Icon } from "../Icon"; interface EditorRef { editor: EditorView; - config: Compartment; - theme: Compartment; - wrap: Compartment; + configComp: Compartment; + themeComp: Compartment; + wrapComp: Compartment; } export interface CodePreviewProps extends PaperProps { @@ -44,6 +45,9 @@ export function CodePreview({ const editorRef = useRef(); const ref = useRef(null); + const colorScheme = useTheme(); + const syntaxTheme = useConfigStore((s) => s.settings.appearance.syntaxTheme); + const code = useMemo(() => { return withDedent ? dedent(value) : value; }, [value, withDedent]); @@ -52,18 +56,16 @@ export function CodePreview({ useEffect(() => { if (!ref.current) return; - const config = new Compartment(); - const theme = new Compartment(); - const wrap = new Compartment(); - const configExt = config.of(extensions || surrealql()); - const wrapExt = wrap.of(withWrapping ? EditorView.lineWrapping : []); + const configComp = new Compartment(); + const themeComp = new Compartment(); + const wrapComp = new Compartment(); const initialState = EditorState.create({ doc: code, extensions: [ - configExt, - theme.of(colorTheme(isLight)), - wrapExt, + configComp.of(extensions || surrealql()), + themeComp.of(editorTheme(colorScheme, syntaxTheme)), + wrapComp.of(withWrapping ? EditorView.lineWrapping : []), EditorState.readOnly.of(true), EditorView.editable.of(false), ], @@ -76,9 +78,9 @@ export function CodePreview({ editorRef.current = { editor, - config, - theme, - wrap, + configComp, + themeComp, + wrapComp, }; return () => { @@ -109,30 +111,30 @@ export function CodePreview({ useEffect(() => { if (!editorRef.current) return; - const { editor, config } = editorRef.current; + const { editor, configComp } = editorRef.current; editor.dispatch({ - effects: config.reconfigure(extensions || surrealql()), + effects: configComp.reconfigure(extensions || surrealql()), }); }, [extensions]); useEffect(() => { if (!editorRef.current) return; - const { editor, theme } = editorRef.current; + const { editor, themeComp } = editorRef.current; editor.dispatch({ - effects: theme.reconfigure(colorTheme(isLight)), + effects: themeComp.reconfigure(editorTheme(colorScheme, syntaxTheme)), }); - }, [isLight]); + }, [colorScheme, syntaxTheme]); useEffect(() => { if (!editorRef.current) return; - const { editor, wrap } = editorRef.current; + const { editor, wrapComp } = editorRef.current; editor.dispatch({ - effects: wrap.reconfigure(withWrapping ? EditorView.lineWrapping : []), + effects: wrapComp.reconfigure(withWrapping ? EditorView.lineWrapping : []), }); }, [withWrapping]); diff --git a/src/components/Inputs/index.tsx b/src/components/Inputs/index.tsx index 57fece37..f4e34c6e 100644 --- a/src/components/Inputs/index.tsx +++ b/src/components/Inputs/index.tsx @@ -1,5 +1,7 @@ import classes from "./style.module.scss"; +// TODO Split into multiple files + import { ActionIcon, Autocomplete, @@ -30,10 +32,11 @@ import { clamp, useInputState } from "@mantine/hooks"; import { surrealql } from "@surrealdb/codemirror"; import clsx from "clsx"; import { Icon } from "~/components/Icon"; -import { acceptWithTab, colorTheme, inputBase } from "~/editor"; +import { acceptWithTab, editorTheme, inputBase } from "~/editor"; import { useKindList } from "~/hooks/schema"; import { useStable } from "~/hooks/stable"; -import { useIsLight } from "~/hooks/theme"; +import { useIsLight, useTheme } from "~/hooks/theme"; +import { useConfigStore } from "~/stores/config"; import { iconCancel, iconCheck } from "~/util/icons"; export interface CodeInputProps @@ -69,24 +72,23 @@ export function CodeInput({ const ref = useRef(null); const editorRef = useRef<{ editor: EditorView; - editable: Compartment; - fallback: Compartment; - keymaps: Compartment; - theme: Compartment; + readOnlyComp: Compartment; + fallbackComp: Compartment; + keymapsComp: Compartment; + themeComp: Compartment; }>(); + const colorScheme = useTheme(); + const syntaxTheme = useConfigStore((s) => s.settings.appearance.syntaxTheme); + // biome-ignore lint/correctness/useExhaustiveDependencies: One-time initialization useEffect(() => { if (!ref.current) return; - const editable = new Compartment(); - const fallback = new Compartment(); - const keymaps = new Compartment(); - const theme = new Compartment(); - - const editableExt = editable.of(EditorState.readOnly.of(!!disabled || !!readOnly)); - const fallbackExt = fallback.of(placeholder ? ph(placeholder) : []); - const keymapsExt = keymaps.of([]); + const readOnlyComp = new Compartment(); + const fallbackComp = new Compartment(); + const keymapsComp = new Compartment(); + const themeComp = new Compartment(); const changeHandler = EditorView.updateListener.of((update) => { if (update.docChanged) { @@ -98,12 +100,12 @@ export function CodeInput({ doc: value, extensions: [ inputBase(), - colorTheme(isLight), - extensions || surrealql(), changeHandler, - editableExt, - fallbackExt, - keymapsExt, + extensions || surrealql(), + themeComp.of(editorTheme(colorScheme, syntaxTheme)), + readOnlyComp.of(EditorState.readOnly.of(!!disabled || !!readOnly)), + fallbackComp.of(placeholder ? ph(placeholder) : []), + keymapsComp.of([]), ], }); @@ -114,10 +116,10 @@ export function CodeInput({ editorRef.current = { editor, - editable, - fallback, - keymaps, - theme, + readOnlyComp, + fallbackComp, + keymapsComp, + themeComp, }; if (autoFocus) { @@ -158,29 +160,29 @@ export function CodeInput({ useEffect(() => { if (!editorRef.current) return; - const { editor, editable } = editorRef.current; + const { editor, readOnlyComp } = editorRef.current; const editableExt = EditorState.readOnly.of(!!disabled || !!readOnly); editor.dispatch({ - effects: editable.reconfigure(editableExt), + effects: readOnlyComp.reconfigure(editableExt), }); }, [disabled, readOnly]); useEffect(() => { if (!editorRef.current) return; - const { editor, fallback } = editorRef.current; + const { editor, fallbackComp } = editorRef.current; const fallbackExt = placeholder ? ph(placeholder) : []; editor.dispatch({ - effects: fallback.reconfigure(fallbackExt), + effects: fallbackComp.reconfigure(fallbackExt), }); }, [placeholder]); useEffect(() => { if (!editorRef.current) return; - const { editor, keymaps } = editorRef.current; + const { editor, keymapsComp } = editorRef.current; const value = Prec.highest( keymap.of( multiline @@ -198,19 +200,19 @@ export function CodeInput({ ); editor.dispatch({ - effects: keymaps.reconfigure(value), + effects: keymapsComp.reconfigure(value), }); }, [multiline, onSubmit]); useEffect(() => { if (!editorRef.current) return; - const { editor, theme } = editorRef.current; + const { editor, themeComp } = editorRef.current; editor.dispatch({ - effects: theme.reconfigure(colorTheme(isLight)), + effects: themeComp.reconfigure(editorTheme(colorScheme, syntaxTheme)), }); - }, [isLight]); + }, [colorScheme, syntaxTheme]); return ( controller.options.reader(state)); + + if (controller instanceof CheckboxController) { + return ( + { + applyPreference(controller.options.writer, event.target.checked); + }} + /> + ); + } + + if (controller instanceof NumberController) { + return ( + { + applyPreference( + controller.options.writer, + isNumber(input) ? input : Number.parseInt(input), + ); + }} + /> + ); + } + + if (controller instanceof TextController) { + return ( + { + applyPreference(controller.options.writer, e.currentTarget.value); + }} + /> + ); + } + + if (controller instanceof SelectionController) { + return ( +