mirror of
https://github.com/requarks/wiki.git
synced 2026-10-12 14:01:58 -05:00
feat: add centered page contents layout option
This commit is contained in:
15 files changed
+412
-228
No files matched your search
@@ -1319,6 +1319,8 @@
|
||||
"admin.theme.contentFont": "Content Font",
|
||||
"admin.theme.contentFontHint": "The font used specifically for page content.",
|
||||
"admin.theme.contentWidth": "Content Width",
|
||||
"admin.theme.contentWidthCentered": "Centered",
|
||||
"admin.theme.contentWidthFull": "Full Width",
|
||||
"admin.theme.contentWidthHint": "Should the content use all available viewport space or stay centered.",
|
||||
"admin.theme.cssOverride": "CSS Override",
|
||||
"admin.theme.cssOverrideHint": "CSS code to inject after system default CSS. Injecting too much CSS code can result in poor page load performance!",
|
||||
@@ -1330,6 +1332,7 @@
|
||||
"admin.theme.downloadDownload": "Download",
|
||||
"admin.theme.downloadName": "Name",
|
||||
"admin.theme.downloadThemes": "Download Themes",
|
||||
"admin.theme.fontUserDefaults": "User System Defaults",
|
||||
"admin.theme.fonts": "Fonts",
|
||||
"admin.theme.headHtmlInjection": "Head HTML Injection",
|
||||
"admin.theme.headHtmlInjectionHint": "HTML code to be injected just before the closing head tag. Usually for script tags. Applied the next time a page is loaded.",
|
||||
@@ -1338,10 +1341,16 @@
|
||||
"admin.theme.iconset": "Icon Set",
|
||||
"admin.theme.iconsetHint": "Set of icons to use for the sidebar navigation.",
|
||||
"admin.theme.layout": "Layout",
|
||||
"admin.theme.loadFailed": "Failed to load the site theme configuration.",
|
||||
"admin.theme.options": "Theme Options",
|
||||
"admin.theme.pickColor": "Pick...",
|
||||
"admin.theme.positionHide": "Hide",
|
||||
"admin.theme.positionLeft": "Left",
|
||||
"admin.theme.positionRight": "Right",
|
||||
"admin.theme.primaryColor": "Primary Color",
|
||||
"admin.theme.primaryColorHint": "The main color for primary action buttons and most form elements.",
|
||||
"admin.theme.resetDefaults": "Reset Defaults",
|
||||
"admin.theme.saveFailed": "Failed to save the site theme configuration.",
|
||||
"admin.theme.saveSuccess": "Theme configuration saved successfully!",
|
||||
"admin.theme.secondaryColor": "Secondary Color",
|
||||
"admin.theme.secondaryColorHint": "The alternate color for secondary action buttons and for some other elements.",
|
||||
|
||||
+18
-12
@@ -77,13 +77,9 @@ const LOGOUT_NOTICE_KEY = 'wiki.logoutNotice'
|
||||
// WATCHERS
|
||||
|
||||
watch(
|
||||
() => userStore.appearance,
|
||||
(newValue) => {
|
||||
if (newValue === 'site') {
|
||||
dark.set(siteStore.theme.dark)
|
||||
} else {
|
||||
dark.set(newValue === 'dark')
|
||||
}
|
||||
() => [userStore.appearance, route.meta.forceDark === true],
|
||||
() => {
|
||||
dark.set(wantsDark())
|
||||
}
|
||||
)
|
||||
|
||||
@@ -165,13 +161,23 @@ function isAdminArea() {
|
||||
return route.path === '/_admin' || route.path.startsWith('/_admin/')
|
||||
}
|
||||
|
||||
/**
|
||||
* A route can pin the app dark whatever the reader or the site chose (`meta.forceDark`, the login
|
||||
* page) — and only for as long as it is open, since leaving it lands back here with the preference.
|
||||
*/
|
||||
function wantsDark() {
|
||||
if (route.meta.forceDark === true) {
|
||||
return true
|
||||
}
|
||||
if (userStore.appearance === 'site') {
|
||||
return siteStore.theme.dark
|
||||
}
|
||||
return userStore.appearance === 'dark'
|
||||
}
|
||||
|
||||
async function applyTheme() {
|
||||
// -> Dark Mode
|
||||
if (userStore.appearance === 'site') {
|
||||
dark.set(siteStore.theme.dark)
|
||||
} else {
|
||||
dark.set(userStore.appearance === 'dark')
|
||||
}
|
||||
dark.set(wantsDark())
|
||||
|
||||
// -> CSS Vars
|
||||
const brand = isAdminArea()
|
||||
|
||||
@@ -276,6 +276,7 @@
|
||||
-->
|
||||
<div
|
||||
class="editor-asciidoc-preview-content page-contents is-asciidoc"
|
||||
:class="{ 'is-centered': siteStore.theme.contentWidth === `centered` }"
|
||||
:lang="pageStore.locale"
|
||||
:dir="siteStore.localeDir(pageStore.locale)"
|
||||
ref="editorPreviewContainerRef"
|
||||
@@ -1778,11 +1779,12 @@ $editor-preview-height: calc(100vh - 64px - 96px - 32px);
|
||||
height: $editor-height;
|
||||
overflow: hidden;
|
||||
|
||||
// -> The page's own background in each theme: this is the render, and a tint here is a tint the article does not have
|
||||
@at-root .body--light & {
|
||||
background-color: $grey-2;
|
||||
background-color: #fff;
|
||||
}
|
||||
@at-root .body--dark & {
|
||||
background-color: $dark-6;
|
||||
background-color: $dark;
|
||||
}
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
@@ -1822,6 +1824,19 @@ $editor-preview-height: calc(100vh - 64px - 96px - 32px);
|
||||
padding: 1rem;
|
||||
max-width: calc(50vw - 57px);
|
||||
|
||||
/*
|
||||
The site theme's Centered content width, so the preview wraps where the page will. The pane is
|
||||
the scrolling box and the render goes straight into it (see the template), so the measure is
|
||||
made out of padding rather than a box inside: whatever the element is wider than the measure
|
||||
by goes either side. `min()` is the element's own width -- the pane, up to the cap above --
|
||||
since a percentage here is the pane's. Only a wide window gets past the cap to make any
|
||||
difference. The title's rule stops at the text, as it does on the page.
|
||||
*/
|
||||
&.is-centered {
|
||||
--content-bleed: 0px;
|
||||
padding-inline: max(1rem, calc((min(100%, 50vw - 57px) - $content-centered-max-width) / 2));
|
||||
}
|
||||
|
||||
> div {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -330,6 +330,7 @@
|
||||
-->
|
||||
<div
|
||||
class="editor-markdown-preview-content page-contents"
|
||||
:class="{ 'is-centered': siteStore.theme.contentWidth === `centered` }"
|
||||
:lang="pageStore.locale"
|
||||
:dir="siteStore.localeDir(pageStore.locale)"
|
||||
ref="editorPreviewContainerRef"
|
||||
@@ -2158,11 +2159,12 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
|
||||
height: $editor-height;
|
||||
overflow: hidden;
|
||||
|
||||
// -> The page's own background in each theme: this is the render, and a tint here is a tint the article does not have
|
||||
@at-root .body--light & {
|
||||
background-color: $grey-2;
|
||||
background-color: #fff;
|
||||
}
|
||||
@at-root .body--dark & {
|
||||
background-color: $dark-6;
|
||||
background-color: $dark;
|
||||
}
|
||||
// @include until($tablet) {
|
||||
// display: none;
|
||||
@@ -2222,6 +2224,19 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
|
||||
overflow-y: scroll;
|
||||
padding: 1rem;
|
||||
max-width: calc(50vw - 57px);
|
||||
|
||||
/*
|
||||
The site theme's Centered content width, so the preview wraps where the page will. The pane is
|
||||
the scrolling box and the render goes straight into it (see the template), so the measure is
|
||||
made out of padding rather than a box inside: whatever the element is wider than the measure
|
||||
by goes either side. `min()` is the element's own width -- the pane, up to the cap above --
|
||||
since a percentage here is the pane's. Only a wide window gets past the cap to make any
|
||||
difference. The title's rule stops at the text, as it does on the page.
|
||||
*/
|
||||
&.is-centered {
|
||||
--content-bleed: 0px;
|
||||
padding-inline: max(1rem, calc((min(100%, 50vw - 57px) - $content-centered-max-width) / 2));
|
||||
}
|
||||
// -ms-overflow-style: none;
|
||||
// &::-webkit-scrollbar {
|
||||
// width: 0px;
|
||||
|
||||
@@ -352,6 +352,7 @@
|
||||
<div
|
||||
ref="mountRef"
|
||||
class="editor-visual-surface"
|
||||
:class="{ 'is-centered': siteStore.theme.contentWidth === `centered` }"
|
||||
:lang="pageStore.locale"
|
||||
:dir="siteStore.localeDir(pageStore.locale)">
|
||||
<!--
|
||||
|
||||
@@ -554,6 +554,14 @@ $action-btn-height: 3rem;
|
||||
}
|
||||
|
||||
&.is-editor {
|
||||
/*
|
||||
The height of the bar along the top of every editor -- the Render Preview strip of the Markdown
|
||||
and AsciiDoc editors, the Visual editor's formatting bar -- each stated as 32px in its own
|
||||
stylesheet. Page Properties then starts level with the content below that bar rather than beside
|
||||
it, and the brickwork at the bar's right end steps into a plain stretch of orange.
|
||||
*/
|
||||
padding-top: 32px;
|
||||
|
||||
@at-root .body--light & {
|
||||
background-color: $deep-orange-9;
|
||||
}
|
||||
|
||||
@@ -340,6 +340,26 @@ $toc-overlay-max: 749.98px;
|
||||
--content-bleed: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The theme's Centered content width (`contentWidth`): the article held to a measure in the middle
|
||||
of the column rather than running to its edges, the way the Talk view holds its threads
|
||||
(`.page-talk`). `.page-measure` is the box inside the padding that wraps everything but the site
|
||||
banner, which keeps its full width.
|
||||
|
||||
The title's rule stops reaching back through the padding: the article is now a block of its own
|
||||
in the middle of the column, and a rule hanging 1rem out of its left side points at a sidebar
|
||||
that is nowhere near it. After the phone rule above, and more specific, so it wins there too.
|
||||
*/
|
||||
&.is-centered {
|
||||
.page-measure {
|
||||
max-width: $content-centered-max-width;
|
||||
margin-inline: auto;
|
||||
}
|
||||
.page-contents {
|
||||
--content-bleed: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -77,3 +77,8 @@ $shadow-2:
|
||||
$breakpoint-xs-max: 599.98px;
|
||||
$breakpoint-sm-max: 1023.98px;
|
||||
$breakpoint-md-max: 1439.98px;
|
||||
|
||||
// The measure a page's content is held to when the site theme's Content Width is Centered
|
||||
// (`contentWidth`): the page view, a version of it, the markdown and asciidoc previews and the
|
||||
// visual editor.
|
||||
$content-centered-max-width: 1000px;
|
||||
@@ -147,6 +147,60 @@
|
||||
viewport -- so it scrolls along with the link it belongs to and nothing has to listen for scroll.
|
||||
*/
|
||||
position: relative;
|
||||
|
||||
/*
|
||||
The site theme's Centered content width, drawn as a sheet of paper on a grey desk the way a word
|
||||
processor lays out a page: the text measure from `.visual-editor-content` below, with a margin of
|
||||
white either side of it, and the desk showing wherever the pane is wider than that.
|
||||
|
||||
The sheet is a picture behind the editable rather than the editable itself, and that is the
|
||||
point of drawing it this way. The editable keeps the full width of the pane and makes the measure
|
||||
out of its padding, so a click on the desk beside the sheet still lands in the text -- a sheet
|
||||
that WAS the editable would leave all of that grey dead to the mouse. `isolation` gives the
|
||||
`z-index: -1` somewhere to stop: in front of the desk, behind the text.
|
||||
|
||||
Both are measured against the same box -- the surface's width, which the editable is stretched
|
||||
to -- so the sheet and the text inside it stay centered on each other at every width. Narrower
|
||||
than the sheet, the sheet fills the pane and the text keeps at least the 1rem it always had.
|
||||
|
||||
The gap above and below is the one stretch of the pane where clicking does nothing. That is the
|
||||
desk, and on a page that is shorter than the pane the gap below sits at the bottom of the window,
|
||||
where nobody aims.
|
||||
*/
|
||||
&.is-centered {
|
||||
$sheet-margin: 3rem;
|
||||
$sheet-gap: 1.5rem;
|
||||
|
||||
isolation: isolate;
|
||||
padding-block: $sheet-gap;
|
||||
|
||||
@at-root .body--light & {
|
||||
background-color: $grey-3;
|
||||
}
|
||||
@at-root .body--dark & {
|
||||
background-color: $dark-6;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset-block: $sheet-gap;
|
||||
inset-inline: max(0px, calc((100% - $content-centered-max-width - 2 * $sheet-margin) / 2));
|
||||
|
||||
@at-root .body--light & {
|
||||
background-color: #fff;
|
||||
box-shadow:
|
||||
0 1px 3px rgb(0 0 0 / 0.12),
|
||||
0 1px 2px rgb(0 0 0 / 0.08);
|
||||
}
|
||||
// -> A shadow does not read against a desk this dark, so the edge is a line instead
|
||||
@at-root .body--dark & {
|
||||
background-color: $dark;
|
||||
box-shadow: 0 0 0 1px $dark-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +222,19 @@
|
||||
*/
|
||||
padding: 1rem 1rem 6rem;
|
||||
|
||||
/*
|
||||
The site theme's Centered content width, so the text wraps where the page will. Made out of the
|
||||
side padding rather than a max-width, for the same reason the bottom is: space outside the
|
||||
editable is space a click does not put the caret in, and here it would be most of a wide pane.
|
||||
The surface stretches the editable to its own width, so `100%` -- the surface's -- is the
|
||||
editable's, and whatever that is wider than the measure by goes either side. The title's rule
|
||||
stops at the text, as it does on the page.
|
||||
*/
|
||||
.editor-visual-surface.is-centered > & {
|
||||
--content-bleed: 0px;
|
||||
padding-inline: max(1rem, calc((100% - $content-centered-max-width) / 2));
|
||||
}
|
||||
|
||||
/*
|
||||
ProseMirror's own furniture, recoloured. The behaviour of each of these comes from the base
|
||||
stylesheets the component imports; what is set here is only what should follow the site's theme
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
:style="`background-color: ` + state.config[`color` + startCase(cl)] + `;`"
|
||||
text-color="white">
|
||||
<w-icon class="mr-2" name="la:fill" size="xs" />
|
||||
<span>Pick...</span>
|
||||
<span>{{ t('admin.theme.pickColor') }}</span>
|
||||
<w-menu
|
||||
><w-color-picker v-model="state.config[`color` + startCase(cl)]"
|
||||
/></w-menu>
|
||||
@@ -151,25 +151,23 @@
|
||||
<!-- ----------------------- -->
|
||||
<w-card class="pb-2 mt-4">
|
||||
<w-card-header>{{ t('admin.theme.layout') }}</w-card-header>
|
||||
<template v-if="flagStore.experimental">
|
||||
<w-item>
|
||||
<blueprint-icon icon="width" />
|
||||
<w-item-section>
|
||||
<w-item-label>{{ t(`admin.theme.contentWidth`) }}</w-item-label>
|
||||
<w-item-label caption>{{ t(`admin.theme.contentWidthHint`) }}</w-item-label>
|
||||
</w-item-section>
|
||||
<w-item-section class="flex-none">
|
||||
<w-btn-toggle
|
||||
v-model="state.config.contentWidth"
|
||||
push
|
||||
glossy
|
||||
no-caps
|
||||
toggle-color="primary"
|
||||
:options="widthOptions" />
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
<w-separator class="my-2" inset />
|
||||
</template>
|
||||
<w-item>
|
||||
<blueprint-icon icon="width" />
|
||||
<w-item-section>
|
||||
<w-item-label>{{ t(`admin.theme.contentWidth`) }}</w-item-label>
|
||||
<w-item-label caption>{{ t(`admin.theme.contentWidthHint`) }}</w-item-label>
|
||||
</w-item-section>
|
||||
<w-item-section class="flex-none">
|
||||
<w-btn-toggle
|
||||
v-model="state.config.contentWidth"
|
||||
push
|
||||
glossy
|
||||
no-caps
|
||||
toggle-color="primary"
|
||||
:options="widthOptions" />
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
<w-separator class="my-2" inset />
|
||||
<w-item>
|
||||
<blueprint-icon icon="right-navigation-toolbar" />
|
||||
<w-item-section>
|
||||
@@ -331,7 +329,7 @@
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { onMounted, reactive, watch } from 'vue'
|
||||
import { computed, onMounted, reactive, watch } from 'vue'
|
||||
|
||||
import { useDark } from '@/composables/dark'
|
||||
import { useMeta } from '@/composables/meta'
|
||||
@@ -400,26 +398,28 @@ const state = reactive({
|
||||
|
||||
const colorKeys = ['primary', 'secondary', 'accent', 'header', 'sidebar']
|
||||
|
||||
const widthOptions = [
|
||||
{ label: 'Full Width', value: 'full' },
|
||||
{ label: 'Centered', value: 'centered' }
|
||||
]
|
||||
// -> Computed so the labels follow the interface locale if it changes while the screen is open
|
||||
const widthOptions = computed(() => [
|
||||
{ label: t('admin.theme.contentWidthFull'), value: 'full' },
|
||||
{ label: t('admin.theme.contentWidthCentered'), value: 'centered' }
|
||||
])
|
||||
|
||||
const rightLeftOptions = [
|
||||
{ label: 'Hide', value: 'off' },
|
||||
{ label: 'Left', value: 'left' },
|
||||
{ label: 'Right', value: 'right' }
|
||||
]
|
||||
const rightLeftOptions = computed(() => [
|
||||
{ label: t('admin.theme.positionHide'), value: 'off' },
|
||||
{ label: t('admin.theme.positionLeft'), value: 'left' },
|
||||
{ label: t('admin.theme.positionRight'), value: 'right' }
|
||||
])
|
||||
|
||||
const fonts = [
|
||||
// -> Font names are proper names and stay as they are; only the last entry is a description
|
||||
const fonts = computed(() => [
|
||||
{ label: 'Inter', value: 'inter' },
|
||||
{ label: 'Open Sans', value: 'opensans' },
|
||||
{ label: 'Montserrat', value: 'montserrat' },
|
||||
{ label: 'Roboto', value: 'roboto' },
|
||||
{ label: 'Rubik', value: 'rubik' },
|
||||
{ label: 'Tajawal', value: 'tajawal' },
|
||||
{ label: 'User System Defaults', value: 'user' }
|
||||
]
|
||||
{ label: t('admin.theme.fontUserDefaults'), value: 'user' }
|
||||
])
|
||||
|
||||
const codeThemes = [
|
||||
{ label: 'A 11 Y Dark', value: 'a11y-dark' },
|
||||
@@ -715,7 +715,7 @@ async function load() {
|
||||
} catch (err) {
|
||||
notify({
|
||||
type: 'negative',
|
||||
message: 'Failed to fetch site theme config'
|
||||
message: t('admin.theme.loadFailed')
|
||||
})
|
||||
}
|
||||
loading.hide()
|
||||
@@ -754,7 +754,7 @@ async function save() {
|
||||
}).json()
|
||||
if (!resp?.ok) {
|
||||
throw new Error(
|
||||
t(`admin.theme.${resp?.error}`, resp?.message || 'An unexpected error occured.')
|
||||
t(`admin.theme.${resp?.error}`, resp?.message || t('common.error.unexpected'))
|
||||
)
|
||||
}
|
||||
if (adminStore.currentSiteId === siteStore.id) {
|
||||
@@ -770,7 +770,7 @@ async function save() {
|
||||
} catch (err) {
|
||||
notify({
|
||||
type: 'negative',
|
||||
message: 'Failed to save site theme config',
|
||||
message: t('admin.theme.saveFailed'),
|
||||
caption: err.message
|
||||
})
|
||||
}
|
||||
|
||||
+162
-152
@@ -172,7 +172,9 @@
|
||||
v-else>
|
||||
<!-- -> Half the padding on a phone, where 16px a side is 8% of the window spent on margin;
|
||||
the stylesheet has `--content-bleed` to match -->
|
||||
<div class="page-container-body p-2 sm:p-4">
|
||||
<div
|
||||
class="page-container-body p-2 sm:p-4"
|
||||
:class="{ 'is-centered': siteStore.theme.contentWidth === `centered` }">
|
||||
<!--
|
||||
Above the article rather than above the toolbars: what an administrator raises a banner
|
||||
about is the content, and this is where a reader is already looking.
|
||||
@@ -185,168 +187,176 @@
|
||||
-->
|
||||
<site-banner v-if="activeView === `article`" />
|
||||
<!--
|
||||
That this page is a post, and of what. A wiki page and a blog post look identical
|
||||
otherwise -- the breadcrumbs above name the folder rather than the blog, and nothing
|
||||
else on the page says a listing somewhere is drawing it.
|
||||
Everything below the banner, held to a measure when the theme asks for the content centered
|
||||
(`contentWidth`, see `_page-chrome.scss`). One box rather than a width on each piece: the
|
||||
blog byline is inline and cannot be centered as a box of its own. The banner stays outside,
|
||||
since it is the lid of the column and is drawn to its edges whatever the theme says.
|
||||
-->
|
||||
<div class="page-measure">
|
||||
<!--
|
||||
That this page is a post, and of what. A wiki page and a blog post look identical
|
||||
otherwise -- the breadcrumbs above name the folder rather than the blog, and nothing
|
||||
else on the page says a listing somewhere is drawing it.
|
||||
|
||||
Above the article rather than below it, because it is context for what follows; a link
|
||||
rather than a label, since the blog is where the rest of the posts are.
|
||||
-->
|
||||
<router-link
|
||||
class="page-blog-byline"
|
||||
v-if="pageStore.blog && activeView === `article`"
|
||||
:to="blogHref">
|
||||
<w-icon name="la:newspaper" size="sm" />
|
||||
<span class="pl-2">{{
|
||||
t('common.blog.postedIn', { blog: pageStore.blog.title })
|
||||
}}</span>
|
||||
</router-link>
|
||||
<!--
|
||||
`v-show` rather than `v-if` on the article below, so that leaving the discussion and
|
||||
coming back does not re-run the page's own scripts or lose where the reader was in it.
|
||||
-->
|
||||
<page-talk
|
||||
v-if="activeView === `talk`"
|
||||
ref="talkView"
|
||||
:content-root="pageContents"
|
||||
@locate-annotation="locateAnnotation" />
|
||||
<!--
|
||||
Picking passages of the article to post as a comment, started from the Talk view's column.
|
||||
Over the article and only while it is the view on screen: the panel and the card it draws
|
||||
are teleported to the body, so where this sits in the tree is only what owns it.
|
||||
-->
|
||||
<page-annotator
|
||||
v-if="isAnnotating"
|
||||
ref="annotator"
|
||||
:root="pageContents"
|
||||
:content-revision="state.contentRevision"
|
||||
@close="stopAnnotating"
|
||||
@posted="onAnnotationsPosted" />
|
||||
<!--
|
||||
Every annotation on the page at once, from the Talk view's column. Mounted for as long as
|
||||
it is switched on, whichever view is showing -- it is turned off from the Talk view, not by
|
||||
leaving the article -- and drawing only over the article.
|
||||
-->
|
||||
<page-annotations-layer
|
||||
v-if="state.showAnnotations"
|
||||
:root="pageContents"
|
||||
:active="activeView === `article`"
|
||||
:interactive="!isAnnotating"
|
||||
:reveal-id="state.revealAnnotationId"
|
||||
:content-revision="state.contentRevision"
|
||||
@revealed="state.revealAnnotationId = null"
|
||||
@view-comment="viewComment" />
|
||||
<!--
|
||||
The card over a glossary term in the article, and only where this reader sees glossary
|
||||
links at all. The links themselves are made by `applyGlossaryLinks` below.
|
||||
-->
|
||||
<page-glossary-card
|
||||
v-if="glossaryReadable"
|
||||
ref="glossaryCard"
|
||||
:root="pageContents"
|
||||
:locale="pageStore.locale"
|
||||
:resolve-term-id="resolveGlossaryTermId"
|
||||
@open="openGlossaryLink" />
|
||||
<page-links v-if="activeView === `links`" />
|
||||
<!--
|
||||
A blog's front page, which has no article to draw: its posts are what stands in place of
|
||||
one. Inside the scrolling column and not instead of it, unlike a redirection -- a blog is
|
||||
somewhere a reader stays, scrolls, and reaches the footer at the bottom of.
|
||||
Above the article rather than below it, because it is context for what follows; a link
|
||||
rather than a label, since the blog is where the rest of the posts are.
|
||||
-->
|
||||
<router-link
|
||||
class="page-blog-byline"
|
||||
v-if="pageStore.blog && activeView === `article`"
|
||||
:to="blogHref">
|
||||
<w-icon name="la:newspaper" size="sm" />
|
||||
<span class="pl-2">{{
|
||||
t('common.blog.postedIn', { blog: pageStore.blog.title })
|
||||
}}</span>
|
||||
</router-link>
|
||||
<!--
|
||||
`v-show` rather than `v-if` on the article below, so that leaving the discussion and
|
||||
coming back does not re-run the page's own scripts or lose where the reader was in it.
|
||||
-->
|
||||
<page-talk
|
||||
v-if="activeView === `talk`"
|
||||
ref="talkView"
|
||||
:content-root="pageContents"
|
||||
@locate-annotation="locateAnnotation" />
|
||||
<!--
|
||||
Picking passages of the article to post as a comment, started from the Talk view's column.
|
||||
Over the article and only while it is the view on screen: the panel and the card it draws
|
||||
are teleported to the body, so where this sits in the tree is only what owns it.
|
||||
-->
|
||||
<page-annotator
|
||||
v-if="isAnnotating"
|
||||
ref="annotator"
|
||||
:root="pageContents"
|
||||
:content-revision="state.contentRevision"
|
||||
@close="stopAnnotating"
|
||||
@posted="onAnnotationsPosted" />
|
||||
<!--
|
||||
Every annotation on the page at once, from the Talk view's column. Mounted for as long as
|
||||
it is switched on, whichever view is showing -- it is turned off from the Talk view, not by
|
||||
leaving the article -- and drawing only over the article.
|
||||
-->
|
||||
<page-annotations-layer
|
||||
v-if="state.showAnnotations"
|
||||
:root="pageContents"
|
||||
:active="activeView === `article`"
|
||||
:interactive="!isAnnotating"
|
||||
:reveal-id="state.revealAnnotationId"
|
||||
:content-revision="state.contentRevision"
|
||||
@revealed="state.revealAnnotationId = null"
|
||||
@view-comment="viewComment" />
|
||||
<!--
|
||||
The card over a glossary term in the article, and only where this reader sees glossary
|
||||
links at all. The links themselves are made by `applyGlossaryLinks` below.
|
||||
-->
|
||||
<page-glossary-card
|
||||
v-if="glossaryReadable"
|
||||
ref="glossaryCard"
|
||||
:root="pageContents"
|
||||
:locale="pageStore.locale"
|
||||
:resolve-term-id="resolveGlossaryTermId"
|
||||
@open="openGlossaryLink" />
|
||||
<page-links v-if="activeView === `links`" />
|
||||
<!--
|
||||
A blog's front page, which has no article to draw: its posts are what stands in place of
|
||||
one. Inside the scrolling column and not instead of it, unlike a redirection -- a blog is
|
||||
somewhere a reader stays, scrolls, and reaches the footer at the bottom of.
|
||||
|
||||
No `activeView` test, unlike the article below: a blog shows neither tab (see
|
||||
`showTalkTab`), so there is no other view for this column to be on.
|
||||
-->
|
||||
<page-blog v-if="isBlog" />
|
||||
<!--
|
||||
Delegated rather than bound per link: the anchors are written by `v-html`, so there is
|
||||
nothing here to put a handler on, and they are replaced wholesale on every render.
|
||||
-->
|
||||
<!-- -> Written in the page's own language and direction, which need not be the interface's -->
|
||||
<div
|
||||
class="page-contents"
|
||||
:class="{
|
||||
'is-asciidoc': pageStore.editor === `asciidoc`,
|
||||
'has-red-links': siteStore.colorizeBrokenLinks
|
||||
}"
|
||||
:lang="pageStore.locale"
|
||||
:dir="siteStore.localeDir(pageStore.locale)"
|
||||
ref="pageContents"
|
||||
v-show="activeView === `article` && !isBlog"
|
||||
v-html="pageStore.render"
|
||||
@click="onContentClick" />
|
||||
<!--
|
||||
A box of its own rather than a bare `<template>`, so that `css/_print.scss` has something
|
||||
to hide: these are links onward -- the next page, the previous one, a related topic --
|
||||
which is the one thing a sheet of paper cannot do anything with. The separator goes with
|
||||
them, since it exists only to divide them from the article above.
|
||||
-->
|
||||
<div
|
||||
class="page-relations"
|
||||
v-if="
|
||||
pageStore.relations && pageStore.relations.length > 0 && activeView === `article`
|
||||
">
|
||||
<w-separator class="my-6" />
|
||||
<div class="flex flex-wrap">
|
||||
<div class="min-w-0 flex-1 text-left" v-if="relationsLeft.length > 0">
|
||||
<w-btn
|
||||
class="mr-2 mb-2"
|
||||
padding="sm md"
|
||||
outline
|
||||
no-caps
|
||||
color="primary"
|
||||
v-for="rel of relationsLeft"
|
||||
:key="`rel-id-` + rel.id"
|
||||
v-bind="relationLink(rel)">
|
||||
<w-icon :name="rel.icon" />
|
||||
<div class="flex flex-col text-left pl-4">
|
||||
<div class="text-body2">
|
||||
<strong>{{ rel.label }}</strong>
|
||||
</div>
|
||||
<div class="text-caption">{{ rel.caption }}</div>
|
||||
</div>
|
||||
</w-btn>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-center" v-if="relationsCenter.length > 0">
|
||||
<div class="flex flex-col">
|
||||
No `activeView` test, unlike the article below: a blog shows neither tab (see
|
||||
`showTalkTab`), so there is no other view for this column to be on.
|
||||
-->
|
||||
<page-blog v-if="isBlog" />
|
||||
<!--
|
||||
Delegated rather than bound per link: the anchors are written by `v-html`, so there is
|
||||
nothing here to put a handler on, and they are replaced wholesale on every render.
|
||||
-->
|
||||
<!-- -> Written in the page's own language and direction, which need not be the interface's -->
|
||||
<div
|
||||
class="page-contents"
|
||||
:class="{
|
||||
'is-asciidoc': pageStore.editor === `asciidoc`,
|
||||
'has-red-links': siteStore.colorizeBrokenLinks
|
||||
}"
|
||||
:lang="pageStore.locale"
|
||||
:dir="siteStore.localeDir(pageStore.locale)"
|
||||
ref="pageContents"
|
||||
v-show="activeView === `article` && !isBlog"
|
||||
v-html="pageStore.render"
|
||||
@click="onContentClick" />
|
||||
<!--
|
||||
A box of its own rather than a bare `<template>`, so that `css/_print.scss` has something
|
||||
to hide: these are links onward -- the next page, the previous one, a related topic --
|
||||
which is the one thing a sheet of paper cannot do anything with. The separator goes with
|
||||
them, since it exists only to divide them from the article above.
|
||||
-->
|
||||
<div
|
||||
class="page-relations"
|
||||
v-if="
|
||||
pageStore.relations && pageStore.relations.length > 0 && activeView === `article`
|
||||
">
|
||||
<w-separator class="my-6" />
|
||||
<div class="flex flex-wrap">
|
||||
<div class="min-w-0 flex-1 text-left" v-if="relationsLeft.length > 0">
|
||||
<w-btn
|
||||
color="primary"
|
||||
flat
|
||||
class="mr-2 mb-2"
|
||||
padding="sm md"
|
||||
outline
|
||||
no-caps
|
||||
v-for="rel of relationsCenter"
|
||||
color="primary"
|
||||
v-for="rel of relationsLeft"
|
||||
:key="`rel-id-` + rel.id"
|
||||
v-bind="relationLink(rel)">
|
||||
<w-icon class="mr-2" :name="rel.icon" />
|
||||
<span>{{ rel.label }}</span>
|
||||
<w-icon :name="rel.icon" />
|
||||
<div class="flex flex-col text-left pl-4">
|
||||
<div class="text-body2">
|
||||
<strong>{{ rel.label }}</strong>
|
||||
</div>
|
||||
<div class="text-caption">{{ rel.caption }}</div>
|
||||
</div>
|
||||
</w-btn>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-center" v-if="relationsCenter.length > 0">
|
||||
<div class="flex flex-col">
|
||||
<w-btn
|
||||
color="primary"
|
||||
flat
|
||||
no-caps
|
||||
v-for="rel of relationsCenter"
|
||||
:key="`rel-id-` + rel.id"
|
||||
v-bind="relationLink(rel)">
|
||||
<w-icon class="mr-2" :name="rel.icon" />
|
||||
<span>{{ rel.label }}</span>
|
||||
</w-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-right" v-if="relationsRight.length > 0">
|
||||
<w-btn
|
||||
class="ml-2 mb-2"
|
||||
padding="sm md"
|
||||
outline
|
||||
no-caps
|
||||
color="primary"
|
||||
v-for="rel of relationsRight"
|
||||
:key="`rel-id-` + rel.id"
|
||||
v-bind="relationLink(rel)">
|
||||
<div class="flex flex-col text-left pr-4">
|
||||
<div class="text-body2">
|
||||
<strong>{{ rel.label }}</strong>
|
||||
</div>
|
||||
<div class="text-caption">{{ rel.caption }}</div>
|
||||
</div>
|
||||
<w-icon :name="rel.icon" />
|
||||
</w-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-right" v-if="relationsRight.length > 0">
|
||||
<w-btn
|
||||
class="ml-2 mb-2"
|
||||
padding="sm md"
|
||||
outline
|
||||
no-caps
|
||||
color="primary"
|
||||
v-for="rel of relationsRight"
|
||||
:key="`rel-id-` + rel.id"
|
||||
v-bind="relationLink(rel)">
|
||||
<div class="flex flex-col text-left pr-4">
|
||||
<div class="text-body2">
|
||||
<strong>{{ rel.label }}</strong>
|
||||
</div>
|
||||
<div class="text-caption">{{ rel.caption }}</div>
|
||||
</div>
|
||||
<w-icon :name="rel.icon" />
|
||||
</w-btn>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
Every provider that is not this wiki's own: at the bottom of the article, which is where
|
||||
a site that uses one of them puts its comments and where a reader who uses that provider
|
||||
elsewhere expects to find them.
|
||||
-->
|
||||
<page-comments-embed v-if="showCommentsEmbed" />
|
||||
</div>
|
||||
<!--
|
||||
Every provider that is not this wiki's own: at the bottom of the article, which is where
|
||||
a site that uses one of them puts its comments and where a reader who uses that provider
|
||||
elsewhere expects to find them.
|
||||
-->
|
||||
<page-comments-embed v-if="showCommentsEmbed" />
|
||||
</div>
|
||||
<!--
|
||||
Inside the scrolling column, and last: this is the bottom of the PAGE, so it is reached by
|
||||
|
||||
@@ -47,13 +47,22 @@ useMeta(() => ({
|
||||
*/
|
||||
color: var(--color-black);
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
|
||||
@at-root .body--dark & {
|
||||
background-color: $dark-6;
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
/*
|
||||
Frosted glass over the background image, which runs under the panel as well as beside it. The
|
||||
panel's own white (or near-black) is what keeps the text readable whatever the image is, so it
|
||||
stays mostly opaque; the blur is what stops the image's detail from fighting the form.
|
||||
*/
|
||||
&-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1 0 100%;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
@@ -62,10 +71,23 @@ useMeta(() => ({
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
background-color: #fff;
|
||||
|
||||
@at-root .body--dark & {
|
||||
background-color: rgba($dark-6, 0.8);
|
||||
backdrop-filter: blur(24px) saturate(180%);
|
||||
}
|
||||
|
||||
/* -> Full width on a phone, with nothing beside it to see through to: plain page colour */
|
||||
@media (max-width: $breakpoint-xs-max) {
|
||||
padding: 1rem 2rem;
|
||||
max-width: 100vw;
|
||||
background-color: #fff;
|
||||
backdrop-filter: none;
|
||||
|
||||
@at-root .body--dark & {
|
||||
background-color: $dark-6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,22 +192,20 @@ useMeta(() => ({
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/*
|
||||
Behind the whole screen rather than beside the panel, so that the panel has something to blur.
|
||||
The page's own background colour above is what shows if the image fails to load.
|
||||
*/
|
||||
&-bg {
|
||||
flex: 1;
|
||||
flex-basis: 0;
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -78,21 +78,26 @@
|
||||
<w-scroll-area class="page-container-scrl" v-else style="height: 100%">
|
||||
<!-- -> Half the padding on a phone, matching the page view; `--content-bleed` follows in
|
||||
`_page-chrome.scss` -->
|
||||
<div class="page-container-body p-2 sm:p-4">
|
||||
<!--
|
||||
Delegated rather than bound per link: the anchors are written by `v-html`, so there is
|
||||
nothing here to put a handler on.
|
||||
-->
|
||||
<!-- -> `is-asciidoc` from the version's own editor, not the page's: a version is what the
|
||||
page WAS, and a page converted between editors has history on both sides of it -->
|
||||
<div
|
||||
class="page-contents"
|
||||
:class="{ 'is-asciidoc': state.version?.meta?.editor === `asciidoc` }"
|
||||
:lang="state.version?.pageLocale"
|
||||
:dir="siteStore.localeDir(state.version?.pageLocale)"
|
||||
ref="pageContents"
|
||||
v-html="state.render"
|
||||
@click="onContentClick" />
|
||||
<!-- -> Held to the same measure as the page view when the theme centers the content -->
|
||||
<div
|
||||
class="page-container-body p-2 sm:p-4"
|
||||
:class="{ 'is-centered': siteStore.theme.contentWidth === `centered` }">
|
||||
<div class="page-measure">
|
||||
<!--
|
||||
Delegated rather than bound per link: the anchors are written by `v-html`, so there is
|
||||
nothing here to put a handler on.
|
||||
-->
|
||||
<!-- -> `is-asciidoc` from the version's own editor, not the page's: a version is what the
|
||||
page WAS, and a page converted between editors has history on both sides of it -->
|
||||
<div
|
||||
class="page-contents"
|
||||
:class="{ 'is-asciidoc': state.version?.meta?.editor === `asciidoc` }"
|
||||
:lang="state.version?.pageLocale"
|
||||
:dir="siteStore.localeDir(state.version?.pageLocale)"
|
||||
ref="pageContents"
|
||||
v-html="state.render"
|
||||
@click="onContentClick" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- -> Inside the scrolling column and last, so it is the bottom of the page rather than
|
||||
something sitting over the article -->
|
||||
|
||||
@@ -4,6 +4,8 @@ const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('@/layouts/AuthLayout.vue'),
|
||||
/* -> Always dark, over the login background; see `wantsDark` in App.vue */
|
||||
meta: { forceDark: true },
|
||||
children: [{ path: '', component: () => import('@/pages/Login.vue') }]
|
||||
},
|
||||
/*
|
||||
|
||||
@@ -210,6 +210,7 @@ export const useSiteStore = defineStore('site', {
|
||||
colorHeader: '#000',
|
||||
colorSidebar: '#1976D2',
|
||||
codeBlocksTheme: '',
|
||||
contentWidth: 'full',
|
||||
sidebarPosition: 'left',
|
||||
tocPosition: 'right',
|
||||
showPrintBtn: true
|
||||
|
||||
Reference in new issue
Block a user