feat: better deploys page and single deploy page
This commit is contained in:
+22
-6
@@ -1,3 +1,4 @@
|
|||||||
|
import { normalizeURL } from '@/lib/utils'
|
||||||
import { Value } from './Inspect.jsx'
|
import { Value } from './Inspect.jsx'
|
||||||
|
|
||||||
// const GitRef = ({ type, value }) => {
|
// const GitRef = ({ type, value }) => {
|
||||||
@@ -17,15 +18,30 @@ import { Value } from './Inspect.jsx'
|
|||||||
// <GitRef {...ref} />
|
// <GitRef {...ref} />
|
||||||
// </>
|
// </>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ deploy: import('@/config.js').Deploy }} param0
|
||||||
|
*/
|
||||||
|
|
||||||
export const Deploy = ({ deploy }) => {
|
export const Deploy = ({ deploy }) => {
|
||||||
return (
|
return (
|
||||||
<div class="deploy">
|
<div class="deploy">
|
||||||
<div class="name">{deploy.name}</div>
|
<div class="title">
|
||||||
<div class="type">{deploy.type}</div>
|
<a href={`/deploys/${deploy.name}`}>{deploy.name}</a>
|
||||||
<div class="type">{deploy.type}</div>
|
</div>
|
||||||
<div class="type">{deploy.type}</div>
|
<div class="shield">{deploy.type}</div>
|
||||||
|
{deploy.type === 'docker' ? (
|
||||||
<Value value={deploy} borderless />
|
<div class="description">{deploy.options.image}</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div class="description">
|
||||||
|
{deploy.url.startsWith('/') ? (
|
||||||
|
deploy.url
|
||||||
|
) : (
|
||||||
|
<a href={normalizeURL(deploy.url)}>{deploy.url}</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-3
@@ -1,8 +1,20 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { clsx } from '@/lib/utils'
|
import { clsx } from '@/lib/utils'
|
||||||
|
|
||||||
export const Value = ({ value, borderless }) => {
|
export const Value = ({ value, borderless } = {}) => {
|
||||||
return Array.isArray(value) ? (
|
return value === undefined ? (
|
||||||
|
<em>undefined</em>
|
||||||
|
) : value === null ? (
|
||||||
|
<em>null</em>
|
||||||
|
) : value === true ? (
|
||||||
|
<em>true</em>
|
||||||
|
) : value === false ? (
|
||||||
|
<em>false</em>
|
||||||
|
) : typeof value === 'string' && value.trim().length === 0 ? (
|
||||||
|
<em>Empty string</em>
|
||||||
|
) : !value ? (
|
||||||
|
value
|
||||||
|
) : Array.isArray(value) ? (
|
||||||
<ValueArray value={value} borderless={borderless} />
|
<ValueArray value={value} borderless={borderless} />
|
||||||
) : typeof value === 'object' ? (
|
) : typeof value === 'object' ? (
|
||||||
<ValueObject value={value} borderless={borderless} />
|
<ValueObject value={value} borderless={borderless} />
|
||||||
@@ -12,7 +24,9 @@ export const Value = ({ value, borderless }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ValueArray = ({ value, borderless }) => {
|
export const ValueArray = ({ value, borderless }) => {
|
||||||
return (
|
return value.length === 0 ? (
|
||||||
|
<em>Empty array</em>
|
||||||
|
) : (
|
||||||
<div class={clsx('compound-value array', borderless && 'borderless')}>
|
<div class={clsx('compound-value array', borderless && 'borderless')}>
|
||||||
{value.map(item => (
|
{value.map(item => (
|
||||||
<div class="item">
|
<div class="item">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "preact/hooks"
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
|
||||||
export const JobLogs = ({}) => {
|
export const JobLogs = ({}) => {
|
||||||
const [logLines, setLogLines] = useState([])
|
const [logLines, setLogLines] = useState([])
|
||||||
@@ -13,13 +13,11 @@ export const JobLogs = ({}) => {
|
|||||||
const es = new EventSource(location.href + '/logs?format=sse')
|
const es = new EventSource(location.href + '/logs?format=sse')
|
||||||
es.addEventListener('message', ({ data }) => {
|
es.addEventListener('message', ({ data }) => {
|
||||||
const event = JSON.parse(data)
|
const event = JSON.parse(data)
|
||||||
setLogLines(lines => [
|
setLogLines(lines => [...lines, event.content])
|
||||||
...lines,
|
|
||||||
event.content,
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<pre><code>{logLines.join('\n')}</code></pre>
|
<pre><code>{logLines.join('\n')}</code></pre>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,12 +42,14 @@ export const JobsPage = ({}) => {
|
|||||||
const res = await fetch(location.href + '?format=json')
|
const res = await fetch(location.href + '?format=json')
|
||||||
const jobs = await res.json()
|
const jobs = await res.json()
|
||||||
|
|
||||||
const result = {}
|
// const result = {}
|
||||||
|
|
||||||
for (const item of jobs) {
|
// for (const item of jobs) {
|
||||||
result[item.uuid] = item
|
// result[item.uuid] = item
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
const result = _.keyBy(jobs, 'uuid')
|
||||||
|
console.log(result)
|
||||||
setJobStore(result)
|
setJobStore(result)
|
||||||
|
|
||||||
// Setup SSE
|
// Setup SSE
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="header">phCD</div>
|
<!-- <div class="header">phCD</div> -->
|
||||||
|
<button class="menu-button flat">
|
||||||
|
<span class="material-symbols-outlined">menu</span>
|
||||||
|
</button>
|
||||||
<nav>
|
<nav>
|
||||||
<div class="nav-item group">
|
<div class="nav-item group">
|
||||||
<a class="label" href="/"> <span class="material-symbols-outlined">home</span> Home</a>
|
<a class="label" href="/"> <span class="material-symbols-outlined">home</span> Home</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item group">
|
<div class="nav-item group">
|
||||||
<a class="label" href="/containers"
|
<a class="label" href="/containers">
|
||||||
><span class="material-symbols-outlined">view_list</span> Containers</a
|
<span class="material-symbols-outlined">view_list</span> Containers
|
||||||
>
|
</a>
|
||||||
<!-- <div class="children">
|
<!-- <div class="children">
|
||||||
<div class="nav-item"><div class="label">Container 1</div></div>
|
<div class="nav-item"><div class="label">Container 1</div></div>
|
||||||
<div class="nav-item"><div class="label">Container 2</div></div>
|
<div class="nav-item"><div class="label">Container 2</div></div>
|
||||||
@@ -15,9 +18,9 @@
|
|||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item group">
|
<div class="nav-item group">
|
||||||
<a class="label" href="/deploys"
|
<a class="label" href="/deploys">
|
||||||
><span class="material-symbols-outlined">deployed_code</span> Deploys</a
|
<span class="material-symbols-outlined">deployed_code</span> Deploys
|
||||||
>
|
</a>
|
||||||
<!-- <div class="children">
|
<!-- <div class="children">
|
||||||
<div class="nav-item"><div class="label">Deploy 1</div></div>
|
<div class="nav-item"><div class="label">Deploy 1</div></div>
|
||||||
<div class="nav-item"><div class="label">Deploy 2</div></div>
|
<div class="nav-item"><div class="label">Deploy 2</div></div>
|
||||||
@@ -25,9 +28,9 @@
|
|||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item group">
|
<div class="nav-item group">
|
||||||
<a class="label" href="/jobs"
|
<a class="label" href="/jobs">
|
||||||
><span class="material-symbols-outlined">terminal</span> Jobs</a
|
<span class="material-symbols-outlined">terminal</span> Jobs
|
||||||
>
|
</a>
|
||||||
<!-- <div class="children">
|
<!-- <div class="children">
|
||||||
<div class="nav-item"><div class="label">Deploy 1</div></div>
|
<div class="nav-item"><div class="label">Deploy 1</div></div>
|
||||||
<div class="nav-item"><div class="label">Deploy 2</div></div>
|
<div class="nav-item"><div class="label">Deploy 2</div></div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const { title } = Astro.props
|
|||||||
<body>
|
<body>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div class="scroll-container">
|
<div class="scroll-container">
|
||||||
|
<div class="logo">phCD</div>
|
||||||
<main>
|
<main>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@ export class JsonStreamResponse extends Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createQueryUrl(base: string, options: Record<string, string>) {
|
export function createUrlQuery(base: string, options: Record<string, string>) {
|
||||||
return `${base}?${Object.entries(options)
|
return `${base}?${Object.entries(options)
|
||||||
.map(([k, v]) => k + '=' + encodeURIComponent(v))
|
.map(([k, v]) => k + '=' + encodeURIComponent(v))
|
||||||
.join('&')}`
|
.join('&')}`
|
||||||
|
|||||||
+22
-22
@@ -1,34 +1,34 @@
|
|||||||
---
|
---
|
||||||
|
import { Value } from '@client/Inspect'
|
||||||
import Layout from '../layouts/Layout.astro'
|
import Layout from '../layouts/Layout.astro'
|
||||||
import Docker from 'dockerode'
|
import Docker from 'dockerode'
|
||||||
|
|
||||||
const docker = new Docker({ socketPath: '/var/run/docker.sock' })
|
const docker = new Docker({ socketPath: '/var/run/docker.sock' })
|
||||||
|
|
||||||
const containers = await docker.listContainers({ all: true })
|
const containers = await docker.listContainers({ all: true })
|
||||||
|
|
||||||
|
console.dir(containers, { depth: null })
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Containers | phCD">
|
<Layout title="Containers | phCD">
|
||||||
<h1>Containers</h1>
|
<h1>Containers</h1>
|
||||||
<ul>
|
{containers.map(container => <Value client:load value={container} borderless={false} />)}
|
||||||
{
|
<!-- <ul>
|
||||||
containers.map(container => (
|
<li>
|
||||||
<li>
|
<strong>Name:</strong> {container.Names[0]} <br />
|
||||||
<strong>Name:</strong> {container.Names[0]} <br />
|
<strong>Command:</strong> <code>{container.Command}</code> <br />
|
||||||
<strong>Command:</strong> <code>{container.Command}</code> <br />
|
<strong>Image:</strong> <code>{container.Image}</code> <br />
|
||||||
<strong>Image:</strong> <code>{container.Image}</code> <br />
|
<strong>Status:</strong> {container.Status} <br />
|
||||||
<strong>Status:</strong> {container.Status} <br />
|
<strong>Mounts:</strong>
|
||||||
<strong>Mounts:</strong>
|
<ul>
|
||||||
<ul>
|
{container.Mounts.map(mount => (
|
||||||
{container.Mounts.map(mount => (
|
<li>
|
||||||
<li>
|
<strong>Type:</strong> {mount.Type} <br />
|
||||||
<strong>Type:</strong> {mount.Type} <br />
|
<strong>From:</strong> <code>{mount.Source}</code> <br />
|
||||||
<strong>From:</strong> <code>{mount.Source}</code> <br />
|
<strong>To:</strong> <code>{mount.Destination}</code> <br />
|
||||||
<strong>To:</strong> <code>{mount.Destination}</code> <br />
|
</li>
|
||||||
</li>
|
))}
|
||||||
))}
|
</ul>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
</ul> -->
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
import { Deploy } from '@client/Deploy'
|
||||||
|
import { Value } from '@client/Inspect'
|
||||||
|
|
||||||
|
import { loadConfig } from '@/config'
|
||||||
|
import Layout from '@layouts/Layout.astro'
|
||||||
|
import { createUrlQuery } from '@/lib/utils'
|
||||||
|
|
||||||
|
const { name } = Astro.params
|
||||||
|
|
||||||
|
const { deploys } = await loadConfig()
|
||||||
|
const deploy = deploys.find(d => d.name === name)
|
||||||
|
|
||||||
|
if (!deploy) {
|
||||||
|
return Astro.redirect(
|
||||||
|
createUrlQuery('/error', {
|
||||||
|
message: `No deploy with name "${name}"`,
|
||||||
|
previous: `/deploys`,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={`${name} | Deploy | phCD`}>
|
||||||
|
<h1>
|
||||||
|
<div class="kind">Deploy</div>
|
||||||
|
<div class="title">{name}</div>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<Value value={deploy} borderless />
|
||||||
|
</Layout>
|
||||||
@@ -8,7 +8,9 @@ const { deploys } = await loadConfig()
|
|||||||
|
|
||||||
<Layout title="Deploys | phCD">
|
<Layout title="Deploys | phCD">
|
||||||
<h1>Deploys</h1>
|
<h1>Deploys</h1>
|
||||||
<a class="button" href="/deploys/new">New Deploy</a>
|
<a class="button" href="/deploys/new">
|
||||||
|
<span class="material-symbols-outlined">add</span> New Deploy
|
||||||
|
</a>
|
||||||
<div class="deploys">
|
<div class="deploys">
|
||||||
{deploys.toReversed().map(deploy => <Deploy deploy={deploy} />)}
|
{deploys.toReversed().map(deploy => <Deploy deploy={deploy} />)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Layout from '../layouts/Layout.astro'
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Homepage">
|
<Layout title="Homepage">
|
||||||
<h1>phCD</h1>
|
|
||||||
<h2>Recent Deploys</h2>
|
<h2>Recent Deploys</h2>
|
||||||
<p>Bla bla recent jobs</p>
|
<p>Bla bla recent jobs</p>
|
||||||
<h2>Containers</h2>
|
<h2>Containers</h2>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getJob, getJobLogs, OnJobLog } from '@/jobs'
|
|||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import { JobLogs } from '@client/JobLogs'
|
import { JobLogs } from '@client/JobLogs'
|
||||||
|
|
||||||
import { JsonResponse, createQueryUrl } from '@/lib/utils'
|
import { JsonResponse, createUrlQuery } from '@/lib/utils'
|
||||||
|
|
||||||
const { uuid } = Astro.params
|
const { uuid } = Astro.params
|
||||||
|
|
||||||
@@ -11,10 +11,10 @@ const job = await getJob(uuid!)
|
|||||||
|
|
||||||
if (!job) {
|
if (!job) {
|
||||||
return Astro.redirect(
|
return Astro.redirect(
|
||||||
createQueryUrl('/error', {
|
createUrlQuery('/error', {
|
||||||
message: `No job with uuid "${uuid}"`,
|
message: `No job with uuid "${uuid}"`,
|
||||||
previous: `/jobs`,
|
previous: `/jobs`,
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,11 +25,16 @@ if (job.status === 'completed') {
|
|||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="{`${job.name}" | Jobs | phCD`}>
|
<Layout title={`"${job.name}" | Jobs | phCD`}>
|
||||||
<h1>Job "{job.name}"</h1>
|
<h1>Job "{job.name}"</h1>
|
||||||
<pre><code>{JSON.stringify(job, null, 2)}</code></pre>
|
<pre><code>{JSON.stringify(job, null, 2)}</code></pre>
|
||||||
<h2>Logs</h2>
|
<h2>Logs</h2>
|
||||||
{job.status === 'completed' ?
|
{
|
||||||
<pre><code>{logsContent}</code></pre>
|
job.status === 'completed' ? (
|
||||||
: <JobLogs client:load />}
|
// prettier-ignore
|
||||||
|
<pre><code>{logsContent}</code></pre>
|
||||||
|
) : (
|
||||||
|
<JobLogs client:load />
|
||||||
|
)
|
||||||
|
}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { OnJobLog, getJob, getJobLogs } from '@/jobs'
|
import { OnJobLog, getJob, getJobLogs } from '@/jobs'
|
||||||
import { JsonResponse, JsonStreamResponse, createQueryUrl } from '@/lib/utils'
|
import { JsonResponse, JsonStreamResponse, createUrlQuery } from '@/lib/utils'
|
||||||
import { debug } from '@/logger'
|
import { debug } from '@/logger'
|
||||||
import type { APIRoute } from 'astro'
|
import type { APIRoute } from 'astro'
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export const GET: APIRoute = async ({ params: { uuid }, url, redirect }) => {
|
|||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
return redirect(
|
return redirect(
|
||||||
createQueryUrl('/error', {
|
createUrlQuery('/error', {
|
||||||
message: `Invalid format "${format}"`,
|
message: `Invalid format "${format}"`,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
+130
-11
@@ -101,8 +101,8 @@ button,
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
color: #333;
|
color: darken($accent-400, 80%);
|
||||||
background-color: #00000018;
|
background-color: darken($accent-400, 5%);
|
||||||
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -113,8 +113,27 @@ button,
|
|||||||
|
|
||||||
// border: 1px solid #e0e0e0;
|
// border: 1px solid #e0e0e0;
|
||||||
|
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
& > .material-symbols-outlined {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #00000030;
|
background-color: darken($accent-400, 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.flat {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #00000010;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,11 +317,70 @@ form {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
background: $accent-400;
|
background: $accent-400;
|
||||||
padding: 1rem;
|
padding: 1rem 1rem 2rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
grid-template-areas:
|
||||||
|
'title shield'
|
||||||
|
'description .';
|
||||||
|
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
grid-area: title;
|
||||||
|
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #444;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .shield {
|
||||||
|
grid-area: shield;
|
||||||
|
|
||||||
|
background: darken($accent-400, 4%);
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .description {
|
||||||
|
grid-area: description;
|
||||||
|
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 15px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// border: 1px solid #ddd;
|
// border: 1px solid #ddd;
|
||||||
// padding: 1rem;
|
// padding: 1rem;
|
||||||
// border-radius: 1rem;
|
// border-radius: 1rem;
|
||||||
@@ -328,16 +406,19 @@ body {
|
|||||||
grid-area: sidebar;
|
grid-area: sidebar;
|
||||||
|
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
width: 20vw;
|
width: 15vw;
|
||||||
|
|
||||||
background: $accent-400;
|
background: $accent-400;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
// box-shadow: 0 0 1rem 0 #0002, 0 0 0.25rem 0 #0002;
|
// box-shadow: 0 0 1rem 0 #0002, 0 0 0.25rem 0 #0002;
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -346,6 +427,16 @@ body {
|
|||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-button {
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -353,7 +444,7 @@ body {
|
|||||||
|
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
padding: 0 1rem;
|
padding-top: 1rem;
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -364,14 +455,14 @@ body {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: row;
|
grid-auto-flow: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 150ms ease-in-out;
|
transition: background-color 150ms ease-in-out;
|
||||||
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
||||||
@@ -405,7 +496,7 @@ body {
|
|||||||
&.group {
|
&.group {
|
||||||
& > .label {
|
& > .label {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,8 +509,23 @@ body {
|
|||||||
justify-items: center;
|
justify-items: center;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& > .logo {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 2rem;
|
overflow-x: auto;
|
||||||
|
|
||||||
|
padding: 5rem 1rem 2rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -436,6 +542,19 @@ body {
|
|||||||
// Components
|
// Components
|
||||||
//
|
//
|
||||||
|
|
||||||
|
& > h1 {
|
||||||
|
& > .title {
|
||||||
|
}
|
||||||
|
& > .kind {
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "preact",
|
"jsxImportSource": "preact",
|
||||||
"exactOptionalPropertyTypes": true,
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"],
|
"@/*": ["src/*"],
|
||||||
|
|||||||
Reference in New Issue
Block a user