A focused Docker Compose management web application.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: tab component

Brooke aee3cc2f 46ea8f08

+173 -8
+1 -1
packages/panel/src/lib/api/index.ts
··· 100 100 /** 101 101 * The client used to make API requests to the backend. 102 102 */ 103 - export const client = createClient<CaughtPaths>({ baseUrl: "./" }); 103 + export const client = createClient<CaughtPaths>({ baseUrl: "/" }); 104 104 client.use(middleware); 105 105 106 106 /**
+124
packages/panel/src/lib/component/Tabs.svelte
··· 1 + <!-- 2 + @component 3 + 4 + A Svelte component providing tab functionality through the given list of snippets. 5 + 6 + On smaller screens, this transforms into an accordian component. 7 + 8 + --> 9 + 10 + <script lang="ts"> 11 + import type { IconDefinition } from "@fortawesome/fontawesome-common-types"; 12 + import { Accordion } from "melt/builders"; 13 + import { fade, slide } from "svelte/transition"; 14 + import type { Snippet } from "svelte"; 15 + import Fa from "svelte-fa"; 16 + import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; 17 + 18 + let { tabs }: { tabs: { label: string; icon: IconDefinition; content: Snippet<[]> }[] } = $props(); 19 + 20 + const accordion = new Accordion(); 21 + 22 + let windowWidth = $state(0); 23 + let mobile = $derived(windowWidth <= 425); 24 + 25 + $effect(() => { 26 + if (accordion.value === undefined && !mobile) { 27 + accordion.value = tabs[0].label; 28 + } 29 + }); 30 + </script> 31 + 32 + <svelte:window bind:innerWidth={windowWidth} /> 33 + 34 + <div {...accordion.root}> 35 + <div 36 + class:container={!mobile} 37 + class:flexc={mobile} 38 + class="flexr gap-10" 39 + style:width={mobile ? "100%" : "fit-content"} 40 + > 41 + {#each tabs as tab (tab.label)} 42 + {@const item = accordion.getItem({ id: tab.label })} 43 + <div class:container={mobile}> 44 + <button 45 + class="a switch" 46 + {...item.trigger} 47 + class:active={item.isExpanded} 48 + aria-label="switch to {tab.label} tab" 49 + > 50 + <Fa icon={tab.icon} translateY="0.1" /> 51 + <span {...item.heading}>{tab.label}</span> 52 + 53 + {#if mobile} 54 + <Fa icon={item.isExpanded ? faChevronDown : faChevronUp} style="margin-left: auto;" /> 55 + {/if} 56 + </button> 57 + {#if mobile} 58 + {#if item.isExpanded} 59 + <div style="overflow: hidden;" transition:slide={{ duration: 250 }}> 60 + <hr style="margin-top: 15px;" /> 61 + <div {...item.content}> 62 + {@render tab.content()} 63 + </div> 64 + </div> 65 + {/if} 66 + {/if} 67 + </div> 68 + {/each} 69 + </div> 70 + 71 + {#if !mobile} 72 + <div class="container"> 73 + {#each tabs as tab (tab.label)} 74 + {@const item = accordion.getItem({ id: tab.label })} 75 + {#if item.isExpanded} 76 + <div {...item.content} in:fade={{ duration: 125 }}> 77 + {@render tab.content()} 78 + </div> 79 + {/if} 80 + {/each} 81 + </div> 82 + {/if} 83 + </div> 84 + 85 + <style lang="scss"> 86 + .container { 87 + background-color: var(--surface0); 88 + border-radius: 10px; 89 + 90 + padding: 10px; 91 + 92 + @media (min-width: 426px) { 93 + margin-bottom: 10px; 94 + } 95 + } 96 + 97 + .switch { 98 + display: flex; 99 + align-items: center; 100 + gap: 8px; 101 + 102 + text-transform: lowercase; 103 + color: var(--text); 104 + 105 + width: 100%; 106 + 107 + @media (max-width: 425px) { 108 + font-size: 18px; 109 + } 110 + 111 + &:hover { 112 + text-decoration: none; 113 + color: var(--mauve); 114 + } 115 + 116 + &.active { 117 + color: var(--mauve); 118 + 119 + @media (min-width: 426px) { 120 + pointer-events: none; 121 + } 122 + } 123 + } 124 + </style>
+4
packages/panel/src/lib/style.scss
··· 92 92 } 93 93 } 94 94 95 + hr { 96 + border-color: var(--subtext0); 97 + } 98 + 95 99 label { 96 100 display: block; 97 101 margin-bottom: 5px;
+1 -5
packages/panel/src/routes/(authenticated)/Navbar.svelte
··· 28 28 { icon: faCircleUser, label: "User", href: "/user" }, 29 29 ] satisfies { icon: any; label: string; href: string }[]; 30 30 31 - let expanded = $state(localStorage.getItem(EXPANDED_KEY) === "true"); 31 + let expanded = $state(localStorage.getItem(EXPANDED_KEY) !== "false"); 32 32 let open = $state(false); 33 33 34 34 let navbarWidth = $state(0); ··· 188 188 &:hover, 189 189 &.current { 190 190 color: var(--mauve); 191 - } 192 - 193 - &.current { 194 - pointer-events: none; 195 191 } 196 192 } 197 193
+1 -2
packages/panel/src/routes/(authenticated)/projects/+page.svelte
··· 42 42 {#if item.isExpanded} 43 43 <div class="grid" {...item.content}> 44 44 {#each projects as project (project.name)} 45 - <a href="/project/{project.name}" class="project"> 45 + <a href="/projects/{project.name}" class="project"> 46 46 <h2> 47 47 <StatusIcon status={project.status} /> 48 48 {project.name} ··· 83 83 text-decoration: none !important; 84 84 85 85 hr { 86 - border-color: var(--subtext0); 87 86 flex-grow: 1; 88 87 } 89 88 }
+25
packages/panel/src/routes/(authenticated)/projects/[project]/+page.svelte
··· 1 + <script> 2 + import Tabs from "$lib/component/Tabs.svelte"; 3 + import { faCircleInfo, faGears, faPencil } from "@fortawesome/free-solid-svg-icons"; 4 + </script> 5 + 6 + <Tabs 7 + tabs={[ 8 + { label: "status", icon: faCircleInfo, content: status }, 9 + { label: "compose", icon: faPencil, content: compose }, 10 + { label: "variables", icon: faGears, content: variables }, 11 + ]} 12 + /> 13 + 14 + {#snippet status()} 15 + <h1>Status Tab</h1> 16 + <p>Woah</p> 17 + {/snippet} 18 + 19 + {#snippet compose()} 20 + <h1>Compose Tab</h1> 21 + {/snippet} 22 + 23 + {#snippet variables()} 24 + <h1>Variables Tab</h1> 25 + {/snippet}
+17
packages/panel/src/routes/(authenticated)/projects/[project]/+page.ts
··· 1 + import { api } from "$lib"; 2 + import { error } from "@sveltejs/kit"; 3 + import type { PageLoad } from "./$types"; 4 + 5 + export const prerender = false; 6 + 7 + export const load: PageLoad = async ({ params }) => { 8 + const response = await api.client 9 + .GET("/api/project/{project}/compose", { 10 + params: { path: { project: params.project } }, 11 + }) 12 + .catch(() => error(404, "Project not found")); 13 + 14 + return { 15 + compose: response.data!, 16 + }; 17 + };