Font Awesome theme 👀 (#64)

* font awesome site theme

* separate font awesome.css

* prettier

* remove image borders

* fix search and prism

* select first item

* 30ms'

* fix double render

* fix for turbo loading

* fix preview

* prettier

* fix pagefind

* prettier
This commit is contained in:
Konnor Rogers
2024-03-12 13:45:33 -04:00
committed by GitHub
parent 9647259b5f
commit 5feee64425
9 changed files with 508 additions and 77 deletions

View File

@@ -36,6 +36,11 @@ export default defineConfig({
host: true
},
vite: {
server: {
watch: {
ignored: ['./public/pagefind/**/*.*'] // HERE
}
},
plugins: [
FullReload([
path.relative(__dirname, '../dist/custom-elements.json'),

View File

@@ -8,6 +8,7 @@ import Search from "../Search.astro"
import '../../styles/global.css'
import '../../styles/syntax-highlight.css'
import '../../styles/code-previews.css'
import '../../styles/font-awesome.css'
import { customElementsManifest } from '../../js/cem';
const version = customElementsManifest().package.version

View File

@@ -52,12 +52,12 @@ const pagefindTranslations = {
<script>
class SiteSearch extends HTMLElement {
constructor() {
super();
const openBtn = this.querySelector<HTMLButtonElement>('button[data-open-modal]')!;
const closeBtn = this.querySelector<HTMLButtonElement>('button[data-close-modal]')!;
const dialog = this.querySelector('dialog')!;
const dialogFrame = this.querySelector('.dialog-frame')!;
connectedCallback() {
// super.connectedCallback();
const openBtn = () => this.querySelector<HTMLButtonElement>('button[data-open-modal]')!;
const closeBtn = () => this.querySelector<HTMLButtonElement>('button[data-close-modal]')!;
const dialog = () => this.querySelector('dialog')!;
const dialogFrame = () => this.querySelector('.dialog-frame')!;
/** Close the modal if a user clicks on a link or outside of the modal. */
const onClick = (event: MouseEvent) => {
@@ -65,27 +65,27 @@ const pagefindTranslations = {
if (
isLink ||
(document.body.contains(event.target as Node) &&
!dialogFrame.contains(event.target as Node))
!dialogFrame().contains(event.target as Node))
) {
closeModal();
}
};
const openModal = (event?: MouseEvent) => {
dialog.showModal();
dialog().showModal();
document.body.toggleAttribute('data-search-modal-open', true);
this.querySelector('input')?.focus();
event?.stopPropagation();
window.addEventListener('click', onClick);
};
const closeModal = () => dialog.close();
const closeModal = () => dialog().close();
openBtn.addEventListener('click', openModal);
openBtn.disabled = false;
closeBtn.addEventListener('click', closeModal);
openBtn().addEventListener('click', openModal);
openBtn().disabled = false;
closeBtn().addEventListener('click', closeModal);
dialog.addEventListener('close', () => {
dialog().addEventListener('close', () => {
document.body.toggleAttribute('data-search-modal-open', false);
window.removeEventListener('click', onClick);
});
@@ -97,9 +97,9 @@ const pagefindTranslations = {
(['input', 'select', 'textarea'].includes(document.activeElement.tagName.toLowerCase()) ||
document.activeElement.isContentEditable);
if (e.metaKey === true && e.key === 'k') {
dialog.open ? closeModal() : openModal();
dialog().open ? closeModal() : openModal();
e.preventDefault();
} else if (e.key === '/' && !dialog.open && !isInput) {
} else if (e.key === '/' && !dialog().open && !isInput) {
openModal();
e.preventDefault();
}
@@ -114,15 +114,22 @@ const pagefindTranslations = {
const stripTrailingSlash = (path: string) => path.replace(/(.)\/(#.*)?$/, '$1$2');
const formatURL = shouldStrip ? stripTrailingSlash : (path: string) => path;
window.addEventListener('DOMContentLoaded', () => {
document.querySelector("#starlight__search").innerHTML = ""
window.addEventListener('turbo:load', () => {
document.querySelector("#starlight__search").innerHTML = ""
const onIdle = window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
onIdle(async () => {
if (import.meta.env.DEV) {
// Generate a fake search in dev by calling a JSON endpoint that generates the search.
await fetch(import.meta.env.BASE_URL.replace(/\/$/, '') + '/pagefind-dev.json')
}
// @ts-expect-error — Missing types for @pagefind/default-ui package.
const { PagefindUI } = await import('@pagefind/default-ui');
document.querySelector("#starlight__search").innerHTML = ""
new PagefindUI({
element: '#starlight__search',
baseUrl: import.meta.env.BASE_URL,
@@ -140,9 +147,125 @@ const pagefindTranslations = {
});
});
});
let links: (HTMLAnchorElement | HTMLButtonElement)[] = []
function findSelectionIndex () {
let index = links.findIndex((link) => link.getAttribute("aria-selected") === "true")
return index
}
function selectPrevious () {
selectInDirection("backward")
}
function selectInDirection (direction: "forward" | "backward") {
const index = findSelectionIndex()
const step = direction === "forward" ? 1 : -1
const isOutOfRange = !(index + step < links.length && index + step >= 0)
if (isOutOfRange) {
return
}
const prevLink = links[index]
if (prevLink) {
prevLink.removeAttribute("aria-selected")
let prevResult = prevLink.closest(".pagefind-ui__result-nested, .pagefind-ui__button")
if (!prevResult) {
prevResult = prevLink.closest(".pagefind-ui__result-title")
}
if (prevResult) {
prevResult.classList.remove("is-active")
}
}
const nextLink = links[index + step]
nextLink.setAttribute("aria-selected", "true")
nextLink.scrollIntoView({ block: "center" })
let nextResult = nextLink.closest(".pagefind-ui__result-nested, .pagefind-ui__button")
if (!nextResult) {
nextResult = nextLink.closest(".pagefind-ui__result-title")
}
if (nextResult) {
nextResult.classList.add("is-active")
}
}
function selectNext () {
selectInDirection("forward")
}
function handleKeyDown(e) {
if (e.key === "Enter") {
e.preventDefault()
const currentItem = links[findSelectionIndex()]
// When we're on the page find UI button, we want to jump to the previous result before showing more results.
if (currentItem.tagName === "BUTTON") {
selectPrevious()
}
currentItem.click()
}
if (e.key === "ArrowUp") {
e.preventDefault()
selectPrevious()
return
}
if (e.key === "ArrowDown") {
e.preventDefault()
selectNext()
return
}
}
const observerOptions = {
childList: true,
subtree: true,
};
const search = document.querySelector("#starlight__search")?.closest("dialog")
function debounce(this: any, func: (...args: any[]) => unknown, timeout = 300){
let timer: ReturnType<typeof setTimeout>;
return (...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
const setFirstSelection = debounce(() => {
if (links.length > 0 && links[0].tagName !== "BUTTON" && findSelectionIndex() === -1) {
selectNext()
}
}, 30)
if (search) {
search.addEventListener("keydown", handleKeyDown)
const resultObserver = new MutationObserver(() => {
links = Array.from(document.querySelectorAll(".pagefind-ui__result-link"))
.concat(Array.from(document.querySelectorAll(".pagefind-ui__button")))
for (const link of links) {
if (!link.hasAttribute("aria-selected")) {
link.removeAttribute("aria-selected")
}
link.setAttribute("tabindex", "-1")
}
setFirstSelection()
})
resultObserver.observe(search, observerOptions)
}
}
}
customElements.define('site-search', SiteSearch);
// :is(.pagefind-ui__result-title, .pagefind-ui__result-nested) .pagefind-ui__result-link {}
</script>
<style>
@@ -312,6 +435,12 @@ const pagefindTranslations = {
height: 100%;
}
#starlight__search .pagefind-ui__results-area {
overflow: auto;
max-height: 70vh;
padding: 8px;
}
#starlight__search .pagefind-ui__results > * + * {
margin-top: var(--sl-search-result-spacing);
}
@@ -334,15 +463,17 @@ const pagefindTranslations = {
#starlight__search .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):hover,
#starlight__search
.pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):focus-within,
.pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):is(.is-active, :focus-within),
#starlight__search .pagefind-ui__result-nested:hover,
#starlight__search .pagefind-ui__result-nested:focus-within {
#starlight__search .pagefind-ui__result-nested:is(.is-active, :focus-within),
#starlight__search .pagefind-ui__button.is-active {
outline: 1px solid var(--sl-color-accent-high);
}
#starlight__search
.pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):focus-within,
#starlight__search .pagefind-ui__result-nested:focus-within {
.pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):is(.is-active, :focus-within),
#starlight__search .pagefind-ui__result-nested:is(.is-active, :focus-within),
#starlight__search .pagefind-ui__button.is-active {
background-color: var(--sl-color-accent-low);
}
@@ -433,4 +564,6 @@ const pagefindTranslations = {
background-color: transparent;
font-weight: 600;
}
</style>

View File

@@ -1,37 +0,0 @@
import * as pagefind from 'pagefind';
import * as path from 'node:path';
// clean up once complete
import { getCollection } from 'astro:content';
export async function generateSearch() {
const { index } = await pagefind.createIndex({});
if (!index) return;
// Get all `src/content/docs/` entries
let allContent = await getCollection('docs');
allContent = allContent.filter(doc => {
return doc.data.pagefind !== false;
});
await Promise.allSettled(
allContent.map(async entry => {
const { category, title, description } = entry.data;
return await index?.addCustomRecord({
content: entry.body,
language: 'en',
url: entry.slug,
meta: {
category: category || '',
title,
description: description || ''
}
});
})
);
const { errors } = await index.writeFiles({
outputPath: path.join(process.cwd(), 'public', 'pagefind')
});
}

View File

@@ -94,7 +94,10 @@ const {
To import this component from <a href="https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace">the CDN</a>
using a script tag:
</p>
<pre><code class="language-html" set:html={highlight("html", `<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/${cdndir}/${component.path}"></script>`)}></code></pre>
<div class="code-preview">
<pre><code id="code-block-importing-script" class="language-html" set:html={highlight("html", `<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/${cdndir}/${component.path}"></script>`)}></code></pre>
<wa-copy-button from="code-block-importing-script" class="copy-code-button"></wa-copy-button>
</div>
</wa-tab-panel>
<wa-tab-panel name="import">
@@ -102,23 +105,32 @@ const {
To import this component from <a href="https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace">the CDN</a>
using a JavaScript import:
</p>
<pre><code class="language-js" set:html={
highlight("js", `import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/${cdndir}/${component.path}';`)
}></code></pre>
<div class="code-preview">
<pre><code id="code-block-importing-cdn" class="language-js" set:html={
highlight("js", `import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/${cdndir}/${component.path}';`)
}></code></pre>
<wa-copy-button from="code-block-importing-cdn" class="copy-code-button"></wa-copy-button>
</div>
</wa-tab-panel>
<wa-tab-panel name="bundler">
<p>
To import this component using <a href={ rootUrl('/getting-started/installation#bundling') }>a bundler</a>:
</p>
<pre><code class="language-js" set:html={highlight("js", `import '@shoelace-style/shoelace/${npmdir}/${component.path}';`)}></code></pre>
<div class="code-preview">
<pre><code id="code-block-importing-bundler" class="language-js" set:html={highlight("js", `import '@shoelace-style/shoelace/${npmdir}/${component.path}';`)}></code></pre>
<wa-copy-button from="code-block-importing-bundler" class="copy-code-button"></wa-copy-button>
</div>
</wa-tab-panel>
<wa-tab-panel name="react">
<p>
To import this component as a <a href="/frameworks/react">React component</a>:
</p>
<pre><code class="language-js" set:html={highlight("js", `import ${component.name} from '@shoelace-style/shoelace/${npmdir}/react/${component.tagNameWithoutPrefix}';`)}></code></pre>
<div class="code-preview">
<pre><code id="code-block-importing-react" class="language-js" set:html={highlight("js", `import ${component.name} from '@shoelace-style/shoelace/${npmdir}/react/${component.tagNameWithoutPrefix}';`)}></code></pre>
<wa-copy-button from="code-block-importing-react" class="copy-code-button"></wa-copy-button>
</div>
</wa-tab-panel>
</wa-tab-group>

View File

@@ -1,19 +1,51 @@
// import { APIContext } from "astro";
import * as pagefind from 'pagefind';
import * as path from 'node:path';
import { generateSearch } from '../js/generate-search';
// clean up once complete
import { getCollection } from 'astro:content';
if (process.env.DEV_SEARCH !== 'generated') {
await generateSearch();
export async function generateSearch() {
const { index } = await pagefind.createIndex({});
if (!index) return;
process.env.DEV_SEARCH = 'generated';
let json: Array<{ url: string; content: string }> = [];
// setTimeout(() => {
// process.env.DEV_SEARCH = ""
// }, 200)
// Get all `src/content/docs/` entries
let allContent = await getCollection('docs');
allContent = allContent.filter(doc => {
return doc.data.pagefind !== false;
});
await Promise.allSettled(
allContent.map(async entry => {
const { category, title, description } = entry.data;
const resp = await fetch('http://localhost:4000/' + entry.slug);
const html = await resp.text();
// json.push({
// content: html,
// url: entry.slug
// });
return await index?.addHTMLFile({
content: html,
url: entry.slug
});
})
);
const { errors } = await index.writeFiles({
outputPath: path.join(process.cwd(), 'public', 'pagefind')
});
return json;
}
export async function GET() {
return new Response(null, {
await generateSearch();
return new Response(JSON.stringify({}), {
status: 200,
headers: {
'Content-Type': 'application/json'

View File

@@ -66,10 +66,10 @@ function generateCodeBlock(node: Node) {
language = 'plaintext';
}
node.value = html`<pre><code id='code-block-${++count}' class="language-${language}">${highlight(
language,
node.value
)}</code>${copyButton(`code-block-${count}`)}</pre>`;
node.value = html`<div class="code-preview">
<pre><code id='code-block-${++count}' class="language-${language}">${highlight(language, node.value)}</code></pre>
${copyButton(`code-block-${count}`)}
</div>`;
}
function generatePreviewCodeBlock(node: Node, reactCode: string) {

285
docs/src/styles/font-awesome.css vendored Normal file
View File

@@ -0,0 +1,285 @@
:root {
--sl-font: 'cera-round-pro', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
}
div.ec-line {
/* Fixes extra spacing on code blocks */
margin-top: 0px !important;
}
/* Dark mode colors. */
:root {
--sl-color-text: #ffffff;
--sl-color-accent-low: #1d242f;
--sl-color-accent: #506b92;
--sl-color-accent-high: #bfc9d8;
--sl-color-white: #ffffff;
--sl-color-gray-1: #e3efff;
--sl-color-gray-2: #b4c3d9;
--sl-color-gray-3: #708db5;
--sl-color-gray-4: #3e597e;
--sl-color-gray-5: #1f395b;
--sl-color-gray-6: #0e2748;
--sl-color-black: #0d1928;
--fa-dk-teal: #0ca678;
--fa-gravy: #c3c6d1;
--fa-md-gravy: rgb(97, 109, 138);
--fa-dk-red: #e03131;
--fa-dk-yellow: rgb(250, 176, 5);
--fa-yellow: #ffd43b;
--link-color: rgb(20, 110, 190);
--balloon-border-radius: 2px;
--balloon-color: rgb(24 49 83);
--balloon-font-size: 12px;
--balloon-move: 4px;
--balloon-text-color: #fff;
}
/* Light mode colors. */
:root[data-theme='light'] {
--sl-color-text: rgb(24, 49, 83);
--sl-color-accent-low: #d0d7e3;
--sl-color-accent: #526d94;
--sl-color-accent-high: #273344;
--sl-color-white: #0d1928;
--sl-color-gray-1: #0e2748;
--sl-color-gray-2: #1f395b;
--sl-color-gray-3: #3e597e;
--sl-color-gray-4: #708db5;
--sl-color-gray-5: #b4c3d9;
--sl-color-gray-6: #e3efff;
--sl-color-gray-7: #f1f7ff;
--sl-color-black: #ffffff;
--fa-dk-teal: #0ca678;
--fa-gravy: #c3c6d1;
--fa-md-gravy: rgb(97, 109, 138);
--fa-dk-red: #e03131;
--fa-dk-yellow: rgb(250, 176, 5);
--fa-yellow: #ffd43b;
--link-color: rgb(20, 110, 190);
--balloon-border-radius: 2px;
--balloon-color: rgb(24 49 83);
--balloon-font-size: 12px;
--balloon-move: 4px;
--balloon-text-color: #fff;
}
.icon-padding {
padding: 0 8px 0 8px;
}
.remove-margins {
margin: 0px !important;
}
.action-button {
border-radius: 999rem;
text-decoration: none;
padding: 1rem 1.25rem;
background: var(--sl-color-text-accent);
color: var(--sl-color-black) !important;
}
.action-button > i {
vertical-align: middle;
}
/* table, */
/* tr, */
/* td, */
/* th { */
/* border: none !important; */
/* } */
/**/
/* table thead { */
/* text-align: left; */
/* font-weight: bold; */
/* color: rgb(24, 49, 83); */
/* border-bottom: 2px solid rgb(195, 198, 209); */
/* } */
/**/
/* table tr { */
/* vertical-align: top; */
/* } */
/**/
/* td { */
/* border-bottom: 1px solid rgb(224, 226, 232); */
/* } */
/**/
/* table tr:nth-child(odd) td { */
/* background: #fff; */
/* border-bottom: 1px solid rgb(224, 226, 232) !important; */
/* } */
/**/
/* table tr:nth-child(even) td { */
/* background: #fff; */
/* border-bottom: 1px solid rgb(224, 226, 232) !important; */
/* } */
.frame.is-terminal pre {
margin: 0px !important;
/* margin-top: 1px !important; */
background: rgb(24 49 83) !important;
}
.frame.has-title pre {
margin: 0px !important;
}
figcaption.header {
background: rgb(0, 28, 64) !important;
}
img {
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
.site-title > img {
border-radius: 0px;
color: rgb(20, 110, 190);
}
.starlight-aside {
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
.starlight-aside__content > ul {
list-style-type: none;
}
.starlight-aside__content > ul > li:before {
display: inline-block;
font-style: normal;
font-feature-settings: normal;
font-variant: normal;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
font-family: 'Font Awesome 6 Pro', 'Font Awesome 5 Pro';
font-weight: 900;
content: '\f058';
margin-right: calc(1em * 1);
margin-right: 16px;
margin-left: -2em;
}
.text-center {
text-align: center;
}
.display-block {
display: block;
}
.muted {
color: rgb(97, 109, 138);
}
img + em {
display: block;
color: rgb(97, 109, 138);
text-align: center;
font-size: 0.8em;
}
p > em {
color: rgb(97, 109, 138);
text-align: center;
font-size: 0.8em;
}
h1 h2 h3 {
font-weight: 600 !important;
}
.theme-ravenclaw {
--fa-secondary-opacity: 1;
--fa-primary-color: rgb(4, 56, 161);
--fa-secondary-color: rgb(108, 108, 108);
}
.fa-ul {
padding-left: 0;
margin-left: 2.14285714em;
list-style-type: none;
}
.fa-ul > li {
position: relative;
}
.fa-li {
position: absolute;
left: -2.14285714em;
width: 2.14285714em;
top: 0.14285714em;
text-align: center;
}
@media (min-width: 50em) {
:root {
--sl-text-h1: var(--sl-text-4xl);
--sl-text-h2: var(--sl-text-3xl);
--sl-text-h3: var(--sl-text-2xl);
--sl-text-h4: var(--sl-text-1xl);
}
}
img {
/* border: 2px solid rgb(24, 49, 83); */
}
.card {
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
padding: 0px 20px 10px 20px !important;
border: 2px solid rgb(24, 49, 83) !important;
margin-top: 0px !important;
}
.hero > img {
border: 0px;
}
a.site-title > img {
border: 0px;
}
.grid-container {
display: grid;
grid-template-columns: auto auto auto;
padding: 10px;
}
.grid-button {
padding: 10px 32px 10px 32px;
margin: 10px;
vertical-align: top;
}
.return-button {
border-radius: 999rem;
text-decoration: none;
padding: 0.5rem 1rem;
background: var(--sl-color-text-accent);
color: white !important;
}
.right-sidebar-panel h2 a {
display: inline-block;
}
.right-sidebar-container ul {
margin: 0;
}

View File

@@ -282,12 +282,12 @@ pre .token.italic {
background-color: color-mix(in oklab, var(--wa-color-neutral-fill-subtle), var(--wa-color-mix-active));
}
:is(.code-preview__source, pre) .copy-code-button {
:is(.code-preview__source, pre, .code-preview) > .copy-code-button {
opacity: 0;
scale: 0.75;
}
:is(.code-preview__source, pre):hover .copy-code-button,
:is(.code-preview__source, pre, .code-preview):hover > .copy-code-button,
.copy-code-button:focus-within {
opacity: 1;
scale: 1;