Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add command menu for navigation #20

Merged
merged 31 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ee5d385
Groundwork for search dialog
ieedan Jul 24, 2024
2c106ec
All the commands are there and rendering icons
ieedan Jul 24, 2024
2d5d8d5
Final implementation details
ieedan Jul 24, 2024
fb4a789
drop command k
ieedan Jul 24, 2024
7f33503
cleanup + acessability
ieedan Jul 25, 2024
5c24ea6
change width to reflect Vercel's
ieedan Jul 25, 2024
4515c10
prevent default on Ctrl + K
ieedan Jul 25, 2024
e19851b
Fixes review comments
ieedan Jul 25, 2024
0759054
Format and lint
ieedan Jul 25, 2024
e8cca83
Merge remote-tracking branch 'upstream/main'
ieedan Jul 25, 2024
1833aa5
Fix sizing of header with next button
ieedan Jul 25, 2024
271f24e
Attempt to fix details
ieedan Jul 25, 2024
440d655
format and lint
ieedan Jul 25, 2024
eb69861
Refactor with cmdk-sv
ieedan Jul 26, 2024
0782fd1
test if i can commit directly to `main` of a fork
shyakadavis Jul 26, 2024
3ee58e6
ff support and snake case
ieedan Jul 26, 2024
14571f2
Merge branch 'main' of https://github.com/ieedan/geist
ieedan Jul 26, 2024
a3532ec
Button component in header
ieedan Jul 26, 2024
b1403a9
camel case
ieedan Jul 26, 2024
e0aace1
double focus trap
ieedan Jul 26, 2024
3a4bbcb
format lint
ieedan Jul 26, 2024
6506f58
format lint remove dead code
ieedan Jul 26, 2024
df55ebf
snake casing
ieedan Jul 26, 2024
95fdc8d
Snake casing
ieedan Jul 26, 2024
bc18fd2
Update header.svelte
ieedan Jul 26, 2024
9d24580
`cmd` + `k`
shyakadavis Jul 27, 2024
fd37786
some ghost classes, more composable stuff, focus
shyakadavis Jul 27, 2024
d6dc7b8
more
shyakadavis Jul 27, 2024
555a9ab
ghost classes, more composable stuff
shyakadavis Jul 27, 2024
d439c31
more
shyakadavis Jul 27, 2024
bb56007
stick to how vercel naming
shyakadavis Jul 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@
"dependencies": {
"bits-ui": "^0.21.12",
"clsx": "^2.1.1",
"cmdk-sv": "^0.0.18",
"svelte-legos": "^0.2.3",
"tailwind-merge": "^2.4.0",
"tailwind-variants": "^0.2.1"
"tailwind-variants": "^0.2.1",
"vaul-svelte": "^0.3.2"
},
"packageManager": "pnpm@9.5.0"
}
443 changes: 240 additions & 203 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/lib/assets/icons/cross.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/lib/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ClockDashed from './clock-dashed.svg?component';
import CodeBracket from './code-bracket.svg?component';
import Command from './command.svg?component';
import Copy from './copy.svg?component';
import Cross from './cross.svg?component';
import Database from './database.svg?component';
import ErrorStates from './error-states.svg?component';
import ExternalSmall from './external-small.svg?component';
Expand All @@ -38,6 +39,7 @@ import LogoTurborepo from './logo-turborepo.svg?component';
import LogoV0 from './logo-v0.svg?component';
import LogoVercelCircle from './logo-vercel-circle.svg?component';
import LogoVercel from './logo-vercel.svg?component';
import MagnifyingGlass from './magnifying-glass.svg?component';
import Minus from './minus.svg?component';
import Notification from './notification.svg?component';
import Paperclip from './paperclip.svg?component';
Expand Down Expand Up @@ -70,6 +72,7 @@ export const Icons = {
CodeBracket,
Command,
Copy,
Cross,
Database,
ErrorStates,
ExternalSmall,
Expand All @@ -93,6 +96,7 @@ export const Icons = {
LogoV0,
LogoVercelCircle,
LogoVercel,
MagnifyingGlass,
Minus,
Notification,
Paperclip,
Expand Down
6 changes: 6 additions & 0 deletions src/lib/assets/icons/magnifying-glass.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions src/lib/components/shared/command/command-list.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { Icons } from '$lib/assets/icons';
import { Badge } from '$lib/components/ui/badge';
import * as Command from '$lib/components/ui/command';
import { aside_items } from '$lib/config/sitemap';
import { command_open_state } from '$lib/stores';
import { cn } from '$lib/utils';

export let search: string;

let className = '';

export { className as class };

const navigate = (link: string) => {
goto(link);
command_open_state.toggle();
};
</script>

<Command.List class={cn('px-1 pb-1', className)} {...$$restProps}>
<Command.Empty class="text-gray-600">
No results found for
<span class="font-semibold text-gray-1000">
"{search}"
</span>.
</Command.Empty>
{#each Object.entries(aside_items) as item}
{@const [group, links] = item}
<Command.Group
heading={group}
class="[&_[data-cmdk-group-heading]]:py-3 [&_[data-cmdk-group-heading]]:capitalize [&_[data-cmdk-group-heading]]:text-gray-700"
>
{#each links as link}
{@const disabled = link.status == 'soon'}
<Command.Item
value={link.title}
onSelect={() => navigate(link.href)}
{disabled}
class="flex place-items-center gap-3 rounded-md py-3 hover:cursor-pointer hover:bg-gray-100 aria-selected:bg-gray-100"
>
{#if link.icon}
<span class="text-gray-1000">
<svelte:component this={link.icon} aria-hidden="true" width="16" height="16" />
</span>
{:else}
<span class="text-gray-1000">
<Icons.ArrowRight width="16" height="16" class="text-gray-600" aria-hidden="true" />
</span>
{/if}
<span>{link.title}</span>
{#if link.status === 'new'}
<Badge variant="blue" size="sm">New</Badge>
{/if}
{#if link.status === 'soon'}
<Badge variant="gray-subtle" size="sm">Soon</Badge>
{/if}
{#if link.status === 'draft'}
<Badge variant="purple-subtle" size="sm">Draft</Badge>
{/if}
</Command.Item>
{/each}
</Command.Group>
{/each}
</Command.List>
62 changes: 62 additions & 0 deletions src/lib/components/shared/command/command.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as Command from '$lib/components/ui/command';
import * as Drawer from '$lib/components/ui/drawer/index.js';
import { command_open_state } from '$lib/stores';
import { mediaQuery } from 'svelte-legos';
import CommandList from './command-list.svelte';

function doc_keydown(e: KeyboardEvent) {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
command_open_state.toggle();
}
}

const is_desktop = mediaQuery('(min-width: 640px)');

let search = '';
</script>

<svelte:document on:keydown={doc_keydown} />

{#if $is_desktop}
<Command.Dialog
bind:open={$command_open_state}
class="top-[43%] max-w-[640px] sm:rounded-xl"
close_button="esc"
>
<Command.Input
placeholder="Search..."
hide_search_icon
bind:value={search}
class="h-[53px] p-0 px-1 text-lg"
/>
<CommandList {search} class="h-[436px] max-h-none" />
</Command.Dialog>
{:else}
<Drawer.Root bind:open={$command_open_state}>
<Drawer.Content class="h-3/4" hide_dismiss_bar>
<Command.Root>
<Drawer.Header class="flex h-[53px] items-center justify-between border-b px-2">
<Command.Input
hide_search_icon
placeholder="Search..."
class="h-7 flex-grow border-none text-lg"
bind:value={search}
wrapper_class="border-none w-full flex items-center"
/>
<Button
on:click={command_open_state.toggle}
size="sm"
variant="secondary"
class="h-5 px-1.5 text-xs"
>
Esc
</Button>
</Drawer.Header>
<CommandList {search} class="max-h-full" />
</Command.Root>
</Drawer.Content>
</Drawer.Root>
{/if}
6 changes: 6 additions & 0 deletions src/lib/components/shared/demo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,9 @@
</details>
</div>
</div>

<style>
summary::-webkit-details-marker {
display: none;
}
</style>
4 changes: 2 additions & 2 deletions src/lib/components/ui/badge/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Icons } from '$lib/assets/icons';
import { Icons } from '$lib/assets/icons';
import { type VariantProps, tv } from 'tailwind-variants';

export { default as Badge } from './badge.svelte';
export const badge_variants = tv({
base: 'inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-full font-medium capitalize tabular-nums transition-colors hover:cursor-text focus:outline-none focus:ring-2 focus:ring-focus-color focus:ring-offset-2',
base: 'inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-full font-medium capitalize tabular-nums transition-colors hover:cursor-auto focus:outline-none focus:ring-2 focus:ring-focus-color focus:ring-offset-2',
variants: {
variant: {
gray: 'bg-gray-700 text-contrast-fg',
Expand Down
41 changes: 41 additions & 0 deletions src/lib/components/ui/command/command-dialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { cn } from '$lib/utils';
import type { Dialog as DialogPrimitive } from 'bits-ui';
import type { Command as CommandPrimitive } from 'cmdk-sv';
import Command from './command.svelte';

// TODO: Yuck! I can't believe I'm defining a prop in 2 different places. (Check dialog-content.svelte)
// Find a way to define this prop in one place.
type $$Props = DialogPrimitive.Props &
CommandPrimitive.CommandProps & {
close_button?: 'esc' | 'x';
};

export let open: $$Props['open'] = false;
export let value: $$Props['value'] = undefined;
let class_name: string | undefined | null = undefined;
export { class_name as class };
export let close_button: $$Props['close_button'] = 'x';
</script>

<Dialog.Root bind:open {...$$restProps}>
<Dialog.Content class={cn('overflow-hidden p-0 shadow-lg', class_name)} {close_button}>
<Command
class={cn(
// Command Heading
'[&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:font-medium [&_[data-cmdk-group-heading]]:capitalize [&_[data-cmdk-group-heading]]:text-gray-900',
// Command Group
'[&_[data-cmdk-group]:not([hidden])_~[data-cmdk-group]]:pt-0 [&_[data-cmdk-group]]:px-2',
// Command Input
'[&_[data-cmdk-input-wrapper]_svg]:size-5 [&_[data-cmdk-input]]:h-12',
// Command Item
'[&_[data-cmdk-item]]:px-2 [&_[data-cmdk-item]]:py-3 [&_[data-cmdk-item]_svg]:size-5'
)}
{...$$restProps}
bind:value
>
<slot />
</Command>
</Dialog.Content>
</Dialog.Root>
12 changes: 12 additions & 0 deletions src/lib/components/ui/command/command-empty.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils.js';

type $$Props = CommandPrimitive.EmptyProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>

<CommandPrimitive.Empty class={cn('py-6 text-center text-sm', className)} {...$$restProps}>
<slot />
</CommandPrimitive.Empty>
18 changes: 18 additions & 0 deletions src/lib/components/ui/command/command-group.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Command as CommandPrimitive } from 'cmdk-sv';
type $$Props = CommandPrimitive.GroupProps;

let className: string | undefined | null = undefined;
export { className as class };
</script>

<CommandPrimitive.Group
class={cn(
'overflow-hidden p-1 text-gray-1000 [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:py-1.5 [&_[data-cmdk-group-heading]]:text-xs [&_[data-cmdk-group-heading]]:font-medium [&_[data-cmdk-group-heading]]:text-gray-900',
className
)}
{...$$restProps}
>
<slot />
</CommandPrimitive.Group>
30 changes: 30 additions & 0 deletions src/lib/components/ui/command/command-input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import { Icons } from '$lib/assets/icons';
import { cn } from '$lib/utils.js';
import { Command as CommandPrimitive } from 'cmdk-sv';

type $$Props = CommandPrimitive.InputProps & {
wrapper_class?: string | undefined;
hide_search_icon?: boolean;
};

let className: string | undefined | null = undefined;
export { className as class };
export let value: string = '';
export let wrapper_class: $$Props['wrapper_class'] = undefined;
export let hide_search_icon: $$Props['hide_search_icon'] = false;
</script>

<div class={cn('flex items-center border-b px-2', wrapper_class)} data-cmdk-input-wrapper="">
{#if !hide_search_icon}
<Icons.MagnifyingGlass class="mr-2 size-4 shrink-0 opacity-50" />
{/if}
<CommandPrimitive.Input
class={cn(
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-gray-700 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...$$restProps}
bind:value
/>
</div>
24 changes: 24 additions & 0 deletions src/lib/components/ui/command/command-item.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Command as CommandPrimitive } from 'cmdk-sv';

type $$Props = CommandPrimitive.ItemProps;

export let asChild = false;

let className: string | undefined | null = undefined;
export { className as class };
</script>

<CommandPrimitive.Item
{asChild}
class={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-gray-alpha-100 aria-selected:text-gray-1000 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...$$restProps}
let:action
let:attrs
>
<slot {action} {attrs} />
</CommandPrimitive.Item>
17 changes: 17 additions & 0 deletions src/lib/components/ui/command/command-list.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Command as CommandPrimitive } from 'cmdk-sv';

type $$Props = CommandPrimitive.ListProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>

<!-- ? This element is oddly trapping focus when you tab from `input` trying to get to `close`. i.e. it's input->list->close. Manually specifying `-1` because I don't think it should be focusable. -->
<CommandPrimitive.List
tabindex={-1}
class={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
{...$$restProps}
>
<slot />
</CommandPrimitive.List>
10 changes: 10 additions & 0 deletions src/lib/components/ui/command/command-separator.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Command as CommandPrimitive } from 'cmdk-sv';

type $$Props = CommandPrimitive.SeparatorProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>

<CommandPrimitive.Separator class={cn('-mx-1 h-px bg-gray-400', className)} {...$$restProps} />
13 changes: 13 additions & 0 deletions src/lib/components/ui/command/command-shortcut.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';

type $$Props = HTMLAttributes<HTMLSpanElement>;

let className: string | undefined | null = undefined;
export { className as class };
</script>

<span class={cn('ml-auto text-xs tracking-widest text-gray-900', className)} {...$$restProps}>
<slot />
</span>
Loading