mirror of
https://github.com/requarks/wiki.git
synced 2026-10-12 05:54:49 -05:00
feat: improve glossary overlay layout
This commit is contained in:
5 files changed
+251
-87
No files matched your search
@@ -28,7 +28,7 @@ The constraints this is written against:
|
||||
**Goals**
|
||||
|
||||
- A **Glossary overlay**, opened from the header's **Library** menu: terms listed in a sidebar, the
|
||||
selected term in the main panel, and a **New Term** button at the top of the sidebar that opens the
|
||||
selected term in the main panel, and a **New Term** button in the overlay's header that opens the
|
||||
form in the main panel.
|
||||
- The fields in [§3.1](#31-glossaryterms): term, expansion, definition, aliases, related terms,
|
||||
documentation page and its label, external references, case sensitivity, category, and auto-link.
|
||||
@@ -330,11 +330,10 @@ Features**. Off, the Library menu has no Glossary row and the API behaves as in
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ Glossary [ en ▾ ] [×] │
|
||||
│ Glossary [ en ▾ ][ Filter… ] (?) [+ New Term | × ] │
|
||||
├──────────────────────┬───────────────────────────────────────────────┤
|
||||
│ [ + New Term ] │ │
|
||||
│ [ Filter… ] │ TERM │
|
||||
│ [Category ▾] │ Expansion │
|
||||
│ [ A–Z | Category ] │ TERM │
|
||||
│ │ Expansion │
|
||||
│ │ │
|
||||
│ A │ Definition… │
|
||||
│ API │ │
|
||||
@@ -347,9 +346,11 @@ Features**. Off, the Library menu has no Glossary row and the API behaves as in
|
||||
└──────────────────────┴───────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Header**: title, the locale picker (only where the site has more than one active locale), close.
|
||||
- **Sidebar**: New Term (for `manage:glossary`), a filter field matching names and aliases, an
|
||||
**A–Z / Category** switch, then the terms, each with its expansion as a second line.
|
||||
- **Header**, laid out as the File Manager's: the title; then the locale picker (only where the site
|
||||
has more than one active locale) and a filter matching names and aliases; then help, and New Term
|
||||
(for `manage:glossary`) joined to Close in one button group, as the group editor joins its actions.
|
||||
Below 900px it wraps onto two lines, the locale and the filter on the second.
|
||||
- **Sidebar**: an **A–Z / Category** switch, then the terms, each with its expansion as a second line.
|
||||
- **A–Z**, the default: under letter headings. Letters are the term's first character, upper-cased
|
||||
in the locale; everything that is not a letter goes under `#`.
|
||||
- **Category**: under one heading per category, sorted by name, A–Z within each, with the terms that
|
||||
|
||||
@@ -1,61 +1,98 @@
|
||||
<template>
|
||||
<w-layout ref="rootEl" class="glossary" view="hHh lpR fFf" container>
|
||||
<w-header class="card-header px-4 py-2">
|
||||
<!-- -> Below 900px the list and the term take turns; this is the way back to the list -->
|
||||
<w-btn
|
||||
v-if="!isWide && !state.listOpen"
|
||||
class="mr-2"
|
||||
icon="la:arrow-left"
|
||||
color="white"
|
||||
dense
|
||||
flat
|
||||
:aria-label="t(`glossary.backToList`)"
|
||||
@click="showList" />
|
||||
<w-icon name="img:/_assets/icons/ultraviolet-parchment.svg" left size="md" />
|
||||
<span>{{ t('glossary.title') }}</span>
|
||||
<w-space />
|
||||
<transition name="syncing">
|
||||
<w-spinner class="mr-4" v-show="state.loading > 0" color="accent" size="20px" />
|
||||
</transition>
|
||||
<!-- -> Only where there is a choice to make, as in the link picker -->
|
||||
<w-btn
|
||||
v-if="siteStore.locales.active.length > 1"
|
||||
class="acrylic-btn mr-4"
|
||||
flat
|
||||
dense
|
||||
padding="xs md"
|
||||
color="white"
|
||||
:label="siteStore.localeAlias(state.locale)"
|
||||
:aria-label="siteStore.localeAlias(state.locale)">
|
||||
<locale-selector-menu
|
||||
:selected="state.locale"
|
||||
:navigate="false"
|
||||
anchor="bottom right"
|
||||
self="top right"
|
||||
@select="switchLocale" />
|
||||
</w-btn>
|
||||
<w-btn
|
||||
class="mr-2"
|
||||
flat
|
||||
rounded
|
||||
color="white"
|
||||
:aria-label="t(`common.actions.viewDocs`)"
|
||||
icon="la:question-circle"
|
||||
:href="siteStore.docsBase + `/guide/glossary`"
|
||||
target="_blank">
|
||||
<w-tooltip>{{ t(`common.actions.viewDocs`) }}</w-tooltip>
|
||||
</w-btn>
|
||||
<!-- -> The pushed Close the File Manager and the editing overlays put themselves away with -->
|
||||
<w-btn-group>
|
||||
<!--
|
||||
The File Manager's header, toolbar for toolbar (`FileManager.vue`): the title, then the locale and
|
||||
the filter in the middle, then the help and Close at the end. Below 900px the three wrap onto two
|
||||
lines, the filter's on the second; see the stylesheet.
|
||||
-->
|
||||
<w-header class="card-header">
|
||||
<w-toolbar class="glossary-hdr-title" dark>
|
||||
<!-- -> Below 900px the list and the term take turns; this is the way back to the list -->
|
||||
<w-btn
|
||||
push
|
||||
v-if="!isWide && !state.listOpen"
|
||||
class="mr-2"
|
||||
icon="la:arrow-left"
|
||||
color="white"
|
||||
text-color="grey-7"
|
||||
:label="t(`common.actions.close`)"
|
||||
:aria-label="t(`common.actions.close`)"
|
||||
icon="la:times"
|
||||
@click="close" />
|
||||
</w-btn-group>
|
||||
dense
|
||||
flat
|
||||
:aria-label="t(`glossary.backToList`)"
|
||||
@click="showList" />
|
||||
<w-icon name="img:/_assets/icons/ultraviolet-parchment.svg" left size="md" />
|
||||
<span>{{ t('glossary.title') }}</span>
|
||||
</w-toolbar>
|
||||
<w-toolbar class="glossary-hdr-search" dark>
|
||||
<!-- -> Only where there is a choice to make, as in the link picker -->
|
||||
<w-btn
|
||||
v-if="siteStore.locales.active.length > 1"
|
||||
class="glossary-locale mr-2 acrylic-btn"
|
||||
flat
|
||||
color="white"
|
||||
:label="siteStore.localeAlias(state.locale)"
|
||||
:aria-label="siteStore.localeAlias(state.locale)"
|
||||
style="height: 40px">
|
||||
<locale-selector-menu :selected="state.locale" :navigate="false" @select="switchLocale" />
|
||||
</w-btn>
|
||||
<!-- -> The File Manager's search pill, matching names and aliases. See `.glossary-search` -->
|
||||
<div class="glossary-search" :class="{ 'is-focused': state.filterIsFocused }">
|
||||
<w-icon class="glossary-search-lead" name="la:search" />
|
||||
<input
|
||||
v-model="state.filter"
|
||||
type="text"
|
||||
class="glossary-search-input"
|
||||
:placeholder="t(`glossary.filter`)"
|
||||
:aria-label="t(`glossary.filter`)"
|
||||
autocomplete="off"
|
||||
@focus="state.filterIsFocused = true"
|
||||
@blur="state.filterIsFocused = false" />
|
||||
<button
|
||||
v-if="state.filter.length > 0"
|
||||
type="button"
|
||||
class="glossary-search-clear"
|
||||
:aria-label="t(`common.actions.clear`)"
|
||||
@click="state.filter = ``">
|
||||
<w-icon name="la:times" />
|
||||
</button>
|
||||
</div>
|
||||
</w-toolbar>
|
||||
<w-toolbar class="glossary-hdr-actions" dark>
|
||||
<w-space />
|
||||
<transition name="syncing">
|
||||
<w-spinner class="mr-4" v-show="state.loading > 0" color="accent" size="20px" />
|
||||
</transition>
|
||||
<w-btn
|
||||
class="mr-2"
|
||||
flat
|
||||
rounded
|
||||
color="white"
|
||||
:aria-label="t(`common.actions.viewDocs`)"
|
||||
icon="la:question-circle"
|
||||
:href="siteStore.docsBase + `/guide/glossary`"
|
||||
target="_blank">
|
||||
<w-tooltip>{{ t(`common.actions.viewDocs`) }}</w-tooltip>
|
||||
</w-btn>
|
||||
<!--
|
||||
-> The pushed Close the File Manager and the editing overlays put themselves away with, and
|
||||
New Term joined to it the way the group editor joins its actions to Close
|
||||
-->
|
||||
<w-btn-group push>
|
||||
<w-btn
|
||||
v-if="canManage && !state.forbidden"
|
||||
push
|
||||
color="primary"
|
||||
text-color="white"
|
||||
icon="la:plus"
|
||||
:label="t(`glossary.newTerm`)"
|
||||
@click="newTerm" />
|
||||
<w-btn
|
||||
push
|
||||
color="white"
|
||||
text-color="grey-7"
|
||||
:label="t(`common.actions.close`)"
|
||||
:aria-label="t(`common.actions.close`)"
|
||||
icon="la:times"
|
||||
@click="close" />
|
||||
</w-btn-group>
|
||||
</w-toolbar>
|
||||
</w-header>
|
||||
|
||||
<!-- ----------------------------------------------------- -->
|
||||
@@ -75,25 +112,8 @@
|
||||
:overlay-below="0">
|
||||
<div class="glossary-sidebar-inner">
|
||||
<div class="glossary-sidebar-tools">
|
||||
<w-btn
|
||||
v-if="canManage && !state.forbidden"
|
||||
class="w-full mb-3"
|
||||
unelevated
|
||||
color="primary"
|
||||
icon="la:plus"
|
||||
no-caps
|
||||
:label="t(`glossary.newTerm`)"
|
||||
@click="newTerm" />
|
||||
<w-input
|
||||
v-model="state.filter"
|
||||
outlined
|
||||
dense
|
||||
clearable
|
||||
hide-bottom-space
|
||||
:placeholder="t(`glossary.filter`)"
|
||||
:aria-label="t(`glossary.filter`)" />
|
||||
<w-btn-toggle
|
||||
class="glossary-toggle glossary-grouping mt-3"
|
||||
class="glossary-toggle glossary-grouping"
|
||||
v-model="state.grouping"
|
||||
push
|
||||
no-caps
|
||||
@@ -657,6 +677,8 @@ const state = reactive({
|
||||
forbidden: false,
|
||||
loadFailed: false,
|
||||
filter: '',
|
||||
/** Whether the filter in the header has focus, which turns it to ink-on-white as the File Manager's does. */
|
||||
filterIsFocused: false,
|
||||
grouping: storedGrouping(),
|
||||
/** Which term the sidebar marks. Kept through the form, so cancelling goes back to it. */
|
||||
selectedId: null,
|
||||
@@ -863,6 +885,17 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// -> Below 900px the filter is in the header whichever of list and term is showing: typing in it is
|
||||
// asking for the list
|
||||
watch(
|
||||
() => state.filter,
|
||||
(filter) => {
|
||||
if (filter && !isWide.value) {
|
||||
state.listOpen = true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// -> A server-side refusal is about what was sent; editing the field is the answer to it
|
||||
watch(
|
||||
() => state.form.term,
|
||||
@@ -1413,6 +1446,9 @@ watch(
|
||||
|
||||
onMounted(async () => {
|
||||
await glossaryStore.ensureAccess()
|
||||
// -> Chosen again now the answer is current: the one the state was built from may have predated a
|
||||
// locale being turned on, and would send the overlay to a locale it did list
|
||||
state.locale = initialLocale()
|
||||
await loadTerms()
|
||||
const { termId, name } = siteStore.overlayOpts ?? {}
|
||||
if (termId) {
|
||||
@@ -1429,6 +1465,9 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* -> Where the header wraps onto two lines, as `$fileman-hdr-wrap-max` does -- and where the list and the term start taking turns */
|
||||
$glossary-hdr-wrap-max: 899.98px;
|
||||
|
||||
.glossary {
|
||||
/*
|
||||
The ink, stated: the dialog panel draws no text colour of its own, and an overlay that is not built
|
||||
@@ -1441,6 +1480,103 @@ onMounted(async () => {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/*
|
||||
THE HEADER, as the File Manager's (`FileManager.vue`, "THE HEADER ON A NARROW SCREEN"): three
|
||||
toolbars sharing one row, and below 900px two lines -- the title with the help and Close at its
|
||||
end, then the locale and the filter. Close stays on the first line, where it cannot be pushed off
|
||||
the side of a phone.
|
||||
*/
|
||||
@media (max-width: $glossary-hdr-wrap-max) {
|
||||
> .card-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&-hdr-title {
|
||||
width: auto;
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-hdr-actions {
|
||||
width: auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&-hdr-search {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* -> Cut to the filter's 7px, as `.fileman-locale` is, and unlayered for the reason given there */
|
||||
&-locale {
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
/*
|
||||
The filter: `.fileman-search` in `FileManager.vue`, rule for rule -- the File Manager states its
|
||||
field rather than borrowing the site header's, and this does the same rather than reaching into
|
||||
another overlay's stylesheet. The reasons for the fill and the corners are written there; keep the
|
||||
two in step.
|
||||
*/
|
||||
&-search {
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 40px;
|
||||
padding: 0 8px 0 12px;
|
||||
border-radius: 7px;
|
||||
background-color: $dark-2;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
transition:
|
||||
background-color 0.25s var(--ease-standard),
|
||||
color 0.25s var(--ease-standard);
|
||||
|
||||
&.is-focused {
|
||||
background-color: #fff;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
||||
&-lead {
|
||||
flex-shrink: 0;
|
||||
font-size: 20px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: currentColor;
|
||||
opacity: 0.55;
|
||||
}
|
||||
}
|
||||
|
||||
&-clear {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
padding: 4px;
|
||||
border-radius: 9999px;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-sidebar {
|
||||
background-color: #f5f7fa;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.08);
|
||||
@@ -1474,12 +1610,20 @@ onMounted(async () => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
A shade darker than the list under it, setting the switch apart: the File Manager's tree colour
|
||||
(`.fileman-left`) in light mode. Not in dark mode, where that is `$dark-4` -- lighter than the list,
|
||||
and the main panel's own colour, so the strip read as a piece of the panel. One step down the ramp
|
||||
instead.
|
||||
*/
|
||||
&-sidebar-tools {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background-color: $blue-grey-1;
|
||||
|
||||
@at-root .body--dark & {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||||
background-color: $dark-6;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,9 +66,9 @@ const canUseFileManager = computed(
|
||||
|
||||
// WATCHERS
|
||||
|
||||
// -> Asked again whenever the answer could have changed: a login, a logout, another site
|
||||
// -> Asked again whenever the answer could have changed: see `accessKey`
|
||||
watch(
|
||||
() => [siteStore.id, userStore.authenticated, userStore.id],
|
||||
() => glossaryStore.accessKey,
|
||||
() => {
|
||||
glossaryStore.ensureAccess()
|
||||
},
|
||||
|
||||
@@ -1241,7 +1241,7 @@ const glossaryAutoLink = computed(
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [siteStore.id, userStore.authenticated, userStore.id],
|
||||
() => glossaryStore.accessKey,
|
||||
() => {
|
||||
glossaryStore.ensureAccess()
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useUserStore } from './user'
|
||||
*/
|
||||
export const useGlossaryStore = defineStore('glossary', {
|
||||
state: () => ({
|
||||
/** Whose answer this is -- `siteId:userId` -- so a login, a logout or another site asks again. */
|
||||
/** Which `accessKey` this answer is for. */
|
||||
loadedFor: null,
|
||||
enabled: false,
|
||||
readableLocales: [],
|
||||
@@ -29,6 +29,26 @@ export const useGlossaryStore = defineStore('glossary', {
|
||||
revision: 0
|
||||
}),
|
||||
getters: {
|
||||
/**
|
||||
* Everything the access answer depends on: the site, who is asking, and the two parts of the
|
||||
* site's configuration it is drawn from -- the glossary switch and the active locales. Whatever
|
||||
* changes it is a reason to ask again, which is what the watchers calling `ensureAccess` key on.
|
||||
*
|
||||
* The configuration matters as much as the session: an administrator who turns a locale on and
|
||||
* goes to a page in it has changed nothing about who they are, and was otherwise sent to the
|
||||
* glossary of a locale the stale answer did list, until a reload.
|
||||
*/
|
||||
accessKey() {
|
||||
const siteStore = useSiteStore()
|
||||
const userStore = useUserStore()
|
||||
const locales = siteStore.locales.active.map((lc) => lc.code).join(',')
|
||||
return [
|
||||
siteStore.id,
|
||||
userStore.authenticated ? userStore.id : 'guest',
|
||||
siteStore.features.glossary ? 'on' : 'off',
|
||||
locales
|
||||
].join(':')
|
||||
},
|
||||
/** Whether there is a glossary this session may open, in any locale. */
|
||||
isAvailable: (state) => state.enabled && state.readableLocales.length > 0,
|
||||
/** Whether this session may create, edit and delete terms in a locale. */
|
||||
@@ -36,18 +56,17 @@ export const useGlossaryStore = defineStore('glossary', {
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* Fetch the access answer, unless it is already the one for this site and this session.
|
||||
* Fetch the access answer, unless it is already the one for `accessKey`.
|
||||
*
|
||||
* A failure leaves the glossary unavailable rather than throwing: the menu row is the only thing
|
||||
* waiting on it, and a missing row is the right answer to not knowing.
|
||||
*/
|
||||
async ensureAccess() {
|
||||
const siteStore = useSiteStore()
|
||||
const userStore = useUserStore()
|
||||
if (!siteStore.id) {
|
||||
return
|
||||
}
|
||||
const key = `${siteStore.id}:${userStore.authenticated ? userStore.id : 'guest'}`
|
||||
const key = this.accessKey
|
||||
if (this.loadedFor === key) {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user