mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-05-02 07:53:58 +00:00
shell tab
This commit is contained in:
@@ -5,6 +5,7 @@ import { ThemeDefinition } from 'dbgate-types';
|
|||||||
import ConnectionModal from '../modals/ConnectionModal.svelte';
|
import ConnectionModal from '../modals/ConnectionModal.svelte';
|
||||||
import { showModal } from '../modals/modalTools';
|
import { showModal } from '../modals/modalTools';
|
||||||
import newQuery from '../query/newQuery';
|
import newQuery from '../query/newQuery';
|
||||||
|
import saveTabFile, { saveTabEnabledStore } from '../utility/saveTabFile';
|
||||||
|
|
||||||
function themeCommand(theme: ThemeDefinition) {
|
function themeCommand(theme: ThemeDefinition) {
|
||||||
return {
|
return {
|
||||||
@@ -64,3 +65,94 @@ registerCommand({
|
|||||||
keyText: 'Ctrl+Q',
|
keyText: 'Ctrl+Q',
|
||||||
onClick: () => newQuery(),
|
onClick: () => newQuery(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function registerFileCommands({
|
||||||
|
idPrefix,
|
||||||
|
category,
|
||||||
|
editorStore,
|
||||||
|
editorStatusStore,
|
||||||
|
folder,
|
||||||
|
format,
|
||||||
|
fileExtension,
|
||||||
|
execute = false,
|
||||||
|
toggleComment = false,
|
||||||
|
findReplace = false,
|
||||||
|
}) {
|
||||||
|
registerCommand({
|
||||||
|
id: idPrefix + '.save',
|
||||||
|
category,
|
||||||
|
name: 'Save',
|
||||||
|
keyText: 'Ctrl+S',
|
||||||
|
icon: 'icon save',
|
||||||
|
toolbar: true,
|
||||||
|
enabledStore: saveTabEnabledStore(editorStore),
|
||||||
|
onClick: () => saveTabFile(editorStore, false, folder, format, fileExtension),
|
||||||
|
});
|
||||||
|
registerCommand({
|
||||||
|
id: idPrefix + '.saveAs',
|
||||||
|
category,
|
||||||
|
name: 'Save As',
|
||||||
|
keyText: 'Ctrl+Shift+S',
|
||||||
|
enabledStore: saveTabEnabledStore(editorStore),
|
||||||
|
onClick: () => saveTabFile(editorStore, true, folder, format, fileExtension),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (execute) {
|
||||||
|
registerCommand({
|
||||||
|
id: idPrefix + '.execute',
|
||||||
|
category,
|
||||||
|
name: 'Execute',
|
||||||
|
icon: 'icon run',
|
||||||
|
toolbar: true,
|
||||||
|
keyText: 'F5 | Ctrl+Enter',
|
||||||
|
enabledStore: derived(
|
||||||
|
[editorStore, editorStatusStore],
|
||||||
|
([editor, status]) => editor != null && !(status as any).busy
|
||||||
|
),
|
||||||
|
onClick: () => (get(editorStore) as any).execute(),
|
||||||
|
});
|
||||||
|
registerCommand({
|
||||||
|
id: idPrefix + '.kill',
|
||||||
|
category,
|
||||||
|
name: 'Kill',
|
||||||
|
icon: 'icon close',
|
||||||
|
toolbar: true,
|
||||||
|
enabledStore: derived(
|
||||||
|
[editorStore, editorStatusStore],
|
||||||
|
([query, status]) => query != null && status && (status as any).isConnected
|
||||||
|
),
|
||||||
|
onClick: () => (get(editorStore) as any).kill(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toggleComment) {
|
||||||
|
registerCommand({
|
||||||
|
id: idPrefix + '.toggleComment',
|
||||||
|
category,
|
||||||
|
name: 'Toggle comment',
|
||||||
|
keyText: 'Ctrl+/',
|
||||||
|
disableHandleKeyText: 'Ctrl+/',
|
||||||
|
enabledStore: derived(editorStore, query => query != null),
|
||||||
|
onClick: () => (get(editorStore) as any).toggleComment(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (findReplace) {
|
||||||
|
registerCommand({
|
||||||
|
id: idPrefix + '.find',
|
||||||
|
category,
|
||||||
|
name: 'Find',
|
||||||
|
keyText: 'Ctrl+F',
|
||||||
|
enabledStore: derived(editorStore, query => query != null),
|
||||||
|
onClick: () => (get(editorStore) as any).find(),
|
||||||
|
});
|
||||||
|
registerCommand({
|
||||||
|
id: idPrefix + '.replace',
|
||||||
|
category,
|
||||||
|
keyText: 'Ctrl+H',
|
||||||
|
name: 'Replace',
|
||||||
|
enabledStore: derived(editorStore, query => query != null),
|
||||||
|
onClick: () => (get(editorStore) as any).replace(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
86
packages/web/src/elements/TableControl.svelte
Normal file
86
packages/web/src/elements/TableControl.svelte
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { compact } from 'lodash';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
interface TableColumn {
|
||||||
|
fieldName: string;
|
||||||
|
header: string;
|
||||||
|
component?: any;
|
||||||
|
getProps?: any;
|
||||||
|
formatter?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let columns: TableColumn[];
|
||||||
|
export let rows;
|
||||||
|
export let focusOnCreate = false;
|
||||||
|
export let selectable = false;
|
||||||
|
export let selectedIndex = 0;
|
||||||
|
|
||||||
|
$: columnList = _.compact(_.flatten(columns));
|
||||||
|
|
||||||
|
let domTable;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (focusOnCreate) domTable.focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<table bind:this={domTable} class:selectable>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{#each columnList as col}
|
||||||
|
<td>{col.header}</td>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each rows as row, index}
|
||||||
|
<tr
|
||||||
|
class:selected={selectable && selectedIndex == index}
|
||||||
|
on:click={() => {
|
||||||
|
if (selectable) {
|
||||||
|
selectedIndex = index;
|
||||||
|
domTable.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#each columnList as col}
|
||||||
|
{#if col.component}
|
||||||
|
<svelte:component this={col.component} {...col.getProps(row)} />
|
||||||
|
{:else if col.formatter}
|
||||||
|
{col.formatter(row)}
|
||||||
|
{:else}
|
||||||
|
{row[col.fieldName]}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
table.selectable {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
tbody tr {
|
||||||
|
background: var(--theme-bg-0);
|
||||||
|
}
|
||||||
|
tbody tr.selected {
|
||||||
|
background: var(--theme-bg-selected);
|
||||||
|
}
|
||||||
|
thead td {
|
||||||
|
border: 1px solid var(--theme-border);
|
||||||
|
background-color: var(--theme-bg-1);
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
tbody td {
|
||||||
|
border: 1px solid var(--theme-border);
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
19
packages/web/src/query/RunnerOutputFiles/CopyLink.svelte
Normal file
19
packages/web/src/query/RunnerOutputFiles/CopyLink.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import getElectron from '../../utility/getElectron';
|
||||||
|
|
||||||
|
const electron = getElectron();
|
||||||
|
export let row;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
on:click={() => {
|
||||||
|
const file = electron.remote.dialog.showSaveDialogSync(electron.remote.getCurrentWindow(), {});
|
||||||
|
if (file) {
|
||||||
|
const fs = window.require('fs');
|
||||||
|
fs.copyFile(row.path, file, () => {});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
save
|
||||||
|
</a>
|
||||||
10
packages/web/src/query/RunnerOutputFiles/DownloadLink.svelte
Normal file
10
packages/web/src/query/RunnerOutputFiles/DownloadLink.svelte
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import resolveApi from '../../utility/resolveApi';
|
||||||
|
|
||||||
|
export let runnerId;
|
||||||
|
export let row;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href={`${resolveApi()}/runners/data/${runnerId}/${row.name}`} target="_blank" rel="noopener noreferrer">
|
||||||
|
download
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ErrorInfo from '../../elements/ErrorInfo.svelte';
|
||||||
|
import TableControl from '../../elements/TableControl.svelte';
|
||||||
|
|
||||||
|
import axiosInstance from '../../utility/axiosInstance';
|
||||||
|
import formatFileSize from '../../utility/formatFileSize';
|
||||||
|
import getElectron from '../../utility/getElectron';
|
||||||
|
import socket from '../../utility/socket';
|
||||||
|
import useEffect from '../../utility/useEffect';
|
||||||
|
import CopyLink from './CopyLink.svelte';
|
||||||
|
import DownloadLink from './DownloadLink.svelte';
|
||||||
|
import ShowLink from './ShowLink.svelte';
|
||||||
|
|
||||||
|
export let runnerId;
|
||||||
|
export let executeNumber;
|
||||||
|
|
||||||
|
let files = [];
|
||||||
|
|
||||||
|
$: if (executeNumber >= 0) files = [];
|
||||||
|
|
||||||
|
$: effect = useEffect(() => registerRunnerDone(runnerId));
|
||||||
|
|
||||||
|
const electron = getElectron();
|
||||||
|
|
||||||
|
function registerRunnerDone(rid) {
|
||||||
|
if (rid) {
|
||||||
|
socket.on(`runner-done-${rid}`, handleRunnerDone);
|
||||||
|
return () => {
|
||||||
|
socket.off(`runner-done-${rid}`, handleRunnerDone);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: $effect;
|
||||||
|
|
||||||
|
const handleRunnerDone = async () => {
|
||||||
|
const resp = await axiosInstance.get(`runners/files?runid=${runnerId}`);
|
||||||
|
files = resp.data;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !files || files.length == 0}
|
||||||
|
<ErrorInfo message="No output files" icon="img alert" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<TableControl
|
||||||
|
rows={files}
|
||||||
|
columns={[
|
||||||
|
{ fieldName: 'name', header: 'Name' },
|
||||||
|
{ fieldName: 'size', header: 'Size', formatter: row => formatFileSize(row.size) },
|
||||||
|
!electron && {
|
||||||
|
fieldName: 'download',
|
||||||
|
header: 'Download',
|
||||||
|
component: DownloadLink,
|
||||||
|
getProps: row => ({
|
||||||
|
row,
|
||||||
|
runnerId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
!electron && {
|
||||||
|
fieldName: 'copy',
|
||||||
|
header: 'Copy',
|
||||||
|
component: CopyLink,
|
||||||
|
getProps: row => ({
|
||||||
|
row,
|
||||||
|
runnerId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
!electron && {
|
||||||
|
fieldName: 'show',
|
||||||
|
header: 'Show',
|
||||||
|
component: ShowLink,
|
||||||
|
getProps: row => ({
|
||||||
|
row,
|
||||||
|
runnerId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
15
packages/web/src/query/RunnerOutputFiles/ShowLink.svelte
Normal file
15
packages/web/src/query/RunnerOutputFiles/ShowLink.svelte
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import getElectron from '../../utility/getElectron';
|
||||||
|
|
||||||
|
const electron = getElectron();
|
||||||
|
export let row;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
on:click={() => {
|
||||||
|
electron.remote.shell.showItemInFolder(row.path);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
show
|
||||||
|
</a>
|
||||||
1
packages/web/src/query/RunnerOutputFiles/index.js
Normal file
1
packages/web/src/query/RunnerOutputFiles/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './RunnerOutputFiles.svelte';
|
||||||
28
packages/web/src/query/RunnerOutputPane.svelte
Normal file
28
packages/web/src/query/RunnerOutputPane.svelte
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
|
||||||
|
import WidgetTitle from '../widgets/WidgetTitle.svelte';
|
||||||
|
import RunnerOutputFiles from './RunnerOutputFiles';
|
||||||
|
import SocketMessageView from './SocketMessageView.svelte';
|
||||||
|
|
||||||
|
export let runnerId;
|
||||||
|
export let executeNumber;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<HorizontalSplitter>
|
||||||
|
<div class="container">
|
||||||
|
<WidgetTitle>Messages</WidgetTitle>
|
||||||
|
<SocketMessageView eventName={runnerId ? `runner-info-${runnerId}` : null} {executeNumber} />
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<WidgetTitle>Output files</WidgetTitle>
|
||||||
|
<RunnerOutputFiles {runnerId} {executeNumber} />
|
||||||
|
</div>
|
||||||
|
</HorizontalSplitter>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,40 +5,6 @@
|
|||||||
);
|
);
|
||||||
const currentQueryStatus = memberStore(currentQuery, query => query?.getStatus() || nullStore);
|
const currentQueryStatus = memberStore(currentQuery, query => query?.getStatus() || nullStore);
|
||||||
|
|
||||||
registerCommand({
|
|
||||||
id: 'query.execute',
|
|
||||||
category: 'Query',
|
|
||||||
name: 'Execute',
|
|
||||||
icon: 'icon run',
|
|
||||||
toolbar: true,
|
|
||||||
keyText: 'F5 | Ctrl+Enter',
|
|
||||||
enabledStore: derived(
|
|
||||||
[currentQuery, currentQueryStatus],
|
|
||||||
([query, status]) => query != null && !(status as any).busy
|
|
||||||
),
|
|
||||||
onClick: () => get(currentQuery).execute(),
|
|
||||||
});
|
|
||||||
registerCommand({
|
|
||||||
id: 'query.kill',
|
|
||||||
category: 'Query',
|
|
||||||
name: 'Kill',
|
|
||||||
icon: 'icon close',
|
|
||||||
toolbar: true,
|
|
||||||
enabledStore: derived(
|
|
||||||
[currentQuery, currentQueryStatus],
|
|
||||||
([query, status]) => query != null && status && (status as any).isConnected
|
|
||||||
),
|
|
||||||
onClick: () => get(currentQuery).kill(),
|
|
||||||
});
|
|
||||||
registerCommand({
|
|
||||||
id: 'query.toggleComment',
|
|
||||||
category: 'Query',
|
|
||||||
name: 'Toggle comment',
|
|
||||||
keyText: 'Ctrl+/',
|
|
||||||
disableHandleKeyText: 'Ctrl+/',
|
|
||||||
enabledStore: derived(currentQuery, query => query != null),
|
|
||||||
onClick: () => get(currentQuery).toggleComment(),
|
|
||||||
});
|
|
||||||
registerCommand({
|
registerCommand({
|
||||||
id: 'query.formatCode',
|
id: 'query.formatCode',
|
||||||
category: 'Query',
|
category: 'Query',
|
||||||
@@ -46,29 +12,18 @@
|
|||||||
enabledStore: derived(currentQuery, query => query != null),
|
enabledStore: derived(currentQuery, query => query != null),
|
||||||
onClick: () => get(currentQuery).formatCode(),
|
onClick: () => get(currentQuery).formatCode(),
|
||||||
});
|
});
|
||||||
registerCommand({
|
registerFileCommands({
|
||||||
id: 'query.find',
|
|
||||||
category: 'Query',
|
|
||||||
name: 'Find',
|
|
||||||
keyText: 'Ctrl+F',
|
|
||||||
enabledStore: derived(currentQuery, query => query != null),
|
|
||||||
onClick: () => get(currentQuery).find(),
|
|
||||||
});
|
|
||||||
registerCommand({
|
|
||||||
id: 'query.replace',
|
|
||||||
category: 'Query',
|
|
||||||
keyText: 'Ctrl+H',
|
|
||||||
name: 'Replace',
|
|
||||||
enabledStore: derived(currentQuery, query => query != null),
|
|
||||||
onClick: () => get(currentQuery).replace(),
|
|
||||||
});
|
|
||||||
registerSaveCommands({
|
|
||||||
idPrefix: 'query',
|
idPrefix: 'query',
|
||||||
category: 'Query',
|
category: 'Query',
|
||||||
editorStore: currentQuery,
|
editorStore: currentQuery,
|
||||||
|
editorStatusStore: currentQueryStatus,
|
||||||
folder: 'sql',
|
folder: 'sql',
|
||||||
format: 'text',
|
format: 'text',
|
||||||
fileExtension: 'sql',
|
fileExtension: 'sql',
|
||||||
|
|
||||||
|
execute: true,
|
||||||
|
toggleComment:true,
|
||||||
|
findReplace:true
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -93,7 +48,7 @@
|
|||||||
import memberStore from '../utility/memberStore';
|
import memberStore from '../utility/memberStore';
|
||||||
import useEffect from '../utility/useEffect';
|
import useEffect from '../utility/useEffect';
|
||||||
import ResultTabs from '../query/ResultTabs.svelte';
|
import ResultTabs from '../query/ResultTabs.svelte';
|
||||||
import saveTabFile, { registerSaveCommands, saveTabEnabledStore } from '../utility/saveTabFile';
|
import { registerFileCommands } from '../commands/stdCommands';
|
||||||
|
|
||||||
export let tabid;
|
export let tabid;
|
||||||
export let conid;
|
export let conid;
|
||||||
|
|||||||
6
packages/web/src/utility/formatFileSize.ts
Normal file
6
packages/web/src/utility/formatFileSize.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default function formatFileSize(size) {
|
||||||
|
if (size > 1000000000) return `${Math.round(size / 10000000000) * 10} GB`;
|
||||||
|
if (size > 1000000) return `${Math.round(size / 10000000) * 10} MB`;
|
||||||
|
if (size > 1000) return `${Math.round(size / 10000) * 10} KB`;
|
||||||
|
return `${size} bytes`;
|
||||||
|
}
|
||||||
@@ -52,24 +52,3 @@ export default function saveTabFile(editorStore, saveAs, folder, format, fileExt
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerSaveCommands({ idPrefix, category, editorStore, folder, format, fileExtension }) {
|
|
||||||
registerCommand({
|
|
||||||
id: idPrefix + '.save',
|
|
||||||
category,
|
|
||||||
name: 'Save',
|
|
||||||
keyText: 'Ctrl+S',
|
|
||||||
icon: 'icon save',
|
|
||||||
toolbar: true,
|
|
||||||
enabledStore: saveTabEnabledStore(editorStore),
|
|
||||||
onClick: () => saveTabFile(editorStore, false, folder, format, fileExtension),
|
|
||||||
});
|
|
||||||
registerCommand({
|
|
||||||
id: idPrefix + '.saveAs',
|
|
||||||
category,
|
|
||||||
name: 'Save As',
|
|
||||||
keyText: 'Ctrl+Shift+S',
|
|
||||||
enabledStore: saveTabEnabledStore(editorStore),
|
|
||||||
onClick: () => saveTabFile(editorStore, true, folder, format, fileExtension),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import WidgetTitle from './WidgetTitle.svelte';
|
||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
export let name;
|
export let name;
|
||||||
export let height = null;
|
export let height = null;
|
||||||
@@ -7,21 +9,8 @@
|
|||||||
let visible = !collapsed;
|
let visible = !collapsed;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="title" on:click={() => (visible = !visible)}>{title}</div>
|
<WidgetTitle on:click={() => (visible = !visible)}>{title}</WidgetTitle>
|
||||||
|
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<slot />
|
<slot />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
|
||||||
.title {
|
|
||||||
padding: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
background-color: var(--theme-bg-2);
|
|
||||||
border: 1px solid var(--theme-border);
|
|
||||||
}
|
|
||||||
.title:hover {
|
|
||||||
background-color: var(--theme-bg-3);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
16
packages/web/src/widgets/WidgetTitle.svelte
Normal file
16
packages/web/src/widgets/WidgetTitle.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<div on:click>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
padding: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background-color: var(--theme-bg-2);
|
||||||
|
border: 1px solid var(--theme-border);
|
||||||
|
}
|
||||||
|
div:hover {
|
||||||
|
background-color: var(--theme-bg-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user