feat: add centered page contents layout option

This commit is contained in:
NGPixel committed 2026-10-12 02:26:41 -04:00
1 parent 2831596193
commit c69078c54d
15 files changed
+412 -228

No files matched your search

+9
View File
@@ -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
View File
@@ -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()
+17 -2
View File
@@ -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;
}
+17 -2
View File
@@ -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;
+1
View File
@@ -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;
}
+20
View File
@@ -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;
}
}
}
/*
+5
View File
@@ -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;
+67
View File
@@ -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
+36 -36
View File
@@ -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
View File
@@ -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
+29 -9
View File
@@ -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;
}
+20 -15
View File
@@ -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 -->
+2
View File
@@ -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') }]
},
/*
+1
View File
@@ -210,6 +210,7 @@ export const useSiteStore = defineStore('site', {
colorHeader: '#000',
colorSidebar: '#1976D2',
codeBlocksTheme: '',
contentWidth: 'full',
sidebarPosition: 'left',
tocPosition: 'right',
showPrintBtn: true