Merge branch 'command-center-poc' of https://github.com/appwrite/console into command-center-poc

This commit is contained in:
tglide
2023-05-16 15:46:08 +01:00
8 changed files with 230 additions and 122 deletions
+37 -23
View File
@@ -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>
+58 -26
View File
@@ -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
View File
@@ -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;
}
+1 -2
View File
@@ -34,8 +34,7 @@
callback: () => {
goto('/console');
},
keys: ['h'],
alt: true
keys: ['g', 'h']
}
]);
+79 -26
View File
@@ -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">
+45 -43
View File
@@ -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,