feat: export complete Discourse themes ecosystem (16 themes, SCSS, JS components and color schemes)

This commit is contained in:
eldov
2026-08-30 18:07:57 +02:00
parent 0d76aaa622
commit 3a411c2643
258 changed files with 16878 additions and 0 deletions
@@ -0,0 +1,8 @@
{
"about_url": null,
"license_url": null,
"assets": {
},
"name": "Modern Category + Group Boxes",
"component": true
}
@@ -0,0 +1,372 @@
@use "lib/viewport";
.custom-category-header {
margin: 1em auto;
display: inline-block;
font-size: 3em;
border-bottom: 2px solid var(--primary);
}
.category-boxes {
justify-content: center;
}
.category .muted,
.navigation-categories .muted {
opacity: 25%;
&:hover {
opacity: 100%;
}
}
.custom-category-boxes-container {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
@include viewport.from(sm) {
margin-top: var(--space-8);
}
&:empty {
display: none;
}
}
.category {
.custom-category-boxes {
display: flex;
flex-direction: row;
margin: 0 0 32px 0;
width: 100%;
flex-wrap: wrap;
.category-box {
width: 30%;
margin-right: 1em;
margin-bottom: 1em;
flex-grow: 1;
}
}
}
.category-list:not(.theme-setting, .setting),
.navigation-categories .list-container .full-width #list-area .contents,
.categories-list .list-container .full-width #header-list-area .contents {
.column {
margin-top: 2em;
}
.category-boxes,
.category-boxes-with-topics,
.category-list.subcategory-list,
.column.categories,
.category-list,
.muted-categories {
display: none;
visibility: hidden;
}
}
.category-title-contents {
padding: 87px 0;
[class*="d-icon-category.restricted"],
.d-icon-lock {
display: none;
}
}
.groups-boxes {
display: grid;
gap: 18px;
grid-auto-flow: row;
grid-template-columns: 1fr 1fr;
.group-box {
background-color: var(--secondary);
border: 0 !important;
border-radius: 12px;
color: var(--primary);
padding: 0;
box-shadow:
0 24px 40px rgb(0, 0, 0, 0.07),
0 10.8529px 24.1177px rgb(0, 0, 0, 0.0456112),
0 4.50776px 10.0172px rgb(0, 0, 0, 0.035),
0 1.63037px 3.62304px rgb(0, 0, 0, 0.0243888);
.group-avatar-flair {
grid-area: group-image;
.avatar-flair {
background-size: 64px;
height: 64px;
margin: 0 0;
width: 64px;
}
}
.group-box-inner {
display: inline-grid;
gap: 1em;
grid-auto-flow: row;
grid-template-rows: 1fr 0fr;
.group-description {
color: var(--primary-very-high);
}
.group-info-wrapper {
align-items: center;
gap: 1em;
.group-info {
margin-bottom: unset;
padding-right: 1em;
width: unset;
span {
color: var(--primary);
font-size: 1.125em;
font-weight: 600;
letter-spacing: 0.03em;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.group-user-count {
color: var(--primary-high);
flex-shrink: 0;
font-size: 0.875em;
padding-top: 2px;
}
}
}
}
.group-membership {
color: var(--primary-high);
padding-top: 0;
.is-group-member {
color: var(--success);
}
}
}
}
.custom-category-boxes:not(.above-discovery-categories-outlet) {
display: grid;
grid-auto-flow: row;
gap: 32px;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
width: 100%;
.category-box {
display: flex;
flex-grow: 1;
background-color: var(--secondary);
border-left-width: 0;
border-radius: 12px;
margin: 0 0;
max-height: 160px;
overflow: hidden;
transition: all ease-out 0.2s;
width: initial;
box-shadow:
0 24px 40px rgb(0, 0, 0, 0.07),
0 10.8529px 24.1177px rgb(0, 0, 0, 0.0456112),
0 4.50776px 10.0172px rgb(0, 0, 0, 0.035),
0 1.63037px 3.62304px rgb(0, 0, 0, 0.0243888);
&:hover {
background-color: var(--secondary-very-high);
}
&.muted {
.category-details {
grid-column: 2;
}
}
.category-box-inner {
border: 0;
display: grid;
grid-auto-flow: column;
grid-template-columns: 0fr 1fr 0fr;
column-gap: 48px;
padding: 0;
.category-logo {
align-self: center;
display: inline-block;
width: 100px;
height: 100px;
margin: 0;
img {
height: 100%;
width: 100%;
}
.category-abbreviation {
display: none;
}
&.no-logo-present {
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 100%;
min-height: 100px;
.category-abbreviation {
display: block;
color: white;
font-weight: bold;
font-size: 2em;
}
}
}
.category-details {
align-self: center;
}
}
}
h3 {
align-items: baseline;
color: var(--primary);
display: inline-flex;
font-size: 1.125em;
font-weight: 600;
letter-spacing: 0.03em;
line-height: 140%;
margin-top: 0;
margin-bottom: 0;
text-align: left;
.rtl & {
text-align: right;
}
.d-icon {
color: var(--primary-very-high);
font-size: 16px;
height: 16px !important;
margin-inline-end: 4px !important;
width: 16px !important;
}
}
.category-box-heading {
margin-bottom: 8px;
}
.description {
color: var(--primary-high);
font-size: 1em;
font-weight: 400;
line-height: 140%;
letter-spacing: 0.03em;
margin-bottom: 0;
max-height: 3em;
text-align: left;
.rtl & {
text-align: right;
}
p {
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.subcategory {
display: none;
}
.subcategories {
display: none;
.subcategory {
.subcategory-image-placeholder {
margin-right: 0;
}
}
}
}
// Subcategory theming
#header-list-area {
.custom-category-boxes {
grid-template-columns: 1fr 1fr 1fr;
margin-top: 32px;
.category-box-inner {
column-gap: 24px;
.category-logo {
height: 80px;
width: 80px;
}
}
.category-box-heading {
margin-bottom: 4px;
}
h3 {
font-size: 1em;
}
.description {
display: none;
}
}
.custom-category-boxes-container + .category-boxes {
display: none;
}
}
@media (width <= 1250px) {
body.category,
#header-list-area {
.custom-category-boxes {
grid-template-columns: 1fr 1fr;
}
}
}
@media (width <= 960px) {
.custom-category-boxes {
grid-template-columns: 1fr;
}
}
@media (width <= 800px) {
body.category,
body.navigation-categories {
.custom-category-boxes {
grid-template-columns: 1fr;
}
}
#header-list-area {
.custom-category-boxes {
grid-template-columns: 1fr;
}
}
}
@@ -0,0 +1,122 @@
import Component from "@glimmer/component";
import { trustHTML } from "@ember/template";
import CategoryLogo from "discourse/components/category-logo";
import CategoryTitleBefore from "discourse/components/category-title-before";
import CategoryTitleLink from "discourse/components/category-title-link";
import PluginOutlet from "discourse/components/plugin-outlet";
import borderColor from "discourse/helpers/border-color";
import categoryLink from "discourse/helpers/category-link";
import icon from "discourse/helpers/d-icon";
import lazyHash from "discourse/helpers/lazy-hash";
export default class extends Component {
get backgroundColor() {
return trustHTML(`background-color: #${this.args.category.color}`);
}
get getAbbreviation() {
let abbr = this.args.category.name.replace(" and", "").split(" ");
if (abbr.length > 1) {
abbr = abbr[0].charAt(0).toUpperCase() + abbr[1].charAt(0).toLowerCase();
} else {
abbr = abbr[0].charAt(0).toUpperCase() + abbr[0].charAt(1).toLowerCase();
}
return abbr;
}
<template>
{{! template-lint-disable no-nested-interactive }}
<a
href={{@category.url}}
data-category-id={{@category.id}}
data-notification-level={{@category.notificationLevelString}}
data-url={{@category.url}}
class="category category-box category-box-{{@category.slug}}
{{if @category.isMuted 'muted'}}
{{if this.noCategoryStyle 'no-category-boxes-style'}}"
>
<div class="category-box-inner">
<div
class="category-logo
{{if @category.uploaded_logo.url '' 'no-logo-present'}}"
style={{this.backgroundColor}}
>
{{#if @category.uploaded_logo.url}}
<CategoryLogo @category={{@category}} />
{{else}}
<span class="category-abbreviation">
{{this.getAbbreviation}}
</span>
{{/if}}
</div>
<div class="category-details">
<div class="category-box-heading">
<a class="parent-box-link" href={{@category.url}}>
<h3>
<CategoryTitleBefore @category={{@category}} />
{{#if @category.read_restricted}}
{{icon "lock"}}
{{/if}}
{{@category.name}}
</h3>
</a>
</div>
<div class="description">
<p>{{trustHTML @category.description_excerpt}}</p>
</div>
{{#if @category.isGrandParent}}
{{#each @category.subcategories as |subcategory|}}
<a
href={{subcategory.url}}
data-category-id={{subcategory.id}}
data-notification-level={{subcategory.notificationLevelString}}
style={{borderColor subcategory.color}}
class="subcategory with-subcategories
{{if subcategory.uploaded_logo.url 'has-logo' 'no-logo'}}"
>
<div class="subcategory-box-inner">
<CategoryTitleLink @tagName="h4" @category={{subcategory}} />
{{#if subcategory.subcategories}}
<div class="subcategories">
{{#each subcategory.subcategories as |subsubcategory|}}
{{#unless subsubcategory.isMuted}}
<span class="subcategory">
<CategoryTitleBefore @category={{subsubcategory}} />
{{categoryLink subsubcategory hideParent="true"}}
</span>
{{/unless}}
{{/each}}
</div>
{{/if}}
</div>
</a>
{{/each}}
{{else if @category.subcategories}}
<div class="subcategories">
{{#each @category.subcategories as |sc|}}
<a class="subcategory" href={{sc.url}}>
<span class="subcategory-image-placeholder">
<CategoryLogo @category={{sc}} />
</span>
{{categoryLink sc hideParent="true"}}
</a>
{{/each}}
</div>
{{/if}}
</div>
<PluginOutlet
@name="category-box-below-each-category"
@connectorTagName=""
@outletArgs={{lazyHash category=@category}}
/>
</div>
</a>
</template>
}
@@ -0,0 +1,11 @@
import CategoryBox from "./category-box";
const CategoryBoxes = <template>
<span class="custom-category-boxes">
{{#each @categories as |category|}}
<CategoryBox @category={{category}} />
{{/each}}
</span>
</template>;
export default CategoryBoxes;
@@ -0,0 +1,11 @@
import { gt } from "discourse/truth-helpers";
const CategoryHeader = <template>
{{#if (gt @header.length 0)}}
<h1 class="custom-category-header">
{{@header}}
</h1>
{{/if}}
</template>;
export default CategoryHeader;
@@ -0,0 +1,120 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import CategoryBoxes from "./category-boxes";
import CategoryHeader from "./category-header";
export default class extends Component {
@service router;
@service site;
get classNames() {
const classNames = ["custom-category-boxes-container"];
if (this.noneSelected) {
classNames.push("none-selected");
}
return classNames.join(" ");
}
#allowedCategories(selectedCategories) {
// filters categories to only include selected categories for each section
let availableCategories = this.site.categories.filter((category) => {
if (selectedCategories.indexOf(category.id) !== -1) {
return true;
} else {
return false;
}
});
return availableCategories;
}
get shouldRenderHeadings() {
let isCategoryPage = this.router.currentRoute.name.includes("category");
let hasCategoriesSet = false;
if (
settings.first_categories ||
settings.second_categories ||
settings.third_categories ||
settings.fourth_categories ||
settings.fifth_categories
) {
hasCategoriesSet = true;
}
if (!isCategoryPage && hasCategoriesSet) {
return true;
} else {
return false;
}
}
get noneSelected() {
return this.router.currentRoute.name.includes("None");
}
get firstCategories() {
return this.#allowedCategories(
settings.first_categories.split("|").map((id) => Number(id))
);
}
get secondCategories() {
return this.#allowedCategories(
settings.second_categories.split("|").map((id) => Number(id))
);
}
get thirdCategories() {
return this.#allowedCategories(
settings.third_categories.split("|").map((id) => Number(id))
);
}
get fourthCategories() {
return this.#allowedCategories(
settings.fourth_categories.split("|").map((id) => Number(id))
);
}
get fifthCategories() {
return this.#allowedCategories(
settings.fifth_categories.split("|").map((id) => Number(id))
);
}
<template>
<div class={{this.classNames}}>
{{#if this.shouldRenderHeadings}}
{{#if this.firstCategories}}
<CategoryHeader @header={{settings.first_categories_header}} />
<CategoryBoxes @categories={{this.firstCategories}} />
{{/if}}
{{#if this.secondCategories}}
<CategoryHeader @header={{settings.second_categories_header}} />
<CategoryBoxes @categories={{this.secondCategories}} />
{{/if}}
{{#if this.thirdCategories}}
<CategoryHeader @header={{settings.third_categories_header}} />
<CategoryBoxes @categories={{this.thirdCategories}} />
{{/if}}
{{#if this.fourthCategories}}
<CategoryHeader @header={{settings.fourth_categories_header}} />
<CategoryBoxes @categories={{this.fourthCategories}} />
{{/if}}
{{#if this.fifthCategories}}
<CategoryHeader @header={{settings.fifth_categories_header}} />
<CategoryBoxes @categories={{this.fifthCategories}} />
{{/if}}
{{else}}
<CategoryBoxes @categories={{@outletArgs.categories}} />
{{/if}}
</div>
</template>
}
@@ -0,0 +1,6 @@
import { apiInitializer } from "discourse/lib/api";
import CustomCategoryBoxes from "../components/custom-category-boxes";
export default apiInitializer((api) => {
api.renderInOutlet("above-discovery-categories", CustomCategoryBoxes);
});
@@ -0,0 +1,64 @@
.custom-category-header {
margin-bottom: 1em;
font-size: 2em;
border-bottom: 2px solid var(--primary);
text-align: center;
}
.category-logo:not(.no-logo-present) + .category-details {
max-height: 132px;
height: calc(132px - 1em);
padding: 0.5em !important;
}
.custom-category-boxes {
gap: 1em !important;
}
.custom-category-boxes .category-box .category-box-inner {
column-gap: 1em !important;
}
.custom-category-boxes
.category-box
.category-box-inner
.category-logo.no-logo-present {
height: 100% !important;
}
.custom-category-boxes .description {
max-height: unset !important;
}
.custom-category-boxes .category-box .category-box-inner .category-details {
padding: 1em;
}
.groups-header {
margin: 1em 0 0 0;
}
.groups-header-filters-name {
display: none !important;
}
.groups-boxes {
grid-template-columns: 1fr !important;
}
.category-box {
position: relative;
}
.parent-box-link {
// This avoids an issue with nested links by layering links instead
&::before {
content: "";
position: absolute;
z-index: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
@@ -0,0 +1,40 @@
first_categories_header:
type: string
default: "First Categories Header"
first_categories:
type: list
list_type: category
default: ""
description: "Chose the categories to feature for the First Header"
second_categories_header:
type: string
default: "Second Categories Header"
second_categories:
type: list
list_type: category
default: ""
description: "Choose the categories to feature under the Second Header"
third_categories_header:
type: string
default: "Third Categories Header"
third_categories:
type: list
list_type: category
default: ""
description: "Choose the categories to feature under the Third Header"
fourth_categories_header:
type: string
default: "Fourth Categories Header"
fourth_categories:
type: list
list_type: category
default: ""
description: "Choose the categories to feature under the Fourth Header"
fifth_categories_header:
type: string
default: "Fifth Categories Header"
fifth_categories:
type: list
list_type: category
default: ""
description: "Choose the categories to feature under the Fifth Header"
@@ -0,0 +1,89 @@
{
"id": 42,
"name": "Modern Category + Group Boxes",
"user_selectable": false,
"color_scheme": null,
"fields_count": 10,
"files": [
{
"field_id": 779,
"target_id": 0,
"target_name": "common",
"name": "scss",
"filename": "common_scss.scss",
"bytes": 6897
},
{
"field_id": 786,
"target_id": 2,
"target_name": "mobile",
"name": "scss",
"filename": "mobile_scss.scss",
"bytes": 1136
},
{
"field_id": 787,
"target_id": 3,
"target_name": "settings",
"name": "yaml",
"filename": "settings_yaml.yml",
"bytes": 1090
},
{
"field_id": 785,
"target_id": 4,
"target_name": "translations",
"name": "en",
"filename": "translations_en.yml",
"bytes": 177
},
{
"field_id": 780,
"target_id": 6,
"target_name": "javascript",
"name": "discourse/components/category-box.gjs",
"filename": "javascript_discourse_components_category-box.gjs",
"bytes": 4485
},
{
"field_id": 781,
"target_id": 6,
"target_name": "javascript",
"name": "discourse/components/category-boxes.gjs",
"filename": "javascript_discourse_components_category-boxes.gjs",
"bytes": 268
},
{
"field_id": 782,
"target_id": 6,
"target_name": "javascript",
"name": "discourse/components/category-header.gjs",
"filename": "javascript_discourse_components_category-header.gjs",
"bytes": 236
},
{
"field_id": 783,
"target_id": 6,
"target_name": "javascript",
"name": "discourse/components/custom-category-boxes.gjs",
"filename": "javascript_discourse_components_custom-category-boxes.gjs",
"bytes": 3281
},
{
"field_id": 784,
"target_id": 6,
"target_name": "javascript",
"name": "discourse/initializers/initialize-category-boxes.js",
"filename": "javascript_discourse_initializers_initialize-category-boxes.js",
"bytes": 242
},
{
"field_id": 778,
"target_id": 9,
"target_name": "about",
"name": "about",
"filename": "about_about.yml",
"bytes": 129
}
]
}
@@ -0,0 +1,3 @@
en:
theme_metadata:
description: "A component that allows you to select custom headings for your category page, and chose which categories to feature under each heading."