Add some base styles

This commit is contained in:
Thilo Hohlt
2024-08-02 15:33:18 +02:00
parent b0666f4a8c
commit c86bc68e5c
9 changed files with 520 additions and 204 deletions

169
web-app/src/app.css Normal file
View File

@@ -0,0 +1,169 @@
:root {
--bg-primary: white;
--bg-secondary: hsl(0 0% 95%);
--bg-tertiary: hsl(0 0% 90%);
--color-text: black;
--color-text-invert: white;
--color-accent: hsl(210, 100%, 30%);
--color-success: hsl(105, 100%, 30%);
--color-error: hsl(0, 100%, 30%);
--border-primary: 0.0625rem solid var(--bg-tertiary);
--border-radius: 0.125rem;
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: hsl(0 0% 15%);
--bg-secondary: hsl(0 0% 20%);
--bg-tertiary: hsl(0 0% 25%);
--color-text: white;
--color-text-invert: black;
--color-accent: hsl(210, 100%, 80%);
--color-success: hsl(105, 100%, 80%);
--color-error: hsl(0, 100%, 80%);
color-scheme: dark;
}
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
line-height: 1.5;
font-family: system-ui, sans-serif;
background-color: var(--bg-primary);
display: flex;
flex-direction: column;
min-block-size: 100vh;
}
section {
display: flex;
flex-direction: column;
gap: 1rem;
}
section + section {
margin-block-start: 2rem;
}
button,
label,
select,
summary,
[role="button"],
[role="option"] {
cursor: pointer;
}
input,
button,
textarea,
select,
a[role="button"] {
font: inherit;
color: inherit;
border: var(--border-primary);
border-radius: var(--border-radius);
padding-inline: 0.5rem;
padding-block: 0.25rem;
}
input,
textarea,
select {
background-color: var(--bg-primary);
}
a {
color: var(--color-accent);
}
a[role="button"] {
display: inline-block;
max-inline-size: fit-content;
text-decoration: none;
}
button,
a[role="button"] {
background-color: var(--bg-secondary);
}
:is(button, a[role="button"]):hover {
background-color: var(--bg-tertiary);
}
img,
picture,
svg,
video {
max-inline-size: 100%;
block-size: auto;
}
ul,
ol {
list-style: none;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
h1,
h2,
h3,
h4 {
line-height: 1.2;
text-wrap: balance;
}
form[method="POST"] {
display: flex;
flex-direction: column;
gap: 1rem;
}
form > button[type="submit"] {
max-inline-size: 30ch;
}
form[method="GET"] {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 20ch), 1fr));
align-items: start;
}
form[method="GET"] > button[type="submit"] {
align-self: end;
}
form > label {
display: flex;
flex-direction: column;
gap: 0.25rem;
max-inline-size: 30ch;
}
form > label:has(textarea) {
max-inline-size: 65ch;
}

View File

@@ -4,7 +4,6 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://unpkg.com/@acab/reset.css" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import type { Snippet } from "svelte";
const { children, id, text } = $props<{ children: Snippet; id: string; text: string }>();
</script>
<a href={`#${id}`} role="button">{text}</a>
<div {id} class="modal">
<div class="modal__content">
{@render children()}
<a href="#!" role="button">Close</a>
</div>
<a href="#!" class="modal__closeoverlay" aria-label="Close modal"></a>
</div>
<style>
.modal {
position: fixed;
inset: 0;
display: none;
}
.modal:target {
display: block;
}
.modal__closeoverlay {
position: fixed;
inset: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.5);
}
.modal__content {
display: flex;
flex-direction: column;
gap: 2rem;
padding-inline: 1rem;
padding-block: 2rem;
background-color: var(--bg-primary);
border-radius: var(--border-radius);
border: var(--border-primary);
z-index: 20;
position: absolute;
max-inline-size: 300px;
margin-inline: auto;
inset-block-start: 2rem;
inset-inline-start: 50%;
transform: translateX(-50%);
}
</style>

View File

@@ -12,10 +12,10 @@
<div class="operations"> <div class="operations">
<h1>{title}</h1> <h1>{title}</h1>
<div> <nav class="operations__nav">
<a href="/website/{id}">Settings</a> <a href="/website/{id}">Settings</a>
<a href="/website/{id}/articles">Articles</a> <a href="/website/{id}/articles">Articles</a>
</div> </nav>
{@render children()} {@render children()}
</div> </div>
@@ -34,11 +34,22 @@
.operations { .operations {
inline-size: 50%; inline-size: 50%;
border-inline-end: 0.0625rem solid hsl(0 0% 50%); border-inline-end: var(--border-primary);
resize: horizontal; resize: horizontal;
overflow-y: auto; overflow-y: auto;
} }
.operations__nav {
margin-block: 1rem 2rem;
}
.operations__nav > a {
display: inline-block;
padding-inline: 0.5rem;
padding-block: 0.25rem;
overflow-x: auto;
}
.preview { .preview {
flex-grow: 1; flex-grow: 1;
} }

View File

@@ -3,6 +3,7 @@
import DateTime from "$lib/components/DateTime.svelte"; import DateTime from "$lib/components/DateTime.svelte";
import { sortOptions } from "$lib/utils.js"; import { sortOptions } from "$lib/utils.js";
import { page } from "$app/stores"; import { page } from "$app/stores";
import Modal from "$lib/components/Modal.svelte";
const { form, data } = $props(); const { form, data } = $props();
</script> </script>
@@ -18,21 +19,25 @@
<section> <section>
<h2>Create website</h2> <h2>Create website</h2>
<form method="POST" action="?/createWebsite" use:enhance> <Modal id="create-website" text="Create website">
<label> <h3>Create website</h3>
Type:
<select name="content-type">
<option value="Blog">Blog</option>
<option value="Docs">Docs</option>
</select>
</label>
<label>
Title:
<input type="text" name="title" />
</label>
<button type="submit">Submit</button> <form method="POST" action="?/createWebsite" use:enhance>
</form> <label>
Type:
<select name="content-type">
<option value="Blog">Blog</option>
<option value="Docs">Docs</option>
</select>
</label>
<label>
Title:
<input type="text" name="title" />
</label>
<button type="submit">Submit</button>
</form>
</Modal>
</section> </section>
{#if data.totalWebsiteCount > 0} {#if data.totalWebsiteCount > 0}
@@ -69,52 +74,85 @@
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>
{#each data.websites as { id, content_type, title, created_at }} <div class="website-grid">
<article> {#each data.websites as { id, content_type, title, created_at }}
<h3> <article class="website-card">
<a href="/website/{id}">{title}</a> <h3>
</h3> <a href="/website/{id}">{title}</a>
<p> </h3>
<strong>Type:</strong> <ul>
{content_type} <li>
</p> <strong>Type:</strong>
<p> {content_type}
<strong>Created at:</strong> </li>
<DateTime date={created_at} /> <li>
</p> <strong>Created at:</strong>
<details> <DateTime date={created_at} />
<summary>Update</summary> </li>
<form </ul>
method="POST" <div class="website-card__actions">
action="?/updateWebsite" <Modal id="update-website-{id}" text="Update">
use:enhance={() => { <h4>Update website</h4>
return async ({ update }) => {
await update({ reset: false });
};
}}
>
<input type="hidden" name="id" value={id} />
<label>
Title
<input type="text" name="title" value={title} />
</label>
<button type="submit">Submit</button> <form
</form> method="POST"
</details> action="?/updateWebsite"
<details> use:enhance={() => {
<summary>Delete</summary> return async ({ update }) => {
<p> await update({ reset: false });
<strong>Caution!</strong> };
Deleting this website will irretrievably erase all data. }}
</p> >
<form method="POST" action="?/deleteWebsite" use:enhance> <input type="hidden" name="id" value={id} />
<input type="hidden" name="id" value={id} /> <label>
Title
<input type="text" name="title" value={title} />
</label>
<button type="submit">Permanently delete website</button> <button type="submit">Submit</button>
</form> </form>
</details> </Modal>
</article> <Modal id="delete-website-{id}" text="Delete">
{/each} <h4>Delete website</h4>
<p>
<strong>Caution!</strong>
Deleting this website will irretrievably erase all data.
</p>
<form method="POST" action="?/deleteWebsite" use:enhance>
<input type="hidden" name="id" value={id} />
<button type="submit">Permanently delete website</button>
</form>
</Modal>
</div>
</article>
{/each}
</div>
</section> </section>
{/if} {/if}
<style>
.website-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 35ch), 1fr));
margin-block-start: 1rem;
}
.website-card {
border: var(--border-primary);
display: flex;
flex-direction: column;
gap: 1rem;
padding-inline: 1rem;
padding-block: 2rem;
}
.website-card__actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
</style>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
import Modal from "$lib/components/Modal.svelte";
const { data, form } = $props(); const { data, form } = $props();
</script> </script>
@@ -36,12 +37,21 @@
<section> <section>
<h2>Delete account</h2> <h2>Delete account</h2>
<form method="POST" action="?/deleteAccount" use:enhance> <Modal id="delete-account" text="Delete account">
<label> <h3>Delete account</h3>
Password:
<input type="password" name="password" required />
</label>
<button type="submit">Delete account</button> <p>
</form> <strong>Caution!</strong>
Deleting your account will irretrievably erase all data.
</p>
<form method="POST" action="?/deleteAccount" use:enhance>
<label>
Password:
<input type="password" name="password" required />
</label>
<button type="submit">Permanently delete account</button>
</form>
</Modal>
</section> </section>

View File

@@ -20,114 +20,114 @@
previewContent={data.home.main_content} previewContent={data.home.main_content}
> >
<section> <section>
<h2>Settings</h2> <h2>Global</h2>
<form
action="?/updateGlobal"
method="POST"
enctype="multipart/form-data"
use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}
>
<label>
Light accent color:
<input
type="color"
name="accent-color-light"
value={data.globalSettings.accent_color_light_theme}
/>
</label>
<label>
Light accent color:
<input
type="color"
name="accent-color-dark"
value={data.globalSettings.accent_color_dark_theme}
/>
</label>
<label>
Favicon:
<input type="file" name="favicon" accept={ALLOWED_MIME_TYPES.join(", ")} />
</label>
<section> <button type="submit">Submit</button>
<h3>Global</h3> </form>
<form </section>
action="?/updateGlobal"
method="POST"
enctype="multipart/form-data"
use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}
>
<label>
Light accent color:
<input
type="color"
name="accent-color-light"
value={data.globalSettings.accent_color_light_theme}
/>
</label>
<label>
Light accent color:
<input
type="color"
name="accent-color-dark"
value={data.globalSettings.accent_color_dark_theme}
/>
</label>
<label>
Favicon:
<input type="file" name="favicon" accept={ALLOWED_MIME_TYPES.join(", ")} />
</label>
<button type="submit">Submit</button> <section>
</form> <h2>Header</h2>
</section>
<section> <form
<h3>Header</h3> action="?/updateHeader"
method="POST"
enctype="multipart/form-data"
use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}
>
<label>
Logo type:
<select name="logo-type">
<option value="text" selected={"text" === data.header.logo_type}>Text</option>
<option value="image" selected={"image" === data.header.logo_type}>Image</option>
</select>
</label>
<label>
Logo text:
<input type="text" name="logo-text" value={data.header.logo_text} />
</label>
<label>
Logo image:
<input type="file" name="logo-image" accept={ALLOWED_MIME_TYPES.join(", ")} />
</label>
<form <button type="submit">Submit</button>
action="?/updateHeader" </form>
method="POST" </section>
enctype="multipart/form-data"
use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}
>
<label>
Logo type:
<select name="logo-type">
<option value="text" selected={"text" === data.header.logo_type}>Text</option>
<option value="image" selected={"image" === data.header.logo_type}>Image</option>
</select>
</label>
<label>
Logo text:
<input type="text" name="logo-text" value={data.header.logo_text} />
</label>
<label>
Logo image:
<input type="file" name="logo-image" accept={ALLOWED_MIME_TYPES.join(", ")} />
</label>
<button type="submit">Submit</button> <section>
</form> <h2>Home</h2>
</section>
<section> <form
<h3>Home</h3> action="?/updateHome"
method="POST"
use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}
>
<label>
Main content:
<textarea name="main-content">{data.home.main_content}</textarea>
</label>
<form <button type="submit">Submit</button>
action="?/updateHome" </form>
method="POST" </section>
use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}
>
<label>
Main content:
<textarea name="main-content">{data.home.main_content}</textarea>
</label>
</form>
</section>
<section> <section>
<h3>Footer</h3> <h2>Footer</h2>
<form <form
action="?/updateFooter" action="?/updateFooter"
method="POST" method="POST"
use:enhance={() => { use:enhance={() => {
return async ({ update }) => { return async ({ update }) => {
await update({ reset: false }); await update({ reset: false });
}; };
}} }}
> >
<label> <label>
Additional text: Additional text:
<textarea name="additional-text">{data.footer.additional_text}</textarea> <textarea name="additional-text">{data.footer.additional_text}</textarea>
</label> </label>
</form>
</section> <button type="submit">Submit</button>
</form>
</section> </section>
</WebsiteEditor> </WebsiteEditor>

View File

@@ -3,6 +3,7 @@
import { sortOptions } from "$lib/utils.js"; import { sortOptions } from "$lib/utils.js";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
import Modal from "$lib/components/Modal.svelte";
const { data, form } = $props(); const { data, form } = $props();
</script> </script>
@@ -23,14 +24,18 @@
<section> <section>
<h2>Create article</h2> <h2>Create article</h2>
<form method="POST" action="?/createArticle" use:enhance> <Modal id="create-article" text="Create article">
<label> <h3>Create article</h3>
Title:
<input type="text" name="title" />
</label>
<button type="submit">Submit</button> <form method="POST" action="?/createArticle" use:enhance>
</form> <label>
Title:
<input type="text" name="title" />
</label>
<button type="submit">Submit</button>
</form>
</Modal>
</section> </section>
{#if data.totalArticleCount > 0} {#if data.totalArticleCount > 0}
@@ -60,23 +65,49 @@
</form> </form>
{#each data.articles as { id, title }} {#each data.articles as { id, title }}
<article> <article class="article-card">
<h3>{title}</h3> <h3>{title}</h3>
<a href="/website/{data.website.id}/articles/{id}">Edit</a>
<details>
<summary>Delete</summary>
<p>
<strong>Caution!</strong>
Deleting this article will irretrievably erase all data.
</p>
<form method="POST" action="?/deleteArticle" use:enhance>
<input type="hidden" name="id" value={id} />
<button type="submit">Permanently delete article</button> <div class="article-card__actions">
</form> <a href="/website/{data.website.id}/articles/{id}">Edit</a>
</details> <Modal id="delete-article-{id}" text="Delete">
<h4>Delete article</h4>
<p>
<strong>Caution!</strong>
Deleting this article will irretrievably erase all data.
</p>
<form method="POST" action="?/deleteArticle" use:enhance>
<input type="hidden" name="id" value={id} />
<button type="submit">Permanently delete article</button>
</form>
</Modal>
</div>
</article> </article>
{/each} {/each}
</section> </section>
{/if} {/if}
</WebsiteEditor> </WebsiteEditor>
<style>
.article-card {
display: flex;
align-items: center;
column-gap: 2rem;
row-gap: 0.5rem;
flex-wrap: wrap;
justify-content: space-between;
}
.article-card:nth-of-type(1) {
margin-block-start: 1rem;
}
.article-card__actions {
display: flex;
gap: 1rem;
align-items: center;
}
</style>

View File

@@ -1,8 +1,14 @@
<script lang="ts"> <script lang="ts">
import "../app.css";
import { page } from "$app/stores"; import { page } from "$app/stores";
const { data, children } = $props(); const { data, children } = $props();
const isProjectRoute = $derived($page.url.pathname.startsWith("/website")); const isProjectRoute = $derived($page.url.pathname.startsWith("/website"));
const routeName = $derived(
$page.url.pathname === "/"
? "Dashboard"
: `${$page.url.pathname.charAt(1).toUpperCase()}${$page.url.pathname.slice(2)}`
);
</script> </script>
<nav> <nav>
@@ -18,7 +24,7 @@
{#if !isProjectRoute} {#if !isProjectRoute}
<header> <header>
<h1>{$page.url.pathname}</h1> <h1>{routeName}</h1>
</header> </header>
{/if} {/if}
@@ -42,28 +48,28 @@
margin-inline: auto; margin-inline: auto;
} }
nav {
display: flex;
align-items: center;
gap: 1rem;
overflow-x: auto;
}
nav > *:first-child {
margin-inline-end: auto;
}
footer { footer {
text-align: center; text-align: center;
margin-block-start: auto;
} }
.editor { .editor {
inline-size: min(100% - 2rem, 1536px); inline-size: min(100% - 2rem, 1536px);
block-size: calc(100vh - 7rem); block-size: calc(100vh - 7rem);
border: 0.0625rem solid hsl(0 0% 50%); border: var(--border-primary);
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
padding-block: 0; padding-block: 0;
} }
:global(section) {
display: flex;
flex-direction: column;
gap: 1rem;
}
:global(form) {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
</style> </style>