Match the Technitium canvas: palette, shell, KPI strips, framed tables
The live UI did not follow the canvas because its base layer differed: dark palette in hex (now the canvas oklch values), 13 px body text, flat Frame panels. - Tokens: canvas palette, 14 px body, 1.4 line height; Frame without `dense` gets the 3 px ring; Panel takes a description line. - Shell: 56 px brand button with version, 11 px group headings, user card at the foot, versions moved into the header status popover, Home / group / section crumbs, amber "Attention" status when blocking is off or paused. - New KpiStrip and PageTabs; Dashboard KPIs in three frames and a Refresh button. - Zones: KPI strip, tab with count, views, framed table with the canvas columns. - Apps: Installed / App Store tabs and a grid instead of cards (store inline). - Blocking: state banner and tab counts; one "Blocking" title. - Settings: actions in the header, label-and-help rows with the control on the right. - DHCP, Administration, DNS Client, Logs, Cache, About: subtitles, framed tables, KPI strips where the data exists; Login cover with the three-point list. - docs/compose.shnt.example.yaml: production compose with the console image. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ea98a33b25
commit
87281a87d3
53 files changed
+1011
-487
No files matched your search
@@ -358,7 +358,7 @@ export function BlockLists({
|
||||
if (viewNeed != null) {
|
||||
return (
|
||||
<>
|
||||
<SectionHeader section="Blocking" title="Lists" subtitle={LISTS_SUBTITLE} tabs={tabs} />
|
||||
<SectionHeader title="Blocking" subtitle={LISTS_SUBTITLE} tabs={tabs} />
|
||||
<Locked title="Block Lists" need={viewNeed} />
|
||||
</>
|
||||
)
|
||||
@@ -571,7 +571,7 @@ export function BlockLists({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader section="Blocking" title="Lists" subtitle={LISTS_SUBTITLE} tabs={tabs} />
|
||||
<SectionHeader title="Blocking" subtitle={LISTS_SUBTITLE} tabs={tabs} />
|
||||
<Notifier notice={notice} onClose={() => setNotice(null)} />
|
||||
|
||||
<div className={shared.stack}>
|
||||
|
||||
@@ -82,3 +82,7 @@ second frame, with rounded corners showing under the panel's header. Written und
|
||||
.bad[aria-invalid],
|
||||
.bad[aria-invalid]:focus { border-color: var(--dan); }
|
||||
.invalid { color: var(--dan); font-size: var(--t-2); margin-top: var(--s-4); }
|
||||
|
||||
/* The canvas's state strip above the tabs. */
|
||||
.banner { display:flex; flex-wrap:wrap; align-items:center; gap: var(--s-7); padding: var(--s-7) var(--s-9); margin-bottom: var(--s-9); border:1px solid var(--line); border-radius: var(--r-3); background: color-mix(in oklch, var(--pan2) 50%, transparent); font-size: 13px; color: var(--mute); }
|
||||
.bannerAct { margin-left: auto; }
|
||||
@@ -1,10 +1,17 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getSettings } from '../../api/settings'
|
||||
import { getDashboardStats } from '../../api/dashboard'
|
||||
import { BlockingMenu } from '../dashboard/BlockingMenu'
|
||||
import { Tag } from '../../ui/Tag'
|
||||
import { missing } from './permissions'
|
||||
import { untilText, useBlockingState } from './blocking-state'
|
||||
import { SubTabs } from '../../ui/SubTabs'
|
||||
import { ruleSearch } from './rules-model'
|
||||
import { Overview } from './Overview'
|
||||
import { Rules } from './Rules'
|
||||
import { BlockLists } from './BlockLists'
|
||||
import type { Permissions } from './permissions'
|
||||
import shared from './Blocking.module.css'
|
||||
|
||||
/*
|
||||
The Blocking section, OURS: it replaces Allowed and Blocked with three tabs,
|
||||
@@ -27,6 +34,61 @@ function dropRule(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/* What the tabs count: the rules (blocked and allowed zones) and the lists. Each is read
|
||||
once and only when the session may read it; a failed read leaves the tab without one. */
|
||||
function useCounts(token: string | null, permissions: Permissions) {
|
||||
const [counts, setCounts] = useState<{ Rules?: number; Lists?: number }>({})
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
if (missing(permissions, 'Dashboard.canView') == null) {
|
||||
void getDashboardStats(token).then((o) => {
|
||||
if (alive && o.kind === 'ok') {
|
||||
const s = o.data.stats
|
||||
setCounts((c) => ({ ...c, Rules: s.blockedZones + s.allowedZones }))
|
||||
}
|
||||
})
|
||||
}
|
||||
if (missing(permissions, 'Settings.canView') == null) {
|
||||
void getSettings(token).then((s) => {
|
||||
if (alive && s != null) setCounts((c) => ({ ...c, Lists: s.blockListUrls?.length ?? 0 }))
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
alive = false
|
||||
}
|
||||
// The permission object is read once: the counts are a hint, not a live figure.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token])
|
||||
return counts
|
||||
}
|
||||
|
||||
/* The state strip of the canvas, above the tabs. Overview carries the full status block
|
||||
itself, so the strip is for the other two. */
|
||||
function Banner({ token, permissions }: { token: string | null; permissions: Permissions }) {
|
||||
const { state } = useBlockingState(token, permissions)
|
||||
if (state == null || state === 'failed') return null
|
||||
const canModify = missing(permissions, 'Settings.canModify') == null
|
||||
return (
|
||||
<div className={shared.banner} role="status">
|
||||
{state.kind === 'on' && <Tag tone="ok">Enabled</Tag>}
|
||||
{state.kind === 'paused' && <Tag tone="acc">{untilText(state.till)}</Tag>}
|
||||
{state.kind === 'off' && <Tag>Disabled</Tag>}
|
||||
<span>
|
||||
{state.kind === 'on'
|
||||
? 'Blocking is applied to every query'
|
||||
: state.kind === 'paused'
|
||||
? 'Blocking is paused and resumes automatically'
|
||||
: 'Nothing is being blocked'}
|
||||
</span>
|
||||
{canModify && (
|
||||
<span className={shared.bannerAct}>
|
||||
<BlockingMenu token={token} text={state.kind === 'on' ? 'Temporarily Disable Blocking' : 'Enable Blocking'} onNotice={() => {}} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Blocking({
|
||||
token,
|
||||
sub,
|
||||
@@ -69,7 +131,11 @@ export function Blocking({
|
||||
|
||||
const [listsDirty, setListsDirty] = useState(false)
|
||||
|
||||
const counts = useCounts(token, permissions)
|
||||
|
||||
const tabs = (
|
||||
<>
|
||||
{active !== 'Overview' && <Banner token={token} permissions={permissions} />}
|
||||
<SubTabs
|
||||
label="Blocking sections"
|
||||
section="blocking"
|
||||
@@ -77,7 +143,9 @@ export function Blocking({
|
||||
active={active}
|
||||
onChoose={onSubChange}
|
||||
marks={listsDirty ? { Lists: 'unsaved changes' } : undefined}
|
||||
counts={counts}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
if (active === 'Rules') {
|
||||
|
||||
@@ -330,8 +330,7 @@ export function Overview({
|
||||
</InChrome>
|
||||
|
||||
<SectionHeader
|
||||
section="Blocking"
|
||||
title="Overview"
|
||||
title="Blocking"
|
||||
subtitle={OVERVIEW_SUBTITLE}
|
||||
tabs={tabs}
|
||||
actions={
|
||||
|
||||
@@ -375,7 +375,7 @@ export function Rules({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader section="Blocking" title="Rules" subtitle={RULES_SUBTITLE} tabs={tabs} />
|
||||
<SectionHeader title="Blocking" subtitle={RULES_SUBTITLE} tabs={tabs} />
|
||||
<Notifier notice={notice} onClose={() => setNotice(null)} />
|
||||
|
||||
<div className={shared.stack}>
|
||||
|
||||
Reference in new issue
Block a user