- {{#each @field.choices as |c|}}
+ {{#each @field.choices as |choice|}}
diff --git a/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-homepage-preview.js b/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-homepage-preview.js
index cfe1dcba96b..ee99b9a0111 100644
--- a/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-homepage-preview.js
+++ b/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-homepage-preview.js
@@ -1,4 +1,5 @@
-import { darkLightDiff, LOREM } from "../../../lib/preview";
+import { i18n } from "discourse-i18n";
+import { darkLightDiff } from "../../../lib/preview";
import PreviewBaseComponent from "./-preview-base";
export default class HomepagePreview extends PreviewBaseComponent {
@@ -25,26 +26,26 @@ export default class HomepagePreview extends PreviewBaseComponent {
const homepageStyle = this.getHomepageStyle();
- if (homepageStyle === "latest") {
- this.drawPills(colors, font, height * 0.15);
- this.renderLatest(ctx, colors, font, width, height);
+ if (homepageStyle === "latest" || homepageStyle === "hot") {
+ this.drawPills(colors, font, height * 0.15, { homepageStyle });
+ this.renderNonCategoryHomepage(ctx, colors, font, width, height);
} else if (
["categories_only", "categories_with_featured_topics"].includes(
homepageStyle
)
) {
- this.drawPills(colors, font, height * 0.15, { categories: true });
+ this.drawPills(colors, font, height * 0.15, { homepageStyle });
this.renderCategories(ctx, colors, font, width, height);
} else if (
["categories_boxes", "categories_boxes_with_topics"].includes(
homepageStyle
)
) {
- this.drawPills(colors, font, height * 0.15, { categories: true });
+ this.drawPills(colors, font, height * 0.15, { homepageStyle });
const topics = homepageStyle === "categories_boxes_with_topics";
this.renderCategoriesBoxes(ctx, colors, font, width, height, { topics });
} else {
- this.drawPills(colors, font, height * 0.15, { categories: true });
+ this.drawPills(colors, font, height * 0.15, { homepageStyle });
this.renderCategoriesWithTopics(ctx, colors, font, width, height);
}
}
@@ -82,14 +83,10 @@ export default class HomepagePreview extends PreviewBaseComponent {
]
);
- ctx.font = `Bold ${bodyFontSize * 1.3}em '${font}'`;
+ ctx.font = `700 ${bodyFontSize * 1.3}em '${font}'`;
ctx.fillStyle = colors.primary;
ctx.textAlign = "center";
- ctx.fillText(
- category.displayName,
- boxStartX + boxWidth / 2,
- boxStartY + 25
- );
+ ctx.fillText(category.name, boxStartX + boxWidth / 2, boxStartY + 25);
ctx.textAlign = "left";
if (opts.topics) {
@@ -167,16 +164,16 @@ export default class HomepagePreview extends PreviewBaseComponent {
drawLine(width / 2, y);
// Categories
- this.categories().forEach((category) => {
+ this.categories().forEach((category, idx) => {
const textPos = y + categoryHeight * 0.35;
- ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`;
+ ctx.font = `700 ${bodyFontSize * 1.1}em '${font}'`;
ctx.fillStyle = colors.primary;
- ctx.fillText(category.displayName, cols[0], textPos);
+ ctx.fillText(category.name, cols[0], textPos);
ctx.font = `${bodyFontSize * 0.8}em '${font}'`;
ctx.fillStyle = textColor;
ctx.fillText(
- titles[0],
+ titles[idx],
cols[0] - margin * 0.25,
textPos + categoryHeight * 0.36
);
@@ -263,16 +260,16 @@ export default class HomepagePreview extends PreviewBaseComponent {
const titles = this.getTitles();
// Categories
- this.categories().forEach((category) => {
+ this.categories().forEach((category, idx) => {
const textPos = y + categoryHeight * 0.35;
- ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`;
+ ctx.font = `700 ${bodyFontSize * 1.1}em '${font}'`;
ctx.fillStyle = colors.primary;
- ctx.fillText(category.displayName, cols[0], textPos);
+ ctx.fillText(category.name, cols[0], textPos);
ctx.font = `${bodyFontSize * 0.8}em '${font}'`;
ctx.fillStyle = textColor;
ctx.fillText(
- titles[0],
+ titles[idx],
cols[0] - margin * 0.25,
textPos + categoryHeight * 0.36
);
@@ -303,7 +300,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
const category = this.categories()[0];
ctx.font = `${bodyFontSize}em '${font}'`;
const textPos = y + topicHeight * 0.45;
- ctx.fillStyle = textColor;
+ ctx.fillStyle = colors.primary;
this.scaleImage(
this.avatar,
cols[2],
@@ -313,7 +310,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
);
ctx.fillText(title, cols[3], textPos);
- ctx.font = `Bold ${bodyFontSize}em '${font}'`;
+ ctx.font = `700 ${bodyFontSize}em '${font}'`;
ctx.fillText(Math.floor(Math.random() * 90) + 10, cols[4], textPos);
ctx.font = `${bodyFontSize}em '${font}'`;
ctx.fillText(`1h`, cols[4], textPos + topicHeight * 0.4);
@@ -321,9 +318,9 @@ export default class HomepagePreview extends PreviewBaseComponent {
ctx.beginPath();
ctx.fillStyle = category.color;
const badgeSize = topicHeight * 0.1;
- ctx.font = `Bold ${bodyFontSize * 0.5}em '${font}'`;
+ ctx.font = `700 ${bodyFontSize * 0.5}em '${font}'`;
ctx.rect(
- cols[3] + margin * 0.5,
+ cols[3] + margin * 0.25,
y + topicHeight * 0.65,
badgeSize,
badgeSize
@@ -332,8 +329,8 @@ export default class HomepagePreview extends PreviewBaseComponent {
ctx.fillStyle = colors.primary;
ctx.fillText(
- category.displayName,
- cols[3] + badgeSize * 3,
+ category.name,
+ cols[3] + badgeSize * 2,
y + topicHeight * 0.76
);
y += topicHeight;
@@ -347,16 +344,23 @@ export default class HomepagePreview extends PreviewBaseComponent {
}
getTitles() {
- return LOREM.split(".")
- .slice(0, 8)
- .map((t) => t.substring(0, 40));
+ return [
+ i18n("wizard.homepage_preview.topic_titles.what_books"),
+ i18n("wizard.homepage_preview.topic_titles.what_movies"),
+ i18n("wizard.homepage_preview.topic_titles.random_fact"),
+ i18n("wizard.homepage_preview.topic_titles.tv_show"),
+ ];
}
getDescriptions() {
- return LOREM.split(".");
+ return [
+ i18n("wizard.homepage_preview.category_descriptions.icebreakers"),
+ i18n("wizard.homepage_preview.category_descriptions.news"),
+ i18n("wizard.homepage_preview.category_descriptions.site_feedback"),
+ ];
}
- renderLatest(ctx, colors, font, width, height) {
+ renderNonCategoryHomepage(ctx, colors, font, width, height) {
const rowHeight = height / 6.6;
// accounts for hard-set color variables in solarized themes
const textColor =
@@ -379,17 +383,33 @@ export default class HomepagePreview extends PreviewBaseComponent {
ctx.stroke();
};
- const cols = [0.02, 0.66, 0.8, 0.87, 0.93].map((c) => c * width);
+ const cols = [0.02, 0.66, 0.75, 0.83, 0.9].map((c) => c * width);
// Headings
const headingY = height * 0.33;
ctx.fillStyle = textColor;
ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
- ctx.fillText("Topic", cols[0], headingY);
- ctx.fillText("Replies", cols[2], headingY);
- ctx.fillText("Views", cols[3], headingY);
- ctx.fillText("Activity", cols[4], headingY);
+ ctx.fillText(
+ i18n("wizard.homepage_preview.table_headers.topic"),
+ cols[0],
+ headingY
+ );
+ ctx.fillText(
+ i18n("wizard.homepage_preview.table_headers.replies"),
+ cols[2],
+ headingY
+ );
+ ctx.fillText(
+ i18n("wizard.homepage_preview.table_headers.views"),
+ cols[3],
+ headingY
+ );
+ ctx.fillText(
+ i18n("wizard.homepage_preview.table_headers.activity"),
+ cols[4],
+ headingY
+ );
// Topics
let y = headingY + rowHeight / 2.6;
@@ -400,20 +420,21 @@ export default class HomepagePreview extends PreviewBaseComponent {
ctx.lineWidth = 1;
this.getTitles().forEach((title) => {
const textPos = y + rowHeight * 0.4;
- ctx.fillStyle = textColor;
+ ctx.fillStyle = colors.primary;
ctx.fillText(title, cols[0], textPos);
+ // Category badge
const category = this.categories()[0];
ctx.beginPath();
ctx.fillStyle = category.color;
const badgeSize = rowHeight * 0.15;
- ctx.font = `Bold ${bodyFontSize * 0.75}em '${font}'`;
+ ctx.font = `700 ${bodyFontSize * 0.75}em '${font}'`;
ctx.rect(cols[0] + 4, y + rowHeight * 0.6, badgeSize, badgeSize);
ctx.fill();
ctx.fillStyle = colors.primary;
ctx.fillText(
- category.displayName,
+ category.name,
cols[0] + badgeSize * 2,
y + rowHeight * 0.73
);
diff --git a/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-preview-base.js b/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-preview-base.js
index c474e3da276..ea97daf6140 100644
--- a/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-preview-base.js
+++ b/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-preview-base.js
@@ -6,18 +6,9 @@ import { htmlSafe } from "@ember/template";
import { Promise } from "rsvp";
import PreloadStore from "discourse/lib/preload-store";
import getUrl from "discourse-common/lib/get-url";
+import { i18n } from "discourse-i18n";
import { darkLightDiff, drawHeader } from "../../../lib/preview";
-export const LOREM = `
-Lorem ipsum dolor sit amet,
-consectetur adipiscing elit.
-Nullam eget sem non elit
-tincidunt rhoncus. Fusce
-velit nisl, porttitor sed
-nisl ac, consectetur interdum
-metus. Fusce in consequat
-augue, vel facilisis felis.`;
-
const scaled = {};
function canvasFor(image, w, h) {
@@ -125,7 +116,7 @@ export default class PreviewBase extends Component {
});
});
- Promise.all(
+ return Promise.all(
fontFaces.map((fontFace) =>
fontFace.load().then((loadedFont) => {
document.fonts.add(loadedFont);
@@ -144,7 +135,7 @@ export default class PreviewBase extends Component {
this.loadingFontVariants = false;
});
} else if (this.loadedFonts.has(font.id)) {
- this.triggerRepaint();
+ return Promise.resolve(this.triggerRepaint());
}
}
@@ -171,8 +162,14 @@ export default class PreviewBase extends Component {
reload() {
Promise.all([this.loadFonts(), this.loadImages()]).then(() => {
- this.loaded = true;
- this.triggerRepaint();
+ // NOTE: This must be done otherwise the "bold" variant of the body font
+ // will not be loaded for some reason before rendering the canvas.
+ //
+ // The header font does not suffer from this issue.
+ this.loadFontVariants(this.wizard.font).then(() => {
+ this.loaded = true;
+ this.triggerRepaint();
+ });
});
}
@@ -219,9 +216,18 @@ export default class PreviewBase extends Component {
categories() {
return [
- { name: "consecteteur", color: "#652D90" },
- { name: "ultrices", color: "#3AB54A" },
- { name: "placerat", color: "#25AAE2" },
+ {
+ name: i18n("wizard.homepage_preview.category_names.icebreakers"),
+ color: "#652D90",
+ },
+ {
+ name: i18n("wizard.homepage_preview.category_names.news"),
+ color: "#3AB54A",
+ },
+ {
+ name: i18n("wizard.homepage_preview.category_names.site_feedback"),
+ color: "#25AAE2",
+ },
];
}
@@ -248,17 +254,20 @@ export default class PreviewBase extends Component {
ctx.drawImage(scaled[key], x, y, w, h);
}
+ get headerHeight() {
+ return this.height * 0.15;
+ }
+
drawFullHeader(colors, font, logo) {
const { ctx } = this;
- const headerHeight = this.height * 0.15;
- drawHeader(ctx, colors, this.width, headerHeight);
+ drawHeader(ctx, colors, this.width, this.headerHeight);
const avatarSize = this.height * 0.1;
- const headerMargin = headerHeight * 0.2;
+ const headerMargin = this.headerHeight * 0.2;
if (logo) {
- const logoHeight = headerHeight - headerMargin * 2;
+ const logoHeight = this.headerHeight - headerMargin * 2;
const ratio = logoHeight / logo.height;
this.scaleImage(
@@ -280,23 +289,25 @@ export default class PreviewBase extends Component {
avatarSize,
avatarSize
);
+
// accounts for hard-set color variables in solarized themes
ctx.fillStyle =
colors.primary_low_mid ||
darkLightDiff(colors.primary, colors.secondary, 45, 55);
- const pathScale = headerHeight / 1200;
- // search icon SVG path
+ const pathScale = this.headerHeight / 1200;
const searchIcon = new Path2D(
"M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"
);
- // hamburger icon
const hamburgerIcon = new Path2D(
"M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"
);
+ const chatIcon = new Path2D(
+ "M512 240c0 114.9-114.6 208-256 208c-37.1 0-72.3-6.4-104.1-17.9c-11.9 8.7-31.3 20.6-54.3 30.6C73.6 471.1 44.7 480 16 480c-6.5 0-12.3-3.9-14.8-9.9c-2.5-6-1.1-12.8 3.4-17.4c0 0 0 0 0 0s0 0 0 0s0 0 0 0c0 0 0 0 0 0l.3-.3c.3-.3 .7-.7 1.3-1.4c1.1-1.2 2.8-3.1 4.9-5.7c4.1-5 9.6-12.4 15.2-21.6c10-16.6 19.5-38.4 21.4-62.9C17.7 326.8 0 285.1 0 240C0 125.1 114.6 32 256 32s256 93.1 256 208z"
+ );
ctx.save(); // Save the previous state for translation and scale
ctx.translate(
- this.width - avatarSize * 3 - headerMargin * 0.5,
+ this.width - avatarSize * 2 - headerMargin * 0.5,
avatarSize / 2
);
// need to scale paths otherwise they're too large
@@ -305,10 +316,15 @@ export default class PreviewBase extends Component {
ctx.restore();
ctx.save();
ctx.translate(
- this.width - avatarSize * 2 - headerMargin * 0.5,
+ this.width - avatarSize * 3 - headerMargin * 0.5,
avatarSize / 2
);
ctx.scale(pathScale, pathScale);
+ ctx.fill(chatIcon);
+ ctx.restore();
+ ctx.save();
+ ctx.translate(headerMargin * 1.75, avatarSize / 2);
+ ctx.scale(pathScale, pathScale);
ctx.fill(hamburgerIcon);
ctx.restore();
}
@@ -318,77 +334,109 @@ export default class PreviewBase extends Component {
const { ctx } = this;
- const categoriesSize = headerHeight * 2;
- const badgeHeight = categoriesSize * 0.25;
+ const badgeHeight = headerHeight * 2 * 0.25;
const headerMargin = headerHeight * 0.2;
+ const fontSize = Math.round(badgeHeight * 0.5);
+ ctx.font = `${fontSize}px '${font}'`;
+ const allCategoriesText = i18n(
+ "wizard.homepage_preview.nav_buttons.all_categories"
+ );
+ const categoriesWidth = ctx.measureText(allCategoriesText).width;
+ const categoriesBoxWidth = categoriesWidth + headerMargin * 2;
+
+ // Box around "all categories >"
ctx.beginPath();
ctx.strokeStyle = colors.primary;
ctx.lineWidth = 0.5;
ctx.rect(
headerMargin,
headerHeight + headerMargin,
- categoriesSize,
+ categoriesBoxWidth,
badgeHeight
);
ctx.stroke();
- const fontSize = Math.round(badgeHeight * 0.5);
-
- ctx.font = `${fontSize}px '${font}'`;
ctx.fillStyle = colors.primary;
ctx.fillText(
- "all categories",
+ allCategoriesText,
headerMargin * 1.5,
headerHeight + headerMargin * 1.4 + fontSize
);
+ // Caret (>) at the end of "all categories" box
const pathScale = badgeHeight / 1000;
- // caret icon
const caretIcon = new Path2D(
"M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
);
ctx.save();
ctx.translate(
- categoriesSize - headerMargin / 4,
+ categoriesBoxWidth,
headerHeight + headerMargin + badgeHeight / 4
);
ctx.scale(pathScale, pathScale);
ctx.fill(caretIcon);
ctx.restore();
- const text = opts.categories ? "Categories" : "Latest";
+ const categoryHomepage =
+ opts.homepageStyle !== "hot" && opts.homepageStyle !== "latest";
+
+ // First top menu item
+ let otherHomepageText;
+ switch (opts.homepageStyle) {
+ case "hot":
+ otherHomepageText = i18n("wizard.homepage_preview.nav_buttons.hot");
+ break;
+ case "latest":
+ otherHomepageText = i18n("wizard.homepage_preview.nav_buttons.latest");
+ break;
+ }
+
+ const firstTopMenuItemText = categoryHomepage
+ ? i18n("wizard.homepage_preview.nav_buttons.categories")
+ : otherHomepageText;
+
+ const newText = i18n("wizard.homepage_preview.nav_buttons.new");
+ const unreadText = i18n("wizard.homepage_preview.nav_buttons.unread");
+ const topText = i18n("wizard.homepage_preview.nav_buttons.top");
- const activeWidth = categoriesSize * (opts.categories ? 0.8 : 0.55);
ctx.beginPath();
ctx.fillStyle = colors.tertiary;
ctx.rect(
- headerMargin * 2 + categoriesSize,
+ categoriesBoxWidth + headerMargin * 2,
headerHeight + headerMargin,
- activeWidth,
+ ctx.measureText(firstTopMenuItemText).width + headerMargin * 2,
badgeHeight
);
ctx.fill();
ctx.font = `${fontSize}px '${font}'`;
ctx.fillStyle = colors.secondary;
- let x = headerMargin * 3.0 + categoriesSize;
+ const pillButtonTextY = headerHeight + headerMargin * 1.4 + fontSize;
+ const firstTopMenuItemX = headerMargin * 3.0 + categoriesBoxWidth;
ctx.fillText(
- text,
- x - headerMargin * 0.1,
- headerHeight + headerMargin * 1.5 + fontSize
+ firstTopMenuItemText,
+ firstTopMenuItemX,
+ pillButtonTextY,
+ ctx.measureText(firstTopMenuItemText).width
);
ctx.fillStyle = colors.primary;
- x += categoriesSize * (opts.categories ? 0.8 : 0.6);
- ctx.fillText("New", x, headerHeight + headerMargin * 1.5 + fontSize);
- x += categoriesSize * 0.4;
- ctx.fillText("Unread", x, headerHeight + headerMargin * 1.5 + fontSize);
+ const newTextX =
+ firstTopMenuItemX +
+ ctx.measureText(firstTopMenuItemText).width +
+ headerMargin * 2.0;
+ ctx.fillText(newText, newTextX, pillButtonTextY);
- x += categoriesSize * 0.6;
- ctx.fillText("Top", x, headerHeight + headerMargin * 1.5 + fontSize);
+ const unreadTextX =
+ newTextX + ctx.measureText(newText).width + headerMargin * 2.0;
+ ctx.fillText(unreadText, unreadTextX, pillButtonTextY);
+
+ const topTextX =
+ unreadTextX + ctx.measureText(unreadText).width + headerMargin * 2.0;
+ ctx.fillText(topText, topTextX, pillButtonTextY);
}
}
diff --git a/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/index.js b/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/index.js
index e0b80cf57ad..8baf7cd67f6 100644
--- a/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/index.js
+++ b/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/index.js
@@ -2,19 +2,17 @@ import { action } from "@ember/object";
import { observes } from "@ember-decorators/object";
import { bind } from "discourse-common/utils/decorators";
import { i18n } from "discourse-i18n";
-import { chooseDarker, darkLightDiff } from "../../../lib/preview";
+import {
+ chooseDarker,
+ darkLightDiff,
+ resizeTextLinesToFitRect,
+} from "../../../lib/preview";
import HomepagePreview from "./-homepage-preview";
import PreviewBaseComponent from "./-preview-base";
-const LOREM = `
-Lorem ipsum dolor sit amet, consectetur adipiscing.
-Nullam eget sem non elit tincidunt rhoncus. Fusce
-velit nisl, porttitor sed nisl ac, consectetur interdum
-metus. Fusce in consequat augue, vel facilisis felis.`;
-
export default class Index extends PreviewBaseComponent {
- width = 628;
- height = 322;
+ width = 630;
+ height = 380;
logo = null;
avatar = null;
previewTopic = true;
@@ -117,79 +115,94 @@ export default class Index extends PreviewBaseComponent {
}
paint({ ctx, colors, font, headingFont, width, height }) {
- const headerHeight = height * 0.3;
-
this.drawFullHeader(colors, headingFont, this.logo);
const margin = 20;
- const avatarSize = height * 0.15;
+ const avatarSize = height * 0.1 + 5;
const lineHeight = height / 14;
+ const leftHandTextGutter = margin + avatarSize + margin;
+ const timelineX = width * 0.86;
// Draw a fake topic
this.scaleImage(
this.avatar,
margin,
- headerHeight + height * 0.09,
+ this.headerHeight + height * 0.22,
avatarSize,
avatarSize
);
- const titleFontSize = headerHeight / 55;
+ const titleFontSize = this.headerHeight / 30;
+ // Topic title
ctx.beginPath();
ctx.fillStyle = colors.primary;
- ctx.font = `bold ${titleFontSize}em '${headingFont}'`;
+ ctx.font = `700 ${titleFontSize}em '${headingFont}'`;
ctx.fillText(i18n("wizard.previews.topic_title"), margin, height * 0.3);
- const bodyFontSize = height / 330.0;
+ // Topic OP text
+ const bodyFontSize = 1;
ctx.font = `${bodyFontSize}em '${font}'`;
- let line = 0;
- const lines = LOREM.split("\n");
- for (let i = 0; i < 5; i++) {
- line = height * 0.35 + i * lineHeight;
- ctx.fillText(lines[i], margin + avatarSize + margin, line);
- }
+ let verticalLinePos = 0;
+ const topicOp = i18n("wizard.homepage_preview.topic_ops.what_books");
+ const topicOpLines = topicOp.split("\n");
- // Share Button
- const shareButtonWidth = i18n("wizard.previews.share_button").length * 11;
+ resizeTextLinesToFitRect(
+ topicOpLines,
+ timelineX - leftHandTextGutter,
+ ctx,
+ bodyFontSize,
+ font,
+ (textLine, idx) => {
+ verticalLinePos = height * 0.4 + idx * lineHeight;
+ ctx.fillText(textLine, leftHandTextGutter, verticalLinePos);
+ }
+ );
+
+ ctx.font = `${bodyFontSize}em '${font}'`;
+
+ // Share button
+ const shareButtonWidth =
+ Math.round(ctx.measureText(i18n("wizard.previews.share_button")).width) +
+ margin;
ctx.beginPath();
- ctx.rect(margin, line + lineHeight, shareButtonWidth, height * 0.1);
+ ctx.rect(margin, verticalLinePos, shareButtonWidth, height * 0.1);
// accounts for hard-set color variables in solarized themes
ctx.fillStyle =
colors.primary_low ||
darkLightDiff(colors.primary, colors.secondary, 90, 65);
+ ctx.fill();
ctx.fillStyle = chooseDarker(colors.primary, colors.secondary);
- ctx.font = `${bodyFontSize}em '${font}'`;
ctx.fillText(
i18n("wizard.previews.share_button"),
margin + 10,
- line + lineHeight * 1.9
+ verticalLinePos + lineHeight * 0.9
);
- // Reply Button
- const replyButtonWidth = i18n("wizard.previews.reply_button").length * 11;
+ // Reply button
+ const replyButtonWidth =
+ Math.round(ctx.measureText(i18n("wizard.previews.reply_button")).width) +
+ margin;
ctx.beginPath();
ctx.rect(
shareButtonWidth + margin + 10,
- line + lineHeight,
+ verticalLinePos,
replyButtonWidth,
height * 0.1
);
ctx.fillStyle = colors.tertiary;
ctx.fill();
ctx.fillStyle = colors.secondary;
- ctx.font = `${bodyFontSize}em '${font}'`;
ctx.fillText(
i18n("wizard.previews.reply_button"),
- shareButtonWidth + margin + 20,
- line + lineHeight * 1.9
+ shareButtonWidth + margin * 2,
+ verticalLinePos + lineHeight * 0.9
);
- // Draw Timeline
- const timelineX = width * 0.86;
+ // Draw timeline
ctx.beginPath();
ctx.strokeStyle = colors.tertiary;
ctx.lineWidth = 0.5;
@@ -197,17 +210,30 @@ export default class Index extends PreviewBaseComponent {
ctx.lineTo(timelineX, height * 0.7);
ctx.stroke();
- // Timeline
+ // Timeline handle
ctx.beginPath();
ctx.strokeStyle = colors.tertiary;
- ctx.lineWidth = 2;
- ctx.moveTo(timelineX, height * 0.3);
+ ctx.lineWidth = 3;
+ ctx.moveTo(timelineX, height * 0.3 + 10);
ctx.lineTo(timelineX, height * 0.4);
ctx.stroke();
- ctx.font = `Bold ${bodyFontSize}em ${font}`;
+ // Timeline post count
+ const postCountY = height * 0.3 + margin + 10;
+ ctx.beginPath();
+ ctx.font = `700 ${bodyFontSize}em '${font}'`;
ctx.fillStyle = colors.primary;
- ctx.fillText("1 / 20", timelineX + margin, height * 0.3 + margin * 1.5);
+ ctx.fillText("1 / 20", timelineX + margin / 2, postCountY);
+
+ // Timeline post date
+ ctx.beginPath();
+ ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
+ ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 70, 65);
+ ctx.fillText(
+ "Nov 22",
+ timelineX + margin / 2,
+ postCountY + lineHeight * 0.75
+ );
}
@action
diff --git a/app/assets/javascripts/discourse/app/static/wizard/lib/preview.js b/app/assets/javascripts/discourse/app/static/wizard/lib/preview.js
index fc87e5c9c02..90cfd4ec361 100644
--- a/app/assets/javascripts/discourse/app/static/wizard/lib/preview.js
+++ b/app/assets/javascripts/discourse/app/static/wizard/lib/preview.js
@@ -1,15 +1,5 @@
/*eslint no-bitwise:0 */
-export const LOREM = `
-Lorem ipsum dolor sit amet,
-consectetur adipiscing elit.
-Nullam eget sem non elit
-tincidunt rhoncus. Fusce
-velit nisl, porttitor sed
-nisl ac, consectetur interdum
-metus. Fusce in consequat
-augue, vel facilisis felis.`;
-
export function parseColor(color) {
const m = color.match(/^#([0-9a-f]{6})$/i);
if (m) {
@@ -148,3 +138,26 @@ export function drawHeader(ctx, colors, width, headerHeight) {
ctx.fill();
ctx.restore();
}
+
+export function resizeTextLinesToFitRect(
+ textLines,
+ rectWidth,
+ ctx,
+ fontSize,
+ font,
+ renderCallback
+) {
+ const maxLengthLine = textLines.reduce((a, b) =>
+ a.length > b.length ? a : b
+ );
+
+ let fontSizeDecreaseMultiplier = 1;
+ while (ctx.measureText(maxLengthLine).width > rectWidth) {
+ fontSizeDecreaseMultiplier -= 0.1;
+ ctx.font = `${fontSize * fontSizeDecreaseMultiplier}em '${font}'`;
+ }
+
+ for (let i = 0; i < textLines.length; i++) {
+ renderCallback(textLines[i], i);
+ }
+}
diff --git a/app/assets/javascripts/discourse/tests/helpers/select-kit-helper.js b/app/assets/javascripts/discourse/tests/helpers/select-kit-helper.js
index 43d97f88fe4..134be2a9a3f 100644
--- a/app/assets/javascripts/discourse/tests/helpers/select-kit-helper.js
+++ b/app/assets/javascripts/discourse/tests/helpers/select-kit-helper.js
@@ -115,6 +115,9 @@ function rowHelper(row) {
el() {
return row;
},
+ hasClass(className) {
+ return row.classList.contains(className);
+ },
};
}
diff --git a/app/assets/javascripts/discourse/tests/integration/components/wizard-fields-dropdown-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/wizard-fields-dropdown-test.gjs
new file mode 100644
index 00000000000..e0598aa90d5
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/integration/components/wizard-fields-dropdown-test.gjs
@@ -0,0 +1,255 @@
+import { render } from "@ember/test-helpers";
+import { module, test } from "qunit";
+import Dropdown from "discourse/static/wizard/components/fields/dropdown";
+import { Choice, Field } from "discourse/static/wizard/models/wizard";
+import { setupRenderingTest } from "discourse/tests/helpers/component-test";
+import selectKit from "discourse/tests/helpers/select-kit-helper";
+
+function buildFontChoices() {
+ return [
+ {
+ id: "arial",
+ label: "Arial",
+ classNames: "body-font-arial",
+ },
+ {
+ id: "helvetica",
+ label: "Helvetica",
+ classNames: "body-font-helvetica",
+ },
+ {
+ id: "lato",
+ label: "Lato",
+ classNames: "body-font-lato",
+ },
+ {
+ id: "montserrat",
+ label: "Montserrat",
+ classNames: "body-font-montserrat",
+ },
+ {
+ id: "noto_sans",
+ label: "NotoSans",
+ classNames: "body-font-noto-sans",
+ },
+ {
+ id: "roboto",
+ label: "Roboto",
+ classNames: "body-font-roboto",
+ },
+ {
+ id: "ubuntu",
+ label: "Ubuntu",
+ classNames: "body-font-ubuntu",
+ },
+ ];
+}
+
+module(
+ "Integration | Component | Wizard | Fields | Dropdown",
+ function (hooks) {
+ setupRenderingTest(hooks);
+
+ test("color_scheme field sets colors data on each field choice to render palettes in dropdown", async function (assert) {
+ const lightColors = [
+ {
+ name: "primary",
+ hex: "222222",
+ },
+ {
+ name: "secondary",
+ hex: "ffffff",
+ },
+ {
+ name: "tertiary",
+ hex: "0088cc",
+ },
+ {
+ name: "quaternary",
+ hex: "e45735",
+ },
+ {
+ name: "header_background",
+ hex: "ffffff",
+ },
+ {
+ name: "header_primary",
+ hex: "333333",
+ },
+ {
+ name: "highlight",
+ hex: "ffff4d",
+ },
+ {
+ name: "selected",
+ hex: "d1f0ff",
+ },
+ {
+ name: "hover",
+ hex: "f2f2f2",
+ },
+ {
+ name: "danger",
+ hex: "c80001",
+ },
+ {
+ name: "success",
+ hex: "009900",
+ },
+ {
+ name: "love",
+ hex: "fa6c8d",
+ },
+ ];
+
+ const darkColors = [
+ {
+ name: "primary",
+ hex: "dddddd",
+ },
+ {
+ name: "secondary",
+ hex: "222222",
+ },
+ {
+ name: "tertiary",
+ hex: "099dd7",
+ },
+ {
+ name: "quaternary",
+ hex: "c14924",
+ },
+ {
+ name: "header_background",
+ hex: "111111",
+ },
+ {
+ name: "header_primary",
+ hex: "dddddd",
+ },
+ {
+ name: "highlight",
+ hex: "a87137",
+ },
+ {
+ name: "selected",
+ hex: "052e3d",
+ },
+ {
+ name: "hover",
+ hex: "313131",
+ },
+ {
+ name: "danger",
+ hex: "e45735",
+ },
+ {
+ name: "success",
+ hex: "1ca551",
+ },
+ {
+ name: "love",
+ hex: "fa6c8d",
+ },
+ ];
+
+ const field = new Field({
+ type: "dropdown",
+ id: "color_scheme",
+ label: "Color palette",
+ choices: [
+ new Choice({
+ id: "light",
+ label: "Light",
+ data: { colors: lightColors },
+ }),
+ new Choice({
+ id: "dark",
+ label: "Dark",
+ data: { colors: darkColors },
+ }),
+ ],
+ });
+
+ await render(
);
+ const colorPalettesSelector = selectKit(
+ ".wizard-container__dropdown.color-palettes"
+ );
+ await colorPalettesSelector.expand();
+
+ lightColors
+ .reject((colorDef) => colorDef.name === "secondary")
+ .forEach((colorDef) => {
+ assert
+ .dom(
+ `.palettes .palette[style*='background-color:#${colorDef.hex}']`,
+ colorPalettesSelector.rowByValue("light").el()
+ )
+ .exists();
+ });
+
+ darkColors
+ .reject((colorDef) => colorDef.name === "secondary")
+ .forEach((colorDef) => {
+ assert
+ .dom(
+ `.palettes .palette[style*='background-color:#${colorDef.hex}']`,
+ colorPalettesSelector.rowByValue("dark").el()
+ )
+ .exists();
+ });
+ });
+
+ test("body_font sets body-font-X classNames on each field choice", async function (assert) {
+ const fontChoices = buildFontChoices();
+
+ const field = new Field({
+ type: "dropdown",
+ id: "body_font",
+ label: "Body font",
+ choices: fontChoices.map((choice) => new Choice(choice)),
+ });
+
+ await render(
);
+ const fontSelector = selectKit(
+ ".wizard-container__dropdown.font-selector"
+ );
+ await fontSelector.expand();
+
+ fontChoices.forEach((choice) => {
+ assert.true(
+ fontSelector
+ .rowByValue(choice.id)
+ .hasClass(`body-font-${choice.id.replace("_", "-")}`),
+ `has body-font-${choice.id} CSS class`
+ );
+ });
+ });
+
+ test("heading_font sets heading-font-x classNames on each field choice", async function (assert) {
+ const fontChoices = buildFontChoices();
+
+ const field = new Field({
+ type: "dropdown",
+ id: "heading_font",
+ label: "heading font",
+ choices: fontChoices.map((choice) => new Choice(choice)),
+ });
+
+ await render(
);
+ const fontSelector = selectKit(
+ ".wizard-container__dropdown.font-selector"
+ );
+ await fontSelector.expand();
+
+ fontChoices.forEach((choice) => {
+ assert.true(
+ fontSelector
+ .rowByValue(choice.id)
+ .hasClass(`heading-font-${choice.id.replace("_", "-")}`),
+ `has heading-font-${choice.id} CSS class`
+ );
+ });
+ });
+ }
+);
diff --git a/app/assets/javascripts/select-kit/addon/components/font-selector.js b/app/assets/javascripts/select-kit/addon/components/font-selector.js
new file mode 100644
index 00000000000..2aa4c8068f5
--- /dev/null
+++ b/app/assets/javascripts/select-kit/addon/components/font-selector.js
@@ -0,0 +1,14 @@
+import { classNames } from "@ember-decorators/component";
+import ComboBoxComponent from "select-kit/components/combo-box";
+import { pluginApiIdentifiers, selectKitOptions } from "./select-kit";
+
+@classNames("font-selector")
+@pluginApiIdentifiers(["font-selector"])
+@selectKitOptions({
+ selectedNameComponent: "selected-font",
+})
+export default class FontSelector extends ComboBoxComponent {
+ modifyComponentForRow() {
+ return "font-selector/font-selector-row";
+ }
+}
diff --git a/app/assets/javascripts/select-kit/addon/components/font-selector/font-selector-row.js b/app/assets/javascripts/select-kit/addon/components/font-selector/font-selector-row.js
new file mode 100644
index 00000000000..e018972bf09
--- /dev/null
+++ b/app/assets/javascripts/select-kit/addon/components/font-selector/font-selector-row.js
@@ -0,0 +1,5 @@
+import { classNames } from "@ember-decorators/component";
+import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
+
+@classNames("font-selector-row")
+export default class FontSelectorRow extends SelectKitRowComponent {}
diff --git a/app/assets/javascripts/select-kit/addon/components/homepage-style-selector.js b/app/assets/javascripts/select-kit/addon/components/homepage-style-selector.js
new file mode 100644
index 00000000000..e567bc47eb0
--- /dev/null
+++ b/app/assets/javascripts/select-kit/addon/components/homepage-style-selector.js
@@ -0,0 +1,11 @@
+import { classNames } from "@ember-decorators/component";
+import ComboBoxComponent from "select-kit/components/combo-box";
+import { pluginApiIdentifiers } from "./select-kit";
+
+@classNames("homepage-style-selector")
+@pluginApiIdentifiers(["homepage-style-selector"])
+export default class HomepageStyleSelector extends ComboBoxComponent {
+ modifyComponentForRow() {
+ return "homepage-style-selector/homepage-style-selector-row";
+ }
+}
diff --git a/app/assets/javascripts/select-kit/addon/components/homepage-style-selector/homepage-style-selector-row.hbs b/app/assets/javascripts/select-kit/addon/components/homepage-style-selector/homepage-style-selector-row.hbs
new file mode 100644
index 00000000000..79a16ca4809
--- /dev/null
+++ b/app/assets/javascripts/select-kit/addon/components/homepage-style-selector/homepage-style-selector-row.hbs
@@ -0,0 +1,6 @@
+
+ {{html-safe this.label}}
+ {{#if this.item.description}}
+ {{html-safe this.item.description}}
+ {{/if}}
+
\ No newline at end of file
diff --git a/app/assets/javascripts/select-kit/addon/components/homepage-style-selector/homepage-style-selector-row.js b/app/assets/javascripts/select-kit/addon/components/homepage-style-selector/homepage-style-selector-row.js
new file mode 100644
index 00000000000..6304554cb3d
--- /dev/null
+++ b/app/assets/javascripts/select-kit/addon/components/homepage-style-selector/homepage-style-selector-row.js
@@ -0,0 +1,5 @@
+import { classNames } from "@ember-decorators/component";
+import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
+
+@classNames("homepage-style-selector-row")
+export default class HomepageStyleSelectorRow extends SelectKitRowComponent {}
diff --git a/app/assets/javascripts/select-kit/addon/components/selected-font.gjs b/app/assets/javascripts/select-kit/addon/components/selected-font.gjs
new file mode 100644
index 00000000000..21c153b2469
--- /dev/null
+++ b/app/assets/javascripts/select-kit/addon/components/selected-font.gjs
@@ -0,0 +1,10 @@
+import concatClass from "discourse/helpers/concat-class";
+import SelectedNameComponent from "select-kit/components/selected-name";
+
+export default class SelectedFont extends SelectedNameComponent {
+
+
+ {{this.label}}
+
+
+}
diff --git a/app/assets/stylesheets/wizard.scss b/app/assets/stylesheets/wizard.scss
index 3b95ddba997..d17f58f6bbf 100644
--- a/app/assets/stylesheets/wizard.scss
+++ b/app/assets/stylesheets/wizard.scss
@@ -1,3 +1,6 @@
+$blob-bg: absolute-image-url("/branded-background.svg");
+$blob-mobile-bg: absolute-image-url("/branded-background-mobile.svg");
+
@keyframes bump {
0% {
transform: scale(1);
@@ -13,7 +16,12 @@
}
body.wizard {
- background-color: var(--primary-50);
+ background-color: var(--secondary);
+ background-image: $blob-bg;
+ background-size: 110vw 110vh; // crops better than cover at various viewport sizes
+ background-repeat: no-repeat;
+ background-position: bottom;
+
color: var(--primary-very-high);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen-Sans, Ubuntu, Cantarell, Arial, sans-serif;
@@ -22,12 +30,18 @@ body.wizard {
max-width: 100%;
}
+ @include breakpoint("mobile-extra-large") {
+ background: $blob-mobile-bg;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: bottom;
+ }
+
#wizard-main {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 1.5em;
- background-image: absolute-image-url("/bubbles-bg.png");
height: 100%;
}
@@ -137,6 +151,12 @@ body.wizard {
}
}
+ &__step.styling .wizard-container__field.styling-preview-field {
+ label {
+ display: none;
+ }
+ }
+
&__field {
margin-bottom: 1em;
}
@@ -165,12 +185,18 @@ body.wizard {
}
}
+ &__field.dropdown-field.dropdown-homepage-style {
+ .wizard-container__dropdown {
+ height: auto;
+ }
+ }
+
&__step-form {
display: flex;
}
&__sidebar {
- width: 170px;
+ width: 230px;
box-sizing: border-box;
margin-right: 1em;
@@ -307,7 +333,6 @@ body.wizard {
&__step-text {
display: inline;
- margin-right: 0.25em;
@media only screen and (max-width: 568px) {
display: none;
}
@@ -539,6 +564,7 @@ body.wizard {
&__dropdown {
width: 100%;
+ height: 38px;
}
&__dropdown .select-kit-header:not(.btn) {
@@ -553,6 +579,19 @@ body.wizard {
overflow: hidden;
}
+ &__dropdown.homepage-style-selector {
+ .select-kit-row {
+ .name {
+ font-weight: bold;
+ }
+
+ .desc {
+ display: block;
+ margin-top: 0.1em;
+ }
+ }
+ }
+
&__field.checkbox-field .wizard-container__label {
cursor: pointer;
display: inline-block;
diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb
index 244ff30a52b..2a22e56d234 100644
--- a/app/models/color_scheme.rb
+++ b/app/models/color_scheme.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class ColorScheme < ActiveRecord::Base
- CUSTOM_SCHEMES = {
+ BUILT_IN_SCHEMES = {
Dark: {
"primary" => "dddddd",
"secondary" => "222222",
@@ -286,7 +286,7 @@ class ColorScheme < ActiveRecord::Base
list = [{ id: LIGHT_THEME_ID, colors: base_with_hash }]
- CUSTOM_SCHEMES.each do |k, v|
+ BUILT_IN_SCHEMES.each do |k, v|
colors = []
v.each { |name, color| colors << { name: name, hex: "#{color}" } }
list.push(id: k.to_s, colors: colors)
@@ -385,7 +385,7 @@ class ColorScheme < ActiveRecord::Base
new_color_scheme.user_selectable = true
colors =
- CUSTOM_SCHEMES[params[:base_scheme_id].to_sym]&.map do |name, hex|
+ BUILT_IN_SCHEMES[params[:base_scheme_id].to_sym]&.map do |name, hex|
{ name: name, hex: hex }
end if params[:base_scheme_id]
colors ||= base.colors_hashes
@@ -439,7 +439,7 @@ class ColorScheme < ActiveRecord::Base
def base_colors
colors = nil
- colors = CUSTOM_SCHEMES[base_scheme_id.to_sym] if base_scheme_id && base_scheme_id != "Light"
+ colors = BUILT_IN_SCHEMES[base_scheme_id.to_sym] if base_scheme_id && base_scheme_id != "Light"
colors || ColorScheme.base_colors
end
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index 137278bcc4d..c1394963bb3 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -72,7 +72,7 @@ class SiteSetting < ActiveRecord::Base
end
def self.top_menu_items
- top_menu.split("|").map { |menu_item| TopMenuItem.new(menu_item) }
+ top_menu_map.map { |menu_item| TopMenuItem.new(menu_item) }
end
def self.homepage
diff --git a/app/models/theme.rb b/app/models/theme.rb
index 8a6e3199984..b5d6c26aaa5 100644
--- a/app/models/theme.rb
+++ b/app/models/theme.rb
@@ -372,6 +372,10 @@ class Theme < ActiveRecord::Base
end
end
+ def self.find_default
+ find_by(id: SiteSetting.default_theme_id)
+ end
+
def self.lookup_field(theme_id, target, field, skip_transformation: false, csp_nonce: nil)
return "" if theme_id.blank?
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 90fb65c76d8..a8ef98ab1eb 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -7408,8 +7408,42 @@ en:
regular: "Regular User"
previews:
- topic_title: "A discussion topic heading"
+ topic_title: "What books are you reading?"
share_button: "Share"
reply_button: "Reply"
topic_preview: "Topic preview"
homepage_preview: "Homepage preview"
+
+ homepage_preview:
+ nav_buttons:
+ all_categories: "all categories"
+ new: "New"
+ unread: "Unread"
+ top: "Top"
+ latest: "Latest"
+ hot: "Hot"
+ categories: "Categories"
+ topic_titles:
+ what_books: "What books are you reading?"
+ what_movies: "What movies have you seen recently?"
+ random_fact: "Random fact of the day"
+ tv_show: "Recommend a TV show"
+ topic_ops:
+ what_books: |
+ We all love to read, let's use this topic to share our
+ current or recent reads. I'm a fantasy fan and I've been
+ re-reading The Lord of the Rings for the 100th time.
+ What about you?
+ category_descriptions:
+ icebreakers: "Get to know your fellow community members with fun questions."
+ news: "Discuss the latest news and events."
+ site_feedback: "Share your thoughts on the community and suggest improvements."
+ category_names:
+ icebreakers: "Icebreakers"
+ news: "News"
+ site_feedback: "Site Feedback"
+ table_headers:
+ topic: "Topic"
+ replies: "Replies"
+ views: "Views"
+ activity: "Activity"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 0ea35cdbfe2..6b314c3e802 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -6264,23 +6264,17 @@ en:
label: "Homepage style"
choices:
latest:
- label: "Latest Topics"
- categories_only:
- label: "Categories Only"
- categories_with_featured_topics:
- label: "Categories with Featured Topics"
+ label: "Latest"
+ description: "Displays the most recently active topics in all categories, helping members stay up-to-date with discussions they care about"
+ hot:
+ label: "Hot"
+ description: "Surfaces trending topics by blending recent and overall popularity, showcases what members are talking about in your community right now"
categories_and_latest_topics:
- label: "Categories and Latest Topics"
- categories_and_latest_topics_created_date:
- label: "Categories and Latest Topics (sort by topic created date)"
- categories_and_top_topics:
- label: "Categories and Top Topics"
+ label: "Categories with latest topics"
+ description: "Combines the recently active topics across all categories with a list of categories, their description, and total topics"
categories_boxes:
- label: "Categories Boxes"
- categories_boxes_with_topics:
- label: "Categories Boxes with Topics"
- subcategories_with_featured_topics:
- label: "Subcategories with Featured Topics"
+ label: "Category boxes"
+ description: "Displays the categories and their description in a grid, ideal for members to see an overview of the sub-communities of your site"
branding:
title: "Site logo"
diff --git a/lib/stylesheet/importer.rb b/lib/stylesheet/importer.rb
index 6e390a828e3..855ac522bc8 100644
--- a/lib/stylesheet/importer.rb
+++ b/lib/stylesheet/importer.rb
@@ -86,7 +86,7 @@ module Stylesheet
.body-font-#{font[:key].tr("_", "-")} {
font-family: #{font[:stack]};
}
- .heading-font-#{font[:key].tr("_", "-")} h2 {
+ .heading-font-#{font[:key].tr("_", "-")} {
font-family: #{font[:stack]};
}
CSS
@@ -222,7 +222,7 @@ module Stylesheet
)
contents << <<~CSS
@font-face {
- font-family: #{font[:name]};
+ font-family: '#{font[:name]}';
src: #{src};
font-weight: #{variant[:weight]};
}
diff --git a/lib/wizard/builder.rb b/lib/wizard/builder.rb
index 07eddc89ab4..4c2600aac48 100644
--- a/lib/wizard/builder.rb
+++ b/lib/wizard/builder.rb
@@ -9,6 +9,20 @@ class Wizard
def build
return @wizard unless SiteSetting.wizard_enabled? && @wizard.user.try(:staff?)
+ append_introduction_step
+ append_privacy_step
+ append_styling_step
+ append_ready_step
+ append_branding_step
+ append_corporate_step
+
+ DiscourseEvent.trigger(:build_wizard, @wizard)
+ @wizard
+ end
+
+ protected
+
+ def append_introduction_step
@wizard.append_step("introduction") do |step|
step.emoji = "wave"
step.description_vars = { base_path: Discourse.base_path }
@@ -56,7 +70,9 @@ class Wizard
end
end
end
+ end
+ def append_privacy_step
@wizard.append_step("privacy") do |step|
step.emoji = "hugs"
@@ -93,12 +109,16 @@ class Wizard
updater.update_setting(:must_approve_users, updater.fields[:must_approve_users] == "yes")
end
end
+ end
+ def append_ready_step
@wizard.append_step("ready") do |step|
# no form on this page, just info.
step.emoji = "rocket"
end
+ end
+ def append_branding_step
@wizard.append_step("branding") do |step|
step.emoji = "framed_picture"
step.add_field(id: "logo", type: "image", value: SiteSetting.site_logo_url)
@@ -112,10 +132,12 @@ class Wizard
end
end
end
+ end
+ def append_styling_step
@wizard.append_step("styling") do |step|
step.emoji = "art"
- default_theme = Theme.find_by(id: SiteSetting.default_theme_id)
+ default_theme = Theme.find_default
default_theme_override = SiteSetting.exists?(name: "default_theme_id")
base_scheme = default_theme&.color_scheme&.base_scheme_id
@@ -159,17 +181,20 @@ class Wizard
show_in_sidebar: true,
)
- DiscourseFonts.fonts.each do |font|
- body_font.add_choice(font[:key], label: font[:name])
- heading_font.add_choice(font[:key], label: font[:name])
- end
+ DiscourseFonts
+ .fonts
+ .sort_by { |f| f[:name] }
+ .each do |font|
+ body_font.add_choice(font[:key], label: font[:name])
+ heading_font.add_choice(font[:key], label: font[:name])
+ end
current =
(
- if SiteSetting.top_menu.starts_with?("categories")
+ if SiteSetting.homepage == "categories"
SiteSetting.desktop_category_page_style
else
- "latest"
+ SiteSetting.homepage
end
)
style =
@@ -181,7 +206,11 @@ class Wizard
show_in_sidebar: true,
)
style.add_choice("latest")
- CategoryPageStyle.values.each { |page| style.add_choice(page[:value]) }
+ style.add_choice("hot")
+
+ # Subset of CategoryPageStyle, we don't want to show all the options here.
+ style.add_choice("categories_and_latest_topics")
+ style.add_choice("categories_boxes")
step.add_field(id: "styling_preview", type: "styling-preview")
@@ -189,10 +218,11 @@ class Wizard
updater.update_setting(:base_font, updater.fields[:body_font])
updater.update_setting(:heading_font, updater.fields[:heading_font])
- top_menu = SiteSetting.top_menu.split("|")
- if updater.fields[:homepage_style] == "latest" && top_menu[0] != "latest"
- top_menu.delete("latest")
- top_menu.insert(0, "latest")
+ top_menu = SiteSetting.top_menu_map
+ if %w[latest hot].include?(updater.fields[:homepage_style]) &&
+ top_menu.first != updater.fields[:homepage_style]
+ top_menu.delete(updater.fields[:homepage_style])
+ top_menu.insert(0, updater.fields[:homepage_style])
elsif updater.fields[:homepage_style] != "latest"
top_menu.delete("categories")
top_menu.insert(0, "categories")
@@ -228,7 +258,9 @@ class Wizard
updater.refresh_required = true
end
end
+ end
+ def append_corporate_step
@wizard.append_step("corporate") do |step|
step.emoji = "briefcase"
step.description_vars = { base_path: Discourse.base_path }
@@ -256,13 +288,8 @@ class Wizard
end
end
end
-
- DiscourseEvent.trigger(:build_wizard, @wizard)
- @wizard
end
- protected
-
def replace_setting_value(updater, raw, field_name)
old_value = SiteSetting.get(field_name)
old_value = field_name if old_value.blank?
diff --git a/spec/fabricators/topic_fabricator.rb b/spec/fabricators/topic_fabricator.rb
index 436e1db954f..6bf64bc4082 100644
--- a/spec/fabricators/topic_fabricator.rb
+++ b/spec/fabricators/topic_fabricator.rb
@@ -8,6 +8,8 @@ Fabricator(:topic) do
end
end
+Fabricator(:topic_with_op, from: :topic) { after_create { |topic| Fabricate(:post, topic: topic) } }
+
Fabricator(:deleted_topic, from: :topic) { deleted_at { 1.minute.ago } }
Fabricator(:closed_topic, from: :topic) { closed true }
diff --git a/spec/lib/wizard/wizard_builder_spec.rb b/spec/lib/wizard/wizard_builder_spec.rb
index 5289b9c05b2..fff48241d90 100644
--- a/spec/lib/wizard/wizard_builder_spec.rb
+++ b/spec/lib/wizard/wizard_builder_spec.rb
@@ -92,12 +92,12 @@ RSpec.describe Wizard::Builder do
describe "styling" do
let(:styling_step) { wizard.steps.find { |s| s.id == "styling" } }
- let(:font_field) { styling_step.fields[1] }
+ let(:font_field) { styling_step.fields.find { |f| f.id == "body_font" } }
fab!(:theme)
let(:colors_field) { styling_step.fields.first }
- it "has the full list of available fonts" do
- expect(font_field.choices.size).to eq(DiscourseFonts.fonts.size)
+ it "has the full list of available fonts in alphabetical order" do
+ expect(font_field.choices.map(&:label)).to eq(DiscourseFonts.fonts.map { |f| f[:name] }.sort)
end
context "with colors" do
diff --git a/spec/models/color_scheme_spec.rb b/spec/models/color_scheme_spec.rb
index 3a718daa001..ad04392ac19 100644
--- a/spec/models/color_scheme_spec.rb
+++ b/spec/models/color_scheme_spec.rb
@@ -146,7 +146,7 @@ RSpec.describe ColorScheme do
it "falls back to default scheme if base scheme does not have color" do
custom_scheme_id = "BaseSchemeWithNoHighlightColor"
- ColorScheme::CUSTOM_SCHEMES[custom_scheme_id.to_sym] = { "secondary" => "123123" }
+ ColorScheme::BUILT_IN_SCHEMES[custom_scheme_id.to_sym] = { "secondary" => "123123" }
color_scheme = ColorScheme.new(base_scheme_id: custom_scheme_id)
color_scheme.color_scheme_colors << ColorSchemeColor.new(name: "primary", hex: "121212")
@@ -156,7 +156,7 @@ RSpec.describe ColorScheme do
expect(resolved["secondary"]).to eq("123123") # From custom scheme
expect(resolved["tertiary"]).to eq("0088cc") # From `foundation/colors.scss`
ensure
- ColorScheme::CUSTOM_SCHEMES.delete(custom_scheme_id.to_sym)
+ ColorScheme::BUILT_IN_SCHEMES.delete(custom_scheme_id.to_sym)
end
it "calculates 'hover' and 'selected' from existing db colors in dark mode" do
diff --git a/spec/system/page_objects/pages/wizard.rb b/spec/system/page_objects/pages/wizard.rb
index 6fe3eb41187..0cf4b0291b2 100644
--- a/spec/system/page_objects/pages/wizard.rb
+++ b/spec/system/page_objects/pages/wizard.rb
@@ -3,17 +3,148 @@
module PageObjects
module Pages
class Wizard < PageObjects::Pages::Base
+ attr_reader :introduction_step,
+ :privacy_step,
+ :ready_step,
+ :branding_step,
+ :styling_step,
+ :corporate_step
+
+ def initialize
+ @introduction_step = PageObjects::Pages::Wizard::IntroductionStep.new(self)
+ @privacy_step = PageObjects::Pages::Wizard::PrivacyStep.new(self)
+ @ready_step = PageObjects::Pages::Wizard::ReadyStep.new(self)
+ @branding_step = PageObjects::Pages::Wizard::BrandingStep.new(self)
+ @styling_step = PageObjects::Pages::Wizard::StylingStep.new(self)
+ @corporate_step = PageObjects::Pages::Wizard::CorporateStep.new(self)
+ end
+
+ def go_to_step(step_id)
+ visit("/wizard/steps/#{step_id}")
+ end
+
+ def on_step?(step_id)
+ has_css?(".wizard-container__step.#{step_id}")
+ end
+
def click_jump_in
- find(".jump-in").click
+ find(".wizard-container__button.jump-in").click
+ end
+
+ def click_configure_more
+ find(".wizard-container__button.configure-more").click
end
def go_to_next_step
find(".wizard-container__button.next").click
end
- def select_access_option(label)
- find(".wizard-container__radio-choice", text: label).click
+ def find_field(field_type, field_id)
+ find(".wizard-container__field.#{field_type}-field.#{field_type}-#{field_id}")
+ end
+
+ def fill_field(field_type, field_id, value)
+ find_field(field_type, field_id).fill_in(with: value)
+ end
+
+ def has_field_with_value?(field_type, field_id, value)
+ find_field(field_type, field_id).find("input").value == value
end
end
end
end
+
+class PageObjects::Pages::Wizard::StepBase < PageObjects::Pages::Base
+ attr_reader :wizard
+
+ def initialize(wizard)
+ @wizard = wizard
+ end
+end
+
+class PageObjects::Pages::Wizard::IntroductionStep < PageObjects::Pages::Wizard::StepBase
+end
+
+class PageObjects::Pages::Wizard::PrivacyStep < PageObjects::Pages::Wizard::StepBase
+ def choice_selector(choice_id)
+ ".wizard-container__radio-choice[data-choice-id='#{choice_id}']"
+ end
+
+ def select_access_option(section, choice_id)
+ wizard.find_field("radio", section).find(choice_selector(choice_id)).click
+ end
+
+ def has_selected_choice?(section, choice_id)
+ wizard.find_field("radio", section).has_css?(choice_selector(choice_id) + ".--selected")
+ end
+end
+
+class PageObjects::Pages::Wizard::ReadyStep < PageObjects::Pages::Wizard::StepBase
+end
+
+class PageObjects::Pages::Wizard::BrandingStep < PageObjects::Pages::Wizard::StepBase
+ def click_upload_button(field_id)
+ wizard.find_field("image", field_id).find(".wizard-container__button-upload").click
+ end
+
+ def has_upload?(field_id)
+ wizard.find_field("image", field_id).has_css?(".wizard-container__button-upload.has-upload")
+ end
+end
+
+class PageObjects::Pages::Wizard::StylingStep < PageObjects::Pages::Wizard::StepBase
+ def select_color_palette_option(palette)
+ select_kit =
+ PageObjects::Components::SelectKit.new(".dropdown-color-scheme .wizard-container__dropdown")
+ select_kit.expand
+ select_kit.select_row_by_value(palette)
+ end
+
+ def select_body_font_option(font)
+ select_kit =
+ PageObjects::Components::SelectKit.new(".dropdown-body-font .wizard-container__dropdown")
+ select_kit.expand
+ select_kit.select_row_by_value(font)
+ end
+
+ def select_heading_font_option(font)
+ select_kit =
+ PageObjects::Components::SelectKit.new(".dropdown-heading-font .wizard-container__dropdown")
+ select_kit.expand
+ select_kit.select_row_by_value(font)
+ end
+
+ def select_homepage_style_option(homepage)
+ select_kit =
+ PageObjects::Components::SelectKit.new(".dropdown-homepage-style .wizard-container__dropdown")
+ select_kit.expand
+ select_kit.select_row_by_value(homepage)
+ end
+
+ def has_selected_color_palette?(palette)
+ select_kit =
+ PageObjects::Components::SelectKit.new(".dropdown-color-scheme .wizard-container__dropdown")
+ select_kit.has_selected_value?(palette)
+ end
+
+ def has_selected_body_font?(font)
+ select_kit =
+ PageObjects::Components::SelectKit.new(".dropdown-body-font .wizard-container__dropdown")
+ select_kit.has_selected_value?(font)
+ end
+
+ def has_selected_heading_font?(font)
+ select_kit =
+ PageObjects::Components::SelectKit.new(".dropdown-heading-font .wizard-container__dropdown")
+ select_kit.has_selected_value?(font)
+ end
+
+ def has_selected_homepage_style?(hompage)
+ select_kit =
+ PageObjects::Components::SelectKit.new(".dropdown-homepage-style .wizard-container__dropdown")
+ select_kit.has_selected_value?(hompage)
+ end
+end
+
+class PageObjects::Pages::Wizard::CorporateStep < PageObjects::Pages::Wizard::StepBase
+end
diff --git a/spec/system/wizard_spec.rb b/spec/system/wizard_spec.rb
index f33b7721898..d8f1fa6fdc2 100644
--- a/spec/system/wizard_spec.rb
+++ b/spec/system/wizard_spec.rb
@@ -2,55 +2,153 @@
describe "Wizard", type: :system do
fab!(:admin)
- fab!(:topic) { Fabricate(:topic, title: "admin guide with 15 chars") }
- fab!(:post) { Fabricate(:post, topic: topic) }
let(:wizard_page) { PageObjects::Pages::Wizard.new }
before { sign_in(admin) }
- it "lets user configure member access" do
- visit("/wizard/steps/privacy")
-
- expect(page).to have_css(
- ".wizard-container__radio-choice.--selected",
- text: I18n.t("wizard.step.privacy.fields.login_required.choices.public.label"),
- )
-
- wizard_page.select_access_option("Private")
-
- expect(page).to have_css(
- ".wizard-container__radio-choice.--selected",
- text: I18n.t("wizard.step.privacy.fields.login_required.choices.private.label"),
- )
-
+ it "successfully goes through every step of the wizard" do
+ visit("/wizard")
+ expect(wizard_page).to be_on_step("introduction")
+ wizard_page.fill_field("text", "title", "My Test Site")
wizard_page.go_to_next_step
-
- expect(page).to have_current_path("/wizard/steps/ready")
- expect(SiteSetting.login_required).to eq(true)
-
- visit("/wizard/steps/privacy")
-
- expect(page).to have_css(
- ".wizard-container__radio-choice.--selected",
- text: I18n.t("wizard.step.privacy.fields.login_required.choices.private.label"),
- )
- end
-
- it "redirects to latest when wizard is completed" do
- visit("/wizard/steps/ready")
+ expect(wizard_page).to be_on_step("privacy")
+ wizard_page.go_to_next_step
+ expect(wizard_page).to be_on_step("styling")
+ wizard_page.go_to_next_step
+ expect(wizard_page).to be_on_step("ready")
+ wizard_page.click_configure_more
+ expect(wizard_page).to be_on_step("branding")
+ wizard_page.go_to_next_step
+ expect(wizard_page).to be_on_step("corporate")
wizard_page.click_jump_in
-
expect(page).to have_current_path("/latest")
end
- it "redirects to admin guide when wizard is completed and bootstrap mode is enabled" do
- SiteSetting.bootstrap_mode_enabled = true
- SiteSetting.admin_quick_start_topic_id = topic.id
+ describe "Wizard Step: Privacy" do
+ it "lets user configure member access" do
+ wizard_page.go_to_step("privacy")
+ expect(SiteSetting.login_required).to eq(false)
+ expect(SiteSetting.invite_only).to eq(false)
+ expect(SiteSetting.must_approve_users).to eq(false)
- visit("/wizard/steps/ready")
- wizard_page.click_jump_in
+ expect(wizard_page.privacy_step).to have_selected_choice("login-required", "public")
+ expect(wizard_page.privacy_step).to have_selected_choice("invite-only", "sign_up")
+ expect(wizard_page.privacy_step).to have_selected_choice("must-approve-users", "no")
- expect(page).to have_current_path("/t/admin-guide-with-15-chars/#{topic.id}")
+ wizard_page.privacy_step.select_access_option("login-required", "private")
+ wizard_page.privacy_step.select_access_option("invite-only", "invite_only")
+ wizard_page.privacy_step.select_access_option("must-approve-users", "yes")
+
+ wizard_page.go_to_next_step
+
+ expect(wizard_page).to be_on_step("styling")
+ expect(SiteSetting.login_required).to eq(true)
+ expect(SiteSetting.invite_only).to eq(true)
+ expect(SiteSetting.must_approve_users).to eq(true)
+
+ wizard_page.go_to_step("privacy")
+
+ expect(wizard_page.privacy_step).to have_selected_choice("login-required", "private")
+ expect(wizard_page.privacy_step).to have_selected_choice("invite-only", "invite_only")
+ expect(wizard_page.privacy_step).to have_selected_choice("must-approve-users", "yes")
+ end
+ end
+
+ describe "Wizard Step: Branding" do
+ let(:file_path_1) { file_from_fixtures("logo.png", "images").path }
+ let(:file_path_2) { file_from_fixtures("logo.jpg", "images").path }
+
+ it "lets user configure logos" do
+ wizard_page.go_to_step("branding")
+ expect(wizard_page).to be_on_step("branding")
+ attach_file(file_path_1) { wizard_page.branding_step.click_upload_button("logo") }
+ expect(wizard_page.branding_step).to have_upload("logo")
+ attach_file(file_path_2) { wizard_page.branding_step.click_upload_button("logo-small") }
+ expect(wizard_page.branding_step).to have_upload("logo-small")
+ wizard_page.go_to_next_step
+ expect(wizard_page).to be_on_step("corporate")
+
+ expect(SiteSetting.logo).to eq(Upload.find_by(original_filename: File.basename(file_path_1)))
+ expect(SiteSetting.logo_small).to eq(
+ Upload.find_by(original_filename: File.basename(file_path_2)),
+ )
+ end
+ end
+
+ describe "Wizard Step: Styling" do
+ it "lets user configure styling including fonts and colors" do
+ wizard_page.go_to_step("styling")
+ expect(wizard_page).to be_on_step("styling")
+
+ wizard_page.styling_step.select_color_palette_option("Dark")
+ wizard_page.styling_step.select_body_font_option("lato")
+ wizard_page.styling_step.select_heading_font_option("merriweather")
+ wizard_page.styling_step.select_homepage_style_option("hot")
+
+ wizard_page.go_to_next_step
+ expect(wizard_page).to be_on_step("ready")
+
+ expect(Theme.find_default.color_scheme_id).to eq(
+ ColorScheme.find_by(base_scheme_id: "Dark", via_wizard: true).id,
+ )
+ expect(SiteSetting.base_font).to eq("lato")
+ expect(SiteSetting.heading_font).to eq("merriweather")
+ expect(SiteSetting.homepage).to eq("hot")
+
+ wizard_page.go_to_step("styling")
+
+ expect(wizard_page.styling_step).to have_selected_color_palette("Dark")
+ expect(wizard_page.styling_step).to have_selected_body_font("lato")
+ expect(wizard_page.styling_step).to have_selected_heading_font("merriweather")
+ expect(wizard_page.styling_step).to have_selected_homepage_style("hot")
+ end
+ end
+
+ describe "Wizard Step: Ready" do
+ it "redirects to latest" do
+ wizard_page.go_to_step("ready")
+ wizard_page.click_jump_in
+
+ expect(page).to have_current_path("/latest")
+ end
+
+ it "redirects to admin guide when bootstrap mode is enabled" do
+ topic = Fabricate(:topic_with_op, title: "Admin Getting Started Guide")
+ SiteSetting.bootstrap_mode_enabled = true
+ SiteSetting.admin_quick_start_topic_id = topic.id
+
+ wizard_page.go_to_step("ready")
+ wizard_page.click_jump_in
+
+ expect(page).to have_current_path(topic.url)
+ end
+ end
+
+ describe "Wizard Step: Corporate" do
+ it "lets user configure corporate including governing law and city for disputes" do
+ wizard_page.go_to_step("corporate")
+ expect(wizard_page).to be_on_step("corporate")
+ wizard_page.fill_field("text", "company-name", "ACME")
+ wizard_page.fill_field("text", "governing-law", "California")
+ wizard_page.fill_field("text", "contact-url", "https://ac.me")
+ wizard_page.fill_field("text", "city-for-disputes", "San Francisco")
+ wizard_page.fill_field("text", "contact-email", "coyote@ac.me")
+ wizard_page.click_jump_in
+ expect(page).to have_current_path("/latest")
+
+ expect(SiteSetting.company_name).to eq("ACME")
+ expect(SiteSetting.governing_law).to eq("California")
+ expect(SiteSetting.city_for_disputes).to eq("San Francisco")
+ expect(SiteSetting.contact_url).to eq("https://ac.me")
+ expect(SiteSetting.contact_email).to eq("coyote@ac.me")
+
+ wizard_page.go_to_step("corporate")
+ expect(wizard_page).to have_field_with_value("text", "company-name", "ACME")
+ expect(wizard_page).to have_field_with_value("text", "governing-law", "California")
+ expect(wizard_page).to have_field_with_value("text", "contact-url", "https://ac.me")
+ expect(wizard_page).to have_field_with_value("text", "city-for-disputes", "San Francisco")
+ expect(wizard_page).to have_field_with_value("text", "contact-email", "coyote@ac.me")
+ end
end
end