feat: additional component and styling choices
|
@ -38,6 +38,8 @@
|
|||
"dependencies": {
|
||||
"bits-ui": "^0.15.1",
|
||||
"clsx": "^2.1.0",
|
||||
"lucide-svelte": "^0.316.0",
|
||||
"mode-watcher": "^0.1.2",
|
||||
"pocketbase": "^0.21.0",
|
||||
"radix-icons-svelte": "^1.2.1",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
|
|
|
@ -11,6 +11,12 @@ dependencies:
|
|||
clsx:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
lucide-svelte:
|
||||
specifier: ^0.316.0
|
||||
version: 0.316.0(svelte@4.2.9)
|
||||
mode-watcher:
|
||||
specifier: ^0.1.2
|
||||
version: 0.1.2(svelte@4.2.9)
|
||||
pocketbase:
|
||||
specifier: ^0.21.0
|
||||
version: 0.21.0
|
||||
|
@ -1651,6 +1657,14 @@ packages:
|
|||
yallist: 4.0.0
|
||||
dev: true
|
||||
|
||||
/lucide-svelte@0.316.0(svelte@4.2.9):
|
||||
resolution: {integrity: sha512-6mtBw/aU1IIVXfN6AXLSwsbeCFX/2/tTy84rrDTDsOq7BEijFbvObQsPEWPxJ0EyCjUXOYfAvfkZzMosYjyGqA==}
|
||||
peerDependencies:
|
||||
svelte: '>=3 <5'
|
||||
dependencies:
|
||||
svelte: 4.2.9
|
||||
dev: false
|
||||
|
||||
/magic-string@0.30.5:
|
||||
resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -1703,6 +1717,14 @@ packages:
|
|||
minimist: 1.2.8
|
||||
dev: true
|
||||
|
||||
/mode-watcher@0.1.2(svelte@4.2.9):
|
||||
resolution: {integrity: sha512-XTdPCdqC3kqSvB+Q262Kor983YJkkB2Z3vj9uqg5IqKQpOdiz+xB99Jihp8sWbyM67drC7KKp0Nt5FzCypZi2g==}
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
dependencies:
|
||||
svelte: 4.2.9
|
||||
dev: false
|
||||
|
||||
/mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
|
@ -2,11 +2,23 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%sveltekit.assets%/favicon-16x16.png" />
|
||||
<link rel="manifest" href="%sveltekit.assets%/site.webmanifest" />
|
||||
<link rel="mask-icon" href="%sveltekit.assets%/safari-pinned-tab.svg" color="#000000" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
<body
|
||||
data-sveltekit-preload-data="hover"
|
||||
class="min-h-screen bg-background font-sans antialiased"
|
||||
>
|
||||
<div style="display: contents" class="relative flex min-h-screen flex-col">
|
||||
%sveltekit.body%
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
|
@ -3,69 +3,49 @@
|
|||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--ring: 0 0% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--ring: 0 0% 83.1%;
|
||||
}
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 72.2% 50.6%;
|
||||
--primary-foreground: 0 85.7% 97.3%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 72.22% 50.59%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 72.2% 50.6%;
|
||||
--radius: 0rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 72.2% 50.6%;
|
||||
--primary-foreground: 0 85.7% 97.3%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 72.2% 50.6%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
|
4
apps/web/src/lib/components/site/icons/github.svelte
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg viewBox="0 0 24 24" {...$$restProps}>
|
||||
<path d="M0 0h24v24H0z" />
|
||||
<path d="M3 19h18l-9 -15z" />
|
||||
</svg>
|
After Width: | Height: | Size: 109 B |
14
apps/web/src/lib/components/site/icons/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import type { Icon as LucideIcon } from 'lucide-svelte';
|
||||
import { GithubLogo, VercelLogo, LinkedinLogo } from 'radix-icons-svelte';
|
||||
import Logo from './logo.svelte';
|
||||
import Svelte from './svelte.svelte';
|
||||
|
||||
export type Icon = LucideIcon;
|
||||
|
||||
export const Icons = {
|
||||
logo: Logo,
|
||||
gitHub: GithubLogo,
|
||||
svelte: Svelte,
|
||||
vercel: VercelLogo,
|
||||
linkedIn: LinkedinLogo
|
||||
};
|
7
apps/web/src/lib/components/site/icons/logo.svelte
Normal file
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="7" height="9" x="3" y="3" rx="1" stroke="#FF6C22" />
|
||||
<rect width="7" height="5" x="14" y="3" rx="1" stroke="#FF9209" />
|
||||
<rect width="7" height="9" x="14" y="12" rx="1" stroke="#2B3499" />
|
||||
<rect width="7" height="5" x="3" y="16" rx="1" stroke="#FFD099" />
|
||||
</svg>
|
After Width: | Height: | Size: 446 B |
15
apps/web/src/lib/components/site/icons/svelte.svelte
Normal file
|
@ -0,0 +1,15 @@
|
|||
<svg
|
||||
class="inline-svg"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...$$restProps}
|
||||
><path
|
||||
d="M10.354 21.125a4.44 4.44 0 0 1-4.765-1.767 4.109 4.109 0 0 1-.703-3.107 3.898 3.898 0 0 1 .134-.522l.105-.321.287.21a7.21 7.21 0 0 0 2.186 1.092l.208.063-.02.208a1.253 1.253 0 0 0 .226.83 1.337 1.337 0 0 0 1.435.533 1.231 1.231 0 0 0 .343-.15l5.59-3.562a1.164 1.164 0 0 0 .524-.778 1.242 1.242 0 0 0-.211-.937 1.338 1.338 0 0 0-1.435-.533 1.23 1.23 0 0 0-.343.15l-2.133 1.36a4.078 4.078 0 0 1-1.135.499 4.44 4.44 0 0 1-4.765-1.766 4.108 4.108 0 0 1-.702-3.108 3.855 3.855 0 0 1 1.742-2.582l5.589-3.563a4.072 4.072 0 0 1 1.135-.499 4.44 4.44 0 0 1 4.765 1.767 4.109 4.109 0 0 1 .703 3.107 3.943 3.943 0 0 1-.134.522l-.105.321-.286-.21a7.204 7.204 0 0 0-2.187-1.093l-.208-.063.02-.207a1.255 1.255 0 0 0-.226-.831 1.337 1.337 0 0 0-1.435-.532 1.231 1.231 0 0 0-.343.15L8.62 9.368a1.162 1.162 0 0 0-.524.778 1.24 1.24 0 0 0 .211.937 1.338 1.338 0 0 0 1.435.533 1.235 1.235 0 0 0 .344-.151l2.132-1.36a4.067 4.067 0 0 1 1.135-.498 4.44 4.44 0 0 1 4.765 1.766 4.108 4.108 0 0 1 .702 3.108 3.857 3.857 0 0 1-1.742 2.583l-5.589 3.562a4.072 4.072 0 0 1-1.135.499m10.358-17.95C18.484-.015 14.082-.96 10.9 1.068L5.31 4.63a6.412 6.412 0 0 0-2.896 4.295 6.753 6.753 0 0 0 .666 4.336 6.43 6.43 0 0 0-.96 2.396 6.833 6.833 0 0 0 1.168 5.167c2.229 3.19 6.63 4.135 9.812 2.108l5.59-3.562a6.41 6.41 0 0 0 2.896-4.295 6.756 6.756 0 0 0-.665-4.336 6.429 6.429 0 0 0 .958-2.396 6.831 6.831 0 0 0-1.167-5.168Z"
|
||||
/></svg
|
||||
>
|
After Width: | Height: | Size: 1.6 KiB |
4
apps/web/src/lib/components/site/icons/vercel.svelte
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg viewBox="0 0 24 24" {...$$restProps}>
|
||||
<path stroke="none" d="M0 0h24v24H0z" />
|
||||
<path d="M3 19h18l-9 -15z" />
|
||||
</svg>
|
After Width: | Height: | Size: 123 B |
8
apps/web/src/lib/components/site/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export { default as Metadata } from './metadata.svelte';
|
||||
export { default as SiteFooter } from './site-footer.svelte';
|
||||
export { default as SiteNavBar } from './site-navbar.svelte';
|
||||
export { default as TailwindIndicator } from './tailwind-indicator.svelte';
|
||||
export { default as ModeToggle } from './mode-toggle.svelte';;
|
||||
|
||||
export * from './icons';
|
||||
export * from './nav';
|
36
apps/web/src/lib/components/site/metadata.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { siteConfig } from '$lib/config/site';
|
||||
|
||||
export let title: string = siteConfig.name;
|
||||
|
||||
$: title = $page.data?.name ? `${$page.data.name} — ${siteConfig.name}` : siteConfig.name;
|
||||
$: description = $page.data?.subTitle ?? siteConfig.description;
|
||||
$: ogImage = encodeURI(
|
||||
`${siteConfig.ogImage}?title=${$page.data.title}&subTitle=${$page.data.subTitle}`
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="keywords" content={siteConfig.keywords} />
|
||||
<meta name="author" content="Bart van der Braak" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content={siteConfig.url} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
<meta name="twitter:image:alt" content={siteConfig.name} />
|
||||
<meta name="twitter:creator" content="Bart van der Braak" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content={siteConfig.url + $page.url.pathname} />
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:alt" content={siteConfig.name} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:site_name" content={siteConfig.name} />
|
||||
<meta property="og:locale" content="EN_US" />
|
||||
</svelte:head>
|
25
apps/web/src/lib/components/site/mode-toggle.svelte
Normal file
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { Moon, Sun } from 'lucide-svelte';
|
||||
import { Button } from '../ui/button';
|
||||
import * as DropdownMenu from '../ui/dropdown-menu';
|
||||
import { resetMode, setMode } from 'mode-watcher';
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
<Button builders={[builder]} variant="ghost" class="h-9 w-9">
|
||||
<Sun
|
||||
class="absolute h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
||||
/>
|
||||
<Moon
|
||||
class="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
|
||||
/>
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item on:click={() => setMode('light')}>Light</DropdownMenu.Item>
|
||||
<DropdownMenu.Item on:click={() => setMode('dark')}>Dark</DropdownMenu.Item>
|
||||
<DropdownMenu.Item on:click={() => resetMode()}>System</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
2
apps/web/src/lib/components/site/nav/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as MainNav } from './main-nav.svelte';
|
||||
export { default as MobileNav } from './mobile-nav.svelte';
|
23
apps/web/src/lib/components/site/nav/main-nav.svelte
Normal file
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { cn } from '$lib/utils';
|
||||
import { navConfig } from '$lib/config/nav';
|
||||
</script>
|
||||
|
||||
<div class="mr-4 hidden md:flex">
|
||||
<nav class="flex items-center space-x-6 text-sm font-medium">
|
||||
{#each navConfig.mainNav as navItem, index (navItem + index.toString())}
|
||||
{#if navItem.href}
|
||||
<a
|
||||
href={navItem.href}
|
||||
class={cn(
|
||||
'transition-colors hover:text-foreground/80',
|
||||
$page.url.pathname === navItem.href ? 'text-foreground' : 'text-foreground/60'
|
||||
)}
|
||||
>
|
||||
{navItem.title}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
23
apps/web/src/lib/components/site/nav/mobile-link.svelte
Normal file
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
export let href: string;
|
||||
export let open: boolean;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<a
|
||||
{href}
|
||||
on:click={() => (open = false)}
|
||||
class={cn(
|
||||
$page.url.pathname === href ? 'text-foreground' : 'text-foreground/60',
|
||||
'hover:text-foreground',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</a>
|
65
apps/web/src/lib/components/site/nav/mobile-nav.svelte
Normal file
|
@ -0,0 +1,65 @@
|
|||
<script lang="ts">
|
||||
import * as Sheet from '$lib/components/ui/sheet/';
|
||||
import { HamburgerMenu } from 'radix-icons-svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { navConfig } from '$lib/config/nav';
|
||||
import { siteConfig } from '$lib/config/site';
|
||||
import { Icons } from '../icons';
|
||||
import MobileLink from './mobile-link.svelte';
|
||||
|
||||
let open = false;
|
||||
</script>
|
||||
|
||||
<Sheet.Root bind:open>
|
||||
<Sheet.Trigger asChild let:builder>
|
||||
<Button
|
||||
builders={[builder]}
|
||||
variant="ghost"
|
||||
class="mr-2 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden"
|
||||
>
|
||||
<HamburgerMenu class="h-5 w-5" />
|
||||
<span class="sr-only">Toggle Menu</span>
|
||||
</Button>
|
||||
</Sheet.Trigger>
|
||||
<Sheet.Content side="right" class="pr-0">
|
||||
<MobileLink href="/" class="flex items-center" bind:open>
|
||||
<span class="sr-only">Logo icon (return home)</span>
|
||||
<img src={Icons.logoIcon} class="mr-2 h-4 w-4" alt="icon of hellob.art" />
|
||||
<span class="font-mono font-bold tracking-tighter">{siteConfig.name}</span>
|
||||
</MobileLink>
|
||||
<div class="my-4 h-[calc(100vh-8rem)] overflow-auto pl-1 pt-10">
|
||||
<div class="flex flex-col space-y-3">
|
||||
{#each navConfig.mainNav as navItem, index (navItem + index.toString())}
|
||||
{#if navItem.href}
|
||||
<MobileLink href={navItem.href} bind:open class="pt-2 text-5xl font-bold">
|
||||
{navItem.title}
|
||||
</MobileLink>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
{#each navConfig.sidebarNav as navItem, index (index)}
|
||||
<div class="flex flex-col space-y-3 pt-6">
|
||||
<h4 class="font-medium">{navItem.title}</h4>
|
||||
{#if navItem?.items?.length}
|
||||
{#each navItem.items as item}
|
||||
{#if !item.disabled && item.href}
|
||||
<MobileLink href={item.href} bind:open class="text-muted-foreground">
|
||||
{item.title}
|
||||
{#if item.label}
|
||||
<span
|
||||
class="ml-2 rounded-md bg-[#adfa1d] px-1.5 py-0.5 text-xs leading-none text-[#000000]"
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
{/if}
|
||||
</MobileLink>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
13
apps/web/src/lib/components/site/site-footer.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { Blocks, Cloud } from 'lucide-svelte';
|
||||
import { siteConfig } from '$lib/config/site';
|
||||
import { Code } from 'radix-icons-svelte';
|
||||
</script>
|
||||
|
||||
<footer class="container py-6">
|
||||
<div class="space-y-1">
|
||||
<p class="text-center text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} — {siteConfig.name} by {siteConfig.author}. Licensed under GPL-3.0.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
12
apps/web/src/lib/components/site/site-header.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import * as PageHeader from './page-header';
|
||||
export let title: string, subTitle: string;
|
||||
</script>
|
||||
|
||||
<PageHeader.Root class="pb-8">
|
||||
<PageHeader.Heading>{title}</PageHeader.Heading>
|
||||
<PageHeader.Description>
|
||||
{subTitle}
|
||||
</PageHeader.Description>
|
||||
<slot />
|
||||
</PageHeader.Root>
|
52
apps/web/src/lib/components/site/site-navbar.svelte
Normal file
|
@ -0,0 +1,52 @@
|
|||
<script lang="ts">
|
||||
import { Icons, ModeToggle, MainNav, MobileNav } from '$lib/components/site';
|
||||
import { siteConfig } from '$lib/config/site';
|
||||
import { cn } from '$lib/utils';
|
||||
import { buttonVariants } from '../ui/button';
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="sticky top-0 z-50 w-full border-b bg-background/95 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
||||
>
|
||||
<div class="container flex h-14 items-center">
|
||||
<a href="/" class="mr-6 flex items-center space-x-2">
|
||||
<span class="sr-only">Logo (return home)</span>
|
||||
<Icons.logo />
|
||||
</a>
|
||||
<MainNav />
|
||||
<div class="flex flex-1 items-center justify-between space-x-2 sm:space-x-4 md:justify-end">
|
||||
<nav class="flex">
|
||||
<a href={siteConfig.links.gitHubProfile} target="_blank" rel="noopener noreferrer">
|
||||
<div
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
size: 'sm',
|
||||
variant: 'ghost'
|
||||
}),
|
||||
'h-9 w-9 px-0'
|
||||
)}
|
||||
>
|
||||
<Icons.gitHub class="h-4 w-4" />
|
||||
<span class="sr-only">GitHub</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href={siteConfig.links.linkedIn} target="_blank" rel="noreferrer">
|
||||
<div
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
size: 'sm',
|
||||
variant: 'ghost'
|
||||
}),
|
||||
'h-9 w-9 px-0'
|
||||
)}
|
||||
>
|
||||
<Icons.linkedIn class="h-4 w-4" />
|
||||
<span class="sr-only">LinkedIn</span>
|
||||
</div>
|
||||
</a>
|
||||
<ModeToggle />
|
||||
</nav>
|
||||
</div>
|
||||
<MobileNav />
|
||||
</div>
|
||||
</header>
|
10
apps/web/src/lib/components/site/tailwind-indicator.svelte
Normal file
|
@ -0,0 +1,10 @@
|
|||
<div
|
||||
class="fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white"
|
||||
>
|
||||
<div class="block sm:hidden">xs</div>
|
||||
<div class="hidden sm:block md:hidden lg:hidden xl:hidden 2xl:hidden">sm</div>
|
||||
<div class="hidden md:block lg:hidden xl:hidden 2xl:hidden">md</div>
|
||||
<div class="hidden lg:block xl:hidden 2xl:hidden">lg</div>
|
||||
<div class="hidden xl:block 2xl:hidden">xl</div>
|
||||
<div class="hidden 2xl:block">2xl</div>
|
||||
</div>
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Check } from "radix-icons-svelte";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let checked: $$Props["checked"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:checked
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.CheckboxIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.CheckboxIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn, flyAndScale } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.ContentProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let sideOffset: $$Props["sideOffset"] = 4;
|
||||
export let transition: $$Props["transition"] = flyAndScale;
|
||||
export let transitionConfig: $$Props["transitionConfig"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Content
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:keydown
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.Content>
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.ItemProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
type $$Events = DropdownMenuPrimitive.ItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let inset: $$Props["inset"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Item
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.Item>
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.LabelProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let inset: $$Props["inset"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Label
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.Label>
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
|
||||
|
||||
export let value: $$Props["value"] = undefined;
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.RadioGroup>
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { DotFilled } from "radix-icons-svelte";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.RadioItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: DropdownMenuPrimitive.RadioItemProps["value"];
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{value}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.RadioIndicator>
|
||||
<DotFilled class="h-4 w-4 fill-current" />
|
||||
</DropdownMenuPrimitive.RadioIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.RadioItem>
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.SeparatorProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Separator
|
||||
class={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...$$restProps}
|
||||
/>
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<span class={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</span>
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn, flyAndScale } from "$lib/utils";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.SubContentProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let transition: $$Props["transition"] = flyAndScale;
|
||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||
x: -10,
|
||||
y: 0
|
||||
};
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn(
|
||||
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:keydown
|
||||
on:focusout
|
||||
on:pointermove
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.SubContent>
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { ChevronRight } from "radix-icons-svelte";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let inset: $$Props["inset"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
class={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
48
apps/web/src/lib/components/ui/dropdown-menu/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import Item from "./dropdown-menu-item.svelte";
|
||||
import Label from "./dropdown-menu-label.svelte";
|
||||
import Content from "./dropdown-menu-content.svelte";
|
||||
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||
import Separator from "./dropdown-menu-separator.svelte";
|
||||
import RadioGroup from "./dropdown-menu-radio-group.svelte";
|
||||
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||
|
||||
const Sub = DropdownMenuPrimitive.Sub;
|
||||
const Root = DropdownMenuPrimitive.Root;
|
||||
const Trigger = DropdownMenuPrimitive.Trigger;
|
||||
const Group = DropdownMenuPrimitive.Group;
|
||||
|
||||
export {
|
||||
Sub,
|
||||
Root,
|
||||
Item,
|
||||
Label,
|
||||
Group,
|
||||
Trigger,
|
||||
Content,
|
||||
Shortcut,
|
||||
Separator,
|
||||
RadioItem,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
RadioGroup,
|
||||
CheckboxItem,
|
||||
//
|
||||
Root as DropdownMenu,
|
||||
Sub as DropdownMenuSub,
|
||||
Item as DropdownMenuItem,
|
||||
Label as DropdownMenuLabel,
|
||||
Group as DropdownMenuGroup,
|
||||
Content as DropdownMenuContent,
|
||||
Trigger as DropdownMenuTrigger,
|
||||
Shortcut as DropdownMenuShortcut,
|
||||
RadioItem as DropdownMenuRadioItem,
|
||||
Separator as DropdownMenuSeparator,
|
||||
RadioGroup as DropdownMenuRadioGroup,
|
||||
SubContent as DropdownMenuSubContent,
|
||||
SubTrigger as DropdownMenuSubTrigger,
|
||||
CheckboxItem as DropdownMenuCheckboxItem
|
||||
};
|
106
apps/web/src/lib/components/ui/sheet/index.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
|
||||
import Portal from "./sheet-portal.svelte";
|
||||
import Overlay from "./sheet-overlay.svelte";
|
||||
import Content from "./sheet-content.svelte";
|
||||
import Header from "./sheet-header.svelte";
|
||||
import Footer from "./sheet-footer.svelte";
|
||||
import Title from "./sheet-title.svelte";
|
||||
import Description from "./sheet-description.svelte";
|
||||
|
||||
const Root = SheetPrimitive.Root;
|
||||
const Close = SheetPrimitive.Close;
|
||||
const Trigger = SheetPrimitive.Trigger;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Close,
|
||||
Trigger,
|
||||
Portal,
|
||||
Overlay,
|
||||
Content,
|
||||
Header,
|
||||
Footer,
|
||||
Title,
|
||||
Description,
|
||||
//
|
||||
Root as Sheet,
|
||||
Close as SheetClose,
|
||||
Trigger as SheetTrigger,
|
||||
Portal as SheetPortal,
|
||||
Overlay as SheetOverlay,
|
||||
Content as SheetContent,
|
||||
Header as SheetHeader,
|
||||
Footer as SheetFooter,
|
||||
Title as SheetTitle,
|
||||
Description as SheetDescription
|
||||
};
|
||||
|
||||
export const sheetVariants = tv({
|
||||
base: "fixed z-50 gap-4 bg-background p-6 shadow-lg",
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b ",
|
||||
bottom: "inset-x-0 bottom-0 border-t",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
right: "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right"
|
||||
}
|
||||
});
|
||||
|
||||
export const sheetTransitions = {
|
||||
top: {
|
||||
in: {
|
||||
y: "-100%",
|
||||
duration: 500,
|
||||
opacity: 1
|
||||
},
|
||||
out: {
|
||||
y: "-100%",
|
||||
duration: 300,
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
bottom: {
|
||||
in: {
|
||||
y: "100%",
|
||||
duration: 500,
|
||||
opacity: 1
|
||||
},
|
||||
out: {
|
||||
y: "100%",
|
||||
duration: 300,
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
left: {
|
||||
in: {
|
||||
x: "-100%",
|
||||
duration: 500,
|
||||
opacity: 1
|
||||
},
|
||||
out: {
|
||||
x: "-100%",
|
||||
duration: 300,
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
right: {
|
||||
in: {
|
||||
x: "100%",
|
||||
duration: 500,
|
||||
opacity: 1
|
||||
},
|
||||
out: {
|
||||
x: "100%",
|
||||
duration: 300,
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export type Side = VariantProps<typeof sheetVariants>["side"];
|
41
apps/web/src/lib/components/ui/sheet/sheet-content.svelte
Normal file
|
@ -0,0 +1,41 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { SheetOverlay, SheetPortal, sheetVariants, sheetTransitions, type Side } from ".";
|
||||
import { Cross2 } from "radix-icons-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { fly } from "svelte/transition";
|
||||
|
||||
type $$Props = SheetPrimitive.ContentProps & {
|
||||
side?: Side;
|
||||
};
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let side: $$Props["side"] = "right";
|
||||
export { className as class };
|
||||
export let inTransition: $$Props["inTransition"] = fly;
|
||||
export let inTransitionConfig: $$Props["inTransitionConfig"] =
|
||||
sheetTransitions[side ? side : "right"]["in"];
|
||||
export let outTransition: $$Props["outTransition"] = fly;
|
||||
export let outTransitionConfig: $$Props["outTransitionConfig"] =
|
||||
sheetTransitions[side ? side : "right"]["out"];
|
||||
</script>
|
||||
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
{inTransition}
|
||||
{inTransitionConfig}
|
||||
{outTransition}
|
||||
{outTransitionConfig}
|
||||
class={cn(sheetVariants({ side }), className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
<SheetPrimitive.Close
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
|
||||
>
|
||||
<Cross2 class="h-4 w-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = SheetPrimitive.DescriptionProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Description class={cn("text-sm text-muted-foreground", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</SheetPrimitive.Description>
|
16
apps/web/src/lib/components/ui/sheet/sheet-footer.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
13
apps/web/src/lib/components/ui/sheet/sheet-header.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
21
apps/web/src/lib/components/ui/sheet/sheet-overlay.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
type $$Props = SheetPrimitive.OverlayProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
export let transition: $$Props["transition"] = fade;
|
||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||
duration: 150
|
||||
};
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Overlay
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn("fixed inset-0 z-50 bg-background/80 backdrop-blur-sm", className)}
|
||||
{...$$restProps}
|
||||
/>
|
13
apps/web/src/lib/components/ui/sheet/sheet-portal.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = SheetPrimitive.PortalProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Portal class={cn(className)} {...$$restProps}>
|
||||
<slot />
|
||||
</SheetPrimitive.Portal>
|
16
apps/web/src/lib/components/ui/sheet/sheet-title.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = SheetPrimitive.TitleProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Title
|
||||
class={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</SheetPrimitive.Title>
|
24
apps/web/src/lib/config/nav.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import type { NavItem, SidebarNavItem } from '$lib/types/nav';
|
||||
|
||||
interface NavConfig {
|
||||
mainNav: NavItem[];
|
||||
sidebarNav: SidebarNavItem[];
|
||||
}
|
||||
|
||||
export const navConfig: NavConfig = {
|
||||
mainNav: [
|
||||
{
|
||||
title: 'Home',
|
||||
href: '/'
|
||||
},
|
||||
{
|
||||
title: 'Login',
|
||||
href: '/login'
|
||||
},
|
||||
{
|
||||
title: 'Signup',
|
||||
href: '/signup'
|
||||
}
|
||||
],
|
||||
sidebarNav: []
|
||||
};
|
17
apps/web/src/lib/config/site.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
const SITE_URL =
|
||||
import.meta.env.VERCEL_ENV === 'preview' ? import.meta.env.VERCEL_URL : 'omnidash.io';
|
||||
|
||||
export const siteConfig = {
|
||||
name: 'Omnidash',
|
||||
author: 'Bart van der Braak',
|
||||
url: SITE_URL,
|
||||
description: 'Self-hostable dashboard using connectors to a multitude of ticketing systems.',
|
||||
ogImage: `https://${SITE_URL}/og.png`,
|
||||
links: {
|
||||
gitHubProfile: 'https://github.com/bartvdbraak',
|
||||
gitHubProject: 'https://github.com/bartvdbraak/omnidash',
|
||||
},
|
||||
keywords: `Ticket,Dashboard,Self-hosted,${SITE_URL},Bart van der Braak,Omnidash`,
|
||||
};
|
||||
|
||||
export type SiteConfig = typeof siteConfig;
|
18
apps/web/src/lib/types/nav.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import type { Icons } from '$lib/components/site/icons';
|
||||
|
||||
export type NavItem = {
|
||||
title: string;
|
||||
href: string;
|
||||
disabled?: boolean;
|
||||
external?: boolean;
|
||||
icon?: keyof typeof Icons;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export type SidebarNavItem = NavItem & {
|
||||
items: SidebarNavItem[];
|
||||
};
|
||||
|
||||
export type NavItemWithChildren = NavItem & {
|
||||
items: NavItemWithChildren[];
|
||||
};
|
|
@ -1,5 +1,26 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { dev } from '$app/environment';
|
||||
import { Metadata, SiteFooter, SiteNavBar, TailwindIndicator } from '$lib/components/site';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
import '../app.pcss';
|
||||
import { fade } from 'svelte/transition';
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
<ModeWatcher />
|
||||
|
||||
<Metadata />
|
||||
|
||||
<div class="relative flex min-h-screen flex-col" id="page">
|
||||
<SiteNavBar />
|
||||
<main class="container relative mb-4 max-w-[980px] flex-1">
|
||||
<!-- {#key data.url} -->
|
||||
<div in:fade={{ duration: 200, delay: 100 }} out:fade={{ duration: 100 }}>
|
||||
<slot />
|
||||
</div>
|
||||
<!-- {/key} -->
|
||||
</main>
|
||||
<SiteFooter />
|
||||
{#if dev}
|
||||
<TailwindIndicator />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -16,11 +16,3 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
BIN
apps/web/static/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
apps/web/static/favicon-16x16.png
Normal file
After Width: | Height: | Size: 801 B |
BIN
apps/web/static/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
apps/web/static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.5 KiB |
7
apps/web/static/omnidash.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="7" height="9" x="3" y="3" rx="1" stroke="#FF6C22" />
|
||||
<rect width="7" height="5" x="14" y="3" rx="1" stroke="#FF9209" />
|
||||
<rect width="7" height="9" x="14" y="12" rx="1" stroke="#2B3499" />
|
||||
<rect width="7" height="5" x="3" y="16" rx="1" stroke="#FFD099" />
|
||||
</svg>
|
After Width: | Height: | Size: 446 B |
40
apps/web/static/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1128 6415 c-2 -2 -28 -5 -58 -8 -164 -18 -339 -147 -421 -312 -68
|
||||
-138 -66 -90 -64 -1310 l2 -1100 22 -65 c59 -170 173 -296 326 -362 56 -23 75
|
||||
-29 145 -43 47 -10 1604 -6 1656 4 208 40 377 194 450 411 18 52 19 115 21
|
||||
1155 3 1159 3 1163 -42 1268 -19 45 -72 133 -100 165 -25 29 -129 111 -170
|
||||
133 -16 9 -61 27 -100 40 l-70 22 -797 2 c-439 1 -799 1 -800 0z m1482 -584
|
||||
c13 -1 15 -120 15 -1018 l0 -1018 -729 0 -729 0 -1 1013 c-1 769 2 1015 11
|
||||
1020 9 5 1347 8 1433 3z"/>
|
||||
<path d="M4317 6414 c-1 -1 -20 -4 -43 -7 -88 -12 -225 -79 -297 -146 -46 -42
|
||||
-113 -141 -140 -206 -45 -112 -48 -149 -44 -696 4 -545 2 -526 52 -644 64
|
||||
-150 213 -274 386 -321 58 -16 134 -17 854 -18 435 0 810 3 835 7 236 33 424
|
||||
210 487 458 3 13 7 256 8 539 2 547 0 572 -47 683 -65 154 -198 274 -363 328
|
||||
l-70 23 -808 1 c-444 1 -808 1 -810 -1z m1509 -587 c9 -8 11 -845 3 -858 -6
|
||||
-10 -1432 -15 -1447 -6 -5 4 -9 844 -3 867 2 8 1440 4 1447 -3z"/>
|
||||
<path d="M4218 3769 c-207 -58 -367 -229 -413 -440 -21 -98 -21 -2184 0 -2282
|
||||
45 -207 190 -368 395 -436 70 -24 138 -26 930 -25 l805 0 78 27 c98 34 172 81
|
||||
243 154 65 68 129 189 147 282 11 51 13 296 12 1155 -1 600 -5 1100 -8 1111
|
||||
-2 11 -10 40 -17 65 -34 128 -167 287 -290 347 -135 65 -105 63 -1002 62 -766
|
||||
-1 -817 -2 -880 -20z m1613 -582 c5 -125 2 -2005 -3 -2012 -4 -7 -253 -9 -720
|
||||
-8 -392 1 -718 2 -723 2 -7 1 -10 344 -9 1019 1 559 2 1018 3 1019 0 1 327 2
|
||||
726 2 l725 0 1 -22z"/>
|
||||
<path d="M1095 2619 c-62 -8 -175 -41 -175 -51 0 -4 -9 -8 -20 -8 -11 0 -20
|
||||
-4 -20 -10 0 -5 -5 -10 -11 -10 -20 0 -111 -79 -158 -138 -25 -31 -58 -85 -73
|
||||
-118 -53 -117 -55 -148 -53 -705 2 -458 4 -520 19 -569 64 -199 205 -342 399
|
||||
-402 l72 -23 800 0 c768 1 852 3 921 24 191 57 356 235 400 432 12 51 15 1045
|
||||
3 1104 -43 225 -207 401 -430 461 -58 16 -137 17 -844 18 -429 1 -802 -2 -830
|
||||
-5z m1523 -584 c4 -4 7 -200 7 -436 0 -375 -2 -429 -15 -430 -110 -6 -1433 -2
|
||||
-1437 5 -5 7 -8 799 -4 853 1 6 9 13 18 15 32 6 1425 -1 1431 -7z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
19
apps/web/static/site.webmanifest
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|