feat: deploy form
This commit is contained in:
+3
-1
@@ -21,4 +21,6 @@ pnpm-debug.log*
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# editors
|
# editors
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
config.yaml
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
deploys:
|
|
||||||
- name: project1
|
|
||||||
url: https://github.com/username/project1
|
|
||||||
branch: main
|
|
||||||
type: docker
|
|
||||||
options:
|
|
||||||
ports:
|
|
||||||
- 80:8080
|
|
||||||
volumes:
|
|
||||||
- /var/www/html:/var/www/html
|
|
||||||
- name: project2
|
|
||||||
url: https://github.com/username/project2
|
|
||||||
type: dockerfile
|
|
||||||
options:
|
|
||||||
ports:
|
|
||||||
- 9000:8080
|
|
||||||
volumes:
|
|
||||||
- /var/www/html:/var/www/html
|
|
||||||
- name: project2
|
|
||||||
url: ssh://example.org/username/project2.git
|
|
||||||
commit: 04c540647a
|
|
||||||
type: docker-compose
|
|
||||||
options:
|
|
||||||
path: ./docker-compose.yml
|
|
||||||
- name: project3
|
|
||||||
url: https://github.com/username/project3
|
|
||||||
type: shell
|
|
||||||
options:
|
|
||||||
path: ./deploy.sh
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+2
-1
@@ -23,7 +23,8 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nodegit": "^0.27.0",
|
"nodegit": "^0.27.0",
|
||||||
"preact": "^10.19.4",
|
"preact": "^10.19.4",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3",
|
||||||
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dockerode": "^3.3.23",
|
"@types/dockerode": "^3.3.23",
|
||||||
|
|||||||
Generated
+3
@@ -47,6 +47,9 @@ dependencies:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.3.3
|
specifier: ^5.3.3
|
||||||
version: 5.3.3
|
version: 5.3.3
|
||||||
|
zod:
|
||||||
|
specifier: ^3.22.4
|
||||||
|
version: 3.22.4
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/dockerode':
|
'@types/dockerode':
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
|
|
||||||
export const NewDeployForm = () => {
|
export const NewDeployForm = () => {
|
||||||
const [refType, setRefType] = useState('default')
|
|
||||||
const [deployType, setDeployType] = useState('initial')
|
const [deployType, setDeployType] = useState('initial')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -9,6 +8,36 @@ export const NewDeployForm = () => {
|
|||||||
<label for="deploy-name">Name</label>
|
<label for="deploy-name">Name</label>
|
||||||
<input id="deploy-name" name="deploy-name" type="text" placeholder="Project name..." />
|
<input id="deploy-name" name="deploy-name" type="text" placeholder="Project name..." />
|
||||||
|
|
||||||
|
<label for="deploy-type">Type</label>
|
||||||
|
<select
|
||||||
|
id="deploy-type"
|
||||||
|
name="deploy-type"
|
||||||
|
value={deployType}
|
||||||
|
onChange={e => setDeployType(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="initial" disabled>
|
||||||
|
Select a deploy type...
|
||||||
|
</option>
|
||||||
|
<option value="docker">Docker</option>
|
||||||
|
<option value="dockerfile">Dockerfile</option>
|
||||||
|
<option value="docker-compose">Docker Compose</option>
|
||||||
|
<option value="shell">Shell</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<DeployOptions type={deployType} />
|
||||||
|
|
||||||
|
<div class="row right">
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeployRefOptions = ({}) => {
|
||||||
|
const [refType, setRefType] = useState('default')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<label for="deploy-url">Url</label>
|
<label for="deploy-url">Url</label>
|
||||||
<input
|
<input
|
||||||
id="deploy-url"
|
id="deploy-url"
|
||||||
@@ -39,33 +68,11 @@ export const NewDeployForm = () => {
|
|||||||
placeholder="Ref value..."
|
placeholder="Ref value..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<label for="deploy-type">Type</label>
|
|
||||||
<select
|
|
||||||
id="deploy-type"
|
|
||||||
name="deploy-type"
|
|
||||||
value={deployType}
|
|
||||||
onChange={e => setDeployType(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="initial" disabled>
|
|
||||||
Select a deploy type...
|
|
||||||
</option>
|
|
||||||
<option value="docker">Docker</option>
|
|
||||||
<option value="dockerfile">Dockerfile</option>
|
|
||||||
<option value="docker-compose">Docker Compose</option>
|
|
||||||
<option value="shell">Shell</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<DeployOptions type={deployType} />
|
|
||||||
|
|
||||||
<div class="row right">
|
|
||||||
<button type="submit">Create</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeployOptions = ({ type }) => {
|
const DeployOptions = ({ type }) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'docker':
|
case 'docker':
|
||||||
return <DockerDeploy />
|
return <DockerDeploy />
|
||||||
@@ -83,6 +90,14 @@ export const DeployOptions = ({ type }) => {
|
|||||||
const DockerDeploy = () => {
|
const DockerDeploy = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<label for="deploy-options-image">Container Name</label>
|
||||||
|
<input
|
||||||
|
id="deploy-options-name"
|
||||||
|
name="deploy-options-name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Custom container name..."
|
||||||
|
/>
|
||||||
|
|
||||||
<label for="deploy-options-image">Image</label>
|
<label for="deploy-options-image">Image</label>
|
||||||
<input
|
<input
|
||||||
id="deploy-options-image"
|
id="deploy-options-image"
|
||||||
@@ -121,6 +136,8 @@ const DockerDeploy = () => {
|
|||||||
const DockerfileDeploy = () => {
|
const DockerfileDeploy = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DeployRefOptions />
|
||||||
|
|
||||||
<label for="deploy-options-path">Path</label>
|
<label for="deploy-options-path">Path</label>
|
||||||
<input
|
<input
|
||||||
id="deploy-options-path"
|
id="deploy-options-path"
|
||||||
@@ -159,6 +176,8 @@ const DockerfileDeploy = () => {
|
|||||||
const ShellDeploy = () => {
|
const ShellDeploy = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DeployRefOptions />
|
||||||
|
|
||||||
<label for="deploy-options-path">Path</label>
|
<label for="deploy-options-path">Path</label>
|
||||||
<input
|
<input
|
||||||
id="deploy-options-path"
|
id="deploy-options-path"
|
||||||
@@ -181,6 +200,8 @@ const ShellDeploy = () => {
|
|||||||
const DockerComposeDeploy = () => {
|
const DockerComposeDeploy = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DeployRefOptions />
|
||||||
|
|
||||||
<label for="deploy-options-path">Path</label>
|
<label for="deploy-options-path">Path</label>
|
||||||
<input
|
<input
|
||||||
id="deploy-options-path"
|
id="deploy-options-path"
|
||||||
|
|||||||
+5
-5
@@ -56,8 +56,8 @@ export type Config = {
|
|||||||
deploys: Deploy[]
|
deploys: Deploy[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const refToString = (ref: GitRef) => {
|
export const refToString = (ref: GitRef, defaultStr: string = '<default>') => {
|
||||||
return ref.type === 'default' ? '<default>' : ref.value
|
return ref.type === 'default' ? defaultStr : ref.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutex = new Mutex()
|
const mutex = new Mutex()
|
||||||
@@ -69,13 +69,13 @@ export function loadConfig(): Promise<Config> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateConfig(fn: (config: Config) => Promise<Config>) {
|
export function updateConfig(fn: (config: Config) => Promise<void>) {
|
||||||
return mutex.runExclusive(async () => {
|
return mutex.runExclusive(async () => {
|
||||||
const data = await readFile('./config.yaml', 'utf8')
|
const data = await readFile('./config.yaml', 'utf8')
|
||||||
const config = yaml.load(data) as Config
|
const config = yaml.load(data) as Config
|
||||||
|
|
||||||
const newConfig = await fn(config)
|
await fn(config)
|
||||||
|
|
||||||
await writeFile('./config.yaml', yaml.dump(newConfig))
|
await writeFile('./config.yaml', yaml.dump(config))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
import type { Deploy, GitRef } from './config'
|
||||||
|
|
||||||
|
function cutString(s: string, sep: string): [string, string] {
|
||||||
|
return [s.slice(0, s.indexOf(sep)), s.slice(s.indexOf(sep) + sep.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
function notEmpty(s: string, message: string) {
|
||||||
|
if (s.trim().length === 0) {
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRef(form: Record<string, any>): GitRef {
|
||||||
|
const type = form['deploy-ref-type']
|
||||||
|
const value = form['deploy-ref-value']
|
||||||
|
|
||||||
|
return type === 'default' ? { type } : { type, value }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseDeploy(form: Record<string, any>): Deploy {
|
||||||
|
const name = form['deploy-name']
|
||||||
|
const type = form['deploy-type']
|
||||||
|
|
||||||
|
if (type === 'docker') {
|
||||||
|
// const url = notEmpty(form['deploy-url'], 'must provide an url')
|
||||||
|
const containerName = form['deploy-options-name'] as string
|
||||||
|
const image = notEmpty(form['deploy-options-image'], 'must provide an image name')
|
||||||
|
const ports = form['deploy-options-ports'] as string
|
||||||
|
const env = form['deploy-options-env'] as string
|
||||||
|
const volumes = form['deploy-options-volumes'] as string
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
type: 'docker',
|
||||||
|
options: {
|
||||||
|
image,
|
||||||
|
name: containerName,
|
||||||
|
env: Object.fromEntries(env.split(/\n/g).map(line => cutString(line.trim(), '='))),
|
||||||
|
ports: ports.split(/\n/g).map(line => line.trim()),
|
||||||
|
volumes: volumes.split(/\n/g).map(line => line.trim()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'shell') {
|
||||||
|
const path = form['deploy-options-path'] as string
|
||||||
|
const env = form['deploy-options-env'] as string
|
||||||
|
|
||||||
|
const url = form['deploy-url'] as string
|
||||||
|
const ref = parseRef(form)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
ref,
|
||||||
|
type: 'shell',
|
||||||
|
options: {
|
||||||
|
path,
|
||||||
|
env: Object.fromEntries(env.split(/\n/g).map(line => cutString(line.trim(), '='))),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'dockerfile') {
|
||||||
|
const url = form['deploy-url'] as string
|
||||||
|
const ref = parseRef(form)
|
||||||
|
|
||||||
|
const path = form['deploy-options-path'] as string
|
||||||
|
const ports = form['deploy-options-ports'] as string
|
||||||
|
const env = form['deploy-options-env'] as string
|
||||||
|
const volumes = form['deploy-options-volumes'] as string
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
ref,
|
||||||
|
type: 'dockerfile',
|
||||||
|
options: {
|
||||||
|
path,
|
||||||
|
env: Object.fromEntries(env.split(/\n/g).map(line => cutString(line.trim(), '='))),
|
||||||
|
ports: ports.split(/\n/g).map(line => line.trim()),
|
||||||
|
volumes: volumes.split(/\n/g).map(line => line.trim()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'docker-compose') {
|
||||||
|
const url = form['deploy-url'] as string
|
||||||
|
const ref = parseRef(form)
|
||||||
|
|
||||||
|
const path = form['deploy-options-path'] as string
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
ref,
|
||||||
|
type: 'docker-compose',
|
||||||
|
options: {
|
||||||
|
path,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('invalid deploy type')
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
const queue: (() => Promise<void>)[] = []
|
||||||
|
|
||||||
|
// to ensure that the while loop inside triggerProcessQueue is getting executed from only one call at a time
|
||||||
|
let working = false
|
||||||
|
|
||||||
|
export function runPendingJobs() {
|
||||||
|
triggerProcessQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerProcessQueue() {
|
||||||
|
if (working) return
|
||||||
|
|
||||||
|
working = true
|
||||||
|
{
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const job = queue.shift()!
|
||||||
|
await job()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
working = false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addJob(job: () => Promise<void>) {
|
||||||
|
queue.push(job)
|
||||||
|
|
||||||
|
// starts concurrently a function to process jobs
|
||||||
|
triggerProcessQueue()
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
import { loadConfig } from '../../config'
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
const body = await request.json()
|
||||||
|
const { html_url: htmlUrl, clone_url: cloneUrl, ssh_url: sshUrl } = body.repository
|
||||||
|
|
||||||
|
const URLS = [htmlUrl, cloneUrl, sshUrl]
|
||||||
|
|
||||||
|
const { deploys } = await loadConfig()
|
||||||
|
|
||||||
|
for (const deploy of deploys) {
|
||||||
|
if (deploy.type !== 'docker') {
|
||||||
|
if (URLS.includes(deploy.url)) {
|
||||||
|
// TODO: trigger deploy
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('ok')
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { Deploy } from '../../client/Deploy'
|
import { Deploy } from '../../client/Deploy'
|
||||||
import { loadConfig, refToString } from '../../config'
|
import { loadConfig } from '../../config'
|
||||||
import Layout from '../../layouts/Layout.astro'
|
import Layout from '../../layouts/Layout.astro'
|
||||||
|
|
||||||
const { deploys } = await loadConfig()
|
const { deploys } = await loadConfig()
|
||||||
@@ -10,6 +10,6 @@ const { deploys } = await loadConfig()
|
|||||||
<h1>Deploys</h1>
|
<h1>Deploys</h1>
|
||||||
<a class="button" href="/deploys/new">New Deploy</a>
|
<a class="button" href="/deploys/new">New Deploy</a>
|
||||||
<div class="deploys">
|
<div class="deploys">
|
||||||
{deploys.map(deploy => <Deploy deploy={deploy} />)}
|
{deploys.toReversed().map(deploy => <Deploy deploy={deploy} />)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
import Layout from '../../layouts/Layout.astro'
|
import Layout from '../../layouts/Layout.astro'
|
||||||
|
|
||||||
import { NewDeployForm } from '../../client/NewDeployForm.jsx'
|
import { NewDeployForm } from '../../client/NewDeployForm.jsx'
|
||||||
|
import { parseDeploy } from '../../forms.ts'
|
||||||
|
import { updateConfig } from '../../config'
|
||||||
|
|
||||||
if (Astro.request.method === 'POST') {
|
if (Astro.request.method === 'POST') {
|
||||||
const body = await Astro.request.formData()
|
const body = await Astro.request.formData()
|
||||||
const formData = Object.fromEntries(body.entries())
|
const formData = Object.fromEntries(body.entries())
|
||||||
|
|
||||||
console.log(formData)
|
const newDeploy = parseDeploy(formData)
|
||||||
|
|
||||||
|
updateConfig(async config => {
|
||||||
|
config.deploys.push(newDeploy)
|
||||||
|
})
|
||||||
|
|
||||||
return Astro.redirect('/deploys')
|
return Astro.redirect('/deploys')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { refToString, type GitRef, type ShellDeploy, type Deploy } from './config'
|
||||||
|
|
||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import child_process from 'child_process'
|
||||||
|
|
||||||
|
const exec = promisify(child_process.exec)
|
||||||
|
|
||||||
|
function getDeployDirectory(deploy: Deploy & { url: string; ref: GitRef }): string {
|
||||||
|
const { url, ref } = deploy
|
||||||
|
|
||||||
|
const repoSlug = url.replace(/(^\w+:|^)\/\//, '').replace(/[^a-zA-Z]+/g, '-')
|
||||||
|
const slug =
|
||||||
|
ref.type === 'default'
|
||||||
|
? `${deploy.name}_${repoSlug}`
|
||||||
|
: `${deploy.name}_${repoSlug}@${ref.value}`
|
||||||
|
|
||||||
|
return `${process.env.CLONE_PATH ?? './data.local/repos'}/${slug}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cloneOrUpdateRepo(deploy: Deploy & { url: string; ref: GitRef }) {
|
||||||
|
const repoDir = getDeployDirectory(deploy)
|
||||||
|
|
||||||
|
if (existsSync(repoDir)) {
|
||||||
|
await exec(`git -C ${repoDir} pull`)
|
||||||
|
} else {
|
||||||
|
await exec(`git clone ${deploy.url} ${repoDir}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function shellRunner(deploy: ShellDeploy) {
|
||||||
|
const { url, ref, options } = deploy
|
||||||
|
const { path, env } = options
|
||||||
|
|
||||||
|
const projectName = url.split('/').slice(-1)[0].replace('.git', '')
|
||||||
|
|
||||||
|
await cloneOrUpdateRepo(deploy)
|
||||||
|
|
||||||
|
const envStr = Object.entries(env as any)
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join(' ')
|
||||||
|
|
||||||
|
return exec(`cd ${projectName} && ${envStr} ${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+32
-30
@@ -82,19 +82,19 @@ button,
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
color: #333;
|
color: #333;
|
||||||
background-color: #fff;
|
background-color: #00000018;
|
||||||
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border-radius: 4px;
|
border-radius: 2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 150ms ease-in-out;
|
transition: background-color 150ms ease-in-out;
|
||||||
|
|
||||||
border: 1px solid #e0e0e0;
|
// border: 1px solid #e0e0e0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #e0e0e0;
|
background-color: #00000030;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,9 +114,9 @@ textarea {
|
|||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 6px;
|
border-radius: 1rem;
|
||||||
|
|
||||||
padding: 0 0.25rem;
|
padding: 0.25rem 0.75rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -134,7 +134,9 @@ textarea {
|
|||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
padding: 7px 0.25rem;
|
// padding: 7px 0.25rem;
|
||||||
|
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
@@ -275,7 +277,7 @@ form {
|
|||||||
.deploy {
|
.deploy {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
background: #fff;
|
border: 1px solid #ddd;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
@@ -290,7 +292,7 @@ body {
|
|||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
|
|
||||||
color: #333;
|
color: #333;
|
||||||
background: #f4f4f4;
|
background: #fff;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
@@ -301,7 +303,7 @@ body {
|
|||||||
|
|
||||||
min-width: 20vw;
|
min-width: 20vw;
|
||||||
|
|
||||||
background-color: #fff;
|
background-color: #ededf0;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -309,12 +311,12 @@ body {
|
|||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
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 {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 300;
|
||||||
font-size: 42px;
|
font-size: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
@@ -339,18 +341,18 @@ body {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
gap: 0.25rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 150ms ease-in-out;
|
transition: background 150ms ease-in-out;
|
||||||
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
||||||
padding: 0.125rem 0.25rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border-radius: 6px;
|
border-radius: 100px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f0f0f0;
|
background-color: #00000018;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-symbols-outlined {
|
.material-symbols-outlined {
|
||||||
@@ -360,7 +362,7 @@ body {
|
|||||||
|
|
||||||
.children {
|
.children {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-left: 2rem;
|
padding-left: 1.5rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -371,21 +373,21 @@ body {
|
|||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::before {
|
// &::before {
|
||||||
content: '';
|
// content: '';
|
||||||
background: #666;
|
// background: #666;
|
||||||
|
|
||||||
position: absolute;
|
// position: absolute;
|
||||||
left: -0.25rem;
|
// left: -0.5rem;
|
||||||
|
|
||||||
width: 5px;
|
// width: 5px;
|
||||||
height: 5px;
|
// height: 5px;
|
||||||
|
|
||||||
border-radius: 100%;
|
// border-radius: 100%;
|
||||||
|
|
||||||
top: 50%;
|
// top: 50%;
|
||||||
transform: translate(0, calc(-50% + 1px));
|
// transform: translate(0, calc(-50% + 1px));
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "astro/tsconfigs/strict",
|
"extends": "astro/tsconfigs/strict",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "preact",
|
"jsxImportSource": "preact",
|
||||||
"exactOptionalPropertyTypes": true
|
"exactOptionalPropertyTypes": true
|
||||||
|
|||||||
Reference in New Issue
Block a user