Compare commits
37 Commits
d7eb20cb3c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0af46954d7 | |||
| 8df14ebbe3 | |||
| 39862fab3b | |||
| 8f2f310fe2 | |||
| 19d6a2883c | |||
| 75c57092f8 | |||
| f61705afb8 | |||
| 140a57a029 | |||
| f9445ed5ae | |||
| 044adb3a18 | |||
| 842190d225 | |||
| a807944b50 | |||
| 1b7e57d9f5 | |||
| 083a918729 | |||
| de30ff13e6 | |||
| 32f7b49f98 | |||
| 80011e6de5 | |||
| d651cfbe2c | |||
| bdf3ad085f | |||
| 5caaf38e9a | |||
| 7630da8abd | |||
| 45b968b77d | |||
| ca2d6d225e | |||
| 8b54efef61 | |||
| e8c1197613 | |||
| b84d71dd7a | |||
| d08007cdd7 | |||
| fdcadad23b | |||
| 8585993602 | |||
| d5dc96178f | |||
| 1d8d274fdd | |||
| 6ee9760168 | |||
| 3847513594 | |||
| 0c2e136cae | |||
| 6ad4cb2354 | |||
| 0eb91f1937 | |||
| db67f0c92b |
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"env": { "browser": true, "node": true, "es2021": true },
|
||||||
|
"parserOptions": { "ecmaVersion": 2021, "sourceType": "script" },
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": "warn",
|
||||||
|
"no-undef": "warn",
|
||||||
|
"no-empty": "warn",
|
||||||
|
"semi": ["error", "always"],
|
||||||
|
"eqeqeq": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
js-lint:
|
||||||
|
name: JS (eslint)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install ESLint
|
||||||
|
run: npm install --save-dev eslint@8
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: npx eslint --ext .js base.js node/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
# Authelia Portal — LotusGuild Terminal Design System Integration
|
||||||
|
|
||||||
|
This document covers everything that had to be **written from scratch** to theme the
|
||||||
|
Authelia authentication portal using the LotusGuild Terminal Design System v1.2.
|
||||||
|
It is a companion to `base.css` / `base.js` for future integrations with third-party
|
||||||
|
applications that use their own component frameworks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why a separate file at all?
|
||||||
|
|
||||||
|
`base.css` targets exclusively `.lt-*` class names — the design system's own prefix.
|
||||||
|
Authelia's frontend is a React single-page application built on
|
||||||
|
**Material UI (MUI) v5**, which generates its own stable class names like
|
||||||
|
`.MuiCard-root`, `.MuiButton-containedPrimary`, etc.
|
||||||
|
Bridging the two requires a translation layer: the same design tokens and visual
|
||||||
|
patterns from `base.css`, re-expressed against MUI's class structure.
|
||||||
|
|
||||||
|
Additionally, Authelia v4.x (latest: 4.39.x) does **not** support native CSS
|
||||||
|
injection — `server.asset_path` only handles `logo.png` and `favicon.ico`.
|
||||||
|
The CSS is injected via **nginx `sub_filter`** in Nginx Proxy Manager, which rewrites
|
||||||
|
Authelia's HTML on the fly to include a `<link>` tag before `</head>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser
|
||||||
|
└─► https://auth.lotusguild.org (NPM — LXC 139)
|
||||||
|
├─ GET /custom.css ──────── served from /data/custom_assets/authelia-custom.css
|
||||||
|
│ (nginx alias, no upstream request)
|
||||||
|
└─ GET /* ──────────────── proxied to Authelia (LXC 167 :9091)
|
||||||
|
HTML response is rewritten by sub_filter:
|
||||||
|
</head> → <link rel="stylesheet" href="/custom.css"></head>
|
||||||
|
```
|
||||||
|
|
||||||
|
**NPM config changed:** `/data/nginx/proxy_host/29.conf` (auth.lotusguild.org proxy host)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Serve LotusGuild Terminal custom CSS
|
||||||
|
location = /custom.css {
|
||||||
|
alias /data/custom_assets/authelia-custom.css;
|
||||||
|
add_header Content-Type "text/css";
|
||||||
|
add_header Cache-Control "public, max-age=3600";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
...existing headers...
|
||||||
|
|
||||||
|
# Prevent upstream gzip so sub_filter can inspect the body
|
||||||
|
proxy_set_header Accept-Encoding "";
|
||||||
|
|
||||||
|
# Inject CSS link before </head>
|
||||||
|
sub_filter '</head>' '<link rel="stylesheet" href="/custom.css"></head>';
|
||||||
|
sub_filter_once on;
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/include/proxy.conf;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Warning:** NPM regenerates proxy_host conf files when you edit a proxy host in
|
||||||
|
> the UI. If host 29 is ever modified through NPM, re-apply the `sub_filter` block
|
||||||
|
> and the `/custom.css` location manually, or set the config via NPM's
|
||||||
|
> "Advanced" tab before saving.
|
||||||
|
|
||||||
|
**Authelia config changed:** `/etc/authelia/configuration.yml` — added `asset_path`
|
||||||
|
under `server:` so Authelia serves the resized logo and favicon:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
address: tcp://0.0.0.0:9091
|
||||||
|
asset_path: /etc/authelia/assets
|
||||||
|
```
|
||||||
|
|
||||||
|
Assets in `/etc/authelia/assets/`:
|
||||||
|
- `logo.png` — 256 × 256 PNG resized from Tinker Tickets `assets/images/favicon.png`
|
||||||
|
- `favicon.ico` — 32 × 32 ICO from the same source
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What is NOT in `base.css` and had to be written
|
||||||
|
|
||||||
|
The sections below are ordered as they appear in `authelia-custom.css`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Re-declaring design tokens with `!important`
|
||||||
|
|
||||||
|
**In `base.css`:** `:root { ... }` is declared once, without `!important`.
|
||||||
|
|
||||||
|
**What had to be added:** The entire `:root` block is repeated in `custom.css` because
|
||||||
|
Authelia's HTML is a self-contained SPA — `base.css` is never loaded. More critically,
|
||||||
|
MUI injects its own inline styles and CSS-in-JS rules at very high specificity.
|
||||||
|
Every property that needs to override MUI requires `!important`. `base.css` never
|
||||||
|
uses `!important` because it owns its namespace; here we are guests in MUI's DOM.
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* base.css does NOT use !important anywhere */
|
||||||
|
body { background-color: var(--bg-primary); }
|
||||||
|
|
||||||
|
/* custom.css must fight MUI's inline styles */
|
||||||
|
body { background-color: var(--bg-primary) !important; }
|
||||||
|
```
|
||||||
|
|
||||||
|
The token values themselves are identical to `base.css` — only the override
|
||||||
|
mechanism is new.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Universal box-sizing reset with `!important`
|
||||||
|
|
||||||
|
**In `base.css`:** `*, *::before, *::after { box-sizing: border-box; }` — no flag needed
|
||||||
|
because `base.css` loads first.
|
||||||
|
|
||||||
|
**What had to be added:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
MUI components set `box-sizing` inline on some elements. Without the flag the
|
||||||
|
clip-path geometry breaks on inputs and buttons.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. MUI typography selectors
|
||||||
|
|
||||||
|
**In `base.css`:** Typography rules target `h1–h6`, `a`, `p`, `body` — standard HTML
|
||||||
|
elements. No MUI class names exist in the design system.
|
||||||
|
|
||||||
|
**What had to be added:** Authelia renders text almost exclusively through MUI's
|
||||||
|
`<Typography>` component, which produces elements with `.MuiTypography-root` and
|
||||||
|
variant classes. Without targeting these, all text inherits MUI's Roboto font and
|
||||||
|
light grey colour instead of JetBrains Mono and `--text-primary`.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.MuiTypography-root,
|
||||||
|
.MuiInputBase-input,
|
||||||
|
.MuiFormLabel-root,
|
||||||
|
.MuiFormHelperText-root,
|
||||||
|
label, p, span, div {
|
||||||
|
font-family: var(--font-mono) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6,
|
||||||
|
.MuiTypography-h5,
|
||||||
|
.MuiTypography-h6 {
|
||||||
|
color: var(--accent-orange) !important;
|
||||||
|
text-shadow: var(--glow-orange) !important;
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. `.MuiCard-root` / `.MuiPaper-root` — the login card
|
||||||
|
|
||||||
|
**In `base.css`:** `.lt-card` implements the terminal card with `clip-path`, border,
|
||||||
|
background, and the `::before` corner triangle accent.
|
||||||
|
|
||||||
|
**What had to be added:** Authelia wraps its login form in a MUI `<Card>` (which
|
||||||
|
extends `<Paper>`). Neither selector exists in `base.css`. The visual result is
|
||||||
|
identical to `.lt-card` — the clip-path polygon, cyan border, box-glow, and
|
||||||
|
corner triangle are all copied — but they target different class names and require
|
||||||
|
`!important` throughout to override MUI's elevation shadows and `border-radius: 4px`.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.MuiCard-root,
|
||||||
|
.MuiPaper-root {
|
||||||
|
background: var(--bg-card) !important;
|
||||||
|
border: 1px solid var(--border-color) !important;
|
||||||
|
border-radius: 0 !important; /* ← overrides MUI default */
|
||||||
|
clip-path: polygon(...) !important; /* same geometry as .lt-card */
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiCard-root::before,
|
||||||
|
.MuiPaper-root::before {
|
||||||
|
/* same corner triangle as .lt-card::before */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Logo element — `img[alt="Authelia"]`
|
||||||
|
|
||||||
|
**In `base.css`:** No equivalent. The design system has no logo slot component.
|
||||||
|
|
||||||
|
**What had to be added:** Authelia renders the portal logo as
|
||||||
|
`<img alt="Authelia" src="./static/media/logo.png">`. This selector targets it
|
||||||
|
specifically to apply the cyan drop-shadow and orange hover glow. Without it the
|
||||||
|
logo renders without any terminal aesthetic treatment.
|
||||||
|
|
||||||
|
```css
|
||||||
|
img[alt="Authelia"] {
|
||||||
|
filter: drop-shadow(0 0 12px rgba(0,212,255,0.4)) !important;
|
||||||
|
transition: filter 0.25s ease !important;
|
||||||
|
}
|
||||||
|
img[alt="Authelia"]:hover {
|
||||||
|
filter: drop-shadow(0 0 18px rgba(255,107,0,0.5)) !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. MUI outlined input family
|
||||||
|
|
||||||
|
**In `base.css`:** `.lt-input`, `.lt-select`, `.lt-textarea` — custom elements with
|
||||||
|
`clip-path`, terminal background, and cyan focus ring.
|
||||||
|
|
||||||
|
**What had to be added:** MUI's outlined text field is a composition of three
|
||||||
|
separate elements that each need individual targeting:
|
||||||
|
|
||||||
|
| MUI class | What it controls | Equivalent in `base.css` |
|
||||||
|
|-----------|-----------------|--------------------------|
|
||||||
|
| `.MuiOutlinedInput-root` | Outer wrapper — background, clip-path | `.lt-input` outer |
|
||||||
|
| `.MuiOutlinedInput-notchedOutline` | The SVG border element | `.lt-input` border |
|
||||||
|
| `.MuiInputBase-input` | The actual `<input>` inside | `.lt-input` inner text |
|
||||||
|
| `.MuiInputLabel-root` | Floating label | `.lt-label` |
|
||||||
|
| `.MuiFormHelperText-root` | Error / hint text below field | `.lt-form-hint` |
|
||||||
|
|
||||||
|
MUI splits the border rendering into a separate SVG `<fieldset>` element
|
||||||
|
(`.MuiOutlinedInput-notchedOutline`), which requires its own border-color override.
|
||||||
|
`base.css` has no equivalent splitting since `.lt-input` is a single element with a
|
||||||
|
CSS border.
|
||||||
|
|
||||||
|
The `.Mui-focused` and `.Mui-error` state classes also need explicit overrides because
|
||||||
|
MUI applies its own colour through CSS-in-JS with high specificity.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline {
|
||||||
|
border-color: var(--accent-cyan) !important;
|
||||||
|
box-shadow: var(--box-glow-cyan) !important;
|
||||||
|
}
|
||||||
|
.MuiFormHelperText-root.Mui-error {
|
||||||
|
color: var(--accent-red) !important;
|
||||||
|
text-shadow: var(--glow-red) !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, `caret-color` is set explicitly on the input because MUI does not
|
||||||
|
expose this through its theme — the blinking text cursor would otherwise be white.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. MUI button family
|
||||||
|
|
||||||
|
**In `base.css`:** `.lt-btn`, `.lt-btn-primary`, `.lt-btn-danger`, `.lt-btn-ghost` —
|
||||||
|
all using `clip-path` hexagon cuts, transparent backgrounds, and border+glow styling.
|
||||||
|
|
||||||
|
**What had to be added:** MUI uses four separate button variant classes. The primary
|
||||||
|
(sign-in) button is `.MuiButton-containedPrimary` which by default renders as a
|
||||||
|
solid filled rectangle. The entire button appearance — transparent background,
|
||||||
|
border, clip-path, text-transform, letter-spacing — has to be explicitly overridden
|
||||||
|
because MUI's `contained` variant applies an opaque `background-color` inline.
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* MUI default: background-color: #1976d2; border-radius: 4px; box-shadow: ... */
|
||||||
|
.MuiButton-containedPrimary {
|
||||||
|
background: transparent !important; /* fight the inline fill */
|
||||||
|
border: 1px solid var(--accent-orange-border) !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
clip-path: polygon(...) !important;
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Text/link buttons (`.MuiButton-text`) are separate from contained buttons in MUI but
|
||||||
|
map to the same `.lt-btn-ghost` pattern in `base.css`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. MUI checkbox and switch
|
||||||
|
|
||||||
|
**In `base.css`:** `.lt-checkbox`, `.lt-switch` — fully custom-drawn using CSS only.
|
||||||
|
|
||||||
|
**What had to be added:** Authelia uses MUI's `<Checkbox>` (remember-me) and
|
||||||
|
`<Switch>` (settings toggles). These are SVG-based components. Because MUI controls
|
||||||
|
their colour through injected CSS variables, the only reliable override is to target
|
||||||
|
`.Mui-checked` state classes and use `!important`. There is no equivalent split
|
||||||
|
between `track` and `thumb` in `base.css`.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.MuiSwitch-switchBase.Mui-checked { color: var(--accent-cyan) !important; }
|
||||||
|
.MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track {
|
||||||
|
background-color: var(--accent-cyan) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. MUI alert / notification banners
|
||||||
|
|
||||||
|
**In `base.css`:** `.lt-inline-*` for inline messages (`.lt-inline-success`,
|
||||||
|
`.lt-inline-error`, etc.) and `.lt-toast-*` for toasts. These use a left border
|
||||||
|
accent pattern.
|
||||||
|
|
||||||
|
**What had to be added:** Authelia uses MUI `<Alert>` for login errors, success
|
||||||
|
messages, and 2FA prompts. MUI Alert has its own severity system with four variant
|
||||||
|
classes (`.MuiAlert-standardError`, etc.). Each severity needs explicit colour,
|
||||||
|
background, and border-color overrides. The `border-left: 3px solid currentColor`
|
||||||
|
pattern is adapted from `base.css`'s inline message style but applied to MUI's
|
||||||
|
class names with `clip-path: none !important` to cancel MUI's rounded corners.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. MUI select, menu, and popover
|
||||||
|
|
||||||
|
**In `base.css`:** `.lt-select` for the select element itself; no dropdown overlay
|
||||||
|
component exists since the design system uses native `<select>`.
|
||||||
|
|
||||||
|
**What had to be added:** MUI's `<Select>` renders its dropdown as a `<Paper>`
|
||||||
|
element portalled to `<body>` with class `.MuiMenu-paper`. Without targeting this
|
||||||
|
separately, the dropdown popup inherits the browser default white background and
|
||||||
|
Roboto font, appearing completely unstyled. `.MuiMenuItem-root` hover and selected
|
||||||
|
states also need individual rules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. OTP / 2FA digit inputs — `input[type="tel"]`, `input[type="number"]`
|
||||||
|
|
||||||
|
**In `base.css`:** No equivalent. The design system has no numeric code input.
|
||||||
|
|
||||||
|
**What had to be added:** Authelia's one-time-password entry uses `<input type="tel">`
|
||||||
|
elements arranged in a row. These need oversized, centered, cyan-glowing characters
|
||||||
|
with wide letter-spacing to render like a retro code display.
|
||||||
|
|
||||||
|
```css
|
||||||
|
input[type="tel"],
|
||||||
|
input[type="number"] {
|
||||||
|
font-size: 1.2rem !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
text-shadow: var(--glow-cyan) !important;
|
||||||
|
letter-spacing: 0.15em !important;
|
||||||
|
text-align: center !important;
|
||||||
|
caret-color: var(--accent-orange) !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. MUI stepper — 2FA flow breadcrumb
|
||||||
|
|
||||||
|
**In `base.css`:** `.lt-stepper`, `.lt-step`, `.lt-step-num` — a custom CSS stepper
|
||||||
|
for multi-step wizard flows.
|
||||||
|
|
||||||
|
**What had to be added:** Authelia renders a MUI `<Stepper>` at the top of the 2FA
|
||||||
|
flow showing steps like "Username → Password → TOTP". MUI Stepper uses distinct
|
||||||
|
classes for label, icon, and connector that don't map 1-to-1 to `.lt-step-*`.
|
||||||
|
The step icon is an SVG circle rendered by React so it can only be tinted via
|
||||||
|
`color` and `filter: drop-shadow(...)`. The connector line between steps is a
|
||||||
|
separate `<hr>` with class `.MuiStepConnector-line`.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.MuiStepIcon-root.Mui-active {
|
||||||
|
color: var(--accent-orange) !important;
|
||||||
|
filter: drop-shadow(0 0 6px rgba(255,107,0,0.6)) !important;
|
||||||
|
}
|
||||||
|
.MuiStepIcon-root.Mui-completed {
|
||||||
|
color: var(--accent-green) !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. MUI linear and circular progress
|
||||||
|
|
||||||
|
**In `base.css`:** `.lt-progress` / `.lt-progress-bar` — a standard CSS progress bar
|
||||||
|
using `width` transitions. `.lt-spinner` — a rotating pseudo-element spinner.
|
||||||
|
|
||||||
|
**What had to be added:** Authelia uses MUI `<LinearProgress>` during API calls and
|
||||||
|
`<CircularProgress>` as the main loading spinner. MUI Linear Progress animates its
|
||||||
|
own internal elements (`.MuiLinearProgress-bar`) with keyframe animations; it cannot
|
||||||
|
be replaced with the `.lt-progress` pattern. The cyan-to-orange gradient and
|
||||||
|
`box-shadow` glow are new here — `base.css`'s progress bar uses a solid
|
||||||
|
`--accent-orange` fill.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.MuiLinearProgress-bar {
|
||||||
|
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-orange)) !important;
|
||||||
|
box-shadow: 0 0 8px rgba(0,212,255,0.5) !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14. MUI tooltip
|
||||||
|
|
||||||
|
**In `base.css`:** `.lt-tooltip` — a CSS-only tooltip shown via `:hover` on a
|
||||||
|
`[data-tooltip]` attribute.
|
||||||
|
|
||||||
|
**What had to be added:** MUI `<Tooltip>` portals its bubble to `<body>` with class
|
||||||
|
`.MuiTooltip-tooltip`. It is a separate DOM node, not a pseudo-element, so
|
||||||
|
`.lt-tooltip` styles cannot reach it. A standalone rule sets the terminal background,
|
||||||
|
monospace font, and squared border.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 15. `*:focus-visible` global ring
|
||||||
|
|
||||||
|
**In `base.css`:** Focus rings are defined individually per component
|
||||||
|
(`.lt-btn:focus-visible`, `.lt-input:focus-visible`, etc.).
|
||||||
|
|
||||||
|
**What had to be added:** A single global rule is more practical here because
|
||||||
|
Authelia contains many interactive MUI elements not known at design time, and
|
||||||
|
adding per-component rules for every possible focusable element would be brittle.
|
||||||
|
|
||||||
|
```css
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-cyan) !important;
|
||||||
|
outline-offset: 2px !important;
|
||||||
|
box-shadow: var(--box-glow-cyan) !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 16. Branding footer watermark — `#root::after`
|
||||||
|
|
||||||
|
**In `base.css`:** No equivalent. The design system does not add page-level
|
||||||
|
watermarks.
|
||||||
|
|
||||||
|
**What had to be added:** Authelia has no footer. The `#root::after` pseudo-element
|
||||||
|
attaches a fixed-position text watermark reading
|
||||||
|
`LOTUSGUILD SECURE PORTAL // AUTH.LOTUSGUILD.ORG` at the bottom of the viewport.
|
||||||
|
This uses `--text-muted`, `--font-mono`, uppercase, and wide letter-spacing to match
|
||||||
|
the terminal aesthetic without adding any HTML.
|
||||||
|
|
||||||
|
```css
|
||||||
|
#root::after {
|
||||||
|
content: 'LOTUSGUILD SECURE PORTAL // AUTH.LOTUSGUILD.ORG';
|
||||||
|
position: fixed;
|
||||||
|
bottom: 12px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 0.55rem;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Patterns shared with `base.css` (not new)
|
||||||
|
|
||||||
|
The following are **not** documented above because they are direct re-uses of
|
||||||
|
existing `base.css` patterns, only re-targeted to new selectors:
|
||||||
|
|
||||||
|
| Pattern | `base.css` source | Re-used in `custom.css` for |
|
||||||
|
|---------|------------------|-----------------------------|
|
||||||
|
| Dot-grid background | `html { background-image: radial-gradient(...) }` | `body` |
|
||||||
|
| Scanlines overlay | `body::before` | `body::before` (same rule, `!important` added) |
|
||||||
|
| Corner vignette | `body::after` | `body::after` (same rule, `!important` added) |
|
||||||
|
| Card clip-path polygon | `.lt-card { clip-path: polygon(...) }` | `.MuiCard-root` |
|
||||||
|
| Card corner triangle | `.lt-card::before` | `.MuiCard-root::before` |
|
||||||
|
| Button clip-path hexagon | `.lt-btn { clip-path: polygon(...) }` | `.MuiButton-containedPrimary` |
|
||||||
|
| Input clip-path cut | `.lt-input { clip-path: polygon(...) }` | `.MuiOutlinedInput-root` |
|
||||||
|
| Scrollbar | `::-webkit-scrollbar-*` section 45 | identical, `border-radius: 0` added |
|
||||||
|
| Link colours | `a { color: var(--accent-cyan) }` | `a` (same, `!important` added) |
|
||||||
|
| Divider | `.lt-divider` border-color | `.MuiDivider-root` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for future third-party integrations
|
||||||
|
|
||||||
|
1. **Always check for `!important` requirements.** CSS-in-JS frameworks (MUI, Emotion,
|
||||||
|
styled-components) inject styles at runtime with high specificity. Without
|
||||||
|
`!important`, most design token overrides will lose to injected inline styles.
|
||||||
|
|
||||||
|
2. **The split border problem.** MUI (and many component libraries) separate the
|
||||||
|
border from the element it visually belongs to. Always inspect the actual DOM
|
||||||
|
before writing selectors — what looks like one element is often three.
|
||||||
|
|
||||||
|
3. **Portalled overlays.** Dropdowns, tooltips, and modals in MUI are rendered into
|
||||||
|
a separate DOM tree (`<body>` portal). Scoped selectors inside a card or form
|
||||||
|
won't reach them. Target their root classes (`.MuiMenu-paper`, `.MuiTooltip-tooltip`)
|
||||||
|
directly.
|
||||||
|
|
||||||
|
4. **State classes.** MUI uses `.Mui-focused`, `.Mui-checked`, `.Mui-error`,
|
||||||
|
`.Mui-active`, `.Mui-completed`, `.Mui-disabled` as state modifiers. These are the
|
||||||
|
equivalent of `:focus`, `:checked`, etc. but applied by JavaScript — pseudo-class
|
||||||
|
selectors alone will sometimes not fire.
|
||||||
|
|
||||||
|
5. **SVG-based components.** Checkboxes, radio buttons, step icons, and spinners are
|
||||||
|
SVGs injected by React. They cannot be styled with `background`, `border`, or
|
||||||
|
`clip-path`. Use `color` (SVG `currentColor` inheritance) and `filter:
|
||||||
|
drop-shadow()` instead.
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
<%–
|
||||||
|
LOTUSGUILD TERMINAL DESIGN SYSTEM — Node.js / Express EJS Base Layout
|
||||||
|
Extend this in every page template via res.render('page', { ... }).
|
||||||
|
|
||||||
|
Required Express setup (server.js / app.js):
|
||||||
|
const { requireAuth, cspNonce, injectLocals } = require('./middleware');
|
||||||
|
app.use(cspNonce);
|
||||||
|
app.use(requireAuth);
|
||||||
|
app.use(injectLocals);
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
|
||||||
|
Locals injected automatically by middleware.js:
|
||||||
|
user { username, name, email, groups, isAdmin }
|
||||||
|
nonce CSP nonce string
|
||||||
|
appName process.env.APP_NAME
|
||||||
|
appSubtitle process.env.APP_SUBTITLE
|
||||||
|
|
||||||
|
Locals to set per-route (or via a second res.locals middleware):
|
||||||
|
pageTitle string — page <title> suffix
|
||||||
|
activeNav string — must match a navLinks[].key
|
||||||
|
navLinks array — navigation items:
|
||||||
|
[{ href: '/path', key: 'mykey', label: 'My Page' }, ...]
|
||||||
|
Dropdown:
|
||||||
|
{ label: 'Admin', key: 'admin', adminOnly: true, children: [
|
||||||
|
{ href: '/admin/users', label: 'Users' }
|
||||||
|
]}
|
||||||
|
pageStyles array — optional extra CSS hrefs
|
||||||
|
pageScripts array — optional extra <script src> paths
|
||||||
|
–%>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<meta name="theme-color" content="#030508">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<title><%= pageTitle ? pageTitle + ' — ' : '' %><%= appName || 'LotusGuild' %></title>
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=VT323&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Design system -->
|
||||||
|
<link rel="stylesheet" href="/web_template/base.css">
|
||||||
|
<!-- App-specific CSS (extends base, never overrides variables without good reason) -->
|
||||||
|
<link rel="stylesheet" href="/assets/app.css">
|
||||||
|
|
||||||
|
<% if (typeof pageStyles !== 'undefined') { pageStyles.forEach(href => { %>
|
||||||
|
<link rel="stylesheet" href="<%- href %>">
|
||||||
|
<% }); } %>
|
||||||
|
|
||||||
|
<link rel="icon" href="/assets/favicon.png" type="image/png">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Boot overlay -->
|
||||||
|
<div id="lt-boot" class="lt-boot-overlay"
|
||||||
|
data-app-name="<%= (appName || 'APP').toUpperCase() %>"
|
||||||
|
style="display:none">
|
||||||
|
<pre id="lt-boot-text" class="lt-boot-text"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- =========================================================
|
||||||
|
HEADER
|
||||||
|
========================================================= -->
|
||||||
|
<header class="lt-header">
|
||||||
|
<div class="lt-header-left">
|
||||||
|
|
||||||
|
<div class="lt-brand">
|
||||||
|
<a href="/" class="lt-brand-title" style="text-decoration:none">
|
||||||
|
<%= (appName || 'APP').toUpperCase() %>
|
||||||
|
</a>
|
||||||
|
<span class="lt-brand-subtitle"><%= appSubtitle || 'LotusGuild Infrastructure' %></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="lt-nav" aria-label="Main navigation">
|
||||||
|
<% (navLinks || []).forEach(link => {
|
||||||
|
if (link.adminOnly && !user.isAdmin) return;
|
||||||
|
if (link.children) { %>
|
||||||
|
<div class="lt-nav-dropdown">
|
||||||
|
<a href="#" class="lt-nav-link <%= (activeNav || '').startsWith(link.key) ? 'active' : '' %>">
|
||||||
|
<%= link.label %> ▾
|
||||||
|
</a>
|
||||||
|
<ul class="lt-nav-dropdown-menu">
|
||||||
|
<% link.children.forEach(child => { %>
|
||||||
|
<li><a href="<%= child.href %>"><%= child.label %></a></li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<a href="<%= link.href %>"
|
||||||
|
class="lt-nav-link <%= activeNav === link.key ? 'active' : '' %>">
|
||||||
|
<%= link.label %>
|
||||||
|
</a>
|
||||||
|
<% } }); %>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lt-header-right">
|
||||||
|
<% if (user && (user.name || user.username)) { %>
|
||||||
|
<span class="lt-header-user"><%= user.name || user.username %></span>
|
||||||
|
<% } %>
|
||||||
|
<% if (user && user.isAdmin) { %>
|
||||||
|
<span class="lt-badge-admin">admin</span>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- =========================================================
|
||||||
|
MAIN CONTENT — provided by the including view via <%- body %>
|
||||||
|
or by structuring routes to render with this as a wrapper.
|
||||||
|
========================================================= -->
|
||||||
|
<main class="lt-main lt-container">
|
||||||
|
<%- body %>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- =========================================================
|
||||||
|
SCRIPTS — all tags carry the CSP nonce
|
||||||
|
========================================================= -->
|
||||||
|
|
||||||
|
<!-- Runtime globals -->
|
||||||
|
<script nonce="<%= nonce %>">
|
||||||
|
window.CSRF_TOKEN = <%= JSON.stringify(csrfToken || '') %>;
|
||||||
|
window.CURRENT_USER = {
|
||||||
|
username: <%= JSON.stringify(user.username || '') %>,
|
||||||
|
name: <%= JSON.stringify(user.name || '') %>,
|
||||||
|
groups: <%= JSON.stringify(user.groups || []) %>,
|
||||||
|
isAdmin: <%= user.isAdmin ? 'true' : 'false' %>,
|
||||||
|
};
|
||||||
|
// App-specific config: set window.APP_CONFIG in your route's inline script,
|
||||||
|
// not here. This file is shared across all apps.
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Design system -->
|
||||||
|
<script nonce="<%= nonce %>" src="/web_template/base.js"></script>
|
||||||
|
|
||||||
|
<!-- App JS -->
|
||||||
|
<script nonce="<%= nonce %>" src="/assets/app.js"></script>
|
||||||
|
|
||||||
|
<% if (typeof pageScripts !== 'undefined') { pageScripts.forEach(src => { %>
|
||||||
|
<script nonce="<%= nonce %>" src="<%- src %>"></script>
|
||||||
|
<% }); } %>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+1140
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.57.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-15
@@ -17,8 +17,14 @@
|
|||||||
* $nonce string CSP nonce from SecurityHeadersMiddleware::getNonce()
|
* $nonce string CSP nonce from SecurityHeadersMiddleware::getNonce()
|
||||||
* $currentUser array ['username', 'name', 'is_admin', 'groups']
|
* $currentUser array ['username', 'name', 'is_admin', 'groups']
|
||||||
* $pageTitle string Page <title> suffix
|
* $pageTitle string Page <title> suffix
|
||||||
* $activeNav string Which nav link is active ('dashboard','tickets',etc.)
|
* $activeNav string Which nav key is active — must match a $navLinks entry
|
||||||
* $config array From config/config.php
|
* $config array From config/config.php
|
||||||
|
* $navLinks array Navigation items:
|
||||||
|
* [['href' => '/path', 'key' => 'mykey', 'label' => 'My Page'], ...]
|
||||||
|
* Nested (dropdown):
|
||||||
|
* ['label' => 'Admin', 'key' => 'admin', 'adminOnly' => true, 'children' => [
|
||||||
|
* ['href' => '/admin/users', 'label' => 'Users'],
|
||||||
|
* ]]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Defensive defaults
|
// Defensive defaults
|
||||||
@@ -27,6 +33,7 @@ $currentUser = $currentUser ?? [];
|
|||||||
$pageTitle = $pageTitle ?? 'Dashboard';
|
$pageTitle = $pageTitle ?? 'Dashboard';
|
||||||
$activeNav = $activeNav ?? '';
|
$activeNav = $activeNav ?? '';
|
||||||
$config = $config ?? [];
|
$config = $config ?? [];
|
||||||
|
$navLinks = $navLinks ?? [];
|
||||||
$isAdmin = $currentUser['is_admin'] ?? false;
|
$isAdmin = $currentUser['is_admin'] ?? false;
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -40,7 +47,7 @@ $isAdmin = $currentUser['is_admin'] ?? false;
|
|||||||
<!-- Unified design system CSS -->
|
<!-- Unified design system CSS -->
|
||||||
<link rel="stylesheet" href="/web_template/base.css">
|
<link rel="stylesheet" href="/web_template/base.css">
|
||||||
<!-- App-specific CSS (extends base, never overrides variables without good reason) -->
|
<!-- App-specific CSS (extends base, never overrides variables without good reason) -->
|
||||||
<link rel="stylesheet" href="/assets/css/app.css?v=<?php echo $config['CSS_VERSION'] ?? '20260314'; ?>">
|
<link rel="stylesheet" href="/assets/css/app.css?v=<?php echo $config['CSS_VERSION'] ?? '1'; ?>">
|
||||||
|
|
||||||
<link rel="icon" href="/assets/images/favicon.png" type="image/png">
|
<link rel="icon" href="/assets/images/favicon.png" type="image/png">
|
||||||
</head>
|
</head>
|
||||||
@@ -67,25 +74,31 @@ $isAdmin = $currentUser['is_admin'] ?? false;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="lt-nav" aria-label="Main navigation">
|
<nav class="lt-nav" aria-label="Main navigation">
|
||||||
<a href="/" class="lt-nav-link <?php echo $activeNav === 'dashboard' ? 'active' : ''; ?>">Dashboard</a>
|
<?php foreach ($navLinks as $link): ?>
|
||||||
<a href="/tickets" class="lt-nav-link <?php echo $activeNav === 'tickets' ? 'active' : ''; ?>">Tickets</a>
|
<?php
|
||||||
|
$skipAdminOnly = !empty($link['adminOnly']) && !$isAdmin;
|
||||||
|
if ($skipAdminOnly) continue;
|
||||||
|
?>
|
||||||
|
|
||||||
<?php if ($isAdmin): ?>
|
<?php if (!empty($link['children'])): ?>
|
||||||
|
<?php $parentActive = str_starts_with($activeNav, $link['key']); ?>
|
||||||
<div class="lt-nav-dropdown">
|
<div class="lt-nav-dropdown">
|
||||||
<a href="#" class="lt-nav-link <?php echo str_starts_with($activeNav, 'admin') ? 'active' : ''; ?>">
|
<a href="#" class="lt-nav-link <?php echo $parentActive ? 'active' : ''; ?>">
|
||||||
Admin ▾
|
<?php echo htmlspecialchars($link['label']); ?> ▾
|
||||||
</a>
|
</a>
|
||||||
<ul class="lt-nav-dropdown-menu">
|
<ul class="lt-nav-dropdown-menu">
|
||||||
<li><a href="/admin/templates">Templates</a></li>
|
<?php foreach ($link['children'] as $child): ?>
|
||||||
<li><a href="/admin/workflow">Workflow</a></li>
|
<li><a href="<?php echo htmlspecialchars($child['href']); ?>"><?php echo htmlspecialchars($child['label']); ?></a></li>
|
||||||
<li><a href="/admin/recurring-tickets">Recurring</a></li>
|
<?php endforeach; ?>
|
||||||
<li><a href="/admin/custom-fields">Custom Fields</a></li>
|
|
||||||
<li><a href="/admin/user-activity">User Activity</a></li>
|
|
||||||
<li><a href="/admin/audit-log">Audit Log</a></li>
|
|
||||||
<li><a href="/admin/api-keys">API Keys</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="<?php echo htmlspecialchars($link['href']); ?>"
|
||||||
|
class="lt-nav-link <?php echo $activeNav === $link['key'] ? 'active' : ''; ?>">
|
||||||
|
<?php echo htmlspecialchars($link['label']); ?>
|
||||||
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -130,6 +143,8 @@ $isAdmin = $currentUser['is_admin'] ?? false;
|
|||||||
username: <?php echo json_encode($currentUser['username'] ?? ''); ?>,
|
username: <?php echo json_encode($currentUser['username'] ?? ''); ?>,
|
||||||
isAdmin: <?php echo $isAdmin ? 'true' : 'false'; ?>,
|
isAdmin: <?php echo $isAdmin ? 'true' : 'false'; ?>,
|
||||||
};
|
};
|
||||||
|
// App-specific config: set window.APP_CONFIG in your app's own <script> block,
|
||||||
|
// not here. This file is shared across all apps.
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Unified design system JS -->
|
<!-- Unified design system JS -->
|
||||||
@@ -137,7 +152,7 @@ $isAdmin = $currentUser['is_admin'] ?? false;
|
|||||||
|
|
||||||
<!-- App-specific JS (cache-busted) -->
|
<!-- App-specific JS (cache-busted) -->
|
||||||
<script nonce="<?php echo $nonce; ?>"
|
<script nonce="<?php echo $nonce; ?>"
|
||||||
src="/assets/js/app.js?v=<?php echo $config['JS_VERSION'] ?? '20260314'; ?>">
|
src="/assets/js/app.js?v=<?php echo $config['JS_VERSION'] ?? '1'; ?>">
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Per-page inline JS goes here in the including view, e.g.: -->
|
<!-- Per-page inline JS goes here in the including view, e.g.: -->
|
||||||
|
|||||||
+35
-21
@@ -17,7 +17,8 @@
|
|||||||
Required Flask setup (app.py):
|
Required Flask setup (app.py):
|
||||||
- Pass `nonce` into every render_template() call via a context processor
|
- Pass `nonce` into every render_template() call via a context processor
|
||||||
- Pass `user` dict from _get_user() helper
|
- Pass `user` dict from _get_user() helper
|
||||||
- Pass `config` dict with APP_NAME, etc.
|
- Pass `config` dict with APP_NAME, APP_SUBTITLE, etc.
|
||||||
|
- Pass `nav_links` list of dicts defining navigation
|
||||||
|
|
||||||
Context processor example:
|
Context processor example:
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
@@ -25,6 +26,16 @@
|
|||||||
import base64, os
|
import base64, os
|
||||||
nonce = base64.b64encode(os.urandom(16)).decode()
|
nonce = base64.b64encode(os.urandom(16)).decode()
|
||||||
return dict(nonce=nonce, user=_get_user(), config=_config())
|
return dict(nonce=nonce, user=_get_user(), config=_config())
|
||||||
|
|
||||||
|
nav_links format (pass from route or context processor):
|
||||||
|
nav_links = [
|
||||||
|
{'href': url_for('index'), 'key': 'dashboard', 'label': 'Dashboard'},
|
||||||
|
{'href': url_for('settings'), 'key': 'settings', 'label': 'Settings'},
|
||||||
|
# Admin-only dropdown:
|
||||||
|
{'label': 'Admin', 'key': 'admin', 'admin_only': True, 'children': [
|
||||||
|
{'href': url_for('admin_users'), 'label': 'Users'},
|
||||||
|
]},
|
||||||
|
]
|
||||||
#}
|
#}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -64,24 +75,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="lt-nav" aria-label="Main navigation">
|
<nav class="lt-nav" aria-label="Main navigation">
|
||||||
{# Each page sets {% block active_nav %}pagename{% endblock %} #}
|
|
||||||
{% set active = self.active_nav() | default('') %}
|
{% set active = self.active_nav() | default('') %}
|
||||||
<a href="{{ url_for('index') }}"
|
{% for link in nav_links | default([]) %}
|
||||||
class="lt-nav-link {% if active == 'dashboard' %}active{% endif %}">
|
{% if not link.get('admin_only') or 'admin' in user.groups %}
|
||||||
Dashboard
|
{% if link.get('children') %}
|
||||||
|
<div class="lt-nav-dropdown">
|
||||||
|
<a href="#" class="lt-nav-link {% if active.startswith(link.key) %}active{% endif %}">
|
||||||
|
{{ link.label }} ▾
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('links_page') }}"
|
<ul class="lt-nav-dropdown-menu">
|
||||||
class="lt-nav-link {% if active == 'links' %}active{% endif %}">
|
{% for child in link.children %}
|
||||||
Link Debug
|
<li><a href="{{ child.href }}">{{ child.label }}</a></li>
|
||||||
</a>
|
{% endfor %}
|
||||||
<a href="{{ url_for('inspector') }}"
|
</ul>
|
||||||
class="lt-nav-link {% if active == 'inspector' %}active{% endif %}">
|
</div>
|
||||||
Inspector
|
{% else %}
|
||||||
</a>
|
<a href="{{ link.href }}"
|
||||||
<a href="{{ url_for('suppressions_page') }}"
|
class="lt-nav-link {% if active == link.key %}active{% endif %}">
|
||||||
class="lt-nav-link {% if active == 'suppressions' %}active{% endif %}">
|
{{ link.label }}
|
||||||
Suppressions
|
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -107,17 +122,16 @@
|
|||||||
All <script> tags MUST carry the nonce attribute for CSP.
|
All <script> tags MUST carry the nonce attribute for CSP.
|
||||||
========================================================= -->
|
========================================================= -->
|
||||||
|
|
||||||
<!-- Runtime config (no CSRF needed for Gandalf — SameSite=Strict) -->
|
<!-- Runtime config injected by the server -->
|
||||||
<script nonce="{{ nonce }}">
|
<script nonce="{{ nonce }}">
|
||||||
window.APP_CONFIG = {
|
|
||||||
ticketWebUrl: {{ config.get('ticket_api', {}).get('web_url', 'https://t.lotusguild.org/ticket/') | tojson }},
|
|
||||||
};
|
|
||||||
window.CURRENT_USER = {
|
window.CURRENT_USER = {
|
||||||
username: {{ user.username | tojson }},
|
username: {{ user.username | tojson }},
|
||||||
name: {{ (user.name or user.username) | tojson }},
|
name: {{ (user.name or user.username) | tojson }},
|
||||||
groups: {{ user.groups | tojson }},
|
groups: {{ user.groups | tojson }},
|
||||||
isAdmin: {{ ('admin' in user.groups) | lower }},
|
isAdmin: {{ ('admin' in user.groups) | lower }},
|
||||||
};
|
};
|
||||||
|
// App-specific config: set window.APP_CONFIG in your app's own template block,
|
||||||
|
// not here. This file is shared across all apps.
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Unified design system JS -->
|
<!-- Unified design system JS -->
|
||||||
@@ -136,6 +150,6 @@
|
|||||||
|
|
||||||
{% block active_nav %}dashboard{% endblock %}
|
{% block active_nav %}dashboard{% endblock %}
|
||||||
|
|
||||||
Values: dashboard | links | inspector | suppressions
|
Value must match a 'key' in your nav_links list.
|
||||||
--------------------------------------------------------------- #}
|
--------------------------------------------------------------- #}
|
||||||
{% block active_nav %}{% endblock %}
|
{% block active_nav %}{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user