mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'command-center-poc' of https://github.com/appwrite/console into command-center-poc
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { fade, scale } from 'svelte/transition';
|
||||
|
||||
export let show = false;
|
||||
export let size: 'small' | 'big' = null;
|
||||
export let error: string = null;
|
||||
export let closable = true;
|
||||
export let headerDivider = true;
|
||||
export let animate = true;
|
||||
|
||||
let dialog: HTMLDialogElement;
|
||||
let dialog: HTMLDivElement;
|
||||
let alert: HTMLElement;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -30,20 +32,14 @@
|
||||
}
|
||||
}
|
||||
function openModal() {
|
||||
if (dialog && !dialog.open) {
|
||||
dialog.showModal();
|
||||
document.documentElement.classList.add('u-overflow-hidden');
|
||||
}
|
||||
document.documentElement.classList.add('u-overflow-hidden');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (closable) {
|
||||
if (dialog && dialog.open) {
|
||||
dispatch('close');
|
||||
dialog.close();
|
||||
show = false;
|
||||
document.documentElement.classList.remove('u-overflow-hidden');
|
||||
}
|
||||
dispatch('close');
|
||||
show = false;
|
||||
document.documentElement.classList.remove('u-overflow-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,20 +66,38 @@
|
||||
|
||||
<svelte:window on:mousedown={handleBLur} on:keydown={handleKeydown} />
|
||||
|
||||
<dialog
|
||||
class="modal"
|
||||
class:is-small={size === 'small'}
|
||||
class:is-big={size === 'big'}
|
||||
class:is-separate-header={headerDivider}
|
||||
bind:this={dialog}
|
||||
on:cancel|preventDefault>
|
||||
{#if show}
|
||||
<slot />
|
||||
{/if}
|
||||
</dialog>
|
||||
{#if show}
|
||||
<div
|
||||
class="dialog"
|
||||
class:is-small={size === 'small'}
|
||||
class:is-big={size === 'big'}
|
||||
class:is-separate-header={headerDivider}
|
||||
on:cancel|preventDefault
|
||||
bind:this={dialog}
|
||||
transition:fade={{ duration: animate ? 150 : 0 }}>
|
||||
<div class="card" transition:scale={{ duration: animate ? 150 : 0, start: 0.9 }}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.modal {
|
||||
.dialog {
|
||||
padding: 0.5rem;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
background-color: hsl(var(--color-neutral-500) / 0.5);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-width: 400px;
|
||||
padding: 0.5rem;
|
||||
|
||||
position: absolute;
|
||||
top: clamp(128px, 20vh, 400px);
|
||||
left: 50%;
|
||||
translate: -50%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { debounce } from './debounce';
|
||||
import { isMac } from './platform';
|
||||
|
||||
export type Command = {
|
||||
keys: string[];
|
||||
@@ -16,18 +17,23 @@ export type Command = {
|
||||
|
||||
type CommandCenterState = {
|
||||
commandMap: Map<string, Command[]>;
|
||||
enabled: boolean;
|
||||
disabledMap: Map<string, boolean>;
|
||||
};
|
||||
|
||||
export const commandCenter = writable<CommandCenterState>({
|
||||
commandMap: new Map(),
|
||||
enabled: true
|
||||
disabledMap: new Map()
|
||||
});
|
||||
|
||||
export const commands = derived(commandCenter, ($commandCenter) => {
|
||||
return Array.from($commandCenter.commandMap.values()).flat();
|
||||
});
|
||||
|
||||
const commandsEnabled = derived(commandCenter, ($commandCenter) => {
|
||||
// If there's an item on the disabledMap that's true, then disable the command center
|
||||
return Array.from($commandCenter.disabledMap.values()).every((disabled) => !disabled);
|
||||
});
|
||||
|
||||
export const registerCommand = {
|
||||
subscribe(runner: (cb: (newCommands: Command[]) => void) => void) {
|
||||
const uuid = crypto.randomUUID();
|
||||
@@ -48,33 +54,59 @@ export const registerCommand = {
|
||||
}
|
||||
};
|
||||
|
||||
export const commandCenterKeyDownHandler = derived(commandCenter, ({ commandMap, enabled }) => {
|
||||
const commandsArr = Array.from(commandMap.values()).flat();
|
||||
const recentKeyCodes = new Set<number>();
|
||||
export const disableCommands = {
|
||||
subscribe(runner: (cb: (disabled: boolean) => void) => void) {
|
||||
const uuid = crypto.randomUUID();
|
||||
|
||||
return (event: KeyboardEvent) => {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
runner((disabled: boolean) => {
|
||||
commandCenter.update((curr) => {
|
||||
curr.disabledMap.set(uuid, disabled);
|
||||
return curr;
|
||||
});
|
||||
});
|
||||
|
||||
recentKeyCodes.add(event.keyCode);
|
||||
debounce(() => recentKeyCodes.clear(), 1000)();
|
||||
return () => {
|
||||
commandCenter.update((curr) => {
|
||||
curr.disabledMap.delete(uuid);
|
||||
return curr;
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
for (const command of commandsArr) {
|
||||
if (command.disabled || (!enabled && !command.forceEnable)) continue;
|
||||
export const commandCenterKeyDownHandler = derived(
|
||||
[commandCenter, commandsEnabled],
|
||||
([{ commandMap }, enabled]) => {
|
||||
const commandsArr = Array.from(commandMap.values()).flat();
|
||||
let recentKeyCodes: number[] = [];
|
||||
|
||||
const { keys, ctrl: meta, shift, alt, callback } = command;
|
||||
|
||||
const isMetaPressed = meta ? (isMac ? event.metaKey : event.ctrlKey) : true;
|
||||
const isShiftPressed = shift ? event.shiftKey : true;
|
||||
const isAltPressed = alt ? event.altKey : true;
|
||||
|
||||
const commandKeyCodes = keys.map((key) => key.toUpperCase().charCodeAt(0));
|
||||
const allKeysPressed = commandKeyCodes.every((keyCode) => recentKeyCodes.has(keyCode));
|
||||
|
||||
if (allKeysPressed && isMetaPressed && isShiftPressed && isAltPressed) {
|
||||
event.preventDefault();
|
||||
callback();
|
||||
return (event: KeyboardEvent) => {
|
||||
// ignore keypresses that come from input, textarea and select elements
|
||||
if (['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
recentKeyCodes.push(event.keyCode);
|
||||
debounce(() => (recentKeyCodes = []), 1000)();
|
||||
|
||||
for (const command of commandsArr) {
|
||||
if (command.disabled || (!enabled && !command.forceEnable)) continue;
|
||||
|
||||
const { keys, ctrl: meta, shift, alt, callback } = command;
|
||||
|
||||
const isMetaPressed = meta ? (isMac() ? event.metaKey : event.ctrlKey) : true;
|
||||
const isShiftPressed = shift ? event.shiftKey : true;
|
||||
const isAltPressed = alt ? event.altKey : true;
|
||||
|
||||
const commandKeyCodes = keys.map((key) => key.toUpperCase().charCodeAt(0));
|
||||
const allKeysPressed = recentKeyCodes.join('').includes(commandKeyCodes.join(''));
|
||||
|
||||
if (allKeysPressed && isMetaPressed && isShiftPressed && isAltPressed) {
|
||||
event.preventDefault();
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export function isMac(): boolean {
|
||||
console.log('isMac()', window.navigator.platform.toUpperCase().indexOf('MAC') >= 0);
|
||||
return window.navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
}
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
callback: () => {
|
||||
goto('/console');
|
||||
},
|
||||
keys: ['h'],
|
||||
alt: true
|
||||
keys: ['g', 'h']
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import Dialog from '$lib/components/dialog.svelte';
|
||||
import { commands, commandCenter, registerCommand } from '$lib/helpers/commandCenter';
|
||||
import { commands, disableCommands, registerCommand } from '$lib/helpers/commandCenter';
|
||||
import { isMac } from '$lib/helpers/platform';
|
||||
import { quadOut } from 'svelte/easing';
|
||||
import { crossfade } from 'svelte/transition';
|
||||
|
||||
let open = false;
|
||||
let search = '';
|
||||
@@ -30,10 +33,18 @@
|
||||
if (!open) return;
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
selected = selected === results.length - 1 ? 0 : selected + 1;
|
||||
if (event.metaKey) {
|
||||
selected = results.length - 1;
|
||||
} else {
|
||||
selected = selected === results.length - 1 ? results.length - 1 : selected + 1;
|
||||
}
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
selected = selected === 0 ? results.length - 1 : selected - 1;
|
||||
if (event.metaKey) {
|
||||
selected = 0;
|
||||
} else {
|
||||
selected = selected === 0 ? 0 : selected - 1;
|
||||
}
|
||||
} else if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (results[selected]) {
|
||||
@@ -41,6 +52,12 @@
|
||||
open = false;
|
||||
search = '';
|
||||
}
|
||||
} else if (event.key === 'Home') {
|
||||
event.preventDefault();
|
||||
selected = 0;
|
||||
} else if (event.key === 'End') {
|
||||
event.preventDefault();
|
||||
selected = results.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +66,17 @@
|
||||
selected = 0;
|
||||
}
|
||||
|
||||
$: $commandCenter.enabled = !open;
|
||||
$: $disableCommands(open);
|
||||
|
||||
const [send, receive] = crossfade({
|
||||
duration: 150,
|
||||
easing: quadOut
|
||||
});
|
||||
|
||||
afterNavigate(() => {
|
||||
open = false;
|
||||
search = '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
@@ -60,27 +87,35 @@
|
||||
|
||||
<ul class="u-margin-block-start-16 u-flex u-flex-vertical u-gap-8">
|
||||
{#each results as command, i}
|
||||
<li
|
||||
class="u-flex u-main-space-between result"
|
||||
data-selected={selected === i ? true : undefined}>
|
||||
<span>
|
||||
{command.label}
|
||||
</span>
|
||||
<div class="u-flex u-gap-4">
|
||||
{#if command.ctrl}
|
||||
<kbd class="kbd"> {isMac() ? '⌘' : 'ctrl'} </kbd>
|
||||
{/if}
|
||||
{#if command.shift}
|
||||
<kbd class="kbd"> {isMac() ? '⇧' : 'shift'} </kbd>
|
||||
{/if}
|
||||
{#if command.alt}
|
||||
<kbd class="kbd"> {isMac() ? '⌥' : 'alt'} </kbd>
|
||||
{/if}
|
||||
{#each command.keys as key}
|
||||
<kbd class="kbd">
|
||||
{key.toUpperCase()}
|
||||
</kbd>
|
||||
{/each}
|
||||
<li class="result" data-selected={selected === i ? true : undefined}>
|
||||
{#if selected === i}
|
||||
<div class="bg" in:send={{ key: 'bg' }} out:receive={{ key: 'bg' }} />
|
||||
{/if}
|
||||
<div class="u-flex u-main-space-between content">
|
||||
<span>
|
||||
{command.label}
|
||||
</span>
|
||||
<div class="u-flex u-gap-4 u-cross-center">
|
||||
{#if command.ctrl}
|
||||
<kbd class="kbd"> {isMac() ? '⌘' : 'ctrl'} </kbd>
|
||||
{/if}
|
||||
{#if command.shift}
|
||||
<kbd class="kbd"> {isMac() ? '⇧' : 'shift'} </kbd>
|
||||
{/if}
|
||||
{#if command.alt}
|
||||
<kbd class="kbd"> {isMac() ? '⌥' : 'alt'} </kbd>
|
||||
{/if}
|
||||
{#each command.keys as key, i}
|
||||
{@const hasNext = command.keys.length - 1 !== i}
|
||||
|
||||
<kbd class="kbd">
|
||||
{key.toUpperCase()}
|
||||
</kbd>
|
||||
{#if hasNext}
|
||||
<span class="u-margin-inline-4" style:opacity={0.5}>then</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{:else}
|
||||
@@ -99,12 +134,30 @@
|
||||
}
|
||||
|
||||
.result {
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
transition: 150ms;
|
||||
position: relative;
|
||||
|
||||
opacity: 0.65;
|
||||
transition: 75ms cubic-bezier(0.5, 1, 0.89, 1);
|
||||
}
|
||||
|
||||
.result[data-selected] {
|
||||
opacity: 1;
|
||||
transition: 150ms cubic-bezier(0.5, 1, 0.89, 1);
|
||||
}
|
||||
|
||||
.result .content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.result .bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: hsl(var(--color-neutral-200));
|
||||
border-radius: 0.75rem;
|
||||
translate: 0 -1px;
|
||||
}
|
||||
|
||||
.kbd {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { app } from '$lib/stores/app';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import Wizard from './keys/wizard.svelte';
|
||||
import { registerProjectCommand } from '../store';
|
||||
|
||||
export let projectId: string;
|
||||
|
||||
@@ -42,6 +43,8 @@
|
||||
$: onBoardIntro = $app.themeInUse === 'dark' ? OnboardDarkIntro : OnboardLightIntro;
|
||||
$: onBoardImage1 = $app.themeInUse === 'dark' ? OnboardDark1 : OnboardLight1;
|
||||
$: onBoardImage2 = $app.themeInUse === 'dark' ? OnboardDark2 : OnboardLight2;
|
||||
|
||||
$: $registerProjectCommand();
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
|
||||
@@ -72,7 +72,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
$: $registerProjectCommand();
|
||||
$: $registerProjectCommand([
|
||||
{
|
||||
label: 'Create Web App',
|
||||
callback: () => addPlatform(Platform.Web),
|
||||
keys: ['c']
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<div class="common-section u-flex u-gap-12">
|
||||
|
||||
@@ -7,51 +7,53 @@ import { derived, writable, type Readable } from 'svelte/store';
|
||||
|
||||
export const project = derived(page, ($page) => $page.data.project as Models.Project);
|
||||
|
||||
export const registerProjectCommand = derived([project, registerCommand], ([$project, $register]) => {
|
||||
return (c: Command[] = []) =>
|
||||
$register([
|
||||
...[
|
||||
{
|
||||
label: 'Go to Overview',
|
||||
keys: ['o'],
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}`);
|
||||
}
|
||||
},
|
||||
export const registerProjectCommand = derived(
|
||||
[project, registerCommand],
|
||||
([$project, $register]) => {
|
||||
return (c: Command[] = []) => {
|
||||
const projectCommands: Command[] = [
|
||||
{
|
||||
label: 'Go to Overview',
|
||||
keys: ['g', 'o'],
|
||||
|
||||
{
|
||||
label: 'Go to Auth',
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/auth`);
|
||||
},
|
||||
keys: ['a']
|
||||
},
|
||||
{
|
||||
label: 'Go to Databases',
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/databases`);
|
||||
},
|
||||
keys: ['d']
|
||||
},
|
||||
{
|
||||
label: 'Go to Functions',
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/functions`);
|
||||
},
|
||||
keys: ['f']
|
||||
},
|
||||
{
|
||||
label: 'Go to Storage',
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/storage`);
|
||||
},
|
||||
keys: ['s']
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}`);
|
||||
}
|
||||
],
|
||||
...(c ?? [])
|
||||
])
|
||||
|
||||
}) as Readable<(c?: Command[]) => void>;
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Go to Auth',
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/auth`);
|
||||
},
|
||||
keys: ['g', 'a']
|
||||
},
|
||||
{
|
||||
label: 'Go to Databases',
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/databases`);
|
||||
},
|
||||
keys: ['g', 'd']
|
||||
},
|
||||
{
|
||||
label: 'Go to Functions',
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/functions`);
|
||||
},
|
||||
keys: ['g', 'f']
|
||||
},
|
||||
{
|
||||
label: 'Go to Storage',
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/storage`);
|
||||
},
|
||||
keys: ['g', 's']
|
||||
}
|
||||
];
|
||||
return $register([...projectCommands, ...(c ?? [])]);
|
||||
};
|
||||
}
|
||||
) as Readable<(c?: Command[]) => void>;
|
||||
|
||||
export const onboarding = derived(
|
||||
project,
|
||||
|
||||
Reference in New Issue
Block a user