UX: Look and feel changes (#29245)
This PR: - Removes components from being displayed in the card - Adds a DMenu to house previous footer actions - Allows themes to be updated from this grid, with an animation and different border to show the update is happening - Stops position of cards changing when default changes - Fixes outline colour not changing when default changes - Show a global notice on the page when previewing a theme - Allows updating a theme from the grid, and showing an indicator of what theme needs to be updated - Moves "Set as default" to the dropdown for the theme - Show screenshot for theme if it is available - Prevent page reloading when updating the theme - Fixes theme install modal on grid page - Temporarily remove sorting of default theme to the top
This commit is contained in:
parent
4d7f70b923
commit
f902e0fdd7
|
@ -0,0 +1,69 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import I18n from "discourse-i18n";
|
||||
import AdminPageSubheader from "admin/components/admin-page-subheader";
|
||||
import InstallThemeModal from "admin/components/modal/install-theme";
|
||||
import ThemesGrid from "admin/components/themes-grid";
|
||||
|
||||
export default class AdminConfigAreasLookAndFeelThemes extends Component {
|
||||
@service modal;
|
||||
@service router;
|
||||
@service toasts;
|
||||
|
||||
@action
|
||||
installModal() {
|
||||
this.modal.show(InstallThemeModal, {
|
||||
model: { ...this.installThemeOptions() },
|
||||
});
|
||||
}
|
||||
|
||||
// TODO (martin) These install methods may not belong here and they
|
||||
// are incomplete or have stubbed or omitted properties. We may want
|
||||
// to move this to the new config route or a dedicated component
|
||||
// that sits in the route.
|
||||
installThemeOptions() {
|
||||
return {
|
||||
selectedType: "theme",
|
||||
userId: null,
|
||||
content: [],
|
||||
installedThemes: this.args.themes,
|
||||
addTheme: this.addTheme,
|
||||
updateSelectedType: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
addTheme(theme) {
|
||||
this.toasts.success({
|
||||
data: {
|
||||
message: I18n.t("admin.customize.theme.install_success", {
|
||||
theme: theme.name,
|
||||
}),
|
||||
},
|
||||
duration: 2000,
|
||||
});
|
||||
this.router.refresh();
|
||||
}
|
||||
|
||||
<template>
|
||||
<AdminPageSubheader
|
||||
@titleLabel="admin.config_areas.look_and_feel.themes.title"
|
||||
@descriptionLabel="admin.customize.theme.themes_intro_new"
|
||||
@learnMoreUrl="https://meta.discourse.org/t/93648"
|
||||
>
|
||||
<:actions as |actions|>
|
||||
<actions.Primary
|
||||
@action={{this.installModal}}
|
||||
@label="admin.customize.install"
|
||||
@icon="upload"
|
||||
class="admin-look-and-feel__install-theme"
|
||||
/>
|
||||
</:actions>
|
||||
</AdminPageSubheader>
|
||||
|
||||
<div class="admin-detail">
|
||||
<ThemesGrid @themes={{@themes}} />
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -10,7 +10,7 @@ import { COMPONENTS, THEMES } from "admin/models/theme";
|
|||
|
||||
const MIN_NAME_LENGTH = 4;
|
||||
|
||||
export default class InstallTheme extends Component {
|
||||
export default class InstallThemeModal extends Component {
|
||||
@service store;
|
||||
|
||||
@tracked selection = this.args.model.selection || "popular";
|
||||
|
@ -184,9 +184,11 @@ export default class InstallTheme extends Component {
|
|||
}
|
||||
|
||||
if (this.remote || this.popular || this.directRepoInstall) {
|
||||
const duplicate = this.args.model.content.find((theme) =>
|
||||
this.themeHasSameUrl(theme, this.uploadUrl)
|
||||
);
|
||||
const duplicate =
|
||||
this.args.model.content &&
|
||||
this.args.model.content.find((theme) =>
|
||||
this.themeHasSameUrl(theme, this.uploadUrl)
|
||||
);
|
||||
if (duplicate && !this.duplicateRemoteThemeWarning) {
|
||||
const warning = I18n.t("admin.customize.theme.duplicate_remote_theme", {
|
||||
name: duplicate.name,
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { array } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
import ThemesGridPlaceholder from "./themes-grid-placeholder";
|
||||
|
||||
// NOTE (martin): We will need to revisit and improve this component
|
||||
|
@ -20,24 +23,28 @@ export default class ThemeCard extends Component {
|
|||
@service siteSettings;
|
||||
@service toasts;
|
||||
|
||||
@tracked isUpdating = false;
|
||||
|
||||
get themeCardClasses() {
|
||||
return [
|
||||
"theme-card",
|
||||
this.args.theme.get("default") ? "-active" : "",
|
||||
this.isUpdating ? "--updating" : "",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
get themeRouteModels() {
|
||||
return ["themes", this.args.theme.id];
|
||||
}
|
||||
|
||||
get childrenString() {
|
||||
return this.args.theme.childThemes.reduce((acc, theme, idx) => {
|
||||
if (idx === this.args.theme.childThemes.length - 1) {
|
||||
return acc + theme.name;
|
||||
} else {
|
||||
return acc + theme.name + ", ";
|
||||
}
|
||||
}, "");
|
||||
}
|
||||
|
||||
get themePreviewUrl() {
|
||||
return `/admin/themes/${this.args.theme.id}/preview`;
|
||||
}
|
||||
|
||||
get footerActionIcon() {
|
||||
return this.args.theme.isPendingUpdates ? "sync" : "ellipsis-h";
|
||||
}
|
||||
|
||||
// NOTE: inspired by -> https://github.com/discourse/discourse/blob/24caa36eef826bcdaed88aebfa7df154413fb349/app/assets/javascripts/admin/addon/controllers/admin-customize-themes-show.js#L366
|
||||
//
|
||||
// Will also need some cleanup when refactoring other theme code.
|
||||
|
@ -75,12 +82,43 @@ export default class ThemeCard extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
updateTheme() {
|
||||
if (this.isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUpdating = true;
|
||||
this.args.theme
|
||||
.updateToLatest()
|
||||
.then(() => {
|
||||
this.toasts.success({
|
||||
data: {
|
||||
message: I18n.t("admin.customize.theme.update_success", {
|
||||
theme: this.args.theme.name,
|
||||
}),
|
||||
},
|
||||
duration: 2000,
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => {
|
||||
this.isUpdating = false;
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<AdminConfigAreaCard
|
||||
class={{concatClass "theme-card" (if @theme.default "-active")}}
|
||||
class={{this.themeCardClasses}}
|
||||
@translatedHeading={{@theme.name}}
|
||||
>
|
||||
<:content>
|
||||
{{#if @theme.isPendingUpdates}}
|
||||
<span
|
||||
title={{i18n "admin.customize.theme.updates_available_tooltip"}}
|
||||
class="theme-card__update-available"
|
||||
>{{icon "info-circle"}}</span>
|
||||
{{/if}}
|
||||
<div class="theme-card__image-wrapper">
|
||||
{{#if @theme.screenshot_url}}
|
||||
<img
|
||||
|
@ -96,77 +134,78 @@ export default class ThemeCard extends Component {
|
|||
{{#if @theme.description}}
|
||||
<p class="theme-card__description">{{@theme.description}}</p>
|
||||
{{/if}}
|
||||
{{#if @theme.childThemes}}
|
||||
<span class="theme-card__components">{{i18n
|
||||
"admin.customize.theme.components"
|
||||
}}:
|
||||
{{htmlSafe this.childrenString}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="theme-card__footer">
|
||||
<DButton
|
||||
@action={{this.setDefault}}
|
||||
@translatedLabel={{i18n "admin.customize.theme.edit"}}
|
||||
@route="adminCustomizeThemes.show"
|
||||
@routeModels={{this.themeRouteModels}}
|
||||
@class="btn-primary theme-card__button"
|
||||
@preventFocus={{true}}
|
||||
@icon={{if @theme.default "far-check-square" "far-square"}}
|
||||
@class={{concatClass
|
||||
"theme-card__button"
|
||||
(if @theme.default "btn-primary" "btn-default")
|
||||
}}
|
||||
@translatedLabel={{i18n
|
||||
(if
|
||||
@theme.default
|
||||
"admin.customize.theme.default_theme"
|
||||
"admin.customize.theme.set_default_theme"
|
||||
)
|
||||
}}
|
||||
@disabled={{@theme.default}}
|
||||
/>
|
||||
{{#if @theme.isPendingUpdates}}
|
||||
<DButton
|
||||
@route="adminCustomizeThemes.show"
|
||||
@routeModels={{this.themeRouteModels}}
|
||||
@icon="sync"
|
||||
@class="btn btn-flat theme-card__button"
|
||||
@title="admin.customize.theme.updates_available_tooltip"
|
||||
@preventFocus={{true}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if @theme.isBroken}}
|
||||
<DButton
|
||||
@route="adminCustomizeThemes.show"
|
||||
@routeModels={{this.themeRouteModels}}
|
||||
@icon="exclamation-circle"
|
||||
@class="btn btn-flat theme-card__button broken-indicator"
|
||||
@title="admin.customize.theme.broken_theme_tooltip"
|
||||
@preventFocus={{true}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#unless @theme.enabled}}
|
||||
<DButton
|
||||
@route="adminCustomizeThemes.show"
|
||||
@routeModels={{this.themeRouteModels}}
|
||||
@icon="ban"
|
||||
@class="btn btn-flat theme-card__button broken-indicator light-grey-icon"
|
||||
@title="admin.customize.theme.disabled_component_tooltip"
|
||||
@preventFocus={{true}}
|
||||
/>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
<div class="theme-card__footer-actions">
|
||||
<a
|
||||
href={{this.themePreviewUrl}}
|
||||
title={{i18n "admin.customize.explain_preview"}}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
class="btn btn-flat theme-card__button"
|
||||
>{{icon "eye"}}</a>
|
||||
<DButton
|
||||
@route="adminCustomizeThemes.show"
|
||||
@routeModels={{this.themeRouteModels}}
|
||||
@icon="cog"
|
||||
@class="btn-flat theme-card__button"
|
||||
@preventFocus={{true}}
|
||||
/>
|
||||
<DMenu
|
||||
@identifier="theme-card__footer-menu"
|
||||
@triggerClass="theme-card__footer-menu btn-flat"
|
||||
@modalForMobile={{true}}
|
||||
@icon={{this.footerActionIcon}}
|
||||
@label={{if
|
||||
this.isUpdating
|
||||
(i18n "admin.customize.theme.updating")
|
||||
""
|
||||
}}
|
||||
@triggers={{array "click"}}
|
||||
>
|
||||
<:content>
|
||||
<DropdownMenu as |dropdown|>
|
||||
{{#if @theme.isPendingUpdates}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@action={{this.updateTheme}}
|
||||
@icon="download"
|
||||
@class="theme-card__button -update"
|
||||
@preventFocus={{true}}
|
||||
@translatedLabel={{i18n
|
||||
"admin.customize.theme.update_to_latest"
|
||||
}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/if}}
|
||||
{{! TODO: Jordan
|
||||
solutions for broken, disabled states }}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@action={{this.setDefault}}
|
||||
@preventFocus={{true}}
|
||||
@icon={{if
|
||||
@theme.default
|
||||
"far-check-square"
|
||||
"far-square"
|
||||
}}
|
||||
@class="theme-card__button"
|
||||
@translatedLabel={{i18n
|
||||
(if
|
||||
@theme.default
|
||||
"admin.customize.theme.default_theme"
|
||||
"admin.customize.theme.set_default_theme"
|
||||
)
|
||||
}}
|
||||
@disabled={{@theme.default}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
<dropdown.item>
|
||||
<a
|
||||
href={{this.themePreviewUrl}}
|
||||
title={{i18n "admin.customize.explain_preview"}}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
class="btn btn-transparent theme-card__button"
|
||||
>{{icon "eye"}} {{i18n "admin.customize.theme.preview"}}</a>
|
||||
</dropdown.item>
|
||||
</DropdownMenu>
|
||||
</:content>
|
||||
</DMenu>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
||||
import InstallThemeModal from "../components/modal/install-theme";
|
||||
import ThemesGridCard from "./themes-grid-card";
|
||||
|
||||
// NOTE (martin): Much of the JS code in this component is placeholder code. Much
|
||||
|
@ -16,6 +10,8 @@ export default class ThemesGrid extends Component {
|
|||
@service modal;
|
||||
@service router;
|
||||
|
||||
sortedThemes;
|
||||
|
||||
externalResources = [
|
||||
{
|
||||
key: "admin.customize.theme.beginners_guide_title",
|
||||
|
@ -31,9 +27,17 @@ export default class ThemesGrid extends Component {
|
|||
},
|
||||
];
|
||||
|
||||
// Always show the default theme first in the list
|
||||
get sortedThemes() {
|
||||
return this.args.themes.sort((a, b) => {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
// Show default theme at the top of the list on page load,
|
||||
// but don't move it around dynamically if the admin changes the default.
|
||||
//
|
||||
// TODO (martin) Figure out how to make it so we can sort default to the
|
||||
// top but also allow the list of themes to change if an additional theme is
|
||||
// installed. Basically don't want .get("default") to affect the sort after
|
||||
// the first time, but if the whole array changes this needs to be recalculated.
|
||||
this.sortedThemes = this.args.themes.sort((a, b) => {
|
||||
if (a.get("default")) {
|
||||
return -1;
|
||||
} else if (b.get("default")) {
|
||||
|
@ -42,78 +46,11 @@ export default class ThemesGrid extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
// TODO (martin) These install methods may not belong here and they
|
||||
// are incomplete or have stubbed or omitted properties. We may want
|
||||
// to move this to the new config route or a dedicated component
|
||||
// that sits in the route.
|
||||
installThemeOptions() {
|
||||
return {
|
||||
selectedType: "theme",
|
||||
userId: null,
|
||||
content: null,
|
||||
installedThemes: this.args.themes,
|
||||
addTheme: this.addTheme,
|
||||
updateSelectedType: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
addTheme(theme) {
|
||||
this.refresh();
|
||||
theme.setProperties({ recentlyInstalled: true });
|
||||
this.router.transitionTo("adminCustomizeThemes.show", theme.get("id"), {
|
||||
queryParams: {
|
||||
repoName: null,
|
||||
repoUrl: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
installModal() {
|
||||
this.modal.show(InstallThemeModal, {
|
||||
model: { ...this.installThemeOptions() },
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="themes-cards-container">
|
||||
<div class="themes-cards-container__main">
|
||||
{{#each this.sortedThemes as |theme|}}
|
||||
<ThemesGridCard @theme={{theme}} @allThemes={{@themes}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="themes-cards-container__helper">
|
||||
<AdminConfigAreaCard
|
||||
class="theme-card"
|
||||
@heading="admin.config_areas.look_and_feel.themes.new_theme"
|
||||
>
|
||||
<:content>
|
||||
<p class="theme-card__description">{{i18n
|
||||
"admin.customize.theme.themes_intro_new"
|
||||
}}</p>
|
||||
<div class="external-resources">
|
||||
{{#each this.externalResources as |resource|}}
|
||||
<a
|
||||
href={{resource.link}}
|
||||
class="external-link"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{{i18n resource.key}}
|
||||
{{icon "external-link-alt"}}
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
<DButton
|
||||
@action={{this.installModal}}
|
||||
@icon="upload"
|
||||
@label="admin.customize.install"
|
||||
class="btn-primary theme-card__install-button"
|
||||
/>
|
||||
</:content>
|
||||
</AdminConfigAreaCard>
|
||||
</div>
|
||||
{{#each @themes as |theme|}}
|
||||
<ThemesGridCard @theme={{theme}} @allThemes={{@themes}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { service } from "@ember/service";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminConfigLookAndFeelIndexRoute extends DiscourseRoute {
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
this.router.replaceWith("adminConfig.lookAndFeel.themes");
|
||||
}
|
||||
}
|
|
@ -5,10 +5,6 @@ import I18n from "discourse-i18n";
|
|||
export default class AdminConfigLookAndFeelRoute extends DiscourseRoute {
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
this.router.replaceWith("adminConfig.lookAndFeel.themes");
|
||||
}
|
||||
|
||||
titleToken() {
|
||||
return I18n.t("admin.config_areas.look_and_feel.title");
|
||||
}
|
||||
|
|
|
@ -3,6 +3,4 @@
|
|||
@label={{i18n "admin.config_areas.look_and_feel.themes.title"}}
|
||||
/>
|
||||
|
||||
<div class="admin-detail">
|
||||
<ThemesGrid @themes={{this.model}} />
|
||||
</div>
|
||||
<AdminConfigAreas::LookAndFeelThemes @themes={{this.model}} />
|
|
@ -1,6 +1,7 @@
|
|||
<AdminPageHeader
|
||||
@titleLabel="admin.config_areas.look_and_feel.title"
|
||||
@descriptionLabel="admin.config_areas.look_and_feel.description"
|
||||
@learnMoreUrl="https://meta.discourse.org/t/beginners-guide-to-using-discourse-themes/91966"
|
||||
>
|
||||
<:breadcrumbs>
|
||||
<DBreadcrumbsItem
|
||||
|
|
|
@ -116,6 +116,15 @@ export default class GlobalNotice extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.router.currentRoute?.queryParams?.preview_theme_id) {
|
||||
notices.push(
|
||||
Notice.create({
|
||||
text: I18n.t("theme_preview_notice"),
|
||||
id: "theme-preview",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this.siteSettings.disable_emails === "yes") {
|
||||
notices.push(
|
||||
Notice.create({
|
||||
|
|
|
@ -51,8 +51,11 @@ class LiveDevelopmentInit {
|
|||
// Refresh if necessary
|
||||
document.location.reload(true);
|
||||
} else if (me === "development-mode-theme-changed") {
|
||||
if (window.location.pathname.startsWith("/admin/customize/themes")) {
|
||||
// don't refresh users on routes which make theme changes - would be very inconvenient.
|
||||
if (
|
||||
window.location.pathname.startsWith("/admin/customize/themes") ||
|
||||
window.location.pathname.startsWith("/admin/config/look-and-feel")
|
||||
) {
|
||||
// Don't refresh users on routes which make theme changes - would be very inconvenient.
|
||||
// Instead, refresh on their next route navigation.
|
||||
this.session.requiresRefresh = true;
|
||||
} else {
|
||||
|
|
|
@ -5,25 +5,60 @@
|
|||
|
||||
.themes-cards-container {
|
||||
display: grid;
|
||||
grid-template-columns: calc(66.66% - 0.5em) calc(33.33% - 0.5em);
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1em;
|
||||
|
||||
@media screen and (max-width: 1300px) {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
&__main {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(325px - 1em), 1fr));
|
||||
gap: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
box-shadow: 0px 0px 0px 3px transparent;
|
||||
transition: box-shadow 0.3s ease-in-out;
|
||||
&.--updating {
|
||||
animation: updating 3s ease-in-out 1,
|
||||
updatingInfinite 3s ease-in-out 3s infinite;
|
||||
@keyframes updating {
|
||||
0% {
|
||||
box-shadow: 0px 0px 0px 3px transparent;
|
||||
border: 1px solid var(--success);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0px 0px 0px 6px var(--success-low);
|
||||
border: 1px solid var(--success);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0px 0px 0px 3px var(--success-low);
|
||||
border: 1px solid var(--success);
|
||||
}
|
||||
}
|
||||
@keyframes updatingInfinite {
|
||||
0% {
|
||||
box-shadow: 0px 0px 0px 3px var(--success-low);
|
||||
border: 1px solid var(--success);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0px 0px 0px 6px var(--success-low);
|
||||
border: 1px solid var(--success);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0px 0px 0px 3px var(--success-low);
|
||||
border: 1px solid var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.--updating .d-icon-sync {
|
||||
animation: rotate 3s linear infinite;
|
||||
margin-right: 0.45em;
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.admin-config-area-card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -34,16 +69,14 @@
|
|||
@include theme-card-border(tertiary);
|
||||
}
|
||||
|
||||
&.-disabled {
|
||||
@include theme-card-border(primary);
|
||||
}
|
||||
|
||||
&.-broken {
|
||||
@include theme-card-border(danger);
|
||||
}
|
||||
|
||||
.broken-indicator {
|
||||
color: var(--danger);
|
||||
&__update-available {
|
||||
position: absolute;
|
||||
right: -9px;
|
||||
top: -9px;
|
||||
color: var(--success);
|
||||
font-size: var(--font-up-1);
|
||||
background: var(--secondary);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__icons {
|
||||
|
@ -139,6 +172,5 @@
|
|||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4740,6 +4740,8 @@ en:
|
|||
safe_mode:
|
||||
enabled: "Safe mode is enabled, to exit safe mode close this browser window"
|
||||
|
||||
theme_preview_notice: "You are currently previewing a theme, close this browser tab or window to return to your normal site configuration."
|
||||
|
||||
image_removed: "(image removed)"
|
||||
|
||||
pause_notifications:
|
||||
|
@ -5694,8 +5696,8 @@ en:
|
|||
move_up: "Move up"
|
||||
move_down: "Move down"
|
||||
look_and_feel:
|
||||
title: "Look & feel"
|
||||
description: "Themes, components, and color schemes can be used to customise and brand your Discourse site, giving it a distinctive style."
|
||||
title: "Look and feel"
|
||||
description: "Customize and brand your Discourse site, giving it a distinctive style."
|
||||
themes:
|
||||
title: "Themes"
|
||||
themes_intro: "Install a new theme to get started, or create your own from scratch using these resources."
|
||||
|
@ -5929,12 +5931,14 @@ en:
|
|||
set_default_theme: "Set default"
|
||||
default_theme: "Default theme"
|
||||
set_default_success: "Default theme set to %{theme}"
|
||||
install_success: "%{theme} installed successfully!"
|
||||
inactive_components: "Unused components:"
|
||||
selected:
|
||||
one: "%{count} selected"
|
||||
other: "%{count} selected"
|
||||
cancel: "Cancel"
|
||||
broken_theme_tooltip: "This theme has errors in its CSS, HTML or YAML"
|
||||
broken_theme: "Disable broken theme"
|
||||
disabled_component_tooltip: "This component has been disabled"
|
||||
default_theme_tooltip: "This theme is the site's default theme"
|
||||
updates_available_tooltip: "Updates are available for this theme"
|
||||
|
@ -6001,6 +6005,7 @@ en:
|
|||
update_to_latest: "Update to Latest"
|
||||
check_for_updates: "Check for Updates"
|
||||
updating: "Updating…"
|
||||
update_success: "%{theme} Update complete"
|
||||
up_to_date: "Theme is up-to-date, last checked:"
|
||||
has_overwritten_history: "Current theme version no longer exists because the Git history has been overwritten by a force push."
|
||||
add: "Add"
|
||||
|
|
Loading…
Reference in New Issue