mirror of
https://github.com/requarks/wiki.git
synced 2026-08-26 05:07:30 -05:00
fix: various UI fixes
This commit is contained in:
@@ -105,6 +105,9 @@ async function routes(app: FastifyInstance) {
|
||||
dbHost: {
|
||||
type: 'string'
|
||||
},
|
||||
dbVersion: {
|
||||
type: 'string'
|
||||
},
|
||||
groupsTotal: {
|
||||
type: 'number'
|
||||
},
|
||||
@@ -160,7 +163,7 @@ async function routes(app: FastifyInstance) {
|
||||
type: 'string'
|
||||
},
|
||||
tagsTotal: {
|
||||
type: 'string'
|
||||
type: 'number'
|
||||
},
|
||||
upgradeCapable: {
|
||||
type: 'boolean'
|
||||
|
||||
@@ -235,6 +235,18 @@ export class BlockIndexElement extends LitElement {
|
||||
font-weight: normal;
|
||||
pointer-events: none;
|
||||
}
|
||||
/*
|
||||
On a dark row the writing takes the app's lightened brand shade, the same one the interface
|
||||
gives a heading over a dark surface: primary is a mid-tone picked to read on white and is
|
||||
too dim against #161b22. --color-primary-light is a mix of --q-primary, so a re-themed
|
||||
site's own hue comes with it.
|
||||
|
||||
On .text rather than on the anchor, so it is the title alone that lightens: the icon and the
|
||||
arrow are drawn with currentColor precisely so they keep the link's normal primary.
|
||||
*/
|
||||
:host([dark]) .text {
|
||||
color: var(--color-primary-light);
|
||||
}
|
||||
|
||||
/*
|
||||
The page's own icon. Sized in em so it keeps its place beside writing at whatever size the
|
||||
|
||||
@@ -114,7 +114,7 @@ Content of the second tab.
|
||||
}
|
||||
.tab:hover:not(.is-active) {
|
||||
background-color: rgb(255 255 255 / 0.5);
|
||||
color: var(--tabs-active-fg);
|
||||
color: var(--tabs-active-label);
|
||||
}
|
||||
:host([dark]) .tab:hover:not(.is-active) {
|
||||
background-color: rgb(255 255 255 / 0.05);
|
||||
@@ -130,7 +130,7 @@ Content of the second tab.
|
||||
border-bottom-color: var(--tabs-panel-bg);
|
||||
background-color: var(--tabs-panel-bg);
|
||||
background-image: none;
|
||||
color: var(--tabs-active-fg);
|
||||
color: var(--tabs-active-label);
|
||||
}
|
||||
|
||||
.tab svg {
|
||||
@@ -170,12 +170,21 @@ Content of the second tab.
|
||||
--tabs-strip-bg: linear-gradient(to bottom, #fdfdfd, #eeeeee);
|
||||
--tabs-inactive-fg: #424242;
|
||||
--tabs-active-fg: var(--q-primary, #1976d2);
|
||||
/*
|
||||
The label of the tab being pointed at or opened. The active foreground, except on dark,
|
||||
where writing in a mid-tone picked to read on white is too dim -- the strip and the panel
|
||||
both sit near #1b212a. The edge that marks the open tab and the focus ring keep the plain
|
||||
brand colour: they are shapes rather than writing, and read at that weight.
|
||||
*/
|
||||
--tabs-active-label: var(--tabs-active-fg);
|
||||
--tabs-panel-bg: #fff;
|
||||
}
|
||||
:host([dark]) {
|
||||
--tabs-border: rgba(255, 255, 255, 0.15);
|
||||
--tabs-strip-bg: linear-gradient(to bottom, #1b212a, #12161d);
|
||||
--tabs-inactive-fg: rgba(255, 255, 255, 0.7);
|
||||
/* -> A mix of --q-primary, so a re-themed site's own hue comes with it. See block-index. */
|
||||
--tabs-active-label: var(--color-primary-light);
|
||||
--tabs-panel-bg: #1e232a;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -440,21 +440,27 @@ const HEADER_ICONS = [
|
||||
]
|
||||
|
||||
/*
|
||||
How the preview follows the caret: the line being edited goes to the TOP of the pane.
|
||||
How the preview follows the caret: the line being edited is put a SHORT WAY down the pane.
|
||||
|
||||
`start` rather than `nearest`, which was tried and is wrong here -- `nearest` leaves a line alone as
|
||||
long as it is visible anywhere, so a line sitting on the last row of the pane stays there, with what is
|
||||
being written pinned to the bottom edge and nothing after it in view.
|
||||
Not at its top, which is what this replaces. The top is exact, and that is the problem with it -- it
|
||||
scrolls away everything written just above the caret, which is the paragraph the next one is being
|
||||
written against, so the author is left editing at the edge of a pane with no context on one side of
|
||||
it. Expressed as a fraction of the pane rather than a fixed inset so that a preview half the height
|
||||
gives up half as much of itself to the lines already written.
|
||||
|
||||
Asking for the top on every caret move costs nothing when the caret stays put: the element is already
|
||||
there, so the browser computes the same offset and there is no movement. What used to make this thrash
|
||||
was the pane losing its scroll position to the re-render -- see `processContent` -- and animating up
|
||||
from the top of the document each time, not the alignment asked for here.
|
||||
`nearest` is not the answer either, at any offset: it leaves a line alone as long as it is visible
|
||||
anywhere, so a line sitting on the last row of the pane stays there, with what is being written pinned
|
||||
to the bottom edge and nothing after it in view. The position is computed and applied on every caret
|
||||
move, and costs nothing when the caret stays put -- the arithmetic lands on the offset the pane is
|
||||
already scrolled to and nothing moves. What used to make this thrash was the pane losing its scroll
|
||||
position to the re-render -- see `processContent` -- and animating up from the top of the document each
|
||||
time, not the alignment asked for here.
|
||||
|
||||
`inline: 'nearest'` only so that a wide block -- a table, a diagram -- is never scrolled sideways as a
|
||||
side effect of following the caret down the page.
|
||||
Scrolling the container rather than asking an element to bring itself into view also means a wide block
|
||||
-- a table, a diagram -- is never scrolled sideways as a side effect of following the caret down the
|
||||
page.
|
||||
*/
|
||||
const SYNC_SCROLL = { behavior: 'smooth', block: 'start', inline: 'nearest' }
|
||||
const PREVIEW_CONTEXT_ABOVE = 0.2
|
||||
|
||||
/**
|
||||
* Whether the window is wide enough to open the preview beside the source.
|
||||
@@ -1162,6 +1168,24 @@ function previewAnchorFor(container, line) {
|
||||
return best ?? panel
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the preview so that `el` sits `PREVIEW_CONTEXT_ABOVE` of the way down the pane.
|
||||
*
|
||||
* Measured through `getBoundingClientRect` rather than `offsetTop`, because what is wanted is the
|
||||
* distance to the SCROLL container and `offsetTop` answers to the nearest positioned ancestor -- which
|
||||
* inside rendered page content is whatever a block happened to put around it.
|
||||
*
|
||||
* A negative result is clamped by `scrollTo`, so an anchor near the top of the document puts the
|
||||
* document's own top in view rather than leaving a gap above it.
|
||||
*/
|
||||
function scrollPreviewTo(container, el) {
|
||||
const offset = el.getBoundingClientRect().top - container.getBoundingClientRect().top
|
||||
container.scrollTo({
|
||||
top: container.scrollTop + offset - container.clientHeight * PREVIEW_CONTEXT_ABOVE,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
function processContent(newContent) {
|
||||
/*
|
||||
A render that throws must not become a render that is empty.
|
||||
@@ -1569,7 +1593,10 @@ onMounted(async () => {
|
||||
container.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
return
|
||||
}
|
||||
previewAnchorFor(container, currentLine)?.scrollIntoView(SYNC_SCROLL)
|
||||
const anchor = previewAnchorFor(container, currentLine)
|
||||
if (anchor) {
|
||||
scrollPreviewTo(container, anchor)
|
||||
}
|
||||
}, 500)
|
||||
)
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
<w-card-section class="alt-card pb-6" id="refCardSocial">
|
||||
<div class="w-section-header">{{ t('editor.props.social') }}</div>
|
||||
<w-form class="gap-4 pt-2">
|
||||
<div>
|
||||
<div v-if="flagsStore.experimental">
|
||||
<w-toggle
|
||||
v-model="pageStore.allowComments"
|
||||
dense
|
||||
@@ -270,7 +270,7 @@
|
||||
checked-icon="la:check"
|
||||
unchecked-icon="la:times" />
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="flagsStore.experimental">
|
||||
<w-toggle
|
||||
v-model="pageStore.allowRatings"
|
||||
dense
|
||||
@@ -352,6 +352,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useFlagsStore } from '@/stores/flags'
|
||||
import { usePageStore } from '@/stores/page'
|
||||
import { useSiteStore } from '@/stores/site'
|
||||
|
||||
@@ -364,6 +365,7 @@ import PageTags from './PageTags.vue'
|
||||
// STORES
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const flagsStore = useFlagsStore()
|
||||
const pageStore = usePageStore()
|
||||
const siteStore = useSiteStore()
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<nav class="page-toc" aria-label="Table of contents">
|
||||
<ul class="page-toc-list">
|
||||
<ul
|
||||
class="page-toc-list"
|
||||
:class="{ 'page-toc-list--animated': markerAnimated }"
|
||||
ref="listEl"
|
||||
:style="{
|
||||
'--page-toc-marker-y': `${marker.y}px`,
|
||||
'--page-toc-marker-h': `${marker.h}px`,
|
||||
'--page-toc-marker-opacity': marker.visible ? 1 : 0
|
||||
}">
|
||||
<li
|
||||
v-for="item of visibleItems"
|
||||
:key="item.key"
|
||||
@@ -24,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
|
||||
import { isVisible, scrollToAnchor } from '@/helpers/anchors'
|
||||
import { flattenToc } from '@/helpers/toc'
|
||||
@@ -90,6 +98,30 @@ const TAB_TAG = 'BLOCK-TAB'
|
||||
let spyFrame = null
|
||||
let spySuspendedUntil = 0
|
||||
|
||||
// DATA
|
||||
|
||||
const listEl = ref(null)
|
||||
|
||||
/*
|
||||
Where the marker sits on the rail, in pixels down from the top of the list, and how tall it is.
|
||||
Measured rather than expressed in CSS because the marker is one element for the whole list: that is
|
||||
what lets it travel from one heading to the next instead of being switched off one row and on
|
||||
another, and a pseudo-element on the active row can only ever do the latter.
|
||||
*/
|
||||
const marker = reactive({ y: 0, h: 0, visible: false })
|
||||
|
||||
/*
|
||||
Whether the marker may animate yet. Off for its first placement, so that a reader arriving partway
|
||||
down a page is not shown it sliding from the top of the list -- there is no move to follow there,
|
||||
only the list settling.
|
||||
*/
|
||||
const markerAnimated = ref(false)
|
||||
|
||||
/* The inset the marker keeps at each end of a row, so it marks the label rather than the gap too. */
|
||||
const MARKER_INSET = 2
|
||||
|
||||
let markerObserver = null
|
||||
|
||||
// COMPUTED
|
||||
|
||||
/**
|
||||
@@ -201,6 +233,35 @@ function syncSpy() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put the marker over the active row.
|
||||
*
|
||||
* Offsets are read off the row rather than computed from the list, since every depth has its own
|
||||
* type size and therefore its own height. `offsetTop` is relative to the list, which is the nearest
|
||||
* positioned ancestor and so the marker's own containing block.
|
||||
*/
|
||||
function syncMarker() {
|
||||
const list = listEl.value
|
||||
const active = list?.querySelector('.page-toc-item--active')
|
||||
if (!active) {
|
||||
// -> Nothing marked: fade out where it stands, rather than travelling to the top of the list
|
||||
marker.visible = false
|
||||
return
|
||||
}
|
||||
|
||||
marker.y = active.offsetTop + MARKER_INSET
|
||||
marker.h = Math.max(active.offsetHeight - MARKER_INSET * 2, 0)
|
||||
marker.visible = true
|
||||
|
||||
if (!markerAnimated.value) {
|
||||
// -> A frame after the first placement, so the browser paints it there before the transition
|
||||
// exists to animate away from it
|
||||
requestAnimationFrame(() => {
|
||||
markerAnimated.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** Scroll fires far more often than the marker can move; one read per frame is enough. */
|
||||
function queueSpy() {
|
||||
if (spyFrame !== null) {
|
||||
@@ -217,6 +278,12 @@ function queueSpy() {
|
||||
// -> A new render means new heading positions, and possibly a different set of them
|
||||
watch(() => props.nodes, queueSpy)
|
||||
|
||||
// -> After the class the marker is measured from has been applied
|
||||
watch([() => props.selected, visibleItems], async () => {
|
||||
await nextTick()
|
||||
syncMarker()
|
||||
})
|
||||
|
||||
// MOUNTED
|
||||
|
||||
onMounted(() => {
|
||||
@@ -228,6 +295,15 @@ onMounted(() => {
|
||||
window.addEventListener('scroll', queueSpy, { capture: true, passive: true })
|
||||
window.addEventListener('resize', queueSpy, { passive: true })
|
||||
queueSpy()
|
||||
|
||||
/*
|
||||
The rows move for reasons no event reports: the sidebar narrows at 1400px and long labels wrap
|
||||
onto another line, which changes the height of the row the marker is on and the offset of every
|
||||
row below it. Watching the list itself catches all of that, resizes included.
|
||||
*/
|
||||
markerObserver = new ResizeObserver(syncMarker)
|
||||
markerObserver.observe(listEl.value)
|
||||
syncMarker()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -236,6 +312,7 @@ onBeforeUnmount(() => {
|
||||
if (spyFrame !== null) {
|
||||
cancelAnimationFrame(spyFrame)
|
||||
}
|
||||
markerObserver?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -293,18 +370,33 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
/*
|
||||
The active marker, drawn ON the rail rather than beside it: `left: 0` is the item's own border
|
||||
box, which starts at the rail whatever the indentation, so every depth marks the same line.
|
||||
The active marker, drawn ON the rail rather than beside it: `left: 0` is the list's own border
|
||||
box, which is where the rail is, so every depth marks the same line whatever its indentation.
|
||||
|
||||
One element for the whole list rather than a pseudo on the active row, so that moving to the next
|
||||
heading is a slide down the rail instead of the marker being switched off one row and on another.
|
||||
Where it goes cannot be said in CSS -- each depth has its own type size and so its own height --
|
||||
so `syncMarker` measures the row and hands the two numbers over as custom properties.
|
||||
*/
|
||||
&-item--active::before {
|
||||
&-list::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
height: var(--page-toc-marker-h, 0);
|
||||
border-radius: 1px;
|
||||
background-color: var(--color-primary);
|
||||
opacity: var(--page-toc-marker-opacity, 0);
|
||||
transform: translateY(var(--page-toc-marker-y, 0));
|
||||
}
|
||||
|
||||
/* -> The class arrives a frame after the first placement, so the marker does not travel to it */
|
||||
&-list--animated::after {
|
||||
transition:
|
||||
transform 0.25s var(--ease-standard),
|
||||
height 0.25s var(--ease-standard),
|
||||
opacity 0.15s var(--ease-standard);
|
||||
}
|
||||
|
||||
&-link {
|
||||
@@ -367,7 +459,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
&-link {
|
||||
&-link,
|
||||
&-list--animated::after {
|
||||
transition-duration: 0.01ms;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,6 +733,20 @@
|
||||
color: var(--color-primary-light);
|
||||
}
|
||||
|
||||
/*
|
||||
The heading at the top of every admin page. Only the colour lives here -- the size stays a
|
||||
`text-h5` utility on the element -- because the one thing every copy of that header needs and
|
||||
cannot express inline is the dark-mode shade: `primary` is picked to read on white and at
|
||||
#1976d2 over `dark-5` it is too dim for a heading, the same problem `.w-section-header` has.
|
||||
*/
|
||||
.admin-page-title {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
body.body--dark .admin-page-title {
|
||||
color: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.w-unstyled {
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-rest-api-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.api.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.api.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.api.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-inspection-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.approval.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.approval.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.approval.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-security-lock.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.auth.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.auth.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.auth.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-plugin.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.blocks.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.blocks.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.blocks.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-apps-tab-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.dashboard.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.dashboard.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.dashboard.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-cashbook.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.editors.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.editors.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.editors.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-module.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">
|
||||
{{ t('admin.extensions.title') }}
|
||||
</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-windsock-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.flags.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.flags.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.flags.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-web.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.general.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.general.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.general.subtitle') }}
|
||||
</div>
|
||||
@@ -185,19 +185,21 @@
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
<w-separator class="my-2" inset />
|
||||
<w-item tag="label">
|
||||
<blueprint-icon icon="discussion-forum" />
|
||||
<w-item-section>
|
||||
<w-item-label>{{ t(`admin.general.allowComments`) }}</w-item-label>
|
||||
<w-item-label caption>{{ t(`admin.general.allowCommentsHint`) }}</w-item-label>
|
||||
</w-item-section>
|
||||
<w-item-section avatar>
|
||||
<w-toggle
|
||||
v-model="state.config.features.comments"
|
||||
:aria-label="t(`admin.general.allowComments`)" />
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
<w-separator class="my-2" inset />
|
||||
<template v-if="flagsStore.experimental">
|
||||
<w-item tag="label">
|
||||
<blueprint-icon icon="discussion-forum" />
|
||||
<w-item-section>
|
||||
<w-item-label>{{ t(`admin.general.allowComments`) }}</w-item-label>
|
||||
<w-item-label caption>{{ t(`admin.general.allowCommentsHint`) }}</w-item-label>
|
||||
</w-item-section>
|
||||
<w-item-section avatar>
|
||||
<w-toggle
|
||||
v-model="state.config.features.comments"
|
||||
:aria-label="t(`admin.general.allowComments`)" />
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
<w-separator class="my-2" inset />
|
||||
</template>
|
||||
<w-item tag="label">
|
||||
<blueprint-icon icon="administrator-male" />
|
||||
<w-item-section>
|
||||
@@ -211,23 +213,25 @@
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
<w-separator class="my-2" inset />
|
||||
<w-item>
|
||||
<blueprint-icon icon="star-half-empty" />
|
||||
<w-item-section>
|
||||
<w-item-label>{{ t(`admin.general.allowRatings`) }}</w-item-label>
|
||||
<w-item-label caption>{{ t(`admin.general.allowRatingsHint`) }}</w-item-label>
|
||||
</w-item-section>
|
||||
<w-item-section class="flex-none">
|
||||
<w-btn-toggle
|
||||
v-model="state.config.features.ratingsMode"
|
||||
push
|
||||
glossy
|
||||
no-caps
|
||||
toggle-color="primary"
|
||||
:options="ratingsModes" />
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
<w-separator class="my-2" inset />
|
||||
<template v-if="flagsStore.experimental">
|
||||
<w-item>
|
||||
<blueprint-icon icon="star-half-empty" />
|
||||
<w-item-section>
|
||||
<w-item-label>{{ t(`admin.general.allowRatings`) }}</w-item-label>
|
||||
<w-item-label caption>{{ t(`admin.general.allowRatingsHint`) }}</w-item-label>
|
||||
</w-item-section>
|
||||
<w-item-section class="flex-none">
|
||||
<w-btn-toggle
|
||||
v-model="state.config.features.ratingsMode"
|
||||
push
|
||||
glossy
|
||||
no-caps
|
||||
toggle-color="primary"
|
||||
:options="ratingsModes" />
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
<w-separator class="my-2" inset />
|
||||
</template>
|
||||
<w-item tag="label">
|
||||
<blueprint-icon icon="search" />
|
||||
<w-item-section>
|
||||
@@ -290,6 +294,24 @@
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
</w-card>
|
||||
<!-- ----------------------- -->
|
||||
<!-- Discovery -->
|
||||
<!-- ----------------------- -->
|
||||
<w-card class="pb-2 mt-4">
|
||||
<w-card-header>{{ t('admin.general.discovery') }}</w-card-header>
|
||||
<w-item tag="label">
|
||||
<blueprint-icon icon="cellular-network" />
|
||||
<w-item-section>
|
||||
<w-item-label>{{ t(`admin.general.discoverable`) }}</w-item-label>
|
||||
<w-item-label caption>{{ t(`admin.general.discoverableHint`) }}</w-item-label>
|
||||
</w-item-section>
|
||||
<w-item-section avatar>
|
||||
<w-toggle
|
||||
v-model="state.config.discoverable"
|
||||
:aria-label="t(`admin.general.discoverable`)" />
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
</w-card>
|
||||
</div>
|
||||
<div class="col-span-12 lg:col-span-5">
|
||||
<!-- ----------------------- -->
|
||||
@@ -464,24 +486,6 @@
|
||||
</w-item>
|
||||
</w-card>
|
||||
<!-- ----------------------- -->
|
||||
<!-- Discovery -->
|
||||
<!-- ----------------------- -->
|
||||
<w-card class="pb-2 mt-4">
|
||||
<w-card-header>{{ t('admin.general.discovery') }}</w-card-header>
|
||||
<w-item tag="label">
|
||||
<blueprint-icon icon="cellular-network" />
|
||||
<w-item-section>
|
||||
<w-item-label>{{ t(`admin.general.discoverable`) }}</w-item-label>
|
||||
<w-item-label caption>{{ t(`admin.general.discoverableHint`) }}</w-item-label>
|
||||
</w-item-section>
|
||||
<w-item-section avatar>
|
||||
<w-toggle
|
||||
v-model="state.config.discoverable"
|
||||
:aria-label="t(`admin.general.discoverable`)" />
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
</w-card>
|
||||
<!-- ----------------------- -->
|
||||
<!-- Uploads -->
|
||||
<!-- ----------------------- -->
|
||||
<w-card class="pb-2 mt-4" v-if="state.config.uploads">
|
||||
@@ -585,6 +589,7 @@ import { notify } from '@/composables/notify'
|
||||
import { loading } from '@/composables/loading'
|
||||
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import { useFlagsStore } from '@/stores/flags'
|
||||
import { useSiteStore } from '@/stores/site'
|
||||
|
||||
import UtilCodeEditor from '@/components/UtilCodeEditor.vue'
|
||||
@@ -601,6 +606,7 @@ import { toMerged } from 'es-toolkit/object'
|
||||
// STORES
|
||||
|
||||
const adminStore = useAdminStore()
|
||||
const flagsStore = useFlagsStore()
|
||||
const siteStore = useSiteStore()
|
||||
|
||||
// I18N
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-people.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.groups.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.groups.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.groups.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-spring.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.icons.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.icons.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.icons.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-network-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.instances.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.instances.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.instances.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-language.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.locale.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.locale.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.locale.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-bunch-of-keys-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.login.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.login.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.login.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-message-settings-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.mail.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.mail.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.mail.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-graph.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.metrics.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.metrics.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.metrics.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-tree-structure.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">
|
||||
{{ t('admin.navigation.title') }}
|
||||
</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-rich-text-converter.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">
|
||||
{{ $t('admin.rendering.title') }}
|
||||
</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-bot-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.scheduler.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.scheduler.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.scheduler.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-find-and-replace-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.search.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.search.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.search.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-protect.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.security.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.security.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.security.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-change-theme.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.sites.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.sites.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.sites.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-ssd-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.storage.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.storage.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.storage.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-processor.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.system.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.system.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.system.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-linux-terminal-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.terminal.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.terminal.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.terminal.subtitle') }}
|
||||
</div>
|
||||
@@ -15,7 +15,8 @@
|
||||
<div class="flex-none flex items-center">
|
||||
<div v-if="state.connected" class="mr-4 text-right leading-tight">
|
||||
<div class="text-xs text-grey">{{ t('admin.terminal.instance') }}</div>
|
||||
<div class="flex items-center justify-end gap-1.5 font-mono text-sm">
|
||||
<div
|
||||
class="flex items-center justify-end gap-1.5 font-mono text-sm text-grey-9 dark:text-grey-3">
|
||||
<status-light class="admin-terminal-dot" color="positive" pulse />
|
||||
{{ state.instance }}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-paint-roller-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.theme.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.theme.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.theme.subtitle') }}
|
||||
</div>
|
||||
@@ -217,7 +217,7 @@
|
||||
<!-- ----------------------- -->
|
||||
<!-- Fonts -->
|
||||
<!-- ----------------------- -->
|
||||
<w-card class="pb-2">
|
||||
<w-card class="pb-2" v-if="flagStore.experimental">
|
||||
<w-card-header>
|
||||
{{ t('admin.theme.fonts') }}
|
||||
<template #action>
|
||||
@@ -269,7 +269,7 @@
|
||||
<!-- ----------------------- -->
|
||||
<!-- Code Injection -->
|
||||
<!-- ----------------------- -->
|
||||
<w-card class="pb-2 mt-4">
|
||||
<w-card :class="['pb-2', { 'mt-4': flagStore.experimental }]">
|
||||
<w-card-header>{{ t('admin.theme.codeInjection') }}</w-card-header>
|
||||
<w-item>
|
||||
<blueprint-icon icon="css" />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-account.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.users.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.users.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.users.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-swiss-army-knife-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.utilities.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.utilities.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.utilities.subtitle') }}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
src="/_assets/icons/fluent-lightning-bolt-animated.svg" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pl-4">
|
||||
<div class="text-h5 text-primary animated fadeInLeft">{{ t('admin.webhooks.title') }}</div>
|
||||
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.webhooks.title') }}</div>
|
||||
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
||||
{{ t('admin.webhooks.subtitle') }}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user