fix: blocks dark mode fixes + missing page props quick access

scarlett
NGPixel 1 month ago
parent 23dbb27212
commit 49e3598a15
No known key found for this signature in database

@ -121,9 +121,20 @@ Vite's `dynamicImportVarsOptions`. A block pulling in a heavy library is fine
until its tag turns up in a page — and a library that still ships CommonJS works too, since the
rollup config runs `@rollup/plugin-commonjs` after `resolve()`.
Blocks style themselves with `:host` / `:host-context(body.body--dark)` for dark mode and read the
theme colors via CSS custom properties (`var(--q-primary)` — the `--q-` prefix is historical; the
properties are declared in `css/tailwind.css` and rewritten at runtime for per-site theming).
Blocks style themselves off `:host` and read the theme colors via CSS custom properties
(`var(--q-primary)` — the `--q-` prefix is historical; the properties are declared in
`css/tailwind.css` and rewritten at runtime for per-site theming).
**Dark mode goes through `blocks/shared/theme.js`, never `:host-context()`.** The app's source of
truth is the `body--dark` class on `<body>`, which CSS in a shadow root cannot see; `:host-context()`
is the selector for exactly that and is what every block used to use, but only Chromium ever shipped
it — MDN has it deprecated, Firefox and Safari never implemented it, and there it silently never
matches, so the block stayed light on a dark page. Instead construct a `DarkMode` controller
(`this._darkMode = new DarkMode(this)`) in the block's constructor and write `:host([dark])`; the
controller keeps that attribute in step, sharing one MutationObserver across every block on the page.
A block that must *act* on the change rather than restyle for it passes `onChange`, or reads
`.isDark``block-diagram` redraws mermaid in its own dark theme, `block-map` resolves a per-block
`theme` prop that can pin a map light on a dark page.
## Commands

@ -1,4 +1,5 @@
import { LitElement, html, css } from 'lit'
import { DarkMode } from '../shared/theme.js'
/**
* Block Countdown
@ -71,7 +72,7 @@ export class BlockCountdownElement extends LitElement {
text-align: center;
background-image: linear-gradient(to bottom, #fff, #fafafa);
}
:host-context(body.body--dark) .countdown {
:host([dark]) .countdown {
border-color: rgba(255, 255, 255, 0.15);
background-image: linear-gradient(to bottom, #161b22, #0d1117);
}
@ -95,7 +96,7 @@ export class BlockCountdownElement extends LitElement {
border-radius: 5px;
background-color: rgba(0, 0, 0, 0.04);
}
:host-context(body.body--dark) .segment {
:host([dark]) .segment {
background-color: rgba(255, 255, 255, 0.06);
}
@ -175,6 +176,8 @@ export class BlockCountdownElement extends LitElement {
this._error = ''
this._target = null
this._timer = null
// -> Puts `dark` on this element for the styles above to key off
this._darkMode = new DarkMode(this)
}
/**

@ -1,6 +1,7 @@
import { LitElement, html, css } from 'lit'
import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
import mermaid from 'mermaid'
import { DarkMode } from '../shared/theme.js'
/**
* A number for the next drawing, so every one of them gets an id of its own.
@ -124,7 +125,7 @@ flowchart LR
color: #424242;
font-size: 0.8em;
}
:host-context(body.body--dark) .caption {
:host([dark]) .caption {
color: rgba(255, 255, 255, 0.7);
}
@ -171,7 +172,19 @@ flowchart LR
this.align = 'left'
this._svg = ''
this._error = ''
this._themeWatcher = null
/*
Two jobs at once: `dark` on this element is what the caption colour keys off, and the callback
is what redraws the diagram itself, since mermaid picks its colours as it draws and writes them
into the SVG. Only `auto` has anything to follow -- a diagram asked for a theme by name keeps
it either way -- and only once there is a source to draw, which `firstUpdated` reads.
*/
this._darkMode = new DarkMode(this, {
onChange: () => {
if (this.theme === 'auto' && this._source) {
this._draw()
}
}
})
/** The drawing being waited on, so a stale one cannot land after a newer one. */
this._drawing = 0
/** The source, and whether it came out of a fence. Both read from the body once, on first render. */
@ -182,15 +195,15 @@ flowchart LR
/**
* The theme to draw in.
*
* `auto` reads the class the app puts on the body, which is the same thing every block's CSS keys
* its dark mode off a diagram cannot do it in CSS, because mermaid picks its colours while it
* draws and writes them into the SVG.
* `auto` follows the app, which every other block does in CSS off the `dark` attribute the same
* controller sets a diagram cannot, because mermaid picks its colours while it draws and writes
* them into the SVG.
*/
_theme() {
if (this.theme && this.theme !== 'auto') {
return this.theme
}
return document.body.classList.contains('body--dark') ? 'dark' : 'default'
return this._darkMode.isDark ? 'dark' : 'default'
}
/**
@ -251,18 +264,6 @@ flowchart LR
return
}
this._draw()
// -> Only `auto` has anything to follow; a diagram asked for a theme by name keeps it either way
if (this.theme === 'auto') {
this._themeWatcher = new MutationObserver(() => this._draw())
this._themeWatcher.observe(document.body, { attributeFilter: ['class'] })
}
}
disconnectedCallback() {
super.disconnectedCallback()
this._themeWatcher?.disconnect()
this._themeWatcher = null
}
render() {

@ -1,4 +1,5 @@
import { LitElement, html, css } from 'lit'
import { DarkMode } from '../shared/theme.js'
/**
* Block Index
@ -118,7 +119,7 @@ export class BlockIndexElement extends LitElement {
align-items: stretch;
justify-content: stretch;
}
:host-context(body.body--dark) li {
:host([dark]) li {
background-color: #222;
background-image: linear-gradient(to bottom,#161b22, #0d1117);
border-right: 1px solid rgba(0,0,0,.5);
@ -132,7 +133,7 @@ export class BlockIndexElement extends LitElement {
border-left-color: var(--q-primary);
cursor: pointer;
}
:host-context(body.body--dark) li:hover {
:host([dark]) li:hover {
background-image: linear-gradient(to bottom,#1e232a, #161b22);
border-left-color: var(--q-primary);
}
@ -162,10 +163,10 @@ export class BlockIndexElement extends LitElement {
li a > svg path {
fill: rgba(0,0,0,.2);
}
:host-context(body.body--dark) li a > svg path {
:host([dark]) li a > svg path {
fill: rgba(255,255,255,.2);
}
li:hover a > svg path, :host-context(body.body--dark) li:hover a > svg path {
li:hover a > svg path, :host([dark]) li:hover a > svg path {
fill: color-mix(in srgb, currentColor 50%, transparent);
}
@ -240,6 +241,8 @@ export class BlockIndexElement extends LitElement {
this.orderByDirection = 'asc'
this.depth = 0
this.noResultMsg = 'No pages matching your query.'
// -> Puts `dark` on this element for the styles above to key off
this._darkMode = new DarkMode(this)
}
async connectedCallback() {

@ -1,5 +1,6 @@
import { LitElement, html, css } from 'lit'
import { load as parseYaml } from 'js-yaml'
import { DarkMode } from '../shared/theme.js'
/**
* Yes and no, drawn rather than spelled out.
@ -367,7 +368,7 @@ Website: https://montreal.ca
--infobox-head-top: #f7f8fa;
--infobox-rule: #e3e5e8;
}
:host-context(body.body--dark) {
:host([dark]) {
--infobox-border: rgba(255, 255, 255, 0.15);
--infobox-bg: #161b22;
--infobox-head: #1e232a;
@ -410,6 +411,8 @@ Website: https://montreal.ca
this.imageCaption = ''
this._entries = []
this._error = ''
// -> Puts `dark` on this element for the styles above to key off
this._darkMode = new DarkMode(this)
}
/**

@ -11,6 +11,7 @@ import katexCss from 'katex/dist/katex.min.css'
the same thing in both blocks.
*/
import 'katex/contrib/mhchem'
import { DarkMode } from '../shared/theme.js'
/*
KaTeX's stylesheet, split in two.
@ -119,7 +120,7 @@ x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}
font-size: 0.8em;
text-align: center;
}
:host-context(body.body--dark) .caption {
:host([dark]) .caption {
color: rgba(255, 255, 255, 0.7);
}
@ -160,6 +161,8 @@ x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}
this.align = 'center'
this._markup = ''
this._error = ''
// -> Puts `dark` on this element for the styles above to key off
this._darkMode = new DarkMode(this)
}
/**

@ -1,5 +1,6 @@
import { LitElement, html, css } from 'lit'
import { deflate } from 'pako'
import { DarkMode } from '../shared/theme.js'
/** The default server, which is the one Kroki runs for everybody. */
const DEFAULT_SERVER = 'https://kroki.io'
@ -203,7 +204,7 @@ digraph G {
/* -> A diagram wider than the column scrolls rather than shrinking to illegibility */
overflow-x: auto;
}
:host-context(body.body--dark) .sheet {
:host([dark]) .sheet {
border-color: rgba(255, 255, 255, 0.15);
}
@ -234,7 +235,7 @@ digraph G {
color: #424242;
font-size: 0.8em;
}
:host-context(body.body--dark) .caption {
:host([dark]) .caption {
color: rgba(255, 255, 255, 0.7);
}
@ -297,6 +298,8 @@ digraph G {
this._src = ''
this._unsized = false
this._error = ''
// -> Puts `dark` on this element for the styles above to key off
this._darkMode = new DarkMode(this)
}
/**

@ -3,6 +3,7 @@ import { LitElement, html, css, unsafeCSS } from 'lit'
// apart with a commonjs plugin, and it has no `exports` map to pick the module build for us
import * as L from 'leaflet/dist/leaflet-src.esm.js'
import leafletCss from 'leaflet/dist/leaflet.css'
import { DarkMode } from '../shared/theme.js'
/**
* The marker, drawn rather than fetched.
@ -65,6 +66,14 @@ export class BlockMapElement extends LitElement {
type: 'string',
label: 'Marker Label',
hint: 'Shown in a popup when the marker is clicked. The marker is drawn either way.'
},
{
name: 'theme',
type: 'select',
label: 'Theme',
options: ['auto', 'light', 'dark'],
hint: 'auto follows the light or dark theme the reader is using.',
default: 'auto'
}
]
}
@ -90,20 +99,74 @@ export class BlockMapElement extends LitElement {
margin-bottom: 16px;
}
/*
The palette, as custom properties on the container so that the rules below are written once
and only the values are switched.
Which set applies is decided on the data-theme attribute rather than on a class, because
Leaflet owns the class attribute of this element -- it writes leaflet-container,
leaflet-touch and the animation classes onto it -- and a Lit class binding sets the whole
attribute, so rebinding it (which the theme changing under the reader does) would take
Leaflet's own classes off with it. render() resolves auto to one of the two, so the
attribute in the DOM is always the palette actually in use.
*/
.map {
--tile-filter: none;
--edge: rgba(0, 0, 0, 0.1);
--surface: #f2efe9;
--control-bg: #fff;
--control-fg: #333;
--control-link: #0078a8;
}
.map[data-theme='dark'] {
/*
OpenStreetMap publishes one style and it is a light one, so a dark map is the light tiles
recoloured: inverted for the dark ground and light labels, turned back through half the
colour wheel so water reads as water again, and taken slightly off full contrast, which
the inversion otherwise exaggerates. An approximation -- greenery lands cooler than it
should -- but it costs no second tile provider and no second request.
The filter is on the tile pane alone. Marker, popup and controls sit in panes of their
own and would come back inverted too.
*/
--tile-filter: invert(1) hue-rotate(180deg) brightness(0.92) contrast(0.9) saturate(0.85);
--edge: rgba(255, 255, 255, 0.15);
--surface: #16130f;
--control-bg: #2b2b2b;
--control-fg: #ddd;
--control-link: #6cb6d9;
}
.map {
width: 100%;
border-radius: 5px;
border: 1px solid rgba(0, 0, 0, 0.1);
background-color: #f2efe9;
border: 1px solid var(--edge);
background-color: var(--surface);
}
:host-context(body.body--dark) .map {
border-color: rgba(255, 255, 255, 0.15);
background-color: #16130f;
.map .leaflet-tile-pane {
filter: var(--tile-filter);
}
/* -> The tiles are somebody else's work and the licence asks for the credit to be visible */
.leaflet-container .leaflet-control-attribution {
font-size: 10px;
/* -> Leaflet shows the credit through a translucent plate, which is worth keeping */
background: color-mix(in srgb, var(--control-bg) 80%, transparent);
color: var(--control-fg);
}
.leaflet-container .leaflet-control-attribution a {
color: var(--control-link);
}
/* -> Leaflet's zoom buttons are white by default, which is a lamp on a dark map */
.leaflet-container .leaflet-bar a {
background-color: var(--control-bg);
color: var(--control-fg);
border-bottom-color: var(--edge);
}
.leaflet-container .leaflet-bar a:hover {
background-color: color-mix(in srgb, var(--control-bg) 90%, var(--control-fg));
}
.error {
@ -148,6 +211,12 @@ export class BlockMapElement extends LitElement {
*/
label: { type: String },
/**
* Which palette to draw in: `auto`, `light` or `dark`
* @type {string}
*/
theme: { type: String },
// Internal Properties
_error: { state: true }
}
@ -160,8 +229,14 @@ export class BlockMapElement extends LitElement {
this.zoom = 13
this.height = 400
this.label = ''
this.theme = 'auto'
this._error = ''
this._map = null
/*
No `dark` attribute on the host: this block settles the theme question itself, since the prop
can pin a map light on a dark page, and the answer it arrives at goes on `data-theme` below.
*/
this._darkMode = new DarkMode(this, { attribute: false })
}
firstUpdated() {
@ -221,7 +296,17 @@ export class BlockMapElement extends LitElement {
if (this._error) {
return html`<div class="error">${this._error}</div>`
}
return html`<div class="map" style="height: ${Number(this.height) || 400}px"></div>`
// -> Anything else an author might write is read as `auto`, which is the setting that has an
// answer for every page rather than a guess at what was meant
const theme = ['light', 'dark'].includes(this.theme)
? this.theme
: this._darkMode.isDark
? 'dark'
: 'light'
return html`<div
class="map"
data-theme="${theme}"
style="height: ${Number(this.height) || 400}px"></div>`
}
}

@ -48,6 +48,8 @@ import '@mathjax/src/js/input/tex/unicode/UnicodeConfiguration.js'
import '@mathjax/src/js/input/tex/upgreek/UpgreekConfiguration.js'
import '@mathjax/src/js/input/tex/verb/VerbConfiguration.js'
import { DarkMode } from '../shared/theme.js'
const PACKAGES = [
'base',
'action',
@ -204,7 +206,7 @@ x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}
font-size: 0.8em;
text-align: center;
}
:host-context(body.body--dark) .caption {
:host([dark]) .caption {
color: rgba(255, 255, 255, 0.7);
}
@ -244,6 +246,8 @@ x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}
this.align = 'center'
this._svg = ''
this._error = ''
// -> Puts `dark` on this element for the styles above to key off
this._darkMode = new DarkMode(this)
}
/**

@ -1,5 +1,6 @@
import { LitElement, html, css } from 'lit'
import { deflateRaw } from 'pako'
import { DarkMode } from '../shared/theme.js'
/** The default server, which is the one PlantUML runs for everybody. */
const DEFAULT_SERVER = 'https://www.plantuml.com/plantuml'
@ -138,7 +139,7 @@ Bob --> Alice : hi
/* -> A diagram wider than the column scrolls rather than shrinking to illegibility */
overflow-x: auto;
}
:host-context(body.body--dark) .sheet {
:host([dark]) .sheet {
border-color: rgba(255, 255, 255, 0.15);
}
@ -153,7 +154,7 @@ Bob --> Alice : hi
color: #424242;
font-size: 0.8em;
}
:host-context(body.body--dark) .caption {
:host([dark]) .caption {
color: rgba(255, 255, 255, 0.7);
}
@ -207,6 +208,8 @@ Bob --> Alice : hi
this.align = 'left'
this._src = ''
this._error = ''
// -> Puts `dark` on this element for the styles above to key off
this._darkMode = new DarkMode(this)
}
/**

@ -1,6 +1,7 @@
import { LitElement, html, css } from 'lit'
import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
import { renderSVG } from 'uqr'
import { DarkMode } from '../shared/theme.js'
/**
* Block QR Code
@ -61,7 +62,7 @@ export class BlockQrCodeElement extends LitElement {
*/
background-color: #fff;
}
:host-context(body.body--dark) .qr {
:host([dark]) .qr {
border-color: rgba(255, 255, 255, 0.15);
}
@ -123,6 +124,8 @@ export class BlockQrCodeElement extends LitElement {
this.caption = ''
this._svg = ''
this._error = ''
// -> Puts `dark` on this element for the styles above to key off
this._darkMode = new DarkMode(this)
}
/**

@ -1,4 +1,5 @@
import { LitElement, html, css } from 'lit'
import { DarkMode } from '../shared/theme.js'
/** A crossed-out eye, drawn rather than fetched: it is the same picture on every spoiler there is. */
const EYE_OFF_SVG = html`
@ -110,7 +111,7 @@ export class BlockSpoilerElement extends LitElement {
--spoiler-fg: #424242;
--spoiler-hover: rgba(0, 0, 0, 0.04);
}
:host-context(body.body--dark) {
:host([dark]) {
--spoiler-border: rgba(255, 255, 255, 0.15);
--spoiler-bg: #161b22;
--spoiler-fg: rgba(255, 255, 255, 0.75);
@ -143,6 +144,8 @@ export class BlockSpoilerElement extends LitElement {
this.label = 'Spoiler'
this.hint = 'Click to show content'
this._covered = true
// -> Puts `dark` on this element for the styles above to key off
this._darkMode = new DarkMode(this)
}
/**

@ -1,5 +1,6 @@
import { LitElement, html, css } from 'lit'
import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
import { DarkMode } from '../shared/theme.js'
/**
* Asked of a block that might be hiding the element the event was dispatched on.
@ -84,7 +85,7 @@ Content of the second tab.
0 1px 3px rgb(0 0 0 / 0.1),
0 1px 2px rgb(0 0 0 / 0.06);
}
:host-context(body.body--dark) .tabs {
:host([dark]) .tabs {
box-shadow:
0 1px 3px rgb(0 0 0 / 0.5),
0 1px 2px rgb(0 0 0 / 0.35);
@ -129,7 +130,7 @@ Content of the second tab.
background-color: rgb(255 255 255 / 0.5);
color: var(--tabs-active-fg);
}
:host-context(body.body--dark) .tab:hover:not(.is-active) {
:host([dark]) .tab:hover:not(.is-active) {
background-color: rgb(255 255 255 / 0.05);
}
.tab:focus-visible {
@ -169,7 +170,7 @@ Content of the second tab.
--tabs-active-fg: var(--q-primary, #1976d2);
--tabs-panel-bg: #fff;
}
:host-context(body.body--dark) {
: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);
@ -191,6 +192,8 @@ Content of the second tab.
this._active = 0
// -> Bound once, so that removing the listener later takes the same function that was added
this._onReveal = this._onReveal.bind(this)
// -> Puts `dark` on this element for the styles above to key off
this._darkMode = new DarkMode(this)
}
/**

@ -0,0 +1,102 @@
/**
* Dark mode, for blocks.
*
* The app keeps one source of truth for the theme: a `body--dark` / `body--light` class on <body>,
* set by `frontend/src/composables/dark.js`. CSS inside a shadow root cannot see it. `:host-context()`
* is exactly the selector for that job and every block here used to key its dark mode off it -- but
* only Chromium ever shipped it. MDN has it deprecated, Firefox and Safari have never implemented it,
* and in an unsupported browser the rule simply never matches, so a block stayed light on a dark page
* with nothing to show that anything had gone wrong.
*
* So the class is read in JS instead and pushed into the blocks that ask for it. One observer serves
* the whole page however many blocks are on it, and stops again once the last of them has gone.
*/
/** @type {Set<(dark: boolean) => void>} */
const watchers = new Set()
let observer = null
/**
* Whether the app is in dark mode at this moment.
*/
export function isDark() {
return document.body.classList.contains('body--dark')
}
/**
* Call `onChange` with the new value whenever the theme changes, until the returned function is
* called.
*
* @param {(dark: boolean) => void} onChange
* @returns {() => void} stops the watching
*/
export function watchTheme(onChange) {
watchers.add(onChange)
if (!observer) {
observer = new MutationObserver(() => {
const dark = isDark()
for (const watcher of watchers) {
watcher(dark)
}
})
// -> `body--light` is set in the same breath as `body--dark`, so one attribute is all to watch
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] })
}
return () => {
watchers.delete(onChange)
// -> Nothing left to tell, so nothing left to listen for
if (watchers.size < 1) {
observer?.disconnect()
observer = null
}
}
}
/**
* A Lit reactive controller that keeps a `dark` attribute on its host in step with the app's theme.
*
* A block that only changes colour needs no more than to construct one -- `:host([dark])` in its
* styles then says everything `:host-context(body.body--dark)` used to, at a specificity one class
* lower, which is still above the `:host` rule holding the light values. One that has to act on the
* change rather than restyle for it -- redrawing in a library's own dark theme, say -- passes
* `onChange`, or reads `isDark` off the controller at the moment it needs it.
*
* The attribute is on the host and not on an element inside the shadow root because the host is the
* one element a block always has. Where the shadow tree is a library's to arrange, an attribute
* bound in `render()` risks being written over -- see the `data-theme` note in `block-map`.
*/
export class DarkMode {
/**
* @param {import('lit').ReactiveElement} host
* @param {{ attribute?: boolean, onChange?: (dark: boolean) => void }} [options]
* `attribute: false` for a block that resolves the theme itself and would find a second answer
* sitting on the host misleading. `onChange` is called after the host has been told to update.
*/
constructor(host, { attribute = true, onChange = null } = {}) {
this.host = host
this.isDark = isDark()
this._attribute = attribute
this._onChange = onChange
this._unwatch = null
host.addController(this)
}
hostConnected() {
this._apply(isDark())
this._unwatch = watchTheme((dark) => this._apply(dark))
}
hostDisconnected() {
this._unwatch?.()
this._unwatch = null
}
_apply(dark) {
this.isDark = dark
if (this._attribute) {
this.host.toggleAttribute('dark', dark)
}
this.host.requestUpdate()
this._onChange?.(dark)
}
}

@ -1420,12 +1420,19 @@ onBeforeUnmount(() => {
}
}
/*
Each pane states its own ink alongside its fill. Nothing above these sets a text color for dark
mode -- the app has no global `body--dark { color }` rule, and the panes are not `w-card`s, which
is where that pairing normally lives -- so anything that just inherits (the folder tree's labels,
a file's title, the size in the right-hand column) came out black on the dark fill.
*/
&-left {
@at-root .body--light & {
background-color: $blue-grey-1;
}
@at-root .body--dark & {
background-color: $dark-4;
color: #fff;
}
}
@ -1435,6 +1442,7 @@ onBeforeUnmount(() => {
}
@at-root .body--dark & {
background-color: $dark-6;
color: #fff;
}
}
@ -1444,6 +1452,7 @@ onBeforeUnmount(() => {
}
@at-root .body--dark & {
background-color: $dark-5;
color: #fff;
}
}

@ -179,7 +179,9 @@
<!-- RULES -->
<!-- ----------------------------------------------------------------------- -->
<w-page v-else-if="route.params.section === `rules`">
<w-toolbar class="pl-4" :class="dark.isActive ? `bg-dark-3` : `bg-white`">
<w-toolbar
class="pl-4"
:class="dark.isActive ? `bg-dark-3 text-white` : `bg-white text-dark`">
<div class="text-subtitle1">{{ t('admin.groups.rules') }}</div>
<w-space />
<w-btn
@ -464,7 +466,7 @@
<!-- USERS -->
<!-- ----------------------------------------------------------------------- -->
<w-page v-else-if="route.params.section === `users`">
<w-toolbar :class="dark.isActive ? `bg-dark-3` : `bg-white`">
<w-toolbar :class="dark.isActive ? `bg-dark-3 text-white` : `bg-white text-dark`">
<div class="text-subtitle1">{{ t('admin.groups.users') }}</div>
<w-space />
<w-btn
@ -556,7 +558,7 @@
flat
:to="`/_admin/users/` + props.row.id"
icon="la:pen"
color="indigo"
:color="dark.isActive ? `indigo-4` : `indigo`"
:label="t(`common.actions.edit`)"
no-caps />
<!-- Hidden for system users: the guest account's membership is fixed, and the API -->

@ -105,6 +105,23 @@ const state = reactive({
}
}
/*
The rail below hangs outside the panel, so the panel holding it must not clip.
`WDialog` puts `overflow: auto` on `.w-dialog-panel` -- that is what rounds a centred dialog whose
inner bands would otherwise paint over its corners, and it keeps oversized content reachable. The
rail's containing block is the card INSIDE that panel, so the clip catches it and it disappeared
outright the moment that overflow arrived.
Lifted only for the panel that actually carries a rail, rather than for every side panel: the
other one (`PageDataDialog`) still leans on the panel both to round it and to scroll a card wider
than the panel is. Nothing is given up here -- `PagePropertiesDialog` rounds its own toolbar and
scroll area, and that scroll area is what its body scrolls in.
*/
.w-dialog-panel:has(> .page-properties-dialog) {
overflow: visible;
}
/*
The quick-jump rail, which sits outside the panel's left edge.

Loading…
Cancel
Save