mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-01 06:50:00 +00:00
[frontend] Settings navigation design (#1652)
* change header image alignment (cherry picked from commit df1bb339a5c597a2b668cedb3dafec5a390df120) * big mess navigation refactor * bit of cleanup * minor css tweaks * fix error rendering code for remote emoji * refactor navigation structure code * refactor styling * fix className * stash * restructure navigation generation * url wildcard formatting * remove un-implemented User menu entry * remove commented lines * clarify permissions check * invert permissions logic for clarity
This commit is contained in:
parent
6cf6613540
commit
0746ef741a
20 changed files with 627 additions and 312 deletions
|
@ -101,15 +101,15 @@ $input-border: $blue1;
|
||||||
$input-error-border: $error3;
|
$input-error-border: $error3;
|
||||||
$input-focus-border: $blue3;
|
$input-focus-border: $blue3;
|
||||||
|
|
||||||
$settings-nav-bg: $bg-accent;
|
$settings-nav-bg: $bg;
|
||||||
$settings-nav-header-fg: $gray1;
|
$settings-nav-header-fg: $orange2;
|
||||||
$settings-nav-header-bg: $orange1;
|
|
||||||
|
|
||||||
$settings-nav-bg-hover: $gray3;
|
$settings-nav-bg-hover: $gray3;
|
||||||
/* $settings-nav-fg-hover: $gray1; */
|
$settings-nav-fg-hover: $fg;
|
||||||
|
|
||||||
$settings-nav-bg-active: $gray2;
|
$settings-nav-bg-active: $blue3;
|
||||||
/* $settings-nav-fg-active: $orange2; */
|
$settings-nav-border-active: $info-bg;
|
||||||
|
$settings-nav-fg-active: $gray2;
|
||||||
|
|
||||||
$error-fg: $error1;
|
$error-fg: $error1;
|
||||||
$error-bg: $error2;
|
$error-bg: $error2;
|
||||||
|
|
|
@ -97,11 +97,10 @@ header {
|
||||||
header a {
|
header a {
|
||||||
margin: 2rem;
|
margin: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
margin-bottom: 1rem;
|
|
||||||
align-self: center;
|
align-self: center;
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
}
|
}
|
||||||
|
@ -120,7 +119,6 @@ header a {
|
||||||
}
|
}
|
||||||
|
|
||||||
.excerpt-top {
|
.excerpt-top {
|
||||||
margin-top: -1rem;
|
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
@ -515,12 +513,9 @@ label {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-account-card {
|
}
|
||||||
/* display: inline-grid;
|
|
||||||
grid-template-columns: 4rem auto;
|
.account-card {
|
||||||
grid-template-rows: 4rem;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0.5rem; */
|
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
grid-template-rows: auto auto;
|
grid-template-rows: auto auto;
|
||||||
|
@ -550,7 +545,6 @@ label {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
grid-row: 1 / span 2;
|
grid-row: 1 / span 2;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 30rem) {
|
@media screen and (max-width: 30rem) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ const { useComboBoxInput, useFileInput, useValue } = require("../../../lib/form"
|
||||||
const { CategorySelect } = require("../category-select");
|
const { CategorySelect } = require("../category-select");
|
||||||
|
|
||||||
const useFormSubmit = require("../../../lib/form/submit");
|
const useFormSubmit = require("../../../lib/form/submit");
|
||||||
|
const { useBaseUrl } = require("../../../lib/navigation/util");
|
||||||
|
|
||||||
const FakeToot = require("../../../components/fake-toot");
|
const FakeToot = require("../../../components/fake-toot");
|
||||||
const FormWithData = require("../../../lib/form/form-with-data");
|
const FormWithData = require("../../../lib/form/form-with-data");
|
||||||
|
@ -36,16 +37,15 @@ const { FileInput } = require("../../../components/form/inputs");
|
||||||
const MutationButton = require("../../../components/form/mutation-button");
|
const MutationButton = require("../../../components/form/mutation-button");
|
||||||
const { Error } = require("../../../components/error");
|
const { Error } = require("../../../components/error");
|
||||||
|
|
||||||
const base = "/settings/custom-emoji/local";
|
module.exports = function EmojiDetailRoute({ }) {
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
module.exports = function EmojiDetailRoute() {
|
let [_match, params] = useRoute(`${baseUrl}/:emojiId`);
|
||||||
let [_match, params] = useRoute(`${base}/:emojiId`);
|
|
||||||
if (params?.emojiId == undefined) {
|
if (params?.emojiId == undefined) {
|
||||||
return <Redirect to={base} />;
|
return <Redirect to={baseUrl} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className="emoji-detail">
|
<div className="emoji-detail">
|
||||||
<Link to={base}><a>< go back</a></Link>
|
<Link to={baseUrl}><a>< go back</a></Link>
|
||||||
<FormWithData dataQuery={query.useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} />
|
<FormWithData dataQuery={query.useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -53,6 +53,7 @@ module.exports = function EmojiDetailRoute() {
|
||||||
};
|
};
|
||||||
|
|
||||||
function EmojiDetailForm({ data: emoji }) {
|
function EmojiDetailForm({ data: emoji }) {
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
const form = {
|
const form = {
|
||||||
id: useValue("id", emoji.id),
|
id: useValue("id", emoji.id),
|
||||||
category: useComboBoxInput("category", { source: emoji }),
|
category: useComboBoxInput("category", { source: emoji }),
|
||||||
|
@ -78,7 +79,7 @@ function EmojiDetailForm({ data: emoji }) {
|
||||||
const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation();
|
const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation();
|
||||||
|
|
||||||
if (deleteResult.isSuccess) {
|
if (deleteResult.isSuccess) {
|
||||||
return <Redirect to={base} />;
|
return <Redirect to={baseUrl} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -25,15 +25,13 @@ const { Switch, Route } = require("wouter");
|
||||||
const EmojiOverview = require("./overview");
|
const EmojiOverview = require("./overview");
|
||||||
const EmojiDetail = require("./detail");
|
const EmojiDetail = require("./detail");
|
||||||
|
|
||||||
const base = "/settings/custom-emoji/local";
|
module.exports = function CustomEmoji({ baseUrl }) {
|
||||||
|
|
||||||
module.exports = function CustomEmoji() {
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${base}/:emojiId`}>
|
<Route path={`${baseUrl}/:emojiId`}>
|
||||||
<EmojiDetail baseUrl={base} />
|
<EmojiDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<EmojiOverview baseUrl={base} />
|
<EmojiOverview />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,12 +29,13 @@ const { useTextInput } = require("../../../lib/form");
|
||||||
|
|
||||||
const query = require("../../../lib/query");
|
const query = require("../../../lib/query");
|
||||||
const { useEmojiByCategory } = require("../category-select");
|
const { useEmojiByCategory } = require("../category-select");
|
||||||
|
const { useBaseUrl } = require("../../../lib/navigation/util");
|
||||||
|
|
||||||
const Loading = require("../../../components/loading");
|
const Loading = require("../../../components/loading");
|
||||||
const { Error } = require("../../../components/error");
|
const { Error } = require("../../../components/error");
|
||||||
const { TextInput } = require("../../../components/form/inputs");
|
const { TextInput } = require("../../../components/form/inputs");
|
||||||
|
|
||||||
module.exports = function EmojiOverview({ baseUrl }) {
|
module.exports = function EmojiOverview({ }) {
|
||||||
const {
|
const {
|
||||||
data: emoji = [],
|
data: emoji = [],
|
||||||
isLoading,
|
isLoading,
|
||||||
|
@ -51,7 +52,7 @@ module.exports = function EmojiOverview({ baseUrl }) {
|
||||||
} else {
|
} else {
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
<EmojiList emoji={emoji} baseUrl={baseUrl} />
|
<EmojiList emoji={emoji} />
|
||||||
<NewEmojiForm emoji={emoji} />
|
<NewEmojiForm emoji={emoji} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -70,7 +71,7 @@ module.exports = function EmojiOverview({ baseUrl }) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function EmojiList({ emoji, baseUrl }) {
|
function EmojiList({ emoji }) {
|
||||||
const filterField = useTextInput("filter");
|
const filterField = useTextInput("filter");
|
||||||
const filter = filterField.value;
|
const filter = filterField.value;
|
||||||
|
|
||||||
|
@ -116,7 +117,7 @@ function EmojiList({ emoji, baseUrl }) {
|
||||||
? (
|
? (
|
||||||
<div className="entries scrolling">
|
<div className="entries scrolling">
|
||||||
{filteredEmoji.map(([category, entries]) => {
|
{filteredEmoji.map(([category, entries]) => {
|
||||||
return <EmojiCategory key={category} category={category} entries={entries} baseUrl={baseUrl} />;
|
return <EmojiCategory key={category} category={category} entries={entries} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -128,7 +129,8 @@ function EmojiList({ emoji, baseUrl }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmojiCategory({ category, entries, baseUrl }) {
|
function EmojiCategory({ category, entries }) {
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
return (
|
return (
|
||||||
<div className="entry">
|
<div className="entry">
|
||||||
<b>{category}</b>
|
<b>{category}</b>
|
||||||
|
|
|
@ -25,6 +25,7 @@ const ParseFromToot = require("./parse-from-toot");
|
||||||
|
|
||||||
const query = require("../../../lib/query");
|
const query = require("../../../lib/query");
|
||||||
const Loading = require("../../../components/loading");
|
const Loading = require("../../../components/loading");
|
||||||
|
const { Error } = require("../../../components/error");
|
||||||
|
|
||||||
module.exports = function RemoteEmoji() {
|
module.exports = function RemoteEmoji() {
|
||||||
// local emoji are queried for shortcode collision detection
|
// local emoji are queried for shortcode collision detection
|
||||||
|
@ -42,7 +43,7 @@ module.exports = function RemoteEmoji() {
|
||||||
<>
|
<>
|
||||||
<h1>Custom Emoji (remote)</h1>
|
<h1>Custom Emoji (remote)</h1>
|
||||||
{error &&
|
{error &&
|
||||||
<div className="error accent">{error}</div>
|
<Error error={error} />
|
||||||
}
|
}
|
||||||
{isLoading
|
{isLoading
|
||||||
? <Loading />
|
? <Loading />
|
||||||
|
|
|
@ -79,7 +79,7 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
|
||||||
showError={false}
|
showError={false}
|
||||||
/>
|
/>
|
||||||
<label className="button with-icon">
|
<label className="button with-icon">
|
||||||
<i class="fa fa-fw " aria-hidden="true" />
|
<i className="fa fa-fw " aria-hidden="true" />
|
||||||
Import file
|
Import file
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
|
|
@ -33,9 +33,7 @@ const useFormSubmit = require("../../../lib/form/submit");
|
||||||
const ProcessImport = require("./process");
|
const ProcessImport = require("./process");
|
||||||
const ImportExportForm = require("./form");
|
const ImportExportForm = require("./form");
|
||||||
|
|
||||||
const baseUrl = "/settings/admin/federation/import-export";
|
module.exports = function ImportExport({ baseUrl }) {
|
||||||
|
|
||||||
module.exports = function ImportExport() {
|
|
||||||
const form = {
|
const form = {
|
||||||
domains: useTextInput("domains"),
|
domains: useTextInput("domains"),
|
||||||
exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true })
|
exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true })
|
||||||
|
|
|
@ -22,13 +22,11 @@
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
const { Switch, Route } = require("wouter");
|
const { Switch, Route } = require("wouter");
|
||||||
|
|
||||||
const baseUrl = `/settings/admin/federation`;
|
|
||||||
|
|
||||||
const InstanceOverview = require("./overview");
|
const InstanceOverview = require("./overview");
|
||||||
const InstanceDetail = require("./detail");
|
const InstanceDetail = require("./detail");
|
||||||
const InstanceImportExport = require("./import-export");
|
const InstanceImportExport = require("./import-export");
|
||||||
|
|
||||||
module.exports = function Federation({ }) {
|
module.exports = function Federation({ baseUrl }) {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${baseUrl}/import-export/:list?`}>
|
<Route path={`${baseUrl}/import-export/:list?`}>
|
||||||
|
|
|
@ -34,8 +34,10 @@ const { TextArea } = require("../../components/form/inputs");
|
||||||
|
|
||||||
const MutationButton = require("../../components/form/mutation-button");
|
const MutationButton = require("../../components/form/mutation-button");
|
||||||
const Username = require("./username");
|
const Username = require("./username");
|
||||||
|
const { useBaseUrl } = require("../../lib/navigation/util");
|
||||||
|
|
||||||
module.exports = function ReportDetail({ baseUrl }) {
|
module.exports = function ReportDetail({ }) {
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
let [_match, params] = useRoute(`${baseUrl}/:reportId`);
|
let [_match, params] = useRoute(`${baseUrl}/:reportId`);
|
||||||
if (params?.reportId == undefined) {
|
if (params?.reportId == undefined) {
|
||||||
return <Redirect to={baseUrl} />;
|
return <Redirect to={baseUrl} />;
|
||||||
|
|
|
@ -28,23 +28,22 @@ const FormWithData = require("../../lib/form/form-with-data");
|
||||||
|
|
||||||
const ReportDetail = require("./detail");
|
const ReportDetail = require("./detail");
|
||||||
const Username = require("./username");
|
const Username = require("./username");
|
||||||
|
const { useBaseUrl } = require("../../lib/navigation/util");
|
||||||
|
|
||||||
const baseUrl = "/settings/admin/reports";
|
module.exports = function Reports({ baseUrl }) {
|
||||||
|
|
||||||
module.exports = function Reports() {
|
|
||||||
return (
|
return (
|
||||||
<div className="reports">
|
<div className="reports">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${baseUrl}/:reportId`}>
|
<Route path={`${baseUrl}/:reportId`}>
|
||||||
<ReportDetail baseUrl={baseUrl} />
|
<ReportDetail />
|
||||||
</Route>
|
</Route>
|
||||||
<ReportOverview baseUrl={baseUrl} />
|
<ReportOverview />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function ReportOverview({ _baseUrl }) {
|
function ReportOverview({ }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Reports</h1>
|
<h1>Reports</h1>
|
||||||
|
@ -79,6 +78,7 @@ function ReportsList({ data: reports }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReportEntry({ report }) {
|
function ReportEntry({ report }) {
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
const from = report.account;
|
const from = report.account;
|
||||||
const target = report.target_account;
|
const target = report.target_account;
|
||||||
|
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const React = require("react");
|
|
||||||
const { Link, useRoute } = require("wouter");
|
|
||||||
|
|
||||||
module.exports = function NavButton({ href, name }) {
|
|
||||||
const [isActive] = useRoute(`${href}/:anything?`);
|
|
||||||
return (
|
|
||||||
<Link href={href}>
|
|
||||||
<a className={isActive ? "active" : ""} data-content={name}>
|
|
||||||
{name}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
47
web/source/settings/components/user-logout-card.jsx
Normal file
47
web/source/settings/components/user-logout-card.jsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
|
||||||
|
const query = require("../lib/query");
|
||||||
|
|
||||||
|
const Loading = require("./loading");
|
||||||
|
|
||||||
|
module.exports = function UserLogoutCard() {
|
||||||
|
const { data: profile, isLoading } = query.useVerifyCredentialsQuery();
|
||||||
|
const { data: instance } = query.useInstanceQuery();
|
||||||
|
const [logoutQuery] = query.useLogoutMutation();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="account-card">
|
||||||
|
<img className="avatar" src={profile.avatar} alt="" />
|
||||||
|
<h3 className="text-cutoff">{profile.display_name?.length > 0 ? profile.display_name : profile.acct}</h3>
|
||||||
|
<span className="text-cutoff">@{profile.username}@{instance?.account_domain}</span>
|
||||||
|
<a onClick={logoutQuery} href="#" aria-label="Log out" title="Log out" className="logout">
|
||||||
|
<i className="fa fa-fw fa-sign-out" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -23,61 +23,59 @@ const React = require("react");
|
||||||
const ReactDom = require("react-dom/client");
|
const ReactDom = require("react-dom/client");
|
||||||
const { Provider } = require("react-redux");
|
const { Provider } = require("react-redux");
|
||||||
const { PersistGate } = require("redux-persist/integration/react");
|
const { PersistGate } = require("redux-persist/integration/react");
|
||||||
const { Switch, Route, Redirect } = require("wouter");
|
|
||||||
|
|
||||||
const query = require("./lib/query");
|
|
||||||
|
|
||||||
const { store, persistor } = require("./redux");
|
const { store, persistor } = require("./redux");
|
||||||
|
const { createNavigation, Menu, Item } = require("./lib/navigation");
|
||||||
|
|
||||||
const AuthorizationGate = require("./components/authorization");
|
const AuthorizationGate = require("./components/authorization");
|
||||||
const Loading = require("./components/loading");
|
const Loading = require("./components/loading");
|
||||||
|
const UserLogoutCard = require("./components/user-logout-card");
|
||||||
|
const { RoleContext } = require("./lib/navigation/util");
|
||||||
|
|
||||||
require("./style.css");
|
require("./style.css");
|
||||||
|
|
||||||
// TODO: nested categories?
|
const { Sidebar, ViewRouter } = createNavigation("/settings", [
|
||||||
const nav = {
|
Menu("User", [
|
||||||
"User": {
|
Item("Profile", { icon: "fa-user" }, require("./user/profile")),
|
||||||
"Profile": require("./user/profile.js"),
|
Item("Settings", { icon: "fa-cogs" }, require("./user/settings")),
|
||||||
"Settings": require("./user/settings.js"),
|
]),
|
||||||
},
|
Menu("Moderation", {
|
||||||
"Admin": {
|
url: "admin",
|
||||||
adminOnly: true,
|
permissions: ["admin"]
|
||||||
"Instance Settings": require("./admin/settings.js"),
|
}, [
|
||||||
"Actions": require("./admin/actions"),
|
Item("Reports", { icon: "fa-flag", wildcard: true }, require("./admin/reports")),
|
||||||
"Federation": require("./admin/federation"),
|
Menu("Federation", { icon: "fa-hubzilla" }, [
|
||||||
"Reports": require("./admin/reports")
|
Item("Federation", { icon: "fa-hubzilla", url: "", wildcard: true }, require("./admin/federation")),
|
||||||
},
|
Item("Import/Export", { icon: "fa-floppy-o", wildcard: true }, require("./admin/federation/import-export")),
|
||||||
"Custom Emoji": {
|
])
|
||||||
adminOnly: true,
|
]),
|
||||||
"Local": require("./admin/emoji/local"),
|
Menu("Administration", {
|
||||||
"Remote": require("./admin/emoji/remote"),
|
url: "admin",
|
||||||
}
|
defaultUrl: "/settings/admin/settings",
|
||||||
};
|
permissions: ["admin"]
|
||||||
|
}, [
|
||||||
const { sidebar, panelRouter } = require("./lib/get-views")(nav);
|
Item("Actions", { icon: "fa-bolt" }, require("./admin/actions")),
|
||||||
|
Menu("Custom Emoji", { icon: "fa-smile-o" }, [
|
||||||
|
Item("Local", { icon: "fa-home", wildcard: true }, require("./admin/emoji/local")),
|
||||||
|
Item("Remote", { icon: "fa-cloud" }, require("./admin/emoji/remote"))
|
||||||
|
]),
|
||||||
|
Item("Settings", { icon: "fa-sliders" }, require("./admin/settings"))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
function App({ account }) {
|
function App({ account }) {
|
||||||
const isAdmin = account.role.name == "admin";
|
const permissions = [account.role.name];
|
||||||
const [logoutQuery] = query.useLogoutMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<RoleContext.Provider value={permissions}>
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
{sidebar.all}
|
<UserLogoutCard />
|
||||||
{isAdmin && sidebar.admin}
|
<Sidebar />
|
||||||
<button className="logout" onClick={logoutQuery}>
|
|
||||||
Log out
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<section className="with-sidebar">
|
<section className="with-sidebar">
|
||||||
<Switch>
|
<ViewRouter />
|
||||||
{panelRouter.all}
|
|
||||||
{isAdmin && panelRouter.admin}
|
|
||||||
<Route>
|
|
||||||
<Redirect to="/settings/user" />
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
</section>
|
</section>
|
||||||
</>
|
</RoleContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const React = require("react");
|
|
||||||
const { Link, Route, Redirect } = require("wouter");
|
|
||||||
const { ErrorBoundary } = require("react-error-boundary");
|
|
||||||
|
|
||||||
const { ErrorFallback } = require("../components/error");
|
|
||||||
const NavButton = require("../components/nav-button");
|
|
||||||
|
|
||||||
function urlSafe(str) {
|
|
||||||
return str.toLowerCase().replace(/\s+/g, "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function getViews(struct) {
|
|
||||||
const sidebar = {
|
|
||||||
all: [],
|
|
||||||
admin: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const panelRouter = {
|
|
||||||
all: [],
|
|
||||||
admin: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.entries(struct).forEach(([name, entries]) => {
|
|
||||||
let sidebarEl = sidebar.all;
|
|
||||||
let panelRouterEl = panelRouter.all;
|
|
||||||
|
|
||||||
if (entries.adminOnly) {
|
|
||||||
sidebarEl = sidebar.admin;
|
|
||||||
panelRouterEl = panelRouter.admin;
|
|
||||||
delete entries.adminOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
let base = `/settings/${urlSafe(name)}`;
|
|
||||||
|
|
||||||
let links = [];
|
|
||||||
|
|
||||||
let firstRoute;
|
|
||||||
|
|
||||||
Object.entries(entries).forEach(([name, ViewComponent]) => {
|
|
||||||
let url = `${base}/${urlSafe(name)}`;
|
|
||||||
|
|
||||||
if (firstRoute == undefined) {
|
|
||||||
firstRoute = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
panelRouterEl.push((
|
|
||||||
<Route path={`${url}/:page*`} key={url}>
|
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { }}>
|
|
||||||
{/* FIXME: implement onReset */}
|
|
||||||
<ViewComponent />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</Route>
|
|
||||||
));
|
|
||||||
|
|
||||||
links.push(
|
|
||||||
<NavButton key={url} href={url} name={name} />
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
panelRouterEl.push(
|
|
||||||
<Route key={base} path={base}>
|
|
||||||
<Redirect to={firstRoute} />
|
|
||||||
</Route>
|
|
||||||
);
|
|
||||||
|
|
||||||
sidebarEl.push(
|
|
||||||
<React.Fragment key={name}>
|
|
||||||
<Link href={firstRoute}>
|
|
||||||
<a>
|
|
||||||
<h2>{name}</h2>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
<nav>
|
|
||||||
{links}
|
|
||||||
</nav>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return { sidebar, panelRouter };
|
|
||||||
};
|
|
141
web/source/settings/lib/navigation/components.jsx
Normal file
141
web/source/settings/lib/navigation/components.jsx
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const { Link, Route, Redirect, Switch, useLocation, useRouter } = require("wouter");
|
||||||
|
const { ErrorBoundary } = require("react-error-boundary");
|
||||||
|
const syncpipe = require("syncpipe");
|
||||||
|
|
||||||
|
const { ErrorFallback } = require("../../components/error");
|
||||||
|
|
||||||
|
const {
|
||||||
|
RoleContext,
|
||||||
|
useHasPermission,
|
||||||
|
checkPermission,
|
||||||
|
BaseUrlContext
|
||||||
|
} = require("./util");
|
||||||
|
|
||||||
|
const ActiveRouteCtx = React.createContext();
|
||||||
|
function useActiveRoute() {
|
||||||
|
return React.useContext(ActiveRouteCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sidebar(menuTree, routing) {
|
||||||
|
const components = menuTree.map((m) => m.MenuEntry);
|
||||||
|
|
||||||
|
return function SidebarComponent() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [location] = useLocation();
|
||||||
|
|
||||||
|
let activeRoute = routing.find((l) => {
|
||||||
|
let [match] = router.matcher(l.routingUrl, location);
|
||||||
|
return match;
|
||||||
|
})?.routingUrl;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="menu-tree">
|
||||||
|
<ul className="top-level">
|
||||||
|
<ActiveRouteCtx.Provider value={activeRoute}>
|
||||||
|
{components}
|
||||||
|
</ActiveRouteCtx.Provider>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ViewRouter(routing, defaultRoute) {
|
||||||
|
return function ViewRouterComponent() {
|
||||||
|
const permissions = React.useContext(RoleContext);
|
||||||
|
|
||||||
|
const filteredRoutes = React.useMemo(() => {
|
||||||
|
return syncpipe(routing, [
|
||||||
|
(_) => _.filter((route) => checkPermission(route.permissions, permissions)),
|
||||||
|
(_) => _.map((route) => {
|
||||||
|
return (
|
||||||
|
<Route path={route.routingUrl} key={route.key}>
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { }}>
|
||||||
|
{/* FIXME: implement onReset */}
|
||||||
|
<BaseUrlContext.Provider value={route.url}>
|
||||||
|
{route.view}
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Route>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}, [permissions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
{filteredRoutes}
|
||||||
|
<Redirect to={defaultRoute} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuComponent({ type, name, url, icon, permissions, links, level, children }) {
|
||||||
|
const activeRoute = useActiveRoute();
|
||||||
|
|
||||||
|
if (!useHasPermission(permissions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = [type];
|
||||||
|
|
||||||
|
if (level == 0) {
|
||||||
|
classes.push("top-level");
|
||||||
|
} else if (level == 1) {
|
||||||
|
classes.push("expanding");
|
||||||
|
} else {
|
||||||
|
classes.push("nested");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActive = links.includes(activeRoute);
|
||||||
|
if (isActive) {
|
||||||
|
classes.push("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = classes.join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={className}>
|
||||||
|
<Link href={url}>
|
||||||
|
<a tabIndex={level == 0 ? "-1" : null} className="title">
|
||||||
|
{icon && <i className={`icon fa fa-fw ${icon}`} aria-hidden="true" />}
|
||||||
|
{name}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
{(type == "category" && (level == 0 || isActive) && children?.length > 0) &&
|
||||||
|
<ul>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Sidebar,
|
||||||
|
ViewRouter,
|
||||||
|
MenuComponent
|
||||||
|
};
|
138
web/source/settings/lib/navigation/index.js
Normal file
138
web/source/settings/lib/navigation/index.js
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const { nanoid } = require("nanoid");
|
||||||
|
const { Redirect } = require("wouter");
|
||||||
|
|
||||||
|
const { urlSafe } = require("./util");
|
||||||
|
|
||||||
|
const {
|
||||||
|
Sidebar,
|
||||||
|
ViewRouter,
|
||||||
|
MenuComponent
|
||||||
|
} = require("./components");
|
||||||
|
|
||||||
|
function createNavigation(rootUrl, menus) {
|
||||||
|
const root = {
|
||||||
|
url: rootUrl,
|
||||||
|
links: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const routing = [];
|
||||||
|
|
||||||
|
const menuTree = menus.map((creatorFunc) =>
|
||||||
|
creatorFunc(root, routing)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
Sidebar: Sidebar(menuTree, routing),
|
||||||
|
ViewRouter: ViewRouter(routing, root.redirectUrl)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuEntry(name, opts, contents) {
|
||||||
|
if (contents == undefined) { // opts argument is optional
|
||||||
|
contents = opts;
|
||||||
|
opts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return function createMenuEntry(root, routing) {
|
||||||
|
const type = Array.isArray(contents) ? "category" : "view";
|
||||||
|
|
||||||
|
let urlParts = [root.url];
|
||||||
|
if (opts.url != "") {
|
||||||
|
urlParts.push(opts.url ?? urlSafe(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = urlParts.join("/");
|
||||||
|
let routingUrl = url;
|
||||||
|
|
||||||
|
if (opts.wildcard) {
|
||||||
|
routingUrl += "/:wildcard*";
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
name, type,
|
||||||
|
url, routingUrl,
|
||||||
|
key: nanoid(),
|
||||||
|
permissions: opts.permissions ?? false,
|
||||||
|
icon: opts.icon,
|
||||||
|
links: [routingUrl],
|
||||||
|
level: (root.level ?? -1) + 1,
|
||||||
|
redirectUrl: opts.defaultUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type == "category") {
|
||||||
|
let entries = contents.map((creatorFunc) => creatorFunc(entry, routing));
|
||||||
|
let routes = [];
|
||||||
|
|
||||||
|
entries.forEach((e) => {
|
||||||
|
// move empty wildcard routes to end of category, to prevent overlap
|
||||||
|
if (e.url == entry.url) {
|
||||||
|
routes.unshift(e);
|
||||||
|
} else {
|
||||||
|
routes.push(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
routes.reverse();
|
||||||
|
|
||||||
|
routing.push(...routes);
|
||||||
|
|
||||||
|
if (opts.redirectUrl != entry.url) {
|
||||||
|
routing.push({
|
||||||
|
key: entry.key,
|
||||||
|
url: entry.url,
|
||||||
|
permissions: entry.permissions,
|
||||||
|
routingUrl: entry.redirectUrl + "/:fallback*",
|
||||||
|
view: React.createElement(Redirect, { to: entry.redirectUrl })
|
||||||
|
});
|
||||||
|
entry.url = entry.redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.links.push(...entry.links);
|
||||||
|
|
||||||
|
entry.MenuEntry = React.createElement(
|
||||||
|
MenuComponent,
|
||||||
|
entry,
|
||||||
|
entries.map((e) => e.MenuEntry)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
entry.links.push(routingUrl);
|
||||||
|
root.links.push(routingUrl);
|
||||||
|
|
||||||
|
entry.view = React.createElement(contents, { baseUrl: url });
|
||||||
|
entry.MenuEntry = React.createElement(MenuComponent, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.redirectUrl == undefined) {
|
||||||
|
root.redirectUrl = entry.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createNavigation,
|
||||||
|
Menu: MenuEntry,
|
||||||
|
Item: MenuEntry
|
||||||
|
};
|
51
web/source/settings/lib/navigation/util.js
Normal file
51
web/source/settings/lib/navigation/util.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const RoleContext = React.createContext([]);
|
||||||
|
const BaseUrlContext = React.createContext(null);
|
||||||
|
|
||||||
|
function urlSafe(str) {
|
||||||
|
return str.toLowerCase().replace(/[\s/]+/g, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
function useHasPermission(permissions) {
|
||||||
|
const roles = React.useContext(RoleContext);
|
||||||
|
return checkPermission(permissions, roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPermission(requiredPermissisons, user) {
|
||||||
|
// requiredPermissions can be 'false', in which case there are no restrictions
|
||||||
|
if (requiredPermissisons === false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// or an array of roles, check if one of the user's roles is sufficient
|
||||||
|
return user.some((role) => requiredPermissisons.includes(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
function useBaseUrl() {
|
||||||
|
return React.useContext(BaseUrlContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
urlSafe, RoleContext, useHasPermission, checkPermission, BaseUrlContext, useBaseUrl
|
||||||
|
};
|
|
@ -34,18 +34,36 @@ section {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
justify-content: start;
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin: 1.5rem;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main section {
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
display: grid;
|
display: grid;
|
||||||
/* keep in sync with base.css .page {} */
|
grid-template-columns: 1fr minmax(auto, 60rem) 1fr;
|
||||||
grid-template-columns: auto minmax(auto, 50rem) auto;
|
grid-template-columns: 1fr min(92%, 60rem) 1fr;
|
||||||
grid-template-columns: auto min(92%, 50rem) auto;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
section.with-sidebar {
|
section.with-sidebar {
|
||||||
border-left: none;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
|
|
||||||
& > div, & > form {
|
& > div, & > form {
|
||||||
border-left: 0.2rem solid $border-accent;
|
border-left: 0.2rem solid $border-accent;
|
||||||
padding-left: 0.4rem;
|
padding-left: 0.4rem;
|
||||||
|
@ -79,79 +97,143 @@ section {
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
margin: 0 1rem;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
background: $settings-nav-bg;
|
background: $bg;
|
||||||
border: $boxshadow-border;
|
|
||||||
box-shadow: $boxshadow;
|
|
||||||
border-radius: $br;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 12rem;
|
min-width: 12rem;
|
||||||
|
|
||||||
a {
|
.account-card {
|
||||||
text-decoration: none;
|
grid-template-columns: auto 1fr auto;
|
||||||
|
|
||||||
|
img.avatar {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:first-child h2 {
|
span {
|
||||||
border-top-left-radius: $br;
|
grid-row: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.logout {
|
||||||
margin: 0;
|
font-size: 1.5rem;
|
||||||
padding: 0.5rem;
|
align-self: center;
|
||||||
font-size: 0.9rem;
|
grid-row: 1 / span 2;
|
||||||
font-weight: bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: $settings-nav-header-fg;
|
|
||||||
background: $settings-nav-header-bg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
&:hover {
|
||||||
|
background: $list-entry-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.menu-tree {
|
||||||
|
ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
.icon {
|
||||||
padding: 1rem;
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* top-level ul */
|
||||||
|
& > ul {
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.top-level { /* top-level categories, orange all-caps titles */
|
||||||
|
border-top: 0.1rem solid $gray3;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
& > a.title {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: 0.1s;
|
color: $settings-nav-header-fg;
|
||||||
|
padding: 0.5rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > ul {
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li.expanding { /* second-level categories, expanding box, active shows nested */
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
color: $fg;
|
color: $fg;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
border: 0.1rem solid transparent;
|
||||||
|
border-radius: $br;
|
||||||
|
padding: 0.5rem;
|
||||||
|
transition: background 0.1s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $settings-nav-fg-hover;
|
color: $settings-nav-fg-hover;
|
||||||
background: $settings-nav-bg-hover;
|
background: $settings-nav-bg-hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus, &:active {
|
||||||
|
border-color: $settings-nav-border-active;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
|
border: 0.1rem solid $settings-nav-border-active;
|
||||||
|
border-radius: $br;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
a {
|
||||||
|
transition: background 0s;
|
||||||
|
border: none;
|
||||||
color: $settings-nav-fg-active;
|
color: $settings-nav-fg-active;
|
||||||
background: $settings-nav-bg-active;
|
background: $settings-nav-bg-active;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: underline;
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* reserve space for bold version of the element, so .active doesn't
|
li.nested { /* any deeper nesting, just has indent */
|
||||||
change container size */
|
a.title {
|
||||||
&::after {
|
padding-left: 1rem;
|
||||||
|
font-weight: normal;
|
||||||
|
color: $fg;
|
||||||
|
background: $gray4;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
color: $fg-accent;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $settings-nav-bg-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
a.title {
|
||||||
|
color: $fg-accent;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: underline;
|
|
||||||
display: block;
|
|
||||||
content: attr(data-content);
|
|
||||||
height: 1px;
|
|
||||||
color: transparent;
|
|
||||||
overflow: hidden;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
nav:last-child a:last-child {
|
|
||||||
border-bottom-left-radius: $br;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.capitalize {
|
.capitalize {
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<div>
|
<div>
|
||||||
<h2>Admin Contact</h2>
|
<h2>Admin Contact</h2>
|
||||||
{{if .instance.ContactAccount}}
|
{{if .instance.ContactAccount}}
|
||||||
<a href="{{.instance.ContactAccount.URL}}" class="contact-account-card">
|
<a href="{{.instance.ContactAccount.URL}}" class="account-card">
|
||||||
<img class="avatar" src="{{.instance.ContactAccount.Avatar}}" alt="" />
|
<img class="avatar" src="{{.instance.ContactAccount.Avatar}}" alt="" />
|
||||||
<h3>
|
<h3>
|
||||||
{{if .instance.ContactAccount.DisplayName}}{{emojify .instance.ContactAccount.Emojis (escape .instance.ContactAccount.DisplayName)}}{{else}}{{.instance.ContactAccount.Username}}{{end}}
|
{{if .instance.ContactAccount.DisplayName}}{{emojify .instance.ContactAccount.Emojis (escape .instance.ContactAccount.DisplayName)}}{{else}}{{.instance.ContactAccount.Username}}{{end}}
|
||||||
|
|
Loading…
Reference in a new issue