Add categories for docs template

This commit is contained in:
thiloho
2024-08-27 16:39:29 +02:00
parent 0753345bba
commit 1b1767c0f7
24 changed files with 651 additions and 154 deletions

View File

@@ -0,0 +1,116 @@
-- migrate:up
CREATE TABLE internal.docs_category (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID,
category_name VARCHAR(50) NOT NULL CHECK (TRIM(category_name) != ''),
category_weight INTEGER CHECK (category_weight >= 0) NOT NULL,
UNIQUE (website_id, category_name),
UNIQUE (website_id, category_weight)
);
ALTER TABLE internal.website
ADD COLUMN is_published BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE internal.article
ADD COLUMN category UUID REFERENCES internal.docs_category (id) ON DELETE SET NULL;
ALTER TABLE internal.article
ALTER COLUMN user_id SET DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID;
ALTER TABLE internal.docs_category ENABLE ROW LEVEL SECURITY;
CREATE POLICY view_categories ON internal.docs_category
FOR SELECT
USING (internal.user_has_website_access (website_id, 10));
CREATE POLICY update_category ON internal.docs_category
FOR UPDATE
USING (internal.user_has_website_access (website_id, 20));
CREATE POLICY delete_category ON internal.docs_category
FOR DELETE
USING (internal.user_has_website_access (website_id, 20, article_user_id => user_id));
CREATE POLICY insert_category ON internal.docs_category
FOR INSERT
WITH CHECK (internal.user_has_website_access (website_id, 20));
CREATE VIEW api.docs_category WITH ( security_invoker = ON
) AS
SELECT
id,
website_id,
user_id,
category_name,
category_weight
FROM
internal.docs_category;
CREATE OR REPLACE VIEW api.article WITH ( security_invoker = ON
) AS
SELECT
id,
website_id,
user_id,
title,
meta_description,
meta_author,
cover_image,
publication_date,
main_content,
created_at,
last_modified_at,
last_modified_by,
category -- New column
FROM
internal.article;
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.docs_category TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.docs_category TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
-- migrate:down
DROP POLICY view_categories ON internal.docs_category;
DROP POLICY update_category ON internal.docs_category;
DROP POLICY delete_category ON internal.docs_category;
DROP POLICY insert_category ON internal.docs_category;
DROP VIEW api.article;
CREATE VIEW api.article WITH ( security_invoker = ON
) AS
SELECT
id,
website_id,
user_id,
title,
meta_description,
meta_author,
cover_image,
publication_date,
main_content,
created_at,
last_modified_at,
last_modified_by
FROM
internal.article;
DROP VIEW api.docs_category;
ALTER TABLE internal.article
DROP COLUMN category;
DROP TABLE internal.docs_category;
ALTER TABLE internal.website
DROP COLUMN is_published;
ALTER TABLE internal.article
ALTER COLUMN user_id DROP DEFAULT;

View File

@@ -5,6 +5,7 @@
const {
id,
contentType,
title,
children,
fullPreview = false,
@@ -12,6 +13,7 @@
previewScrollTop = 0
}: {
id: string;
contentType: string;
title: string;
children: Snippet;
fullPreview?: boolean;
@@ -41,6 +43,9 @@
<li>
<a href="/website/{id}/articles">Articles</a>
</li>
{#if contentType === "Docs"}
<a href="/website/{id}/categories">Categories</a>
{/if}
<li>
<a href="/website/{id}/collaborators">Collaborators</a>
</li>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import BlogHead from "./common/BlogHead.svelte";
import BlogNav from "./common/BlogNav.svelte";
import BlogFooter from "./common/BlogFooter.svelte";
import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte";
const {
favicon,
@@ -24,9 +24,9 @@
} = $props();
</script>
<BlogHead {title} {favicon} nestingLevel={1} />
<Head {title} {favicon} nestingLevel={1} />
<BlogNav {logoType} {logo} />
<Nav {logoType} {logo} />
<header>
<div class="container">
@@ -48,4 +48,4 @@
</main>
{/if}
<BlogFooter text={footerAdditionalText} />
<Footer text={footerAdditionalText} />

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import BlogHead from "./common/BlogHead.svelte";
import BlogNav from "./common/BlogNav.svelte";
import BlogFooter from "./common/BlogFooter.svelte";
import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte";
const {
favicon,
@@ -22,9 +22,9 @@
} = $props();
</script>
<BlogHead {title} {favicon} />
<Head {title} {favicon} />
<BlogNav {logoType} {logo} />
<Nav {logoType} {logo} />
<header>
<div class="container">
@@ -62,4 +62,4 @@
</div>
</main>
<BlogFooter text={footerAdditionalText} />
<Footer text={footerAdditionalText} />

View File

@@ -1,17 +0,0 @@
<script lang="ts">
const { logoType, logo }: { logoType: "text" | "image"; logo: string } = $props();
</script>
<nav>
<div class="container">
<a href="../">
{#if logoType === "text"}
<p>
<strong>{logo}</strong>
</p>
{:else}
<img src={logo} width="24" height="24" alt="" />
{/if}
</a>
</div>
</nav>

View File

@@ -0,0 +1,45 @@
<script lang="ts">
const {
logoType,
logo,
isDocsTemplate = false
}: {
logoType: "text" | "image";
logo: string;
isDocsTemplate?: boolean;
} = $props();
</script>
<nav>
<div class="container">
{#if isDocsTemplate}
<input type="checkbox" id="toggle-sidebar" hidden />
<label for="toggle-sidebar">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width="20"
height="20"
>
<path
fill-rule="evenodd"
d="M2 4.75A.75.75 0 0 1 2.75 4h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 4.75ZM2 10a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 10Zm0 5.25a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1-.75-.75Z"
clip-rule="evenodd"
></path>
</svg>
</label>
<ul class="docs-navigation">
<li>nav comes here</li>
</ul>
{/if}
<a href="../">
{#if logoType === "text"}
<strong>{logo}</strong>
{:else}
<img src={logo} width="24" height="24" alt="" />
{/if}
</a>
</div>
</nav>

View File

@@ -0,0 +1,51 @@
<script lang="ts">
import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte";
const {
favicon,
title,
logoType,
logo,
mainContent,
coverImage,
publicationDate,
footerAdditionalText
}: {
favicon: string;
title: string;
logoType: "text" | "image";
logo: string;
mainContent: string;
coverImage: string;
publicationDate: string;
footerAdditionalText: string;
} = $props();
</script>
<Head {title} {favicon} nestingLevel={1} />
<Nav {logoType} {logo} isDocsTemplate={true} />
<header>
<div class="container">
<hgroup>
<p>{publicationDate}</p>
<h1>{title}</h1>
</hgroup>
{#if coverImage}
<img src={coverImage} alt="" />
{/if}
</div>
</header>
{#if mainContent}
<main>
<div class="container">
{@html mainContent}
</div>
</main>
{/if}
<Footer text={footerAdditionalText} />

View File

@@ -1,52 +0,0 @@
<script lang="ts">
const {
title,
logoType,
logo,
mainContent,
coverImage,
publicationDate,
footerAdditionalText
}: {
title: string;
logoType: "text" | "image";
logo: string;
mainContent: string;
coverImage: string;
publicationDate: string;
footerAdditionalText: string;
} = $props();
</script>
<svelte:head>
<head>
<title>{title}</title>
<link rel="stylesheet" href="../styles.css" />
</head>
</svelte:head>
<nav>
{#if logoType === "text"}
<p>
<strong>{logo}</strong>
</p>
{:else}
<img src={logo} alt="" />
{/if}
</nav>
<header>
{#if coverImage}
<img src={coverImage} alt="" />
{/if}
<h1>{title}</h1>
<p>{publicationDate}</p>
</header>
<main>
{@html mainContent}
</main>
<footer>
{footerAdditionalText}
</footer>

View File

@@ -1,5 +1,10 @@
<script lang="ts">
import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte";
const {
favicon,
title,
logoType,
logo,
@@ -7,6 +12,7 @@
articles,
footerAdditionalText
}: {
favicon: string;
title: string;
logoType: "text" | "image";
logo: string;
@@ -16,50 +22,44 @@
} = $props();
</script>
<svelte:head>
<head>
<title>{title}</title>
<link rel="stylesheet" href="./styles.css" />
</head>
</svelte:head>
<Head {title} {favicon} />
<nav>
{#if logoType === "text"}
<p>
<strong>{logo}</strong>
</p>
{:else}
<img src={logo} alt="" />
{/if}
</nav>
<Nav {logoType} {logo} isDocsTemplate={true} />
<header>
<div class="container">
<h1>{title}</h1>
</div>
</header>
<main>
<div class="container">
{@html mainContent}
{#if articles.length > 0}
<section class="articles" id="articles">
<h2>Articles</h2>
<h2>
<a href="#articles">Articles</a>
</h2>
<ul class="unpadded">
{#each articles as article}
{@const articleFileName = article.title.toLowerCase().split(" ").join("-")}
<article>
<li>
<p>{article.publication_date}</p>
<h3>
<a href="./documents/{articleFileName}.html">{article.title}</a>
</h3>
<p>
<strong>
<a href="./articles/{articleFileName}.html">{article.title}</a>
</strong>
</p>
{#if article.meta_description}
<p>{article.meta_description}</p>
{/if}
</article>
</li>
{/each}
</ul>
</section>
{/if}
</div>
</main>
<footer>
{footerAdditionalText}
</footer>
<Footer text={footerAdditionalText} />

View File

@@ -36,7 +36,7 @@ const createMarkdownParser = (showToc = true) => {
const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;
function unescape(html: string) {
const unescape = (html: string) => {
return html.replace(unescapeTest, (_, n) => {
n = n.toLowerCase();
if (n === "colon") return ":";
@@ -47,13 +47,13 @@ const createMarkdownParser = (showToc = true) => {
}
return "";
});
}
};
let slugger = new GithubSlugger();
let headings: { text: string; raw: string; level: number; id: string }[] = [];
let sectionStack: { level: number; id: string }[] = [];
function gfmHeadingId({ prefix = "", showToc = true } = {}) {
const gfmHeadingId = ({ prefix = "", showToc = true } = {}) => {
return {
renderer: {
heading(this: Renderer, { tokens, depth }: { tokens: Token[]; depth: number }) {
@@ -141,7 +141,7 @@ const createMarkdownParser = (showToc = true) => {
}
}
};
}
};
marked.use(gfmHeadingId({ showToc: showToc }));

View File

@@ -30,6 +30,7 @@
<WebsiteEditor
id={data.website.id}
contentType={data.website.content_type}
title={data.website.title}
{previewContent}
previewScrollTop={textareaScrollTop}

View File

@@ -85,7 +85,6 @@ export const actions: Actions = {
},
body: JSON.stringify({
website_id: params.websiteId,
user_id: locals.user.id,
title: data.get("title")
})
});

View File

@@ -14,6 +14,7 @@
<WebsiteEditor
id={data.website.id}
contentType={data.website.content_type}
title={data.website.title}
previewContent={data.home.main_content}
>

View File

@@ -11,10 +11,22 @@ export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) =
}
});
const categoryData = await fetch(
`${API_BASE_PREFIX}/docs_category?website_id=eq.${params.websiteId}&order=category_weight.desc`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`
}
}
);
const article = await articleData.json();
const categories = await categoryData.json();
const { website } = await parent();
return { website, article, API_BASE_PREFIX };
return { website, article, categories, API_BASE_PREFIX };
};
export const actions: Actions = {
@@ -53,7 +65,8 @@ export const actions: Actions = {
meta_author: data.get("author"),
cover_image: uploadedImage.file_id,
publication_date: data.get("publication-date"),
main_content: data.get("main-content")
main_content: data.get("main-content"),
category: data.get("category")
})
});

View File

@@ -30,6 +30,7 @@
<WebsiteEditor
id={data.website.id}
contentType={data.website.content_type}
title={data.website.title}
previewContent={previewContent ||
"Put some markdown content in main content to see a live preview here"}
@@ -50,6 +51,17 @@
};
}}
>
{#if data.website.content_type === "Docs"}
<label>
Category:
<select name="category">
{#each data.categories as { id, category_name }}
<option value={id} selected={id === data.article.category}>{category_name}</option>
{/each}
</select>
</label>
{/if}
<label>
Title:
<input

View File

@@ -0,0 +1,95 @@
import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) => {
const categoryData = await fetch(
`${API_BASE_PREFIX}/docs_category?website_id=eq.${params.websiteId}&order=category_weight.desc`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`
}
}
);
const categories = await categoryData.json();
const { website, home } = await parent();
return {
categories,
website,
home
};
};
export const actions: Actions = {
createCategory: async ({ request, fetch, cookies, params }) => {
const data = await request.formData();
const res = await fetch(`${API_BASE_PREFIX}/docs_category`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`
},
body: JSON.stringify({
website_id: params.websiteId,
category_name: data.get("category-name"),
category_weight: data.get("category-weight")
})
});
if (!res.ok) {
const response = await res.json();
return { success: false, message: response.message };
}
return { success: true, message: "Successfully created category" };
},
updateCategory: async ({ request, fetch, cookies, params }) => {
const data = await request.formData();
const res = await fetch(
`${API_BASE_PREFIX}/docs_category?website_id=eq.${params.websiteId}&id=eq.${data.get("category-id")}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`
},
body: JSON.stringify({
category_weight: data.get("category-weight")
})
}
);
if (!res.ok) {
const response = await res.json();
return { success: false, message: response.message };
}
return { success: true, message: "Successfully updated category" };
},
deleteCategory: async ({ request, fetch, cookies, params }) => {
const data = await request.formData();
const res = await fetch(
`${API_BASE_PREFIX}/docs_category?website_id=eq.${params.websiteId}&id=eq.${data.get("category-id")}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`
}
}
);
if (!res.ok) {
const response = await res.json();
return { success: false, message: response.message };
}
return { success: true, message: "Successfully deleted category" };
}
};

View File

@@ -0,0 +1,138 @@
<script lang="ts">
import { enhance } from "$app/forms";
import WebsiteEditor from "$lib/components/WebsiteEditor.svelte";
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import Modal from "$lib/components/Modal.svelte";
import type { ActionData, PageServerData } from "./$types";
const { data, form }: { data: PageServerData; form: ActionData } = $props();
</script>
<SuccessOrError success={form?.success} message={form?.message} />
<WebsiteEditor
id={data.website.id}
contentType={data.website.content_type}
title={data.website.title}
previewContent={data.home.main_content}
>
<section id="create-category">
<h2>
<a href="#create-category">Create category</a>
</h2>
<Modal id="create-category" text="Create category">
<h3>Create category</h3>
<form
method="POST"
action="?/createCategory"
use:enhance={() => {
return async ({ update }) => {
await update();
window.location.hash = "!";
};
}}
>
<label>
Name:
<input type="text" name="category-name" maxlength="50" required />
</label>
<label>
Weight:
<input name="category-weight" type="number" min="0" required />
</label>
<button type="submit">Submit</button>
</form>
</Modal>
</section>
{#if data.categories.length > 0}
<section id="all-categories">
<h2>
<a href="#all-categories">All categories</a>
</h2>
<ul class="unpadded">
{#each data.categories as { id, website_id, category_name, category_weight } (`${website_id}-${id}`)}
<li class="category-card">
<p>
<strong>{category_name} ({category_weight})</strong>
</p>
<div class="category-card__actions">
<Modal id="update-category-{id}" text="Update">
<h4>Update category</h4>
<form
method="POST"
action="?/updateCategory"
use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
window.location.hash = "!";
};
}}
>
<input type="hidden" name="category-id" value={id} />
<label>
Weight:
<input type="number" name="category-weight" value={category_weight} min="0" />
</label>
<button type="submit">Update category</button>
</form>
</Modal>
<Modal id="delete-category-{id}" text="Delete">
<h4>Delete category</h4>
<p>Do you really want to delete the category?</p>
<form
method="POST"
action="?/deleteCategory"
use:enhance={() => {
return async ({ update }) => {
await update();
window.location.hash = "!";
};
}}
>
<input type="hidden" name="category-id" value={id} />
<button type="submit">Delete category</button>
</form>
</Modal>
</div>
</li>
{/each}
</ul>
</section>
{/if}
</WebsiteEditor>
<style>
.category-card {
display: flex;
align-items: center;
column-gap: var(--space-s);
row-gap: var(--space-2xs);
flex-wrap: wrap;
justify-content: space-between;
margin-block-start: var(--space-xs);
}
.category-card + .category-card {
padding-block-start: var(--space-xs);
border-block-start: var(--border-primary);
}
.category-card__actions {
display: flex;
gap: var(--space-2xs);
align-items: center;
}
</style>

View File

@@ -12,6 +12,7 @@
<WebsiteEditor
id={data.website.id}
contentType={data.website.content_type}
title={data.website.title}
previewContent={data.home.main_content}
>
@@ -35,7 +36,14 @@
>
<label>
User id:
<input type="text" name="user-id" minlength="36" maxlength="36" required />
<input
type="text"
name="user-id"
minlength="36"
maxlength="36"
placeholder="00000000-0000-0000-0000-000000000000"
required
/>
</label>
<label>

View File

@@ -7,7 +7,7 @@ import { render } from "svelte/server";
import BlogIndex from "$lib/templates/blog/BlogIndex.svelte";
import BlogArticle from "$lib/templates/blog/BlogArticle.svelte";
import DocsIndex from "$lib/templates/docs/DocsIndex.svelte";
import DocsEntry from "$lib/templates/docs/DocsEntry.svelte";
import DocsArticle from "$lib/templates/docs/DocsArticle.svelte";
import { dev } from "$app/environment";
export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) => {
@@ -38,10 +38,20 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) =
};
export const actions: Actions = {
publishWebsite: async ({ request }) => {
const data = await request.formData();
const websiteOverview = JSON.parse(data.get("website-overview") as string);
publishWebsite: async ({ fetch, params, cookies }) => {
const websiteOverviewData = await fetch(
`${API_BASE_PREFIX}/website_overview?id=eq.${params.websiteId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`,
Accept: "application/vnd.pgrst.object+json"
}
}
);
const websiteOverview = await websiteOverviewData.json();
generateStaticFiles(websiteOverview, false);
return { success: true, message: "Successfully published website" };
@@ -77,9 +87,15 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true)
{
({ head, body } = render(DocsIndex, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: websiteData.title,
logoType: websiteData.logo_type,
logo: websiteData.logo_text,
logo:
websiteData.logo_type === "text"
? websiteData.logo_text
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(websiteData.main_content ?? "", false),
articles: websiteData.articles ?? [],
footerAdditionalText: md(websiteData.additional_text ?? "")
@@ -101,7 +117,7 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true)
await mkdir(uploadDir, { recursive: true });
await writeFile(join(uploadDir, "index.html"), indexFileContents);
await mkdir(join(uploadDir, websiteData.content_type === "Blog" ? "articles" : "documents"), {
await mkdir(join(uploadDir, "articles"), {
recursive: true
});
@@ -137,11 +153,17 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true)
break;
case "Docs":
{
({ head, body } = render(DocsEntry, {
({ head, body } = render(DocsArticle, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: article.title,
logoType: websiteData.logo_type,
logo: websiteData.logo_text,
logo:
websiteData.logo_type === "text"
? websiteData.logo_text
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
coverImage: article.cover_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${article.cover_image}`
: "",
@@ -156,22 +178,18 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true)
const articleFileContents = head.concat(body);
await writeFile(
join(
uploadDir,
websiteData.content_type === "Blog" ? "articles" : "documents",
`${articleFileName}.html`
),
articleFileContents
);
await writeFile(join(uploadDir, "articles", `${articleFileName}.html`), articleFileContents);
}
const commonStyles = await readFile(`${process.cwd()}/template-styles/common-styles.css`, {
encoding: "utf-8"
});
const specificStyles = await readFile(`${process.cwd()}/template-styles/blog-styles.css`, {
const specificStyles = await readFile(
`${process.cwd()}/template-styles/${websiteData.content_type.toLowerCase()}-styles.css`,
{
encoding: "utf-8"
});
}
);
await writeFile(
join(uploadDir, "styles.css"),
commonStyles

View File

@@ -11,6 +11,7 @@
<WebsiteEditor
id={data.website.id}
contentType={data.website.content_type}
title={data.website.title}
previewContent={data.websitePreviewUrl}
fullPreview={true}
@@ -19,15 +20,13 @@
<h2>
<a href="#publish-website">Publish website</a>
</h2>
{JSON.stringify(data.websiteOverview.articles)}
<p>
The preview area on this page allows you to see exactly how your website will look when it is
is published. If you are happy with the results, click the button below and your website will
be published on the Internet.
</p>
<form method="POST" action="?/publishWebsite" use:enhance>
<input type="hidden" name="website-overview" value={JSON.stringify(data.websiteOverview)} />
<button type="submit">Publish</button>
</form>
</section>

View File

@@ -108,6 +108,7 @@ select,
[role="button"],
[role="option"],
label[for="toggle-mobile-preview"],
label[for="toggle-sidebar"],
summary {
cursor: pointer;
}
@@ -118,6 +119,7 @@ textarea,
select,
a[role="button"],
label[for="toggle-mobile-preview"],
label[for="toggle-sidebar"],
summary {
font: inherit;
color: inherit;
@@ -146,7 +148,6 @@ input[type="color"] {
}
a[role="button"] {
display: inline-block;
max-inline-size: fit-content;
text-decoration: none;
}
@@ -158,16 +159,23 @@ summary {
button,
a[role="button"],
label[for="toggle-mobile-preview"],
label[for="toggle-sidebar"],
summary {
background-color: var(--bg-secondary);
}
:is(button, a[role="button"], label[for="toggle-mobile-preview"], summary):hover {
:is(
button,
a[role="button"],
label[for="toggle-mobile-preview"],
label[for="toggle-sidebar"],
summary
):hover {
background-color: var(--bg-tertiary);
}
:is(button, input, textarea, select, a, summary, pre):focus,
#toggle-mobile-preview:checked + label {
:is(#toggle-mobile-preview, #toggle-sidebar):checked + label {
outline: 0.125rem solid var(--color-accent);
outline-offset: 0.25rem;
}
@@ -314,7 +322,6 @@ table {
th,
td {
text-align: start;
padding-inline: var(--space-2xs);
padding-block: var(--space-3xs);
padding: var(--space-2xs);
border: var(--border-primary);
}

View File

@@ -0,0 +1,58 @@
.container {
margin-inline: auto;
inline-size: min(100% - var(--space-m), 75ch);
}
nav {
position: sticky;
block-size: var(--space-xl);
display: flex;
align-items: center;
inset-block-start: 0;
background-color: var(--bg-primary);
border-block-end: var(--border-primary);
}
nav > .container {
display: flex;
align-items: center;
gap: var(--space-2xs);
}
header > .container {
display: flex;
flex-direction: column;
gap: var(--space-s);
}
nav,
header,
main,
footer {
padding-block: var(--space-s);
}
section {
scroll-margin-block-start: var(--space-xl);
}
label[for="toggle-sidebar"] {
display: inline-grid;
place-content: center;
}
@media (min-width: 1525px) {
#table-of-contents {
position: fixed;
inset-inline-start: calc(50% + 37.5ch + var(--space-m));
inset-block-start: calc(var(--space-xl) + var(--space-s));
max-inline-size: 35ch;
padding: var(--space-s);
background-color: var(--bg-primary);
border: var(--border-primary);
}
#table-of-contents + section {
margin-block-start: 0;
}
}