FEATURE: Separate base and heading font site_settings (#10807)

Allows site administrators to pick different fonts for headings in the wizard and in their site settings. Also correctly displays the header logos in wizard previews.
This commit is contained in:
Penar Musaraj 2020-10-05 13:40:41 -04:00 committed by GitHub
parent bdfb370f19
commit a4356b99af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 171 additions and 105 deletions

View File

@ -1,30 +1,26 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { observes } from "discourse-common/utils/decorators";
import {
createPreviewComponent,
darkLightDiff,
chooseDarker,
LOREM,
} from "wizard/lib/preview";
export default createPreviewComponent(305, 165, {
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 createPreviewComponent(659, 320, {
logo: null,
avatar: null,
classNameBindings: ["isSelected"],
@discourseComputed("selectedId", "fontId")
isSelected(selectedId, fontId) {
return selectedId === fontId;
},
click() {
this.onChange(this.fontId);
},
@observes("step.fieldsById.base_scheme_id.value")
themeChanged() {
@observes(
"step.fieldsById.body_font.value",
"step.fieldsById.heading_font.value"
)
fontChanged() {
this.triggerRepaint();
},
@ -35,47 +31,44 @@ export default createPreviewComponent(305, 165, {
};
},
paint(ctx, colors, font, width, height) {
paint({ ctx, colors, font, headingFont, width, height }) {
const headerHeight = height * 0.3;
this.drawFullHeader(colors, font);
this.drawFullHeader(colors, headingFont, this.logo);
const margin = width * 0.04;
const avatarSize = height * 0.2;
const lineHeight = height / 9.5;
const lineHeight = height / 11;
// Draw a fake topic
this.scaleImage(
this.avatar,
margin,
headerHeight + height * 0.085,
headerHeight + height * 0.11,
avatarSize,
avatarSize
);
const titleFontSize = headerHeight / 44;
const titleFontSize = headerHeight / 55;
ctx.beginPath();
ctx.fillStyle = colors.primary;
ctx.font = `bold ${titleFontSize}em '${font}'`;
ctx.fillText(
I18n.t("wizard.previews.font_title", { font }),
margin,
height * 0.3
);
ctx.font = `bold ${titleFontSize}em '${headingFont}'`;
ctx.fillText(I18n.t("wizard.previews.topic_title"), margin, height * 0.3);
const bodyFontSize = height / 220.0;
const bodyFontSize = height / 330.0;
ctx.font = `${bodyFontSize}em '${font}'`;
let line = 0;
const lines = LOREM.split("\n");
for (let i = 0; i < 4; i++) {
for (let i = 0; i < 5; i++) {
line = height * 0.35 + i * lineHeight;
ctx.fillText(lines[i], margin + avatarSize + margin, line);
}
// Share Button
ctx.beginPath();
ctx.rect(margin, line + lineHeight, width * 0.14, height * 0.14);
ctx.rect(margin, line + lineHeight, width * 0.1, height * 0.12);
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 90, 65);
ctx.fill();
ctx.fillStyle = chooseDarker(colors.primary, colors.secondary);
@ -89,10 +82,10 @@ export default createPreviewComponent(305, 165, {
// Reply Button
ctx.beginPath();
ctx.rect(
margin * 2 + width * 0.14,
margin + width * 0.12,
line + lineHeight,
width * 0.14,
height * 0.14
width * 0.1,
height * 0.12
);
ctx.fillStyle = colors.tertiary;
ctx.fill();
@ -100,12 +93,12 @@ export default createPreviewComponent(305, 165, {
ctx.font = `${bodyFontSize}em '${font}'`;
ctx.fillText(
I18n.t("wizard.previews.reply_button"),
margin * 2 + width * 0.14 + width / 55,
margin + width * 0.12 + width / 55,
line + lineHeight * 1.85
);
// Draw Timeline
const timelineX = width * 0.8;
const timelineX = width * 0.86;
ctx.beginPath();
ctx.strokeStyle = colors.tertiary;
ctx.lineWidth = 0.5;

View File

@ -1,8 +0,0 @@
import Component from "@ember/component";
export default Component.extend({
actions: {
changed(value) {
this.set("field.value", value);
},
},
});

View File

@ -21,8 +21,8 @@ export default createPreviewComponent(659, 320, {
};
},
paint(ctx, colors, font, width, height) {
this.drawFullHeader(colors, font);
paint({ ctx, colors, font, width, height }) {
this.drawFullHeader(colors, font, this.logo);
if (this.get("step.fieldsById.homepage_style.value") === "latest") {
this.drawPills(colors, font, height * 0.15);

View File

@ -14,7 +14,8 @@ export default createPreviewComponent(371, 124, {
return { tab: "/images/wizard/tab.png", image: this.get("field.value") };
},
paint(ctx, colors, font, width, height) {
paint(options) {
const { ctx, width, height } = options;
this.scaleImage(this.tab, 0, 0, width, height);
this.scaleImage(this.image, 40, 25, 30, 30);

View File

@ -17,7 +17,8 @@ export default createPreviewComponent(325, 125, {
};
},
paint(ctx, colors, font, width, height) {
paint(options) {
const { width, height } = options;
this.scaleImage(this.image, 10, 8, 87, 87);
this.scaleImage(this.ios, 0, 0, width, height);
},

View File

@ -13,7 +13,8 @@ export default createPreviewComponent(375, 100, {
return { image: this.get("field.value") };
},
paint(ctx, colors, font, width, height) {
paint(options) {
const { ctx, colors, font, headingFont, width, height } = options;
const headerHeight = height / 2;
drawHeader(ctx, colors, width, headerHeight);
@ -39,7 +40,8 @@ export default createPreviewComponent(375, 100, {
const afterLogo = headerMargin * 1.7 + imageWidth;
const fontSize = Math.round(headerHeight * 0.4);
ctx.font = `Bold ${fontSize}px '${font}'`;
ctx.font = `Bold ${fontSize}px '${headingFont}'`;
ctx.fillStyle = colors.primary;
const title = LOREM.substring(0, 27);
ctx.fillText(

View File

@ -13,12 +13,13 @@ export default createPreviewComponent(400, 100, {
return { image: this.get("field.value") };
},
paint(ctx, colors, font, width, height) {
paint({ ctx, colors, font, width, height }) {
const headerHeight = height / 2;
drawHeader(ctx, colors, width, headerHeight);
const image = this.image;
const headerMargin = headerHeight * 0.2;
const imageHeight = headerHeight - headerMargin * 2;

View File

@ -35,10 +35,10 @@ export default createPreviewComponent(305, 165, {
};
},
paint(ctx, colors, font, width, height) {
paint({ ctx, colors, font, headingFont, width, height }) {
const headerHeight = height * 0.3;
this.drawFullHeader(colors, font);
this.drawFullHeader(colors, headingFont, this.logo);
const margin = width * 0.04;
const avatarSize = height * 0.2;
@ -57,7 +57,7 @@ export default createPreviewComponent(305, 165, {
ctx.beginPath();
ctx.fillStyle = colors.primary;
ctx.font = `bold ${titleFontSize}em '${font}'`;
ctx.font = `bold ${titleFontSize}em '${headingFont}'`;
ctx.fillText(I18n.t("wizard.previews.topic_title"), margin, height * 0.3);
const bodyFontSize = height / 220.0;

View File

@ -1,4 +1,5 @@
import Component from "@ember/component";
export default Component.extend({
keyPress(e) {
e.stopPropagation();

View File

@ -1,4 +1,5 @@
import Controller from "@ember/controller";
import { dasherize } from "@ember/string";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
@ -16,7 +17,9 @@ export default Controller.extend({
return [];
}
const fontField = fontsStep.get("fieldsById.font_previews");
return fontField.choices.map((choice) => `font-${choice.data.class}`);
const fontField = fontsStep.get("fieldsById.body_font");
return fontField.choices.map(
(choice) => `body-font-${dasherize(choice.id)}`
);
},
});

View File

@ -93,6 +93,10 @@ export function createPreviewComponent(width, height, obj) {
}
const font = this.wizard.getCurrentFont(this.fontId);
const headingFont = this.wizard.getCurrentFont(
this.fontId,
"heading_font"
);
if (!font) {
return;
}
@ -102,7 +106,15 @@ export function createPreviewComponent(width, height, obj) {
ctx.fillStyle = colors.secondary;
ctx.fillRect(0, 0, width, height);
this.paint(ctx, colors, font, this.width, this.height);
const options = {
ctx,
colors,
font,
headingFont,
width: this.width,
height: this.height,
};
this.paint(options);
// draw border
ctx.beginPath();
@ -142,7 +154,7 @@ export function createPreviewComponent(width, height, obj) {
ctx.drawImage(scaled[key], x, y, w, h);
},
drawFullHeader(colors, font) {
drawFullHeader(colors, font, logo) {
const { ctx } = this;
const headerHeight = height * 0.15;
@ -154,10 +166,16 @@ export function createPreviewComponent(width, height, obj) {
const headerMargin = headerHeight * 0.2;
const logoHeight = headerHeight - headerMargin * 2;
ctx.beginPath();
ctx.fillStyle = colors.header_primary;
ctx.font = `bold ${logoHeight}px '${font}'`;
ctx.fillText("Discourse", headerMargin, headerHeight - headerMargin);
const ratio = logoHeight / logo.height;
this.scaleImage(
logo,
headerMargin,
headerMargin,
logo.width * ratio,
logoHeight
);
this.scaleImage(logo, width, headerMargin);
// Top right menu
this.scaleImage(

View File

@ -21,7 +21,7 @@ const Wizard = EmberObject.extend({
if (!logoStep) {
return;
}
return logoStep.get("fieldsById.logo_url.value");
return logoStep.get("fieldsById.logo.value");
},
// A bit clunky, but get the current colors from the appropriate step
@ -54,13 +54,13 @@ const Wizard = EmberObject.extend({
return option.data.colors;
},
getCurrentFont(fontId) {
getCurrentFont(fontId, type = "body_font") {
const fontsStep = this.steps.findBy("id", "fonts");
if (!fontsStep) {
return;
}
const fontChoice = fontsStep.get("fieldsById.font_previews");
const fontChoice = fontsStep.get(`fieldsById.${type}`);
if (!fontChoice) {
return;
}
@ -80,7 +80,7 @@ const Wizard = EmberObject.extend({
return;
}
return option.data.name;
return option.label;
},
});

View File

@ -1,14 +0,0 @@
<ul class="grid">
{{#each field.choices as |choice|}}
<li>
{{font-preview wizard=wizard
fontId=choice.id
selectedId=field.value
onChange=(action "changed")}}
{{radio-button radioValue=choice.id
label=choice.label
value=field.value
onChange=(action "changed")}}
</li>
{{/each}}
</ul>

View File

@ -90,6 +90,7 @@ h3,
h4,
h5,
h6 {
font-family: $heading-font-family;
margin-top: 0;
margin-bottom: 0.5rem;
}
@ -566,6 +567,8 @@ table {
}
.control-label {
font-family: $heading-font-family;
font-weight: bold;
font-size: $font-up-2;
line-height: $line-height-large;

View File

@ -48,6 +48,7 @@ $base-font-size: 0.938em !default; // eq. to 15px
$base-font-size-larger: 1.063em !default; // eq. to 17px
$base-font-size-largest: 1.118em !default; // eq. to 19px
$base-font-family: var(--font-family) !default;
$heading-font-family: var(--heading-font-family) !default;
// Font-size defintions, multiplier ^ (step / interval)
$font-up-6: 2.296em;

View File

@ -92,6 +92,10 @@ body.wizard {
width: 400px;
}
.hidden {
display: none;
}
.wizard-canvas {
position: absolute;
top: 0;
@ -157,8 +161,7 @@ body.wizard {
}
}
.wizard-step-colors,
.wizard-step-fonts {
.wizard-step-colors {
max-height: 465px;
overflow-y: auto;
.grid {
@ -203,6 +206,16 @@ body.wizard {
}
}
.wizard-step-fonts {
.dropdown-field {
float: left;
margin-right: 1.5em;
}
.component-field {
clear: both;
}
}
.wizard-column {
position: relative;
z-index: 11;

View File

@ -27,7 +27,7 @@ DiscourseEvent.on(:site_setting_changed) do |name, old_value, new_value|
end
end
Stylesheet::Manager.clear_core_cache!(["desktop", "mobile"]) if name == :base_font
Stylesheet::Manager.clear_core_cache!(["desktop", "mobile"]) if [:base_font, :heading_font].include?(name)
Report.clear_cache(:storage_stats) if [:backup_location, :s3_backup_bucket].include?(name)

View File

@ -2233,7 +2233,8 @@ en:
push_notifications_prompt: "Display user consent prompt."
push_notifications_icon: "The badge icon that appears in the notification corner. A 96×96 monochromatic PNG with transparency is recommended."
base_font: "Font to use in most places on the site. Themes can override."
base_font: "Base font to use for most text on the site. Themes can override via the `--font-family` CSS custom property."
heading_font: "Font to use for headings on the site. Themes can override via the `--heading-font-family` CSS custom property."
short_title: "The short title will be used on the user's home screen, launcher, or other places where space may be limited. It should be limited to 12 characters."
@ -4708,6 +4709,13 @@ en:
fonts:
title: "Fonts"
fields:
body_font:
label: "Body font"
heading_font:
label: "Heading font"
font_preview:
label: "Preview"
logos:
title: "Logos"

View File

@ -329,6 +329,10 @@ basic:
default: "helvetica"
enum: "BaseFontSetting"
refresh: true
heading_font:
default: "helvetica"
enum: "BaseFontSetting"
refresh: true
login:
invite_only:

View File

@ -41,18 +41,28 @@ module Stylesheet
end
register_import "font" do
font = DiscourseFonts.fonts.find { |f| f[:key] == SiteSetting.base_font }
body_font = DiscourseFonts.fonts.find { |f| f[:key] == SiteSetting.base_font }
heading_font = DiscourseFonts.fonts.find { |f| f[:key] == SiteSetting.heading_font }
contents = +""
contents = if font.present?
<<~EOF
#{font_css(font)}
if body_font.present?
contents << <<~EOF
#{font_css(body_font)}
:root {
--font-family: #{font[:stack]};
--font-family: #{body_font[:stack]};
}
EOF
end
if heading_font.present?
contents << <<~EOF
#{font_css(heading_font)}
:root {
--heading-font-family: #{heading_font[:stack]};
}
EOF
else
""
end
Import.new("font.scss", source: contents)
@ -73,7 +83,10 @@ module Stylesheet
contents << font_css(font)
contents << <<~EOF
.font-#{font[:key].tr("_", "-")} {
.body-font-#{font[:key].tr("_", "-")} {
font-family: #{font[:stack]};
}
.heading-font-#{font[:key].tr("_", "-")} h2 {
font-family: #{font[:stack]};
}
EOF

View File

@ -406,12 +406,13 @@ class Stylesheet::Manager
cs = @color_scheme || theme&.color_scheme
category_updated = Category.where("uploaded_background_id IS NOT NULL").pluck(:updated_at).map(&:to_i).sum
fonts = "#{SiteSetting.base_font}-#{SiteSetting.heading_font}"
if cs || category_updated > 0
theme_color_defs = theme&.resolve_baked_field(:common, :color_definitions)
Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.last_file_updated}-#{category_updated}-#{SiteSetting.base_font}"
Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.last_file_updated}-#{category_updated}-#{fonts}"
else
digest_string = "defaults-#{Stylesheet::Manager.last_file_updated}-#{SiteSetting.base_font}"
digest_string = "defaults-#{Stylesheet::Manager.last_file_updated}-#{fonts}"
if cdn_url = GlobalSetting.cdn_url
digest_string = "#{digest_string}-#{cdn_url}"

View File

@ -210,18 +210,22 @@ class Wizard
end
@wizard.append_step('fonts') do |step|
field = step.add_field(
id: 'font_previews',
type: 'component',
value: SiteSetting.base_font
)
body_font = step.add_field(id: 'body_font', type: 'dropdown', value: SiteSetting.base_font)
heading_font = step.add_field(id: 'heading_font', type: 'dropdown', value: SiteSetting.heading_font)
DiscourseFonts.fonts.each do |font|
field.add_choice(font[:key], data: { class: font[:key].tr("_", "-"), name: font[:name] })
body_font.add_choice(font[:key], label: font[:name])
heading_font.add_choice(font[:key], label: font[:name])
end
step.add_field(
id: 'font_preview',
type: 'component'
)
step.on_update do |updater|
updater.update_setting(:base_font, updater.fields[:font_previews])
updater.update_setting(:base_font, updater.fields[:body_font])
updater.update_setting(:heading_font, updater.fields[:heading_font])
end
end

View File

@ -40,8 +40,23 @@ describe Stylesheet::Importer do
.to include(":root{--font-family: Helvetica, Arial, sans-serif}")
end
it "includes separate body and heading font declarations" do
base_font = DiscourseFonts.fonts[2]
heading_font = DiscourseFonts.fonts[3]
SiteSetting.base_font = base_font[:key]
SiteSetting.heading_font = heading_font[:key]
expect(compile_css("desktop"))
.to include(":root{--font-family: #{base_font[:stack]}}")
.and include(":root{--heading-font-family: #{heading_font[:stack]}}")
end
it "includes all fonts in wizard" do
expect(compile_css("wizard").scan(/\.font-/).count)
expect(compile_css("wizard").scan(/\.body-font-/).count)
.to eq(DiscourseFonts.fonts.count)
expect(compile_css("wizard").scan(/\.heading-font-/).count)
.to eq(DiscourseFonts.fonts.count)
expect(compile_css("wizard").scan(/@font-face/).count)

View File

@ -189,13 +189,18 @@ describe Stylesheet::Manager do
expect(digest1).to_not eq(digest2)
end
it "updates digest when setting base font" do
it "updates digest when setting fonts" do
manager = Stylesheet::Manager.new(:desktop_theme, theme.id)
digest1 = manager.color_scheme_digest
SiteSetting.base_font = "nunito"
SiteSetting.base_font = DiscourseFonts.fonts[2][:key]
digest2 = manager.color_scheme_digest
expect(digest1).to_not eq(digest2)
SiteSetting.heading_font = DiscourseFonts.fonts[4][:key]
digest3 = manager.color_scheme_digest
expect(digest3).to_not eq(digest2)
end
end

View File

@ -168,12 +168,13 @@ describe Wizard::StepUpdater do
end
context "fonts step" do
it "updates the font" do
updater = wizard.create_updater('fonts', font_previews: 'open_sans')
it "updates fonts" do
updater = wizard.create_updater('fonts', body_font: 'open_sans', heading_font: 'oswald')
updater.update
expect(updater.success?).to eq(true)
expect(wizard.completed_steps?('fonts')).to eq(true)
expect(SiteSetting.base_font).to eq('open_sans')
expect(SiteSetting.heading_font).to eq('oswald')
end
end