Merge of Insiders features tied to 'Black Pearl' funding goal

This commit is contained in:
squidfunk 2021-03-29 18:46:57 +02:00
parent 8677190f3f
commit ca3da9e3ca
51 changed files with 867 additions and 194 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -39,10 +39,10 @@
{% endif %}
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.c772ddf0.min.css' | url }}">
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.33e2939f.min.css' | url }}">
{% if config.theme.palette %}
{% set palette = config.theme.palette %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.7fa14f5b.min.css' | url }}">
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.ef6f36e2.min.css' | url }}">
{% if palette.primary %}
{% import "partials/palette.html" as map %}
{% set primary = map.primary(
@ -87,13 +87,14 @@
{% set primary = palette.primary | replace(" ", "-") | lower %}
{% set accent = palette.accent | replace(" ", "-") | lower %}
<body dir="{{ direction }}" data-md-color-scheme="{{ scheme }}" data-md-color-primary="{{ primary }}" data-md-color-accent="{{ accent }}">
{% if "preference" == scheme %}
<script>matchMedia("(prefers-color-scheme: dark)").matches&&document.body.setAttribute("data-md-color-scheme","slate")</script>
{% endif %}
{% else %}
<body dir="{{ direction }}">
{% endif %}
{% set features = config.theme.features or [] %}
{% include "partials/javascripts/base.html" %}
{% if not config.theme.palette is mapping %}
{% include "partials/javascripts/palette.html" %}
{% endif %}
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
@ -178,6 +179,11 @@
</article>
</div>
</div>
{% if "navigation.top" in features %}
<a href="#" class="md-top md-icon" data-md-component="top" data-md-state="hidden">
{% include ".icons/material/arrow-up.svg" %}
</a>
{% endif %}
</main>
{% block footer %}
{% include "partials/footer.html" %}
@ -217,7 +223,7 @@
</script>
{% endblock %}
{% block scripts %}
<script src="{{ 'assets/javascripts/bundle.65ce87ac.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.d892486b.min.js' | url }}"></script>
{% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script>
{% endfor %}

View File

@ -35,5 +35,5 @@
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ 'overrides/assets/javascripts/bundle.afdf7228.min.js' | url }}"></script>
<script src="{{ 'overrides/assets/javascripts/bundle.3b3ca511.min.js' | url }}"></script>
{% endblock %}

View File

@ -27,8 +27,20 @@
</div>
</div>
</div>
<div class="md-header__options">
{% if config.extra.alternate %}
{% if not config.theme.palette is mapping %}
<form class="md-header__option" data-md-component="palette">
{% for option in config.theme.palette %}
{% set primary = option.primary | replace(" ", "-") | lower %}
{% set accent = option.accent | replace(" ", "-") | lower %}
<input class="md-option" data-md-color-media="{{ option.media }}" data-md-color-scheme="{{ option.scheme }}" data-md-color-primary="{{ primary }}" data-md-color-accent="{{ accent }}" type="radio" name="__palette" id="__palette_{{ loop.index }}">
<label class="md-header__button md-icon" title="{{ option.toggle.name }}" for="__palette_{{ loop.index0 or loop.length }}" hidden>
{% include ".icons/" ~ option.toggle.icon ~ ".svg" %}
</label>
{% endfor %}
</form>
{% endif %}
{% if config.extra.alternate %}
<div class="md-header__option">
<div class="md-select">
{% set icon = config.theme.icon.alternate or "material/translate" %}
<span class="md-header__button md-icon">
@ -46,8 +58,8 @@
</ul>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
{% if "search" in config["plugins"] %}
<label class="md-header__button md-icon" for="__search">
{% include ".icons/material/magnify.svg" %}

View File

@ -0,0 +1,4 @@
{#-
This file was automatically generated - do not edit
-#}
<script>function __prefix(e){return new URL("{{ base_url }}",location).pathname+"."+e}function __get(e,t=localStorage){return JSON.parse(t.getItem(__prefix(e)))}</script>

View File

@ -0,0 +1,4 @@
{#-
This file was automatically generated - do not edit
-#}
<script>var palette=__get("__palette");if(null!==palette&&"object"==typeof palette.color)for(var key in palette.color)document.body.setAttribute("data-md-color-"+key,palette.color[key])</script>

View File

@ -55,9 +55,20 @@ theme:
- navigation.sections
- navigation.tabs
palette:
scheme: default
primary: indigo
accent: indigo
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
accent: indigo
toggle:
icon: material/toggle-switch-off-outline
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: red
accent: red
toggle:
icon: material/toggle-switch
name: Switch to light mode
font:
text: Roboto
code: Roboto Mono

View File

@ -35,6 +35,7 @@ export type Flag =
| "navigation.instant" /* Instant loading */
| "navigation.sections" /* Sections navigation */
| "navigation.tabs" /* Tabs navigation */
| "navigation.top" /* Back-to-top button */
| "toc.integrate" /* Integrated table of contents */
/* ------------------------------------------------------------------------- */

View File

@ -28,3 +28,4 @@ export * from "./search"
export * from "./sidebar"
export * from "./source"
export * from "./tabs"
export * from "./top"

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set back-to-top state
*
* @param el - Back-to-top element
* @param state - Back-to-top state
*/
export function setBackToTopState(
el: HTMLElement, state: "hidden"
): void {
el.setAttribute("data-md-state", state)
}
/**
* Reset back-to-top state
*
* @param el - Back-to-top element
*/
export function resetBackToTopState(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}

View File

@ -43,7 +43,7 @@ import {
export function request(
url: URL | string, options: RequestInit = { credentials: "same-origin" }
): Observable<Response> {
return from(fetch(url.toString(), options))
return from(fetch(`${url}`, options))
.pipe(
filter(res => res.status === 200),
)

View File

@ -48,10 +48,12 @@ import {
import {
getComponentElement,
getComponentElements,
mountBackToTop,
mountContent,
mountDialog,
mountHeader,
mountHeaderTitle,
mountPalette,
mountSearch,
mountSidebar,
mountSource,
@ -173,17 +175,17 @@ const control$ = merge(
...getComponentElements("header")
.map(el => mountHeader(el, { viewport$, header$, main$ })),
/* Color palette */
...getComponentElements("palette")
.map(el => mountPalette(el)),
/* Search */
...getComponentElements("search")
.map(el => mountSearch(el, { index$, keyboard$ })),
/* Repository information */
...getComponentElements("source")
.map(el => mountSource(el)),
/* Navigation tabs */
...getComponentElements("tabs")
.map(el => mountTabs(el, { viewport$, header$ })),
.map(el => mountSource(el))
)
/* Set up content component observables */
@ -204,9 +206,17 @@ const content$ = defer(() => merge(
: at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))
),
/* Navigation tabs */
...getComponentElements("tabs")
.map(el => mountTabs(el, { viewport$, header$ })),
/* Table of contents */
...getComponentElements("toc")
.map(el => mountTableOfContents(el, { viewport$, header$ })),
/* Back-to-top button */
...getComponentElements("top")
.map(el => mountBackToTop(el, { viewport$, main$ }))
))
/* Set up component observables */

View File

@ -38,6 +38,7 @@ export type ComponentType =
| "header-title" /* Header title */
| "header-topic" /* Header topic */
| "main" /* Main area */
| "palette" /* Color palette */
| "search" /* Search */
| "search-query" /* Search input */
| "search-result" /* Search results */
@ -46,6 +47,7 @@ export type ComponentType =
| "source" /* Repository information */
| "tabs" /* Navigation tabs */
| "toc" /* Table of contents */
| "top" /* Back-to-top button */
/**
* A component
@ -77,6 +79,7 @@ interface ComponentTypeMap {
"header-title": HTMLElement /* Header title */
"header-topic": HTMLElement /* Header topic */
"main": HTMLElement /* Main area */
"palette": HTMLElement /* Color palette */
"search": HTMLElement /* Search */
"search-query": HTMLInputElement /* Search input */
"search-result": HTMLElement /* Search results */
@ -85,6 +88,7 @@ interface ComponentTypeMap {
"source": HTMLAnchorElement /* Repository information */
"tabs": HTMLElement /* Navigation tabs */
"toc": HTMLElement /* Table of contents */
"top": HTMLAnchorElement /* Back-to-top button */
}
/* ----------------------------------------------------------------------------

View File

@ -22,4 +22,5 @@
export * from "./_"
export * from "./code"
export * from "./details"
export * from "./table"

View File

@ -120,7 +120,7 @@ function isHidden({ viewport$ }: WatchOptions): Observable<boolean> {
distinctUntilChanged()
)
/* Compute threshold for autohiding */
/* Compute threshold for hiding */
const search$ = watchToggle("search")
return combineLatest([viewport$, search$])
.pipe(

View File

@ -25,8 +25,10 @@ export * from "./content"
export * from "./dialog"
export * from "./header"
export * from "./main"
export * from "./palette"
export * from "./search"
export * from "./sidebar"
export * from "./source"
export * from "./tabs"
export * from "./toc"
export * from "./top"

View File

@ -0,0 +1,147 @@
/*
* Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import {
Observable,
Subject,
fromEvent,
of
} from "rxjs"
import {
finalize,
map,
mapTo,
mergeMap,
shareReplay,
startWith,
tap
} from "rxjs/operators"
import { getElements } from "~/browser"
import { Component } from "../_"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Palette colors
*/
export interface PaletteColor {
scheme?: string /* Color scheme */
primary?: string /* Primary color */
accent?: string /* Accent color */
}
/**
* Palette
*/
export interface Palette {
index: number /* Palette index */
color: PaletteColor /* Palette colors */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch color palette
*
* @param inputs - Color palette element
*
* @returns Color palette observable
*/
export function watchPalette(
inputs: HTMLInputElement[]
): Observable<Palette> {
const data = localStorage.getItem(__prefix("__palette"))!
const current = JSON.parse(data) || {
index: inputs.findIndex(input => (
matchMedia(input.getAttribute("data-md-color-media")!).matches
))
}
/* Emit changes in color palette */
const palette$ = of(...inputs)
.pipe(
mergeMap(input => fromEvent(input, "change")
.pipe(
mapTo(input)
)
),
startWith(inputs[Math.max(0, current.index)]),
map(input => ({
index: inputs.indexOf(input),
color: {
scheme: input.getAttribute("data-md-color-scheme"),
primary: input.getAttribute("data-md-color-primary"),
accent: input.getAttribute("data-md-color-accent")
}
} as Palette)),
shareReplay(1)
)
/* Persist preference in local storage */
palette$.subscribe(palette => {
localStorage.setItem(__prefix("__palette"), JSON.stringify(palette))
})
/* Return palette */
return palette$
}
/**
* Mount color palette
*
* @param el - Color palette element
*
* @returns Color palette component observable
*/
export function mountPalette(
el: HTMLElement
): Observable<Component<Palette>> {
const internal$ = new Subject<Palette>()
/* Set color palette */
internal$.subscribe(palette => {
for (const [key, value] of Object.entries(palette.color))
if (typeof value === "string")
document.body.setAttribute(`data-md-color-${key}`, value)
/* Toggle visibility */
for (let index = 0; index < inputs.length; index++) {
const label = inputs[index].nextElementSibling as HTMLElement
label.hidden = palette.index !== index
}
})
/* Create and return component */
const inputs = getElements<HTMLInputElement>("input", el)
return watchPalette(inputs)
.pipe(
tap(internal$),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@ -32,7 +32,6 @@ import {
import { setSourceFacts, setSourceState } from "~/actions"
import { renderSourceFacts } from "~/templates"
import { digest } from "~/utilities"
import { Component } from "../../_"
import { SourceFacts, fetchSourceFacts } from "../facts"
@ -75,14 +74,14 @@ export function watchSource(
el: HTMLAnchorElement
): Observable<Source> {
return fetch$ ||= defer(() => {
const data = sessionStorage.getItem(digest("__repo"))
const data = sessionStorage.getItem(__prefix("__source"))
if (data) {
return of<SourceFacts>(JSON.parse(data))
} else {
const value$ = fetchSourceFacts(el.href)
value$.subscribe(value => {
try {
sessionStorage.setItem(digest("__repo"), JSON.stringify(value))
sessionStorage.setItem(__prefix("__source"), JSON.stringify(value))
} catch (err) {
/* Uncritical, just swallow */
}
@ -94,7 +93,7 @@ export function watchSource(
})
.pipe(
catchError(() => NEVER),
filter(facts => facts.length > 0),
filter(facts => Object.keys(facts).length > 0),
map(facts => ({ facts })),
shareReplay(1)
)

View File

@ -29,10 +29,30 @@ import { fetchSourceFactsFromGitLab } from "../gitlab"
* Types
* ------------------------------------------------------------------------- */
/**
* Repository facts for repositories
*/
export interface RepositoryFacts {
stars?: number /* Number of stars */
forks?: number /* Number of forks */
version?: string /* Latest version */
}
/**
* Repository facts for organizations
*/
export interface OrganizationFacts {
repositories?: number /* Number of repositories */
}
/* ------------------------------------------------------------------------- */
/**
* Repository facts
*/
export type SourceFacts = string[]
export type SourceFacts =
| RepositoryFacts
| OrganizationFacts
/* ----------------------------------------------------------------------------
* Functions

View File

@ -21,14 +21,24 @@
*/
import { Repo, User } from "github-types"
import { Observable } from "rxjs"
import { Observable, zip } from "rxjs"
import { defaultIfEmpty, map } from "rxjs/operators"
import { requestJSON } from "~/browser"
import { round } from "~/utilities"
import { SourceFacts } from "../_"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* GitHub release (partial)
*/
interface Release {
tag_name: string /* Tag name */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@ -44,29 +54,42 @@ import { SourceFacts } from "../_"
export function fetchSourceFactsFromGitHub(
user: string, repo?: string
): Observable<SourceFacts> {
const url = typeof repo !== "undefined"
? `https://api.github.com/repos/${user}/${repo}`
: `https://api.github.com/users/${user}`
return requestJSON<Repo & User>(url)
.pipe(
map(data => {
if (typeof repo !== "undefined") {
const url = `https://api.github.com/repos/${user}/${repo}`
return zip(
/* GitHub repository */
if (typeof repo !== "undefined") {
const { stargazers_count, forks_count }: Repo = data
return [
`${round(stargazers_count!)} Stars`,
`${round(forks_count!)} Forks`
]
/* Fetch version */
requestJSON<Release>(`${url}/releases/latest`)
.pipe(
map(release => ({
version: release.tag_name
})),
defaultIfEmpty({})
),
/* GitHub user/organization */
} else {
const { public_repos }: User = data
return [
`${round(public_repos!)} Repositories`
]
}
}),
defaultIfEmpty([])
/* Fetch stars and forks */
requestJSON<Repo>(url)
.pipe(
map(info => ({
stars: info.stargazers_count,
forks: info.forks_count
})),
defaultIfEmpty({})
)
)
.pipe(
map(([release, info]) => ({ ...release, ...info }))
)
/* User or organization */
} else {
const url = `https://api.github.com/repos/${user}`
return requestJSON<User>(url)
.pipe(
map(info => ({
repositories: info.public_repos
})),
defaultIfEmpty({})
)
}
}

View File

@ -25,7 +25,6 @@ import { Observable } from "rxjs"
import { defaultIfEmpty, map } from "rxjs/operators"
import { requestJSON } from "~/browser"
import { round } from "~/utilities"
import { SourceFacts } from "../_"
@ -47,10 +46,10 @@ export function fetchSourceFactsFromGitLab(
const url = `https://${base}/api/v4/projects/${encodeURIComponent(project)}`
return requestJSON<ProjectSchema>(url)
.pipe(
map(({ star_count, forks_count }) => ([
`${round(star_count)} Stars`,
`${round(forks_count)} Forks`
])),
defaultIfEmpty([])
map(({ star_count, forks_count }) => ({
stars: star_count,
forks: forks_count
})),
defaultIfEmpty({})
)
}

View File

@ -26,11 +26,16 @@ import {
finalize,
map,
observeOn,
switchMap,
tap
} from "rxjs/operators"
import { resetTabsState, setTabsState } from "~/actions"
import { Viewport, watchViewportAt } from "~/browser"
import {
Viewport,
watchElementSize,
watchViewportAt
} from "~/browser"
import { Component } from "../_"
import { Header } from "../header"
@ -81,8 +86,9 @@ interface MountOptions {
export function watchTabs(
el: HTMLElement, { viewport$, header$ }: WatchOptions
): Observable<Tabs> {
return watchViewportAt(el, { header$, viewport$ })
return watchElementSize(document.body)
.pipe(
switchMap(() => watchViewportAt(el, { header$, viewport$ })),
map(({ offset: { y } }) => {
return {
hidden: y >= 10

View File

@ -0,0 +1,160 @@
/*
* Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import {
Observable,
Subject,
animationFrameScheduler,
combineLatest
} from "rxjs"
import {
bufferCount,
distinctUntilChanged,
distinctUntilKeyChanged,
finalize,
map,
observeOn,
tap
} from "rxjs/operators"
import { resetBackToTopState, setBackToTopState } from "~/actions"
import { Viewport } from "~/browser"
import { Component } from "../_"
import { Main } from "../main"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Back-to-top button
*/
export interface BackToTop {
hidden: boolean /* User scrolled up */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Watch options
*/
interface WatchOptions {
viewport$: Observable<Viewport> /* Viewport observable */
main$: Observable<Main> /* Main area observable */
}
/**
* Mount options
*/
interface MountOptions {
viewport$: Observable<Viewport> /* Viewport observable */
main$: Observable<Main> /* Main area observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch back-to-top
*
* @param _el - Back-to-top element
* @param options - Options
*
* @returns Back-to-top observable
*/
export function watchBackToTop(
_el: HTMLElement, { viewport$, main$ }: WatchOptions
): Observable<BackToTop> {
/* Compute direction */
const direction$ = viewport$
.pipe(
map(({ offset: { y } }) => y),
bufferCount(2, 1),
map(([a, b]) => a > b),
distinctUntilChanged()
)
/* Compute whether button should be hidden */
const hidden$ = main$
.pipe(
distinctUntilKeyChanged("active")
)
/* Compute threshold for hiding */
return combineLatest([hidden$, direction$])
.pipe(
map(([{ active }, direction]) => ({
hidden: !(active && direction)
})),
distinctUntilChanged((a, b) => (
a.hidden === b.hidden
))
)
}
/* ------------------------------------------------------------------------- */
/**
* Mount back-to-top
*
* @param el - Back-to-top element
* @param options - Options
*
* @returns Back-to-top component observable
*/
export function mountBackToTop(
el: HTMLElement, options: MountOptions
): Observable<Component<BackToTop>> {
const internal$ = new Subject<BackToTop>()
internal$
.pipe(
observeOn(animationFrameScheduler)
)
.subscribe({
/* Update state */
next({ hidden }) {
if (hidden)
setBackToTopState(el, "hidden")
else
resetBackToTopState(el)
},
/* Reset on complete */
complete() {
resetBackToTopState(el)
}
})
/* Create and return component */
return watchBackToTop(el, options)
.pipe(
tap(internal$),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@ -240,7 +240,7 @@ export function setupInstantLoading(
sample(response$)
)
.subscribe(({ url }) => {
history.pushState({}, "", url.toString())
history.pushState({}, "", `${url}`)
})
/* Parse and emit fetched document */
@ -274,14 +274,14 @@ export function setupInstantLoading(
/* Meta tags */
"title",
"link[rel='canonical']",
"meta[name='author']",
"meta[name='description']",
"link[rel=canonical]",
"meta[name=author]",
"meta[name=description]",
/* Components */
"[data-md-component=announce]",
"[data-md-component=header-topic]",
"[data-md-component=container]",
"[data-md-component=header-topic]",
"[data-md-component=logo], .md-logo", // compat
"[data-md-component=skip]"
]) {

View File

@ -21,7 +21,7 @@
*/
import { SourceFacts } from "~/components"
import { h } from "~/utilities"
import { h, round } from "~/utilities"
/* ----------------------------------------------------------------------------
* Functions
@ -37,8 +37,10 @@ import { h } from "~/utilities"
export function renderSourceFacts(facts: SourceFacts): HTMLElement {
return (
<ul class="md-source__facts">
{facts.map(fact => (
<li class="md-source__fact">{fact}</li>
{Object.entries(facts).map(([key, value]) => (
<li class={`md-source__fact md-source__fact--${key}`}>
{typeof value === "number" ? round(value) : value}
</li>
))}
</ul>
)

View File

@ -20,8 +20,6 @@
* IN THE SOFTWARE.
*/
import { configuration } from "~/_"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@ -90,18 +88,3 @@ export function hash(value: string): number {
}
return h
}
/**
* Add a digest to a value to ensure uniqueness
*
* When a single account hosts multiple sites on the same GitHub Pages domain,
* entries would collide, because session and local storage are not unique.
*
* @param value - Value
*
* @returns Value with digest
*/
export function digest(value: string): string {
const config = configuration()
return `${value}[${hash(config.base)}]`
}

View File

@ -55,6 +55,7 @@
@import "main/layout/sidebar";
@import "main/layout/source";
@import "main/layout/tabs";
@import "main/layout/top";
@import "main/layout/version";
@import "main/extensions/markdown/admonition";

View File

@ -121,12 +121,31 @@ body {
// Rules: navigational elements
// ----------------------------------------------------------------------------
// Toggle - this class is applied to the checkbox elements, which are used to
// Toggle - this class is applied to checkbox elements, which are used to
// implement the CSS-only drawer and navigation, as well as the search
.md-toggle {
display: none;
}
// Option - this class is applied to radio elements, which are used to
// implement the color palette toggle
.md-option {
position: absolute;
width: 0;
height: 0;
opacity: 0;
// Option label for checked radio button
&:checked + label:not([hidden]) {
display: block;
}
// Option label on focus
&.focus-visible + label {
outline-style: auto;
}
}
// Skip link
.md-skip {
position: fixed;

View File

@ -39,9 +39,6 @@
box-shadow:
0 0 px2rem(4px) rgba(0, 0, 0, 0),
0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0);
transition:
color 250ms,
background-color 250ms;
// [print]: Hide header
@media print {
@ -54,20 +51,21 @@
0 0 px2rem(4px) rgba(0, 0, 0, 0.1),
0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0.2);
transition:
transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1),
color 250ms,
background-color 250ms,
box-shadow 250ms;
transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1),
box-shadow 250ms;
}
// Header in hidden state, i.e. moved out of sight
&[data-md-state="hidden"] {
transform: translateY(-100%);
transition:
transform 250ms cubic-bezier(0.8, 0, 0.6, 1),
color 250ms,
background-color 250ms,
box-shadow 250ms;
transform 250ms cubic-bezier(0.8, 0, 0.6, 1),
box-shadow 250ms;
}
// Link or button on focus
.focus-visible {
outline-color: currentColor;
}
// Header wrapper
@ -81,7 +79,6 @@
&__button {
position: relative;
z-index: 1;
display: inline-block;
margin: px2rem(4px);
padding: px2rem(8px);
color: currentColor;
@ -89,15 +86,20 @@
cursor: pointer;
transition: opacity 250ms;
// Button on focus/hover
&:focus,
// Button on hover
&:hover {
opacity: 0.7;
}
// Header button is visible
&:not([hidden]) {
display: inline-block;
}
// Hide outline for pointer devices
&:not(.focus-visible) {
outline: none;
-webkit-tap-highlight-color: transparent;
}
// Button with logo, pointing to `config.site_url`
@ -223,8 +225,8 @@
}
}
// Header options
&__options {
// Header option
&__option {
display: flex;
flex-shrink: 0;
max-width: 100%;
@ -233,11 +235,6 @@
max-width 0ms 250ms,
opacity 250ms 250ms;
// Hide inactive buttons
> [data-md-state="hidden"] {
display: none;
}
// Hide toggle when search is active
[data-md-toggle="search"]:checked ~ .md-header & {
max-width: 0;

View File

@ -56,6 +56,16 @@
// Rules
// ----------------------------------------------------------------------------
// Icon definitions
:root {
--md-source-forks-icon: svg-load("octicons/repo-forked-16.svg");
--md-source-repositories-icon: svg-load("octicons/repo-16.svg");
--md-source-stars-icon: svg-load("octicons/star-16.svg");
--md-source-version-icon: svg-load("octicons/tag-16.svg");
}
// ----------------------------------------------------------------------------
// Repository information
.md-source {
display: block;
@ -66,8 +76,7 @@
backface-visibility: hidden;
transition: opacity 250ms;
// Repository information on focus/hover
&:focus,
// Repository information on hover
&:hover {
opacity: 0.7;
}
@ -75,7 +84,7 @@
// Repository icon
&__icon {
display: inline-block;
width: px2rem(48px);
width: px2rem(40px);
height: px2rem(48px);
vertical-align: middle;
@ -112,17 +121,15 @@
max-width: calc(100% - #{px2rem(24px)});
margin-left: px2rem(12px);
overflow: hidden;
font-weight: 700;
text-overflow: ellipsis;
vertical-align: middle;
}
// Repository facts
&__facts {
margin: 0;
margin: px2rem(2px) 0 0;
padding: 0;
overflow: hidden;
font-weight: 700;
font-size: px2rem(11px);
list-style-type: none;
opacity: 0.75;
@ -135,27 +142,61 @@
// Repository fact
&__fact {
float: left;
// Adjust for right-to-left languages
[dir="rtl"] & {
float: right;
}
display: inline-block;
// Show after the data was loaded
[data-md-state="done"] & {
animation: md-source__fact--done 400ms ease-out;
}
// Middle dot before fact
// Repository fact icon
&::before {
margin: 0 px2rem(2px);
content: "\00B7";
display: inline-block;
width: px2rem(12px);
height: px2rem(12px);
margin-right: px2rem(2px);
vertical-align: text-top;
background-color: currentColor;
mask-repeat: no-repeat;
mask-size: contain;
content: "";
}
// Remove middle dot on first fact
&:first-child::before {
display: none;
// Adjust spacing for repository fact icon
&:nth-child(1n+2)::before {
margin-left: px2rem(8px);
}
// Adjust for right-to-left languages
[dir="rtl"] & {
margin-right: initial;
margin-left: px2rem(2px);
// Adjust spacing for repository fact icon
&:nth-child(1n+2)::before {
margin-right: px2rem(8px);
margin-left: initial;
}
}
// Repository fact: version
&--version::before {
mask-image: var(--md-source-version-icon);
}
// Repository fact: stars
&--stars::before {
mask-image: var(--md-source-stars-icon);
}
// Repository fact: forks
&--forks::before {
mask-image: var(--md-source-forks-icon);
}
// Repository fact: repositories
&--repositories::before {
mask-image: var(--md-source-repositories-icon);
}
}
}

View File

@ -30,7 +30,6 @@
overflow: auto;
color: var(--md-primary-bg-color);
background-color: var(--md-primary-fg-color);
transition: background-color 250ms;
// [print]: Hide tabs
@media print {

View File

@ -0,0 +1,65 @@
////
/// Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
///
/// Permission is hereby granted, free of charge, to any person obtaining a
/// copy of this software and associated documentation files (the "Software"),
/// to deal in the Software without restriction, including without limitation
/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
/// and/or sell copies of the Software, and to permit persons to whom the
/// Software is furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
/// DEALINGS
////
// ----------------------------------------------------------------------------
// Rules
// ----------------------------------------------------------------------------
// Back-to-top button
.md-top {
position: sticky;
bottom: px2rem(8px);
z-index: 1;
float: right;
margin: px2rem(-56px) px2rem(8px) px2rem(8px);
padding: px2rem(8px);
color: var(--md-primary-bg-color);
background: var(--md-primary-fg-color);
border-radius: 100%;
outline: none;
box-shadow:
0 px2rem(4px) px2rem(10px) hsla(0, 0%, 0%, 0.1),
0 px2rem(0.5px) px2rem(1px) hsla(0, 0%, 0%, 0.1);
transform: translateY(0);
transition:
opacity 125ms,
transform 125ms cubic-bezier(0.4, 0, 0.2, 1),
background-color 125ms;
// Adjust for right-to-left languages
[dir="rtl"] & {
float: left;
}
// Back-to-top button in hidden state
&[data-md-state="hidden"] {
transform: translateY(px2rem(-4px));
opacity: 0;
}
// Back-to-top button on focus/hover
&:focus,
&:hover {
background: var(--md-accent-fg-color);
transform: scale(1.1);
}
}

View File

@ -57,9 +57,16 @@
--md-code-hl-constant-color: hsla(250, 62%, 70%, 1);
--md-code-hl-keyword-color: hsla(219, 66%, 64%, 1);
--md-code-hl-string-color: hsla(150, 58%, 44%, 1);
--md-code-hl-name-color: var(--md-code-fg-color);
--md-code-hl-operator-color: var(--md-default-fg-color--light);
--md-code-hl-punctuation-color: var(--md-default-fg-color--light);
--md-code-hl-comment-color: var(--md-default-fg-color--light);
--md-code-hl-generic-color: var(--md-default-fg-color--light);
--md-code-hl-variable-color: var(--md-default-fg-color--light);
// Typeset color shades
--md-typeset-a-color: var(--md-primary-fg-color--light);
--md-typeset-color: var(--md-default-fg-color);
--md-typeset-a-color: var(--md-primary-fg-color);
// Typeset `mark` color shades
--md-typeset-mark-color: hsla(#{hex2hsl($clr-blue-a200)}, 0.3);

View File

@ -168,21 +168,18 @@
data-md-color-primary="{{ primary }}"
data-md-color-accent="{{ accent }}"
>
<!-- Experimental: set color scheme based on preference -->
{% if "preference" == scheme %}
<script>
if (matchMedia("(prefers-color-scheme: dark)").matches)
document.body.setAttribute("data-md-color-scheme", "slate")
</script>
{% endif %}
{% else %}
<body dir="{{ direction }}">
{% endif %}
<!-- Retrieve features from configuration -->
{% set features = config.theme.features or [] %}
{% include "partials/javascripts/base.html" %}
<!-- User preference: color palette -->
{% if not config.theme.palette is mapping %}
{% include "partials/javascripts/palette.html" %}
{% endif %}
<!--
State toggles - we need to set autocomplete="off" in order to reset the
@ -338,6 +335,18 @@
</article>
</div>
</div>
<!-- Back-to-top button -->
{% if "navigation.top" in features %}
<a
href="#"
class="md-top md-icon"
data-md-component="top"
data-md-state="hidden"
>
{% include ".icons/material/arrow-up.svg" %}
</a>
{% endif %}
</main>
<!-- Footer -->

View File

@ -83,14 +83,12 @@ export function mountIconSearch(
`${config.base}/overrides/assets/javascripts/iconsearch_index.json`
)
/* Retrieve nested components */
/* Retrieve query and result components */
const query = getComponentElement("iconsearch-query", el)
const result = getComponentElement("iconsearch-result", el)
/* Create and return component */
const query$ = mountIconSearchQuery(query)
return merge(
query$,
mountIconSearchResult(result, { index$, query$ })
)
const query$ = mountIconSearchQuery(query)
const result$ = mountIconSearchResult(result, { index$, query$ })
return merge(query$, result$)
}

View File

@ -63,11 +63,37 @@
</div>
</div>
<!-- Header options -->
<div class="md-header__options">
<!-- Color palette -->
{% if not config.theme.palette is mapping %}
<form class="md-header__option" data-md-component="palette">
{% for option in config.theme.palette %}
{% set primary = option.primary | replace(" ", "-") | lower %}
{% set accent = option.accent | replace(" ", "-") | lower %}
<input
class="md-option"
data-md-color-media="{{ option.media }}"
data-md-color-scheme="{{ option.scheme }}"
data-md-color-primary="{{ primary }}"
data-md-color-accent="{{ accent }}"
type="radio"
name="__palette"
id="__palette_{{ loop.index }}"
/>
<label
class="md-header__button md-icon"
title="{{ option.toggle.name }}"
for="__palette_{{ loop.index0 or loop.length }}"
hidden
>
{% include ".icons/" ~ option.toggle.icon ~ ".svg" %}
</label>
{% endfor %}
</form>
{% endif %}
<!-- Switch to toggle language -->
{% if config.extra.alternate %}
<!-- Site language selector -->
{% if config.extra.alternate %}
<div class="md-header__option"></form>
<div class="md-select">
{% set icon = config.theme.icon.alternate or "material/translate" %}
<span class="md-header__button md-icon">
@ -85,8 +111,8 @@
</ul>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Button to open search modal -->
{% if "search" in config["plugins"] %}

View File

@ -22,7 +22,7 @@
<!-- Google Analytics integration -->
{% set analytics = config.google_analytics %}
<script type="application/javascript">
<script>
window.ga = window.ga || function() {
(ga.q = ga.q || []).push(arguments)
}

View File

@ -30,7 +30,7 @@
{% if not page.is_homepage and disqus %}
<h2 id="__comments">{{ lang.t("meta.comments") }}</h2>
<div id="disqus_thread"></div>
<script type="application/javascript">
<script>
var disqus_config = function () {
this.page.url = "{{ page.canonical_url }}";
this.page.identifier =

View File

@ -0,0 +1,39 @@
<!--
Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
-->
<!--
A collection of functions used from within some partials to allow the usage
of state saved in local or session storage, e.g. to model preferences.
-->
<script>
/* Prepend the base path to the given key to ensure uniqueness */
function __prefix(key) {
var prefix = new URL("{{ base_url }}", location)
return prefix.pathname + "." + key
}
/* Fetch the given key from the given storage */
function __get(key, storage = localStorage) {
return JSON.parse(storage.getItem(__prefix(key)))
}
</script>

View File

@ -0,0 +1,29 @@
<!--
Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
-->
<!-- User preference: color palette -->
<script>
var palette = __get("__palette")
if (palette !== null && typeof palette.color === "object")
for (var key in palette.color)
document.body.setAttribute("data-md-color-" + key, palette.color[key])
</script>