intial commit
commit
d997295848
@ -0,0 +1,24 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# editors
|
||||||
|
.vscode
|
@ -0,0 +1,33 @@
|
|||||||
|
# phCD
|
||||||
|
|
||||||
|
Continuous Deployment for PHC projects.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First install the dependencies
|
||||||
|
$ pnpm install
|
||||||
|
|
||||||
|
# Run the app
|
||||||
|
$ pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Everything is stored in the `config.yaml` file. This file contains a list of projects to deploy and the configuration for each one.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deploys:
|
||||||
|
- name: project1
|
||||||
|
url: https://github.com/username/project1
|
||||||
|
branch: main
|
||||||
|
type: dockerfile
|
||||||
|
options:
|
||||||
|
path: ./Dockerfile
|
||||||
|
- name: project2
|
||||||
|
url: ssh://example.org/username/project2.git
|
||||||
|
commit: 04c540647a
|
||||||
|
type: docker-compose
|
||||||
|
options:
|
||||||
|
path: ./docker-compose.yml
|
||||||
|
```
|
@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
import preact from '@astrojs/preact'
|
||||||
|
|
||||||
|
import node from '@astrojs/node'
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [preact()],
|
||||||
|
output: 'server',
|
||||||
|
adapter: node({
|
||||||
|
mode: 'standalone',
|
||||||
|
}),
|
||||||
|
})
|
@ -0,0 +1,27 @@
|
|||||||
|
deploys:
|
||||||
|
- name: project1
|
||||||
|
url: https://github.com/username/project1
|
||||||
|
branch: main
|
||||||
|
type: dockerfile
|
||||||
|
options:
|
||||||
|
port: 80: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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "phcd",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro check && astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.5.2",
|
||||||
|
"@astrojs/node": "^8.2.0",
|
||||||
|
"@astrojs/preact": "^3.1.0",
|
||||||
|
"@fontsource-variable/material-symbols-outlined": "^5.0.22",
|
||||||
|
"@fontsource/jetbrains-mono": "^5.0.18",
|
||||||
|
"@fontsource/lato": "^5.0.18",
|
||||||
|
"astro": "^4.3.5",
|
||||||
|
"async-mutex": "^0.4.1",
|
||||||
|
"dockerode": "^4.0.2",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"nodegit": "^0.27.0",
|
||||||
|
"preact": "^10.19.4",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/dockerode": "^3.3.23",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"sass": "^1.70.0"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 749 B |
@ -0,0 +1,98 @@
|
|||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
export const NewDeployForm = () => {
|
||||||
|
const [refType, setRefType] = useState('default')
|
||||||
|
const [deployType, setDeployType] = useState('initial')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action="/deploys/new" method="post">
|
||||||
|
<label for="deploy-name">Name</label>
|
||||||
|
<input id="deploy-name" name="deploy-name" type="text" placeholder="Project name..." />
|
||||||
|
|
||||||
|
<label for="deploy-url">Url</label>
|
||||||
|
<input id="deploy-url" name="deploy-url" type="text" placeholder="Valid git clone url..." />
|
||||||
|
|
||||||
|
<label for="deploy-ref-type">Ref</label>
|
||||||
|
<div class="compound">
|
||||||
|
<select id="deploy-ref-type" name="deploy-ref-type" value={refType} onChange={e => setRefType(e.target.value)}>
|
||||||
|
<option value="default">Default</option>
|
||||||
|
<option value="branch">Branch</option>
|
||||||
|
<option value="tag">Tag</option>
|
||||||
|
<option value="commit">Commit</option>
|
||||||
|
</select>
|
||||||
|
<input class="fill" id="deploy-ref-value" name="deploy-ref-value" disabled={refType === 'default'} type="text" placeholder="Ref value..." />
|
||||||
|
</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="shell">Shell</option>
|
||||||
|
<option value="dockerfile">Dockerfile</option>
|
||||||
|
<option value="docker-compose">Docker Compose</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<DeployOptions type={deployType} />
|
||||||
|
|
||||||
|
<div class="row right">
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeployOptions = ({ type }) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'shell':
|
||||||
|
return <ShellDeploy />
|
||||||
|
case 'dockerfile':
|
||||||
|
return <DockerDeploy />
|
||||||
|
case 'docker-compose':
|
||||||
|
return <DockerComposeDeploy />
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DockerDeploy = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label for="deploy-options-path">Path</label>
|
||||||
|
<input id="deploy-options-path" name="deploy-options-path" type="text" placeholder="Default is ./Dockerfile..." />
|
||||||
|
|
||||||
|
<label for="deploy-options-image">Image</label>
|
||||||
|
<input id="deploy-options-image" name="deploy-options-image" type="text" placeholder="organization/image:latest" />
|
||||||
|
|
||||||
|
<label for="deploy-options-ports">Ports</label>
|
||||||
|
<textarea id="deploy-options-ports" name="deploy-options-ports" rows={2} placeholder="80:8080" />
|
||||||
|
|
||||||
|
<label for="deploy-options-env">Environment</label>
|
||||||
|
<textarea id="deploy-options-env" name="deploy-options-env" rows={2} placeholder="FOO=bar" />
|
||||||
|
|
||||||
|
<label for="deploy-options-volumes">Volumes</label>
|
||||||
|
<textarea id="deploy-options-volumes" name="deploy-options-volumes" rows={2} placeholder="/var/www/example:/data" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShellDeploy = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label for="deploy-options-path">Path</label>
|
||||||
|
<input id="deploy-options-path" name="deploy-options-path" type="text" placeholder="./deploy.sh" />
|
||||||
|
|
||||||
|
<label for="deploy-options-env">Environment</label>
|
||||||
|
<textarea id="deploy-options-env" name="deploy-options-env" rows={2} placeholder="FOO=bar" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DockerComposeDeploy = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label for="deploy-options-path">Path</label>
|
||||||
|
<input id="deploy-options-path" name="deploy-options-path" type="text" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
<div class="sidebar">
|
||||||
|
<div class="header">phCD</div>
|
||||||
|
<nav>
|
||||||
|
<div class="nav-item group">
|
||||||
|
<a class="label" href="/"> <span class="material-symbols-outlined">home</span> Home</a>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item group">
|
||||||
|
<a class="label" href="/containers"><span class="material-symbols-outlined">view_list</span> Containers</a>
|
||||||
|
<div class="children">
|
||||||
|
<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 3</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item group">
|
||||||
|
<a class="label" href="/deploys"><span class="material-symbols-outlined">deployed_code</span> Deploys</a>
|
||||||
|
<div class="children">
|
||||||
|
<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 3</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
@ -0,0 +1,66 @@
|
|||||||
|
import yaml from 'js-yaml'
|
||||||
|
|
||||||
|
import { readFile, writeFile } from 'fs/promises'
|
||||||
|
import { Mutex } from 'async-mutex'
|
||||||
|
|
||||||
|
type GitRef = { commit: string } | { branch: string } | { tag: string }
|
||||||
|
|
||||||
|
type BaseDeploy = {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
} & GitRef
|
||||||
|
|
||||||
|
type DockerDeploy = BaseDeploy & {
|
||||||
|
type: 'docker'
|
||||||
|
options: {
|
||||||
|
path?: string
|
||||||
|
image: string
|
||||||
|
volumes?: string[]
|
||||||
|
ports?: string[]
|
||||||
|
env?: Record<string, string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerComposeDeploy = BaseDeploy & {
|
||||||
|
type: 'docker-compose'
|
||||||
|
options: {
|
||||||
|
path?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShellDeploy = BaseDeploy & {
|
||||||
|
type: 'shell'
|
||||||
|
options: {
|
||||||
|
path?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Deploy = DockerDeploy | DockerComposeDeploy | ShellDeploy
|
||||||
|
|
||||||
|
type Config = {
|
||||||
|
deploys: Deploy[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const refToString = (ref: GitRef) => {
|
||||||
|
return 'commit' in ref ? ref.commit : 'branch' in ref ? ref.branch : 'tag' in ref ? ref.tag : '<default>'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutex = new Mutex()
|
||||||
|
|
||||||
|
export function loadConfig(): Promise<Config> {
|
||||||
|
return mutex.runExclusive(async () => {
|
||||||
|
const data = await readFile('./config.yaml', 'utf8')
|
||||||
|
return yaml.load(data) as Config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateConfig(fn: (config: Config) => Promise<Config>) {
|
||||||
|
return mutex.runExclusive(async () => {
|
||||||
|
const data = await readFile('./config.yaml', 'utf8')
|
||||||
|
const config = yaml.load(data) as Config
|
||||||
|
|
||||||
|
const newConfig = await fn(config)
|
||||||
|
|
||||||
|
await writeFile('./config.yaml', yaml.dump(newConfig))
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="astro/client" />
|
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
import '@fontsource/lato/latin.css'
|
||||||
|
import '@fontsource/jetbrains-mono/latin.css'
|
||||||
|
|
||||||
|
import '@fontsource-variable/material-symbols-outlined/full.css'
|
||||||
|
|
||||||
|
import '../styles/main.scss'
|
||||||
|
|
||||||
|
import Sidebar from '../components/Sidebar.astro'
|
||||||
|
|
||||||
|
const { title } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
|
||||||
|
<title>{title ?? 'phCD'}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Sidebar />
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
import Layout from '../layouts/Layout.astro'
|
||||||
|
import Docker from 'dockerode'
|
||||||
|
|
||||||
|
const docker = new Docker({ socketPath: '/var/run/docker.sock' })
|
||||||
|
|
||||||
|
const containers = await docker.listContainers({ all: true })
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Containers | phCD">
|
||||||
|
<h1>Containers</h1>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
containers.map(container => (
|
||||||
|
<li>
|
||||||
|
<strong>Name:</strong> {container.Names[0]} <br />
|
||||||
|
<strong>Command:</strong> <code>{container.Command}</code> <br />
|
||||||
|
<strong>Image:</strong> <code>{container.Image}</code> <br />
|
||||||
|
<strong>Status:</strong> {container.Status} <br />
|
||||||
|
<strong>Mounts:</strong>
|
||||||
|
<ul>
|
||||||
|
{container.Mounts.map(mount => (
|
||||||
|
<li>
|
||||||
|
<strong>Type:</strong> {mount.Type} <br />
|
||||||
|
<strong>From:</strong> <code>{mount.Source}</code> <br />
|
||||||
|
<strong>To:</strong> <code>{mount.Destination}</code> <br />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</Layout>
|
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
import { loadConfig, refToString } from '../../config'
|
||||||
|
import Layout from '../../layouts/Layout.astro'
|
||||||
|
|
||||||
|
const { deploys } = await loadConfig()
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Deplots | phCD">
|
||||||
|
<h1>Deploys</h1>
|
||||||
|
<a class="button" href="/deploys/new">New Deploy</a>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
deploys.map(deploy => (
|
||||||
|
<li>
|
||||||
|
<strong>Name: </strong>
|
||||||
|
{deploy.name} <br />
|
||||||
|
<strong>URL: </strong>
|
||||||
|
{deploy.url.startsWith('http') ? (
|
||||||
|
<a href={deploy.url} target="_blank">
|
||||||
|
{deploy.url}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
deploy.url
|
||||||
|
)}
|
||||||
|
<br />
|
||||||
|
<strong>Ref: </strong>
|
||||||
|
{refToString(deploy)} <br />
|
||||||
|
<strong>Type: </strong>
|
||||||
|
{deploy.type} <br />
|
||||||
|
<strong>Path: </strong>
|
||||||
|
{deploy.options.path} <br />
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</Layout>
|
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
import Layout from '../../layouts/Layout.astro'
|
||||||
|
|
||||||
|
import { NewDeployForm } from '../../client/NewDeployForm.jsx'
|
||||||
|
|
||||||
|
if (Astro.request.method === 'POST') {
|
||||||
|
const body = await Astro.request.formData()
|
||||||
|
const formData = Object.fromEntries(body.entries())
|
||||||
|
|
||||||
|
console.log(formData)
|
||||||
|
|
||||||
|
return Astro.redirect('/deploys')
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="New Deploy | phCD">
|
||||||
|
<h1>New Deploy</h1>
|
||||||
|
<NewDeployForm client:load />
|
||||||
|
</Layout>
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
import Layout from '../layouts/Layout.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Homepage">
|
||||||
|
<h1>Astro</h1>
|
||||||
|
</Layout>
|
@ -0,0 +1,49 @@
|
|||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&.horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
&.top-left {
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
&.top-right {
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
&.bottom-left {
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
&.bottom-right {
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.top-left {
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
&.top-right {
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
&.bottom-left {
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
&.bottom-right {
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.center-center {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,322 @@
|
|||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
color: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Typography
|
||||||
|
//
|
||||||
|
|
||||||
|
$base-font-size: 18px;
|
||||||
|
$heading-scale: 1.33;
|
||||||
|
|
||||||
|
@function pow($number, $exponent) {
|
||||||
|
$value: 1;
|
||||||
|
|
||||||
|
@if $exponent > 0 {
|
||||||
|
@for $i from 1 through $exponent {
|
||||||
|
$value: $value * $number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@for $i from 1 through 5 {
|
||||||
|
h#{$i} {
|
||||||
|
$factor: pow($heading-scale, 5 - $i);
|
||||||
|
font-size: $base-font-size * $factor;
|
||||||
|
|
||||||
|
font-family: 'Lato', sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
direction: ltr;
|
||||||
|
|
||||||
|
font-family: 'Material Symbols Outlined Variable';
|
||||||
|
font-size: 24px; /* Preferred icon size */
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
.button {
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
color: #333;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 150ms ease-in-out;
|
||||||
|
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
&:not(textarea) {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
padding: 5px 0.25rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
resize: vertical;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
border-radius: 1rem;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
|
||||||
|
grid-template-columns: minmax(7rem, auto) 1fr;
|
||||||
|
gap: 0.75rem 1rem;
|
||||||
|
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
grid-column: span 2;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.compound {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
& > .fill {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
textarea {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Structure
|
||||||
|
//
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Lato', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.25;
|
||||||
|
|
||||||
|
color: #333;
|
||||||
|
background: #f4f4f4;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-areas: 'sidebar main';
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
grid-area: sidebar;
|
||||||
|
|
||||||
|
min-width: 20vw;
|
||||||
|
|
||||||
|
background-color: #fff;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
box-shadow: 0 0 1rem 0 #0002, 0 0 0.25rem 0 #0002;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 150ms ease-in-out;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.children {
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: 2rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
background: #666;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: -0.25rem;
|
||||||
|
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(0, calc(-50% + 1px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.group {
|
||||||
|
& > .label {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
grid-area: main;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
width: 80ch;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue