Skip to content

Commit db0d6b3

Browse files
authored
Cleanup UI on primary pages (#88)
* Add sidebar * Port icon implementation from Scriptoria * Add icons to sidebar layout * Display record counts in sidebar * Redo home page info display * Remove unused function * Fix index display in lists * Fix route highlight * Fix layout * Display PENDING for null result * Improve client list display * Create unified application type definition * Fix sort order * Add icons to homepage * Fix some root styling * Improve project list display * Add hover style to links * Improve job list display * Improve build list display * Improve release list display * Improve about page * Add search to projects and jobs * Add search to builds and releases * Resolve CR feedback * Fix pagination issue
1 parent 737bc06 commit db0d6b3

39 files changed

Lines changed: 2872 additions & 627 deletions

src/app.css

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@import 'tailwindcss';
22

33
@plugin 'daisyui' {
4+
exclude: rootscrollgutter;
45
themes:
56
light --default,
67
dark --prefersdark;
@@ -38,6 +39,8 @@
3839
--border: 1px;
3940
--depth: 0;
4041
--noise: 0;
42+
43+
--color-link: #55f;
4144
}
4245
@plugin "daisyui/theme" {
4346
name: 'dark';
@@ -72,6 +75,8 @@
7275
--border: 1px;
7376
--depth: 0;
7477
--noise: 0;
78+
79+
--color-link: #68f;
7580
}
7681

7782
/*
@@ -134,7 +139,11 @@
134139
0 4px 6px -4px rgb(0 0 0 / 0.1);
135140
}
136141
a.link {
137-
color: #55f;
142+
color: var(--color-link);
143+
}
144+
145+
a.link:hover {
146+
color: color-mix(in oklab, var(--color-link), #fff 15%);
138147
}
139148

140149
.toggle {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<script lang="ts">
2+
import type { HTMLSelectAttributes } from 'svelte/elements';
3+
import Dropdown, { type DropdownClasses } from '$lib/components/Dropdown.svelte';
4+
import { getAppIcon } from '$lib/icons';
5+
import IconContainer from '$lib/icons/IconContainer.svelte';
6+
import { byString } from '$lib/utils/sorting';
7+
import { applicationTypes } from '$lib/valibot';
8+
9+
interface Props {
10+
class?: DropdownClasses;
11+
value: string | null;
12+
allowNull?: boolean;
13+
attr?: HTMLSelectAttributes;
14+
}
15+
16+
let { class: classes = {}, value = $bindable(), allowNull = false, attr = {} }: Props = $props();
17+
18+
const current = $derived(applicationTypes.find((type) => type === value));
19+
20+
let open = $state(false);
21+
22+
function onclick(val: string | null) {
23+
open = false;
24+
value = val;
25+
}
26+
</script>
27+
28+
{#if attr.name}
29+
<input type="hidden" name={attr.name} {value} />
30+
{/if}
31+
32+
<Dropdown
33+
class={{
34+
dropdown: ['w-full', classes.dropdown],
35+
label: ['w-full input cursor-auto', classes.label],
36+
content: ['overflow-y-auto w-auto', classes.content]
37+
}}
38+
bind:open
39+
>
40+
{#snippet label()}
41+
<div class="flex flex-row items-center gap-1 w-full">
42+
{#if current}
43+
<IconContainer icon={getAppIcon(current)} width={24} />
44+
<span class="grow text-left">
45+
{current}
46+
</span>
47+
{:else}
48+
<span class="grow font-normal text-left min-w-48">All application types</span>
49+
{/if}
50+
<IconContainer icon="gridicons:dropdown" width={20} />
51+
</div>
52+
{/snippet}
53+
{#snippet content()}
54+
<ul class="menu menu-sm gap-1 p-2">
55+
{#if allowNull}
56+
<li class="w-full">
57+
<div
58+
class={['btn btn-ghost flex-nowrap justify-start pl-2 pr-1']}
59+
onclick={() => onclick(null)}
60+
onkeydown={(e) => {
61+
if (e.key === 'Enter' || e.key === ' ') {
62+
e.preventDefault();
63+
onclick(null);
64+
}
65+
}}
66+
role="button"
67+
tabindex="0"
68+
>
69+
<span class="grow text-left">All application types</span>
70+
</div>
71+
</li>
72+
{/if}
73+
{#each applicationTypes.toSorted((a, b) => byString(a, b)) as type}
74+
<li class="w-full">
75+
<div
76+
class={[
77+
'btn flex-nowrap justify-start pl-2 pr-1',
78+
type === value ? 'btn-secondary' : 'btn-ghost'
79+
]}
80+
onclick={() => onclick(type)}
81+
onkeydown={(e) => {
82+
if (e.key === 'Enter' || e.key === ' ') {
83+
e.preventDefault();
84+
onclick(type);
85+
}
86+
}}
87+
role="button"
88+
tabindex="0"
89+
>
90+
<IconContainer icon={getAppIcon(type)} width={24} />
91+
<span class="grow text-left">
92+
{type}
93+
</span>
94+
</div>
95+
</li>
96+
{/each}
97+
</ul>
98+
{/snippet}
99+
</Dropdown>

src/lib/components/Dropdown.svelte

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<!--
2+
@component
3+
A simple dropdown menu from DaisyUI.
4+
5+
-->
6+
<script lang="ts" module>
7+
export type DropdownClasses = {
8+
/** class="dropdown ..." */
9+
dropdown?: ClassValue;
10+
/** class="btn btn-ghost ..." */
11+
label?: ClassValue;
12+
/** class="dropdown-content z-10 drop-shadow-lg rounded-md bg-base-200 ..." */
13+
content?: ClassValue;
14+
};
15+
</script>
16+
17+
<script lang="ts">
18+
import type { Snippet } from 'svelte';
19+
import type { ClassValue } from 'svelte/elements';
20+
import { onNavigate } from '$app/navigation';
21+
interface Props {
22+
class?: DropdownClasses;
23+
label: Snippet;
24+
content: Snippet;
25+
onclick?: () => void;
26+
open?: boolean;
27+
}
28+
29+
let { class: classes, label, content, onclick, open = $bindable(false) }: Props = $props();
30+
31+
onNavigate(() => {
32+
// close opened dropdown when navigating (this is mostly important for the dropdowns in the navbar)
33+
open = false;
34+
});
35+
36+
let dropEl: HTMLDetailsElement | undefined = $state(undefined);
37+
</script>
38+
39+
<svelte:window
40+
onclick={(e) => {
41+
// Only close if click is outside this dropdown
42+
if (open && dropEl && !dropEl.contains(e.target as Node)) {
43+
open = false;
44+
// stopPropagation prevents the click from registering on the dropdown again and reopening it
45+
e.stopPropagation();
46+
}
47+
}}
48+
/>
49+
50+
<details class={['dropdown', classes?.dropdown]} bind:open bind:this={dropEl}>
51+
<summary class={['btn btn-ghost', classes?.label]} onclick={() => onclick?.()}>
52+
{@render label()}
53+
</summary>
54+
<div
55+
class={[
56+
'dropdown-content z-10 drop-shadow-lg rounded-md bg-base-200 dark:border dark:border-base-content',
57+
classes?.content
58+
]}
59+
>
60+
{@render content()}
61+
</div>
62+
</details>

src/lib/components/IconContainer.svelte

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<script module lang="ts">
2+
/** Make sure to call this in SuperForms onUpdate**d**() */
3+
export function focusSearchBar() {
4+
(document.querySelector('input[type="search"]') as HTMLInputElement)?.focus?.();
5+
}
6+
</script>
7+
8+
<script lang="ts">
9+
import { onDestroy } from 'svelte';
10+
import type { ClassValue } from 'svelte/elements';
11+
import { Icons } from '$lib/icons';
12+
import IconContainer from '$lib/icons/IconContainer.svelte';
13+
14+
interface Props {
15+
class?: ClassValue;
16+
value: string;
17+
requestSubmit?: () => void;
18+
requestDelay?: number;
19+
}
20+
21+
let {
22+
class: classes,
23+
value = $bindable(),
24+
requestSubmit,
25+
requestDelay = 1_000
26+
}: Props = $props();
27+
28+
let timeout: ReturnType<typeof setTimeout> | null = $state(null);
29+
30+
onDestroy(() => {
31+
if (timeout) {
32+
clearTimeout(timeout);
33+
}
34+
});
35+
</script>
36+
37+
<div class={[classes]} data-html="true">
38+
<label class="input flex items-center gap-2 w-full">
39+
<input
40+
type="search"
41+
placeholder="Search"
42+
aria-label="Search"
43+
class="grow"
44+
bind:value
45+
oninput={(e) => {
46+
if (requestSubmit) {
47+
if (timeout) {
48+
clearTimeout(timeout);
49+
}
50+
timeout = setTimeout(requestSubmit, requestDelay);
51+
}
52+
}}
53+
onfocus={(e) => {
54+
const target = e.currentTarget;
55+
// https://stackoverflow.com/a/10576409
56+
setTimeout(() => target.setSelectionRange(value.length, value.length), 0);
57+
}}
58+
/>
59+
<IconContainer icon={Icons.Search} class="ml-auto cursor-pointer" width={24} />
60+
</label>
61+
</div>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script lang="ts" generics="T extends string | null">
2+
import { Icons } from '$lib/icons';
3+
import IconContainer from '$lib/icons/IconContainer.svelte';
4+
5+
interface Props {
6+
value?: T;
7+
}
8+
9+
let { value }: Props = $props();
10+
11+
let visible = $state(false);
12+
13+
// 20 is default width according to HTML 5 spec
14+
</script>
15+
16+
<span class="inline-flex flex-row gap-x-1 font-mono">
17+
<input
18+
type={visible ? 'text' : 'password'}
19+
{value}
20+
readonly
21+
size={value?.length || 20}
22+
class="w-full"
23+
/>
24+
{#if value?.length}
25+
<button type="button" class="flex flex-row items-center" onclick={() => (visible = !visible)}>
26+
<IconContainer icon={visible ? Icons.Visible : Icons.Invisible} width={16} />
27+
</button>
28+
{/if}
29+
</span>

src/lib/components/SortTable.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
-->
55
<script lang="ts" generics="RowItem extends Record<string, unknown>">
66
import type { Snippet } from 'svelte';
7-
import { ArrowDownIcon, ArrowUpIcon } from '$lib/icons';
7+
import { Icons } from '$lib/icons';
8+
import IconContainer from '$lib/icons/IconContainer.svelte';
89
910
interface Props {
1011
data: RowItem[];
@@ -93,9 +94,9 @@
9394
<span class="direction-arrow">
9495
{#if current.id === c.id && c.compare}
9596
{#if descending}
96-
<ArrowDownIcon />
97+
<IconContainer icon={Icons.SortDesc} width={24} />
9798
{:else}
98-
<ArrowUpIcon />
99+
<IconContainer icon={Icons.SortAsc} width={24} />
99100
{/if}
100101
{:else}
101102
&nbsp;

src/lib/components/Tooltip.svelte

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
import type { ClassValue } from 'svelte/elements';
4+
5+
interface Props {
6+
tip?: string | null | undefined;
7+
class?: ClassValue;
8+
// tooltip-content can also be handled in the children snippet
9+
children?: Snippet;
10+
}
11+
12+
let { tip, class: classes, children }: Props = $props();
13+
</script>
14+
15+
<span
16+
class={['tooltip [--tt-bg:var(--color-white)] dark:tooltip-secondary', classes]}
17+
data-tip={tip}
18+
>
19+
{@render children?.()}
20+
</span>
21+
22+
<style>
23+
.tooltip::before,
24+
.tooltip :global(.tooltip-content) {
25+
box-shadow: var(--shadow-md);
26+
}
27+
</style>

src/lib/icons/ArrowDownIcon.svelte

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/lib/icons/ArrowUpIcon.svelte

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)