Skip to content

Commit

Permalink
feat: add icons page (#15)
Browse files Browse the repository at this point in the history
* feat: add icons page

* brush up styles & add meta-tags

* context menu + options

* oops

* an excuse to trigger a (correct?) deployment preview

* troubleshoot

* specify wrangler version

* revert

* .

* add wrangler as a dev dep

* Adrian said no need

* search

* some animation

---------

Co-authored-by: Davis SHYAKA <[email protected]>
  • Loading branch information
ddtamn and shyakadavis authored Jul 27, 2024
1 parent 0a44cd9 commit 3e96952
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 10 deletions.
8 changes: 4 additions & 4 deletions src/lib/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import ArrowUp from './arrow-up.svg?component';
import Bell from './bell.svg?component';
import Box from './box.svg?component';
import BrandAssets from './brand-assets.svg?component';
import CharBarMiddle from './chart-bar-middle.svg?component';
import ChartBarMiddle from './chart-bar-middle.svg?component';
import ChartBarPeak from './chart-bar-peak.svg?component';
import CharTrendingDown from './chart-trending-down.svg?component';
import ChartTrendingDown from './chart-trending-down.svg?component';
import Check from './check.svg?component';
import ChevronLeft from './chevron-left.svg?component';
import ChevronRightSmall from './chevron-right-small.svg?component';
Expand Down Expand Up @@ -63,9 +63,9 @@ export const Icons = {
Bell,
Box,
BrandAssets,
CharBarMiddle,
ChartBarMiddle,
ChartBarPeak,
CharTrendingDown,
ChartTrendingDown,
Check,
ChevronLeft,
ChevronRightSmall,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/ui/input/input.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{/if}
<div
class={cn(
'flex max-w-full items-center rounded-md shadow-shadow-border transition-[border-color,box-shadow] delay-0 focus-within:shadow-shadow-input',
'flex max-w-full items-center rounded-md shadow-shadow-border transition-[border-color,box-shadow] delay-0 focus-within:shadow-shadow-input',
{
'h-8': size === 'sm',
'h-10': size === 'md',
Expand Down
6 changes: 4 additions & 2 deletions src/lib/components/ui/input/search.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,25 @@
let class_name: string | undefined | null = undefined;
export { class_name as class };
export let value: string = '';
// Automatically clear the input if escape is pressed.
function key_down_handler(event: KeyboardEvent) {
if (event.key === 'Escape') {
$$restProps.value = '';
value = '';
}
}
</script>

<svelte:window on:keydown={key_down_handler} />

<div class={cn('', class_name)}>
<div class={cn('bg-background-100', class_name)}>
<Input
type="search"
affix={Icons.MagnifyingGlass}
affix_styling={false}
aria-labelledby="Search"
bind:value
{...$$restProps}
/>
</div>
2 changes: 1 addition & 1 deletion src/lib/config/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const aside_items: Aside = {
{
title: 'Icons',
href: '/icons',
status: 'soon'
status: 'draft'
}
],
brands: [
Expand Down
4 changes: 2 additions & 2 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
const preview_icons = [
Icons.GridSquare,
Icons.External,
Icons.CharBarMiddle,
Icons.ChartBarMiddle,
Icons.ClockDashed,
Icons.Globe,
Icons.Flag,
Expand All @@ -18,7 +18,7 @@
Icons.FileText,
Icons.Notification,
Icons.Paperclip,
Icons.CharTrendingDown,
Icons.ChartTrendingDown,
Icons.CodeBracket,
Icons.Key,
Icons.FloppyDisk,
Expand Down
12 changes: 12 additions & 0 deletions src/routes/icons/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts">
import PageWrapper from '$lib/components/shared/page-wrapper.svelte';
import Icons from './Icons.svelte';
export let data;
</script>

<PageWrapper title={data.title} description={data.description}>
<!-- Todo: add search field -->
<!-- <Search></Search> -->
<Icons />
</PageWrapper>
21 changes: 21 additions & 0 deletions src/routes/icons/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { MetaTagsProps } from 'svelte-meta-tags';

export function load() {
const title = 'Icons';
const description = 'A collection of icons used across Vercel products. Right click to copy.';

const pageMetaTags = Object.freeze({
title,
description,
openGraph: {
title,
description
}
}) satisfies MetaTagsProps;

return {
pageMetaTags,
title,
description
};
}
125 changes: 125 additions & 0 deletions src/routes/icons/Icons.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<script lang="ts">
import { Icons } from '$lib/assets/icons';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger
} from '$lib/components/ui/context-menu';
import { SearchInput } from '$lib/components/ui/input';
import { cn } from '$lib/utils';
import { onMount } from 'svelte';
import { flip } from 'svelte/animate';
import { derived, writable } from 'svelte/store';
import { receive, send } from './transition';
const icons_to_exclude = ['BrandAssets', 'ErrorStates'];
const preview_icons = writable(
Object.entries(Icons).filter(([name]) => !icons_to_exclude.includes(name))
);
const search_term = writable('');
// TODO: Improve search algorithm
const filtered_icons = derived([preview_icons, search_term], ([$preview_icons, $search_term]) => {
const term = $search_term.toLowerCase();
return $preview_icons.filter(([name]) => name.toLowerCase().includes(term));
});
// e.g. "ArrowUp" -> "arrow-up"
function format_name(str: string) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
// e.g. import ArrowLeft from '$lib/assets/icons/arrow-left.svg?component';
function copy_import(name: string) {
const value = `import ${name} from '$lib/assets/icons/${format_name(name)}.svg?component';`;
navigator.clipboard.writeText(value);
}
function copy_name(name: string) {
navigator.clipboard.writeText(name);
}
// e.g. <ArrowLeft aria-hidden="true" height="16" width="16" />
function copy_svelte_component(name: string) {
const value = `<${name} aria-hidden="true" height="16" width="16" />`;
navigator.clipboard.writeText(value);
}
async function copy_svg(name: string) {
const value = await import(`$lib/assets/icons/${format_name(name)}.svg?raw`)
.then((res) => res.default)
.catch((err) => {
console.error(err);
return 'Error loading SVG';
});
navigator.clipboard.writeText(value);
}
const last_row_count = writable(0);
function update_last_row_count() {
filtered_icons.subscribe(($filtered_icons) => {
const columns = window.innerWidth >= 640 ? 4 : 2;
last_row_count.set($filtered_icons.length % columns);
});
}
onMount(update_last_row_count);
</script>

<svelte:window on:resize={update_last_row_count} />

<section id="search" class="grid w-full place-items-center border-b p-10">
<SearchInput placeholder="Search icons..." class="w-full" bind:value={$search_term} />
</section>

{#if $filtered_icons.length === 0}
<p class="p-10 text-gray-900">No icons found.</p>
{:else}
<ul class="grid grid-cols-2 sm:grid-cols-4">
{#each $filtered_icons as [name, icon], i (icon.name)}
<!-- TODO: Improve the shuffle/filter animation -->
<li
in:receive={{ key: icon.name }}
out:send={{ key: icon.name }}
animate:flip={{ duration: 200 }}
>
<ContextMenu>
<ContextMenuTrigger
class={cn(
'flex h-28 w-full cursor-pointer flex-col items-center justify-center gap-1.5 px-4 text-gray-900 hover:bg-background-100',
/*
This code applies border styles to elements based on their position and screen size:
- Adds bottom and right borders to all elements.
- Removes the right border for every second element.
- At the small (sm) breakpoint:
- Adds the right border back to every second element.
- Removes the right border for every fourth element.
- Removes the bottom border for elements in the last row.
*/
'border-b border-r [&:nth-child(2n)]:border-r-0 sm:[&:nth-child(2n)]:border-r sm:[&:nth-child(4n)]:border-r-0',
{
'border-b-0':
i >= $filtered_icons.length - $last_row_count || $filtered_icons.length <= 4
}
)}
>
<svelte:component this={icon} aria-hidden="true" height="16" width="16" />
<span class="text-sm">{format_name(name)}</span>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem on:click={() => copy_import(name)}>Copy Import</ContextMenuItem>
<ContextMenuItem on:click={() => copy_name(format_name(name))}>
Copy Name
</ContextMenuItem>
<ContextMenuItem on:click={() => copy_svelte_component(name)}>
Copy Svelte Component
</ContextMenuItem>
<ContextMenuItem on:click={() => copy_svg(name)}>Copy SVG</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</li>
{/each}
</ul>
{/if}
20 changes: 20 additions & 0 deletions src/routes/icons/transition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { quintInOut } from 'svelte/easing';
import { crossfade } from 'svelte/transition';

export const [send, receive] = crossfade({
duration: (d) => Math.sqrt(d * 200),

fallback(node, _) {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;

return {
duration: 600,
easing: quintInOut,
css: (t) => `
transform: ${transform} scale(${t});
opacity: ${t}
`
};
}
});

0 comments on commit 3e96952

Please sign in to comment.