Ribbon-Tabs in die TopBar-Zeile verlegt: eine Leiste (Chrome + Tabs), Inhalt darunter
This commit is contained in:
@@ -71,6 +71,7 @@ import type { ResourceManagerHandlers } from "./ui/ResourceManager";
|
|||||||
import { HatchSwatch } from "./ui/hatchPreview";
|
import { HatchSwatch } from "./ui/hatchPreview";
|
||||||
import { TopBar, ViewRibbonTab } from "./ui/TopBar";
|
import { TopBar, ViewRibbonTab } from "./ui/TopBar";
|
||||||
import { RibbonBar } from "./ui/ribbon/RibbonBar";
|
import { RibbonBar } from "./ui/ribbon/RibbonBar";
|
||||||
|
import type { RibbonTabId } from "./ui/ribbon/ribbonItems";
|
||||||
import { Dropdown } from "./ui/Dropdown";
|
import { Dropdown } from "./ui/Dropdown";
|
||||||
import type { DropdownItem } from "./ui/Dropdown";
|
import type { DropdownItem } from "./ui/Dropdown";
|
||||||
import { TextEditorDialog } from "./ui/TextEditorDialog";
|
import { TextEditorDialog } from "./ui/TextEditorDialog";
|
||||||
@@ -355,6 +356,10 @@ export default function App() {
|
|||||||
// Einstellungen sowie der laufende Werkzeug-Zustand (Ref, kein Re-Render je
|
// Einstellungen sowie der laufende Werkzeug-Zustand (Ref, kein Re-Render je
|
||||||
// Mausbewegung) und die Live-Vorschau (Draft → Overlay in PlanView).
|
// Mausbewegung) und die Live-Vorschau (Draft → Overlay in PlanView).
|
||||||
const [activeTool, setActiveTool] = useState<ToolId>("select");
|
const [activeTool, setActiveTool] = useState<ToolId>("select");
|
||||||
|
// Aktiver Ribbon-Tab (2D/3D/BIM/Ansichten). Liegt hier (nicht in der RibbonBar),
|
||||||
|
// weil die Tab-Reiter in der TopBar-Zeile sitzen und der Ribbon-Inhalt darunter
|
||||||
|
// — beide teilen sich denselben aktiven Tab.
|
||||||
|
const [ribbonTab, setRibbonTab] = useState<RibbonTabId>("2d");
|
||||||
const [activeWallTypeId, setActiveWallTypeId] = useState<string>(
|
const [activeWallTypeId, setActiveWallTypeId] = useState<string>(
|
||||||
project.wallTypes[0].id,
|
project.wallTypes[0].id,
|
||||||
);
|
);
|
||||||
@@ -3196,6 +3201,8 @@ export default function App() {
|
|||||||
in den „Ansichten"-Ribbon-Tab gewandert (ViewRibbonTab, unten). */}
|
in den „Ansichten"-Ribbon-Tab gewandert (ViewRibbonTab, unten). */}
|
||||||
<TopBar
|
<TopBar
|
||||||
project={project}
|
project={project}
|
||||||
|
ribbonTab={ribbonTab}
|
||||||
|
onRibbonTab={setRibbonTab}
|
||||||
zoomEnabled={planActive}
|
zoomEnabled={planActive}
|
||||||
onExportPdf={() => setExportOpen(true)}
|
onExportPdf={() => setExportOpen(true)}
|
||||||
onExportDxf={() => setExportDxfOpen(true)}
|
onExportDxf={() => setExportDxfOpen(true)}
|
||||||
@@ -3219,6 +3226,7 @@ export default function App() {
|
|||||||
die aus der TopBar gemergte Ansichts-/Zoom-/Darstellungs-Steuerung
|
die aus der TopBar gemergte Ansichts-/Zoom-/Darstellungs-Steuerung
|
||||||
(ViewRibbonTab); die Werkzeug-Sidebar bleibt vorerst erhalten. */}
|
(ViewRibbonTab); die Werkzeug-Sidebar bleibt vorerst erhalten. */}
|
||||||
<RibbonBar
|
<RibbonBar
|
||||||
|
tab={ribbonTab}
|
||||||
activeTool={activeTool}
|
activeTool={activeTool}
|
||||||
activeCommand={engineView.commandName}
|
activeCommand={engineView.commandName}
|
||||||
toolsEnabled={activeLevel.kind === "floor"}
|
toolsEnabled={activeLevel.kind === "floor"}
|
||||||
|
|||||||
+11
-7
@@ -5341,9 +5341,10 @@ body {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Ribbon-Oberleiste (2D · 3D · BIM · Ansichten) ─────────────────────────
|
/* ── Ribbon (2D · 3D · BIM · Ansichten) ────────────────────────────────────
|
||||||
Datengetriebene Tab-Leiste unter der TopBar (RibbonBar). Kompakte Band-Höhe:
|
Die Tab-REITER (`.ribbon-tabs`) sitzen in der TopBar-Zeile; hier folgt nur der
|
||||||
Tab-Reiter oben, darunter die Werkzeug-/Befehls-Gruppen des aktiven Tabs. */
|
INHALT (`.ribbon`/`.ribbon-body`) des aktiven Tabs darunter. So gibt es genau
|
||||||
|
zwei Bänder: TopBar-Zeile (Chrome + Tabs) oben, Ribbon-Inhalt darunter. */
|
||||||
.ribbon {
|
.ribbon {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -5353,12 +5354,13 @@ body {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
/* Tab-Reiter — leben in der TopBar-Zeile: volle Zeilenhöhe, Aktiv-Unterstrich
|
||||||
|
sitzt an der Unterkante (verbindet optisch mit dem Inhalt darunter). */
|
||||||
.ribbon-tabs {
|
.ribbon-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-self: stretch;
|
||||||
|
align-items: stretch;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: 0 10px;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
}
|
}
|
||||||
.ribbon-tab {
|
.ribbon-tab {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
@@ -5369,7 +5371,9 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
padding: 7px 12px 6px;
|
padding: 0 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
transition: color 0.12s, border-color 0.12s;
|
transition: color 0.12s, border-color 0.12s;
|
||||||
|
|||||||
+17
-1
@@ -15,6 +15,8 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { Dropdown } from "./Dropdown";
|
import { Dropdown } from "./Dropdown";
|
||||||
import type { DropdownItem, DropdownOption } from "./Dropdown";
|
import type { DropdownItem, DropdownOption } from "./Dropdown";
|
||||||
import { WindowControls } from "./WindowControls";
|
import { WindowControls } from "./WindowControls";
|
||||||
|
import { RibbonTabs } from "./ribbon/RibbonBar";
|
||||||
|
import type { RibbonTabId } from "./ribbon/ribbonItems";
|
||||||
import {
|
import {
|
||||||
applyMark,
|
applyMark,
|
||||||
applyPreset,
|
applyPreset,
|
||||||
@@ -84,9 +86,14 @@ const ZOOM_CLUSTER_WIDTH = 140;
|
|||||||
export interface TopBarProps {
|
export interface TopBarProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
|
|
||||||
|
// Ribbon-Tab-Reiter (2D/3D/BIM/Ansichten) sitzen in DIESER Zeile — der Ribbon-
|
||||||
|
// Inhalt darunter (RibbonBar). Der aktive Tab liegt in App (geteilter State).
|
||||||
|
ribbonTab: RibbonTabId;
|
||||||
|
onRibbonTab: (id: RibbonTabId) => void;
|
||||||
|
|
||||||
// Ansichts-/Zoom-/Darstellungs-Steuerung ist in den „Ansichten"-Ribbon-Tab
|
// Ansichts-/Zoom-/Darstellungs-Steuerung ist in den „Ansichten"-Ribbon-Tab
|
||||||
// gewandert (siehe ViewRibbonTab). Die TopBar ist nur noch die schmale globale
|
// gewandert (siehe ViewRibbonTab). Die TopBar ist nur noch die schmale globale
|
||||||
// Leiste: Marke/Ressourcen · Text-Formatierung · Datei/Export/Einstellungen.
|
// Leiste: Marke/Ressourcen · Tabs · Text-Formatierung · Datei/Export/Einstell.
|
||||||
|
|
||||||
/** Ob Zoom/Plan aktiv ist — steuert nur noch, ob Export im Datei-Menü geht. */
|
/** Ob Zoom/Plan aktiv ist — steuert nur noch, ob Export im Datei-Menü geht. */
|
||||||
zoomEnabled: boolean;
|
zoomEnabled: boolean;
|
||||||
@@ -1198,6 +1205,8 @@ export function ViewRibbonTab({
|
|||||||
|
|
||||||
export function TopBar({
|
export function TopBar({
|
||||||
project,
|
project,
|
||||||
|
ribbonTab,
|
||||||
|
onRibbonTab,
|
||||||
zoomEnabled,
|
zoomEnabled,
|
||||||
onExportPdf,
|
onExportPdf,
|
||||||
onExportDxf,
|
onExportDxf,
|
||||||
@@ -1259,6 +1268,13 @@ export function TopBar({
|
|||||||
|
|
||||||
<Sep />
|
<Sep />
|
||||||
|
|
||||||
|
{/* Ribbon-Tab-Reiter (2D/3D/BIM/Ansichten) — sitzen in dieser Zeile, der
|
||||||
|
Ribbon-Inhalt folgt darunter (RibbonBar). So gibt es nur ZWEI Bänder:
|
||||||
|
diese Leiste (Chrome + Tabs) und den Inhalt. */}
|
||||||
|
<RibbonTabs tab={ribbonTab} onTab={onRibbonTab} />
|
||||||
|
|
||||||
|
<Sep />
|
||||||
|
|
||||||
{/* Text-Gruppe (kontextabhängig) — Stil/Schrift/Grösse + B/I/U · L/C/R +
|
{/* Text-Gruppe (kontextabhängig) — Stil/Schrift/Grösse + B/I/U · L/C/R +
|
||||||
Zeilenhöhe. Formatiert das aktuelle Text-Ziel (Raumstempel) live. Bleibt
|
Zeilenhöhe. Formatiert das aktuelle Text-Ziel (Raumstempel) live. Bleibt
|
||||||
in der schmalen globalen Leiste, weil sie eine kontextuelle Formatier-
|
in der schmalen globalen Leiste, weil sie eine kontextuelle Formatier-
|
||||||
|
|||||||
+67
-52
@@ -1,17 +1,16 @@
|
|||||||
// Ribbon-Oberleiste (docs/design/ribbon-ui-plan.md, Phase 1) — datengetriebene
|
// Ribbon-Inhaltsleiste (docs/design/ribbon-ui-plan.md) — rendert den INHALT des
|
||||||
// Tab-Leiste unter der TopBar. Zeigt je Tab (2D · 3D · BIM · Ansichten) die
|
// aktiven Ribbon-Tabs. Die Tab-REITER selbst sitzen in der TopBar-Zeile (eine
|
||||||
// gruppierten Werkzeuge/Befehle aus `RIBBON_TABS`. ADDITIV: die Werkzeug-Sidebar
|
// Leiste: globales Chrome + Tabs), damit es nur ZWEI Bänder gibt: TopBar-Zeile
|
||||||
// (ToolsPanel) bleibt vorerst funktionsfähig — beide teilen sich denselben
|
// (mit Tabs) oben, Ribbon-Inhalt darunter. Der aktive Tab wird als `tab`-Prop
|
||||||
// Aktivierungs-Pfad (onSelectTool / engine.start), es gibt keine Duplikat-Logik.
|
// hereingereicht (State liegt in App, geteilt mit der TopBar).
|
||||||
//
|
//
|
||||||
// Werkzeug-Elemente laufen über `onSelectTool` (gekoppelte Befehle inklusive,
|
// Werkzeug-Elemente laufen über `onSelectTool` (gekoppelte Befehle inklusive, wie
|
||||||
// wie in der Sidebar); Befehls-Elemente über `onRunCommand` (engine.start). Das
|
// in der Sidebar); Befehls-Elemente über `onRunCommand` (engine.start). Der
|
||||||
// Aktiv-Highlight spiegelt für Werkzeuge `activeTool`, für Befehle den laufenden
|
// „Ansichten"-Tab trägt reichen Custom-Inhalt (`viewsContent`, aus der TopBar
|
||||||
// Befehl (`activeCommand`).
|
// gemergt) statt datengetriebener Item-Gruppen.
|
||||||
//
|
//
|
||||||
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { ToolIcon } from "../../panels/ToolsPanel";
|
import { ToolIcon } from "../../panels/ToolsPanel";
|
||||||
@@ -23,6 +22,8 @@ import type { RibbonItem, RibbonTabId } from "./ribbonItems";
|
|||||||
import { CommandIcon } from "./CommandIcon";
|
import { CommandIcon } from "./CommandIcon";
|
||||||
|
|
||||||
export interface RibbonBarProps {
|
export interface RibbonBarProps {
|
||||||
|
/** Aktiver Tab (State liegt in App, geteilt mit den Reitern in der TopBar). */
|
||||||
|
tab: RibbonTabId;
|
||||||
/** Aktives Werkzeug (Highlight der `tool`-Elemente). */
|
/** Aktives Werkzeug (Highlight der `tool`-Elemente). */
|
||||||
activeTool: ToolId;
|
activeTool: ToolId;
|
||||||
/** Name des laufenden Engine-Befehls (Highlight der `command`-Elemente). */
|
/** Name des laufenden Engine-Befehls (Highlight der `command`-Elemente). */
|
||||||
@@ -36,29 +37,28 @@ export interface RibbonBarProps {
|
|||||||
/**
|
/**
|
||||||
* Reicher Inhalt des „Ansichten"-Tabs (aus der TopBar gemergte Ansichts-/Zoom-/
|
* Reicher Inhalt des „Ansichten"-Tabs (aus der TopBar gemergte Ansichts-/Zoom-/
|
||||||
* Darstellungs-Steuerung, `ViewRibbonTab`). Von App geliefert, damit dessen
|
* Darstellungs-Steuerung, `ViewRibbonTab`). Von App geliefert, damit dessen
|
||||||
* Setter/State dort bleiben — analog dazu, wie die TopBar das Layout-Menü als
|
* Setter/State dort bleiben. Fehlt er, zeigt der Tab den Leer-Hinweis.
|
||||||
* Node bekommt. Fehlt er, zeigt der Tab den Leer-Hinweis.
|
|
||||||
*/
|
*/
|
||||||
viewsContent?: ReactNode;
|
viewsContent?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Aktivierungs-/Highlight-Kontext, den jeder Button-Typ braucht. */
|
||||||
|
interface ButtonCtx {
|
||||||
|
activeTool: ToolId;
|
||||||
|
activeCommand: string | null;
|
||||||
|
toolsEnabled: boolean;
|
||||||
|
onSelectTool: (id: ToolId) => void;
|
||||||
|
onRunCommand: (name: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
/** Ein einzelner Ribbon-Button (Werkzeug ODER Befehl). */
|
/** Ein einzelner Ribbon-Button (Werkzeug ODER Befehl). */
|
||||||
function RibbonButton({
|
function RibbonButton({ item, ctx }: { item: RibbonItem; ctx: ButtonCtx }) {
|
||||||
item,
|
|
||||||
activeTool,
|
|
||||||
activeCommand,
|
|
||||||
toolsEnabled,
|
|
||||||
onSelectTool,
|
|
||||||
onRunCommand,
|
|
||||||
}: {
|
|
||||||
item: RibbonItem;
|
|
||||||
} & Omit<RibbonBarProps, "activeCommand"> & { activeCommand: string | null }) {
|
|
||||||
if (item.kind === "tool") {
|
if (item.kind === "tool") {
|
||||||
const tool = getTool(item.id);
|
const tool = getTool(item.id);
|
||||||
// Nur Boden-gebundene Werkzeuge sind ohne aktives Geschoss gesperrt; freie
|
// Nur Boden-gebundene Werkzeuge sind ohne aktives Geschoss gesperrt; freie
|
||||||
// 2D-Werkzeuge (Linie/Kreis/…) bleiben auch auf Zeichnungsebenen nutzbar.
|
// 2D-Werkzeuge (Linie/Kreis/…) bleiben auch auf Zeichnungsebenen nutzbar.
|
||||||
const disabled = !!tool.floorOnly && !toolsEnabled;
|
const disabled = !!tool.floorOnly && !ctx.toolsEnabled;
|
||||||
const active = activeTool === item.id;
|
const active = ctx.activeTool === item.id;
|
||||||
const label = t(tool.labelKey);
|
const label = t(tool.labelKey);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -66,7 +66,7 @@ function RibbonButton({
|
|||||||
className={`ribbon-btn${active ? " active" : ""}`}
|
className={`ribbon-btn${active ? " active" : ""}`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
title={disabled ? t("tool.floorOnlyDisabled") : label}
|
title={disabled ? t("tool.floorOnlyDisabled") : label}
|
||||||
onClick={() => onSelectTool(item.id)}
|
onClick={() => ctx.onSelectTool(item.id)}
|
||||||
>
|
>
|
||||||
<ToolIcon id={item.id} />
|
<ToolIcon id={item.id} />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
@@ -76,13 +76,13 @@ function RibbonButton({
|
|||||||
// Befehls-Element: Label aus der Befehls-Registry ableiten.
|
// Befehls-Element: Label aus der Befehls-Registry ableiten.
|
||||||
const cmd = getCommand(item.name);
|
const cmd = getCommand(item.name);
|
||||||
const label = cmd ? t(cmd.labelKey) : item.name;
|
const label = cmd ? t(cmd.labelKey) : item.name;
|
||||||
const active = activeCommand === item.name;
|
const active = ctx.activeCommand === item.name;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`ribbon-btn${active ? " active" : ""}`}
|
className={`ribbon-btn${active ? " active" : ""}`}
|
||||||
title={label}
|
title={label}
|
||||||
onClick={() => onRunCommand(item.name)}
|
onClick={() => ctx.onRunCommand(item.name)}
|
||||||
>
|
>
|
||||||
<CommandIcon name={item.name} />
|
<CommandIcon name={item.name} />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
@@ -90,30 +90,49 @@ function RibbonButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Tab-REITER — als eigene Komponente exportiert, weil sie in der TopBar-Zeile
|
||||||
|
* gerendert werden (nicht hier). Rein gesteuert über `tab`/`onTab`.
|
||||||
|
*/
|
||||||
|
export function RibbonTabs({
|
||||||
|
tab,
|
||||||
|
onTab,
|
||||||
|
}: {
|
||||||
|
tab: RibbonTabId;
|
||||||
|
onTab: (id: RibbonTabId) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="ribbon-tabs" role="tablist">
|
||||||
|
{RIBBON_TABS.map((tt) => (
|
||||||
|
<button
|
||||||
|
key={tt.id}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={tt.id === tab}
|
||||||
|
className={`ribbon-tab${tt.id === tab ? " active" : ""}`}
|
||||||
|
onClick={() => onTab(tt.id)}
|
||||||
|
>
|
||||||
|
{t(tt.labelKey)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function RibbonBar(props: RibbonBarProps) {
|
export function RibbonBar(props: RibbonBarProps) {
|
||||||
const [tab, setTab] = useState<RibbonTabId>("2d");
|
const current = RIBBON_TABS.find((tt) => tt.id === props.tab) ?? RIBBON_TABS[0];
|
||||||
const current = RIBBON_TABS.find((tt) => tt.id === tab) ?? RIBBON_TABS[0];
|
const ctx: ButtonCtx = {
|
||||||
|
activeTool: props.activeTool,
|
||||||
|
activeCommand: props.activeCommand,
|
||||||
|
toolsEnabled: props.toolsEnabled,
|
||||||
|
onSelectTool: props.onSelectTool,
|
||||||
|
onRunCommand: props.onRunCommand,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ribbon">
|
<div className="ribbon">
|
||||||
{/* Tab-Reiter */}
|
{/* Inhalt des aktiven Tabs. Der „Ansichten"-Tab trägt reichen Custom-Inhalt
|
||||||
<div className="ribbon-tabs" role="tablist">
|
(ViewRibbonTab) statt der datengetriebenen Item-Gruppen. */}
|
||||||
{RIBBON_TABS.map((tt) => (
|
|
||||||
<button
|
|
||||||
key={tt.id}
|
|
||||||
type="button"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={tt.id === tab}
|
|
||||||
className={`ribbon-tab${tt.id === tab ? " active" : ""}`}
|
|
||||||
onClick={() => setTab(tt.id)}
|
|
||||||
>
|
|
||||||
{t(tt.labelKey)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Gruppen des aktiven Tabs. Der „Ansichten"-Tab trägt reichen Custom-
|
|
||||||
Inhalt (ViewRibbonTab) statt der datengetriebenen Item-Gruppen. */}
|
|
||||||
<div className="ribbon-body">
|
<div className="ribbon-body">
|
||||||
{current.id === "views" && props.viewsContent ? (
|
{current.id === "views" && props.viewsContent ? (
|
||||||
props.viewsContent
|
props.viewsContent
|
||||||
@@ -127,11 +146,7 @@ export function RibbonBar(props: RibbonBarProps) {
|
|||||||
<RibbonButton
|
<RibbonButton
|
||||||
key={item.kind === "tool" ? `t:${item.id}` : `c:${item.name}`}
|
key={item.kind === "tool" ? `t:${item.id}` : `c:${item.name}`}
|
||||||
item={item}
|
item={item}
|
||||||
activeTool={props.activeTool}
|
ctx={ctx}
|
||||||
activeCommand={props.activeCommand}
|
|
||||||
toolsEnabled={props.toolsEnabled}
|
|
||||||
onSelectTool={props.onSelectTool}
|
|
||||||
onRunCommand={props.onRunCommand}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user