Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 10 additions & 1 deletion src/app.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@import 'tailwindcss';

@plugin 'daisyui' {
exclude: rootscrollgutter;
themes:
light --default,
dark --prefersdark;
Expand Down Expand Up @@ -38,6 +39,8 @@
--border: 1px;
--depth: 0;
--noise: 0;

--color-link: #55f;
}
@plugin "daisyui/theme" {
name: 'dark';
Expand Down Expand Up @@ -72,6 +75,8 @@
--border: 1px;
--depth: 0;
--noise: 0;

--color-link: #68f;
}

/*
Expand Down Expand Up @@ -134,7 +139,11 @@
0 4px 6px -4px rgb(0 0 0 / 0.1);
}
a.link {
color: #55f;
color: var(--color-link);
}

a.link:hover {
color: color-mix(in oklab, var(--color-link), #fff 15%);
}

.toggle {
Expand Down
99 changes: 99 additions & 0 deletions src/lib/components/AppTypeSelector.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script lang="ts">
import type { HTMLSelectAttributes } from 'svelte/elements';
import Dropdown, { type DropdownClasses } from '$lib/components/Dropdown.svelte';
import { getAppIcon } from '$lib/icons';
import IconContainer from '$lib/icons/IconContainer.svelte';
import { byString } from '$lib/utils/sorting';
import { applicationTypes } from '$lib/valibot';

interface Props {
class?: DropdownClasses;
value: string | null;
allowNull?: boolean;
attr?: HTMLSelectAttributes;
}

let { class: classes = {}, value = $bindable(), allowNull = false, attr = {} }: Props = $props();

const current = $derived(applicationTypes.find((type) => type === value));

let open = $state(false);

function onclick(val: string | null) {
open = false;
value = val;
}
</script>

{#if attr.name}
<input type="hidden" name={attr.name} {value} />
{/if}

<Dropdown
class={{
dropdown: ['w-full', classes.dropdown],
label: ['w-full input cursor-auto', classes.label],
content: ['overflow-y-auto w-auto', classes.content]
}}
bind:open
>
{#snippet label()}
<div class="flex flex-row items-center gap-1 w-full">
{#if current}
<IconContainer icon={getAppIcon(current)} width={24} />
<span class="grow text-left">
{current}
</span>
{:else}
<span class="grow font-normal text-left min-w-48">All application types</span>
{/if}
<IconContainer icon="gridicons:dropdown" width={20} />
</div>
{/snippet}
{#snippet content()}
<ul class="menu menu-sm gap-1 p-2">
{#if allowNull}
<li class="w-full">
<div
class={['btn btn-ghost flex-nowrap justify-start pl-2 pr-1']}
onclick={() => onclick(null)}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onclick(null);
}
}}
role="button"
tabindex="0"
>
<span class="grow text-left">All application types</span>
</div>
</li>
{/if}
{#each applicationTypes.toSorted((a, b) => byString(a, b)) as type}
<li class="w-full">
<div
class={[
'btn flex-nowrap justify-start pl-2 pr-1',
type === value ? 'btn-secondary' : 'btn-ghost'
]}
onclick={() => onclick(type)}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onclick(type);
}
}}
role="button"
tabindex="0"
>
<IconContainer icon={getAppIcon(type)} width={24} />
<span class="grow text-left">
{type}
</span>
</div>
</li>
{/each}
</ul>
{/snippet}
</Dropdown>
62 changes: 62 additions & 0 deletions src/lib/components/Dropdown.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!--
@component
A simple dropdown menu from DaisyUI.

-->
<script lang="ts" module>
export type DropdownClasses = {
/** class="dropdown ..." */
dropdown?: ClassValue;
/** class="btn btn-ghost ..." */
label?: ClassValue;
/** class="dropdown-content z-10 drop-shadow-lg rounded-md bg-base-200 ..." */
content?: ClassValue;
};
</script>

<script lang="ts">
import type { Snippet } from 'svelte';
import type { ClassValue } from 'svelte/elements';
import { onNavigate } from '$app/navigation';
interface Props {
class?: DropdownClasses;
label: Snippet;
content: Snippet;
onclick?: () => void;
open?: boolean;
}

let { class: classes, label, content, onclick, open = $bindable(false) }: Props = $props();

onNavigate(() => {
// close opened dropdown when navigating (this is mostly important for the dropdowns in the navbar)
open = false;
});

let dropEl: HTMLDetailsElement | undefined = $state(undefined);
</script>

<svelte:window
onclick={(e) => {
// Only close if click is outside this dropdown
if (open && dropEl && !dropEl.contains(e.target as Node)) {
open = false;
// stopPropagation prevents the click from registering on the dropdown again and reopening it
e.stopPropagation();
}
}}
/>

<details class={['dropdown', classes?.dropdown]} bind:open bind:this={dropEl}>
<summary class={['btn btn-ghost', classes?.label]} onclick={() => onclick?.()}>
{@render label()}
</summary>
<div
class={[
'dropdown-content z-10 drop-shadow-lg rounded-md bg-base-200 dark:border dark:border-base-content',
classes?.content
]}
>
{@render content()}
</div>
</details>
20 changes: 0 additions & 20 deletions src/lib/components/IconContainer.svelte

This file was deleted.

61 changes: 61 additions & 0 deletions src/lib/components/SearchBar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script module lang="ts">
/** Make sure to call this in SuperForms onUpdate**d**() */
export function focusSearchBar() {
(document.querySelector('input[type="search"]') as HTMLInputElement)?.focus?.();
}
</script>

<script lang="ts">
import { onDestroy } from 'svelte';
import type { ClassValue } from 'svelte/elements';
import { Icons } from '$lib/icons';
import IconContainer from '$lib/icons/IconContainer.svelte';

interface Props {
class?: ClassValue;
value: string;
requestSubmit?: () => void;
requestDelay?: number;
}

let {
class: classes,
value = $bindable(),
requestSubmit,
requestDelay = 1_000
}: Props = $props();

let timeout: ReturnType<typeof setTimeout> | null = $state(null);

onDestroy(() => {
if (timeout) {
clearTimeout(timeout);
}
});
</script>

<div class={[classes]} data-html="true">
<label class="input flex items-center gap-2 w-full">
<input
type="search"
placeholder="Search"
aria-label="Search"
class="grow"
bind:value
oninput={(e) => {
if (requestSubmit) {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(requestSubmit, requestDelay);
}
}}
onfocus={(e) => {
const target = e.currentTarget;
// https://stackoverflow.com/a/10576409
setTimeout(() => target.setSelectionRange(value.length, value.length), 0);
}}
/>
<IconContainer icon={Icons.Search} class="ml-auto cursor-pointer" width={24} />
</label>
</div>
29 changes: 29 additions & 0 deletions src/lib/components/SecureDisplay.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script lang="ts" generics="T extends string | null">
import { Icons } from '$lib/icons';
import IconContainer from '$lib/icons/IconContainer.svelte';

interface Props {
value?: T;
}

let { value }: Props = $props();

let visible = $state(false);

// 20 is default width according to HTML 5 spec
</script>

<span class="inline-flex flex-row gap-x-1 font-mono">
<input
type={visible ? 'text' : 'password'}
{value}
readonly
size={value?.length || 20}
class="w-full"
/>
{#if value?.length}
<button type="button" class="flex flex-row items-center" onclick={() => (visible = !visible)}>
<IconContainer icon={visible ? Icons.Visible : Icons.Invisible} width={16} />
</button>
Comment thread
FyreByrd marked this conversation as resolved.
{/if}
</span>
7 changes: 4 additions & 3 deletions src/lib/components/SortTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
-->
<script lang="ts" generics="RowItem extends Record<string, unknown>">
import type { Snippet } from 'svelte';
import { ArrowDownIcon, ArrowUpIcon } from '$lib/icons';
import { Icons } from '$lib/icons';
import IconContainer from '$lib/icons/IconContainer.svelte';

interface Props {
data: RowItem[];
Expand Down Expand Up @@ -93,9 +94,9 @@
<span class="direction-arrow">
{#if current.id === c.id && c.compare}
{#if descending}
<ArrowDownIcon />
<IconContainer icon={Icons.SortDesc} width={24} />
{:else}
<ArrowUpIcon />
<IconContainer icon={Icons.SortAsc} width={24} />
{/if}
{:else}
&nbsp;
Expand Down
27 changes: 27 additions & 0 deletions src/lib/components/Tooltip.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { ClassValue } from 'svelte/elements';

interface Props {
tip?: string | null | undefined;
class?: ClassValue;
// tooltip-content can also be handled in the children snippet
children?: Snippet;
}

let { tip, class: classes, children }: Props = $props();
</script>

<span
class={['tooltip [--tt-bg:var(--color-white)] dark:tooltip-secondary', classes]}
data-tip={tip}
>
{@render children?.()}
</span>

<style>
.tooltip::before,
.tooltip :global(.tooltip-content) {
box-shadow: var(--shadow-md);
}
</style>
10 changes: 0 additions & 10 deletions src/lib/icons/ArrowDownIcon.svelte

This file was deleted.

10 changes: 0 additions & 10 deletions src/lib/icons/ArrowUpIcon.svelte

This file was deleted.

Loading