mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-07-22 05:53:03 -04:00
Merge of Insiders features tied to 'Black Pearl' funding goal
This commit is contained in:
@@ -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 */
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
@@ -28,3 +28,4 @@ export * from "./search"
|
||||
export * from "./sidebar"
|
||||
export * from "./source"
|
||||
export * from "./tabs"
|
||||
export * from "./top"
|
||||
|
||||
48
src/assets/javascripts/actions/top/index.ts
Normal file
48
src/assets/javascripts/actions/top/index.ts
Normal 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")
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
|
||||
@@ -22,4 +22,5 @@
|
||||
|
||||
export * from "./_"
|
||||
export * from "./code"
|
||||
export * from "./details"
|
||||
export * from "./table"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
147
src/assets/javascripts/components/palette/index.ts
Normal file
147
src/assets/javascripts/components/palette/index.ts
Normal 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 }))
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
160
src/assets/javascripts/components/top/index.ts
Normal file
160
src/assets/javascripts/components/top/index.ts
Normal 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 }))
|
||||
)
|
||||
}
|
||||
@@ -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]"
|
||||
]) {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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)}]`
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
65
src/assets/stylesheets/main/layout/_top.scss
Normal file
65
src/assets/stylesheets/main/layout/_top.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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$)
|
||||
}
|
||||
|
||||
@@ -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"] %}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
39
src/partials/javascripts/base.html
Normal file
39
src/partials/javascripts/base.html
Normal 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>
|
||||
29
src/partials/javascripts/palette.html
Normal file
29
src/partials/javascripts/palette.html
Normal 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>
|
||||
Reference in New Issue
Block a user