UX: Preview multiple color schemes in wizard (#6151)

It was a dropdown to provide choices of color schemes,
and only one scheme could be shown.
With this commit, multiple color scheme previews can be displayed on
one page at the same time, making admins choose color schemes more
easily.

Theme preview windows are shrinked.

Imported default color schemes.

Co-Authored-By: Misaka 0x4e21 <misaka4e21@gmail.com>
This commit is contained in:
Joffrey JAFFEUX 2018-07-24 09:00:20 -04:00 committed by GitHub
parent c3b6811651
commit 7a3c541077
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 243 additions and 87 deletions

View File

@ -3,14 +3,18 @@ import { observes } from "ember-addons/ember-computed-decorators";
import { import {
createPreviewComponent, createPreviewComponent,
darkLightDiff, darkLightDiff,
chooseBrighter, chooseDarker,
LOREM LOREM
} from "wizard/lib/preview"; } from "wizard/lib/preview";
export default createPreviewComponent(659, 320, { export default createPreviewComponent(225, 120, {
logo: null, logo: null,
avatar: null, avatar: null,
click() {
this.sendAction("onChange", this.get("colorsId"));
},
@observes("step.fieldsById.base_scheme_id.value") @observes("step.fieldsById.base_scheme_id.value")
themeChanged() { themeChanged() {
this.triggerRepaint(); this.triggerRepaint();
@ -24,19 +28,19 @@ export default createPreviewComponent(659, 320, {
}, },
paint(ctx, colors, width, height) { paint(ctx, colors, width, height) {
const headerHeight = height * 0.15; const headerHeight = height * 0.3;
this.drawFullHeader(colors); this.drawFullHeader(colors);
const margin = width * 0.02; const margin = width * 0.04;
const avatarSize = height * 0.1; const avatarSize = height * 0.2;
const lineHeight = height / 19.0; const lineHeight = height / 9.5;
// Draw a fake topic // Draw a fake topic
this.scaleImage( this.scaleImage(
this.avatar, this.avatar,
margin, margin,
headerHeight + height * 0.17, headerHeight + height * 0.085,
avatarSize, avatarSize,
avatarSize avatarSize
); );
@ -46,33 +50,48 @@ export default createPreviewComponent(659, 320, {
ctx.beginPath(); ctx.beginPath();
ctx.fillStyle = colors.primary; ctx.fillStyle = colors.primary;
ctx.font = `bold ${titleFontSize}em 'Arial'`; ctx.font = `bold ${titleFontSize}em 'Arial'`;
ctx.fillText("Welcome to Discourse", margin, height * 0.25); ctx.fillText(I18n.t("wizard.previews.topic_title"), margin, height * 0.3);
const bodyFontSize = height / 440.0; const bodyFontSize = height / 220.0;
ctx.font = `${bodyFontSize}em 'Arial'`; ctx.font = `${bodyFontSize}em 'Arial'`;
let line = 0; let line = 0;
const lines = LOREM.split("\n"); const lines = LOREM.split("\n");
for (let i = 0; i < 10; i++) { for (let i = 0; i < 4; i++) {
line = height * 0.3 + i * lineHeight; line = height * 0.35 + i * lineHeight;
ctx.fillText(lines[i], margin + avatarSize + margin, line); ctx.fillText(lines[i], margin + avatarSize + margin, line);
} }
// Share Button
ctx.beginPath();
ctx.rect(margin, line + lineHeight, width * 0.14, height * 0.14);
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 90, 65);
ctx.fill();
ctx.fillStyle = chooseDarker(colors.primary, colors.secondary);
ctx.font = `${bodyFontSize}em 'Arial'`;
ctx.fillText(
I18n.t("wizard.previews.share_button"),
margin + width / 55,
line + lineHeight * 1.85
);
// Reply Button // Reply Button
ctx.beginPath(); ctx.beginPath();
ctx.rect(width * 0.57, line + lineHeight, width * 0.1, height * 0.07); ctx.rect(
margin * 2 + width * 0.14,
line + lineHeight,
width * 0.14,
height * 0.14
);
ctx.fillStyle = colors.tertiary; ctx.fillStyle = colors.tertiary;
ctx.fill(); ctx.fill();
ctx.fillStyle = chooseBrighter(colors.primary, colors.secondary); ctx.fillStyle = colors.secondary;
ctx.font = `${bodyFontSize}em 'Arial'`; ctx.font = `${bodyFontSize}em 'Arial'`;
ctx.fillText("Reply", width * 0.595, line + lineHeight * 1.85); ctx.fillText(
I18n.t("wizard.previews.reply_button"),
// Icons margin * 2 + width * 0.14 + width / 55,
ctx.font = `${bodyFontSize}em FontAwesome`; line + lineHeight * 1.85
ctx.fillStyle = colors.love; );
ctx.fillText("\uf004", width * 0.48, line + lineHeight * 1.8);
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 65, 55);
ctx.fillText("\uf040", width * 0.525, line + lineHeight * 1.8);
// Draw Timeline // Draw Timeline
const timelineX = width * 0.8; const timelineX = width * 0.8;
@ -80,7 +99,7 @@ export default createPreviewComponent(659, 320, {
ctx.strokeStyle = colors.tertiary; ctx.strokeStyle = colors.tertiary;
ctx.lineWidth = 0.5; ctx.lineWidth = 0.5;
ctx.moveTo(timelineX, height * 0.3); ctx.moveTo(timelineX, height * 0.3);
ctx.lineTo(timelineX, height * 0.6); ctx.lineTo(timelineX, height * 0.7);
ctx.stroke(); ctx.stroke();
// Timeline // Timeline

View File

@ -0,0 +1,7 @@
export default Ember.Component.extend({
actions: {
changed(value) {
this.set("field.value", value);
}
}
});

View File

@ -2,17 +2,14 @@
import getUrl from "discourse-common/lib/get-url"; import getUrl from "discourse-common/lib/get-url";
export const LOREM = ` export const LOREM = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,
Nullam eget sem non elit tincidunt rhoncus. Fusce velit nisl, consectetur adipiscing elit.
porttitor sed nisl ac, consectetur interdum metus. Fusce in Nullam eget sem non elit
consequat augue, vel facilisis felis. Nunc tellus elit, and tincidunt rhoncus. Fusce
semper vitae orci nec, blandit pharetra enim. Aenean a ebus velit nisl, porttitor sed
posuere nunc. Maecenas ultrices viverra enim ac commodo nisl ac, consectetur interdum
Vestibulum nec quam sit amet libero ultricies sollicitudin. metus. Fusce in consequat
Nulla quis scelerisque sem, eget volutpat velit. Fusce eget augue, vel facilisis felis.`;
accumsan sapien, nec feugiat quam. Quisque non risus.
placerat lacus vitae, lacinia nisi. Sed metus arcu, iaculis
sit amet cursus nec, sodales at eros.`;
const scaled = {}; const scaled = {};
@ -75,7 +72,9 @@ export function createPreviewComponent(width, height, obj) {
return false; return false;
} }
const colors = this.get("wizard").getCurrentColors(); const colors = this.get("wizard").getCurrentColors(
this.get("colorsId")
);
if (!colors) { if (!colors) {
return; return;
} }
@ -137,16 +136,10 @@ export function createPreviewComponent(width, height, obj) {
const headerMargin = headerHeight * 0.2; const headerMargin = headerHeight * 0.2;
const logoHeight = headerHeight - headerMargin * 2; const logoHeight = headerHeight - headerMargin * 2;
if (this.logo) { ctx.beginPath();
const logoWidth = (logoHeight / this.logo.height) * this.logo.width; ctx.fillStyle = colors.header_primary;
this.scaleImage( ctx.font = `bold ${logoHeight}px 'Arial'`;
this.logo, ctx.fillText("Discourse", headerMargin, headerHeight - headerMargin);
headerMargin,
headerMargin,
logoWidth,
logoHeight
);
}
// Top right menu // Top right menu
this.scaleImage( this.scaleImage(
@ -370,6 +363,14 @@ export function chooseBrighter(primary, secondary) {
: primary; : primary;
} }
export function chooseDarker(primary, secondary) {
if (chooseBrighter(primary, secondary) === primary) {
return secondary;
} else {
return primary;
}
}
export function darkLightDiff(adjusted, comparison, lightness, darkness) { export function darkLightDiff(adjusted, comparison, lightness, darkness) {
const adjustedCol = parseColor(adjusted); const adjustedCol = parseColor(adjusted);
const comparisonCol = parseColor(comparison); const comparisonCol = parseColor(comparison);

View File

@ -23,18 +23,18 @@ const Wizard = Ember.Object.extend({
}, },
// A bit clunky, but get the current colors from the appropriate step // A bit clunky, but get the current colors from the appropriate step
getCurrentColors() { getCurrentColors(schemeId) {
const colorStep = this.get("steps").findBy("id", "colors"); const colorStep = this.get("steps").findBy("id", "colors");
if (!colorStep) { if (!colorStep) {
return; return;
} }
const themeChoice = colorStep.get("fieldsById.base_scheme_id"); const themeChoice = colorStep.get("fieldsById.theme_previews");
if (!themeChoice) { if (!themeChoice) {
return; return;
} }
const themeId = themeChoice.get("value"); const themeId = schemeId ? schemeId : themeChoice.get("value");
if (!themeId) { if (!themeId) {
return; return;
} }

View File

@ -0,0 +1,13 @@
<ul class="grid">
{{#each field.choices as |choice|}}
<li>
{{theme-preview colorsId=choice.id
wizard=wizard
onChange="changed"}}
{{radio-button radioValue=choice.id
label=choice.id
value=field.value
onChange="changed"}}
</li>
{{/each}}
</ul>

View File

@ -40,7 +40,9 @@ body.wizard {
} }
.wizard-warning { .wizard-warning {
font-family: sans-serif, p { font-family: sans-serif;
p {
margin-top: 0; margin-top: 0;
} }
@ -71,6 +73,11 @@ body.wizard {
font-weight: bold; font-weight: bold;
} }
.wizard-step-form {
max-height: 500px;
overflow-y: auto;
}
.wizard-step-emoji { .wizard-step-emoji {
.radio-area { .radio-area {
display: flex; display: flex;
@ -106,6 +113,29 @@ body.wizard {
} }
} }
.wizard-step-colors {
.grid {
margin: 0 auto;
list-style-type: none;
text-align: center;
li {
display: inline-block;
vertical-align: top;
margin: 15px;
.radio-area {
text-align: left;
font-size: 14px;
font-weight: bold;
& > * {
position: relative;
right: 7px;
}
}
}
}
}
.wizard-column { .wizard-column {
position: relative; position: relative;
z-index: 11; z-index: 11;
@ -483,6 +513,9 @@ body.wizard {
.wizard-column { .wizard-column {
margin: auto !important; margin: auto !important;
} }
.wizard-step-form {
max-height: auto;
}
.wizard-step-contents { .wizard-step-contents {
min-height: auto !important; min-height: auto !important;
} }

View File

@ -5,7 +5,7 @@ require_dependency 'distributed_cache'
class ColorScheme < ActiveRecord::Base class ColorScheme < ActiveRecord::Base
CUSTOM_SCHEMES = { CUSTOM_SCHEMES = {
dark: { 'Dark': {
"primary" => 'dddddd', "primary" => 'dddddd',
"secondary" => '222222', "secondary" => '222222',
"tertiary" => '0f82af', "tertiary" => '0f82af',
@ -16,6 +16,84 @@ class ColorScheme < ActiveRecord::Base
"danger" => 'e45735', "danger" => 'e45735',
"success" => '1ca551', "success" => '1ca551',
"love" => 'fa6c8d' "love" => 'fa6c8d'
},
# By @itsbhanusharma
'Neutral': {
"primary" => '000000',
"secondary" => 'ffffff',
"tertiary" => '51839b',
"quaternary" => 'b85e48',
"header_background" => '333333',
"header_primary" => 'f3f3f3',
"highlight" => 'ecec70',
"danger" => 'b85e48',
"success" => '518751',
"love" => 'fa6c8d'
},
# By @Flower_Child
'Grey Amber': {
"primary" => 'd9d9d9',
"secondary" => '3d4147',
"tertiary" => 'fdd459',
"quaternary" => 'fdd459',
"header_background" => '36393e',
"header_primary" => 'd9d9d9',
"highlight" => 'fdd459',
"danger" => 'e45735',
"success" => 'fdd459',
"love" => 'fdd459'
},
# By @awesomerobot
'Shades of Blue': {
"primary" => '203243',
"secondary" => 'eef4f7',
"tertiary" => '416376',
"quaternary" => '5e99b9',
"header_background" => '86bddb',
"header_primary" => 'ffffff',
"highlight" => '86bddb',
"danger" => 'bf3c3c',
"success" => '70db82',
"love" => 'fc94cb'
},
# By @mikechristopher
'Latte': {
"primary" => 'f2e507',
"secondary" => '262322',
"tertiary" => 'f7f2ed',
"quaternary" => 'd7c9aa',
"header_background" => 'd7c9aa',
"header_primary" => '262322',
"highlight" => 'd7c9aa',
"danger" => 'db9584',
"success" => '78be78',
"love" => '8f6201'
},
# By @Flower_Child
'Summer': {
"primary" => '874342',
"secondary" => 'fffff4',
"tertiary" => 'fe9896',
"quaternary" => 'fcc9d0',
"header_background" => '96ccbf',
"header_primary" => 'fff1e7',
"highlight" => 'f3c07f',
"danger" => 'cfebdc',
"success" => 'fcb4b5',
"love" => 'f3c07f'
},
# By @Flower_Child
'Dark Rose': {
"primary" => 'ca9cb2',
"secondary" => '3a2a37',
"tertiary" => 'fdd459',
"quaternary" => '7e566a',
"header_background" => 'a97189',
"header_primary" => 'd9b2bb',
"highlight" => '6c3e63',
"danger" => '6c3e63',
"success" => 'd9b2bb',
"love" => 'd9b2bb'
} }
} }
@ -26,7 +104,7 @@ class ColorScheme < ActiveRecord::Base
end end
list = [ list = [
{ id: 'default', colors: base_with_hash } { id: 'Light', colors: base_with_hash }
] ]
CUSTOM_SCHEMES.each do |k, v| CUSTOM_SCHEMES.each do |k, v|
@ -71,7 +149,7 @@ class ColorScheme < ActiveRecord::Base
def self.base_color_schemes def self.base_color_schemes
base_color_scheme_colors.map do |hash| base_color_scheme_colors.map do |hash|
scheme = new(name: I18n.t("color_schemes.#{hash[:id]}"), base_scheme_id: hash[:id]) scheme = new(name: I18n.t("color_schemes.#{hash[:id].downcase.gsub(' ', '_')}"), base_scheme_id: hash[:id])
scheme.colors = hash[:colors].map { |k, v| { name: k.to_s, hex: v.sub("#", "") } } scheme.colors = hash[:colors].map { |k, v| { name: k.to_s, hex: v.sub("#", "") } }
scheme.is_base = true scheme.is_base = true
scheme scheme
@ -140,7 +218,7 @@ class ColorScheme < ActiveRecord::Base
def base_colors def base_colors
colors = nil colors = nil
if base_scheme_id && base_scheme_id != "default" if base_scheme_id && base_scheme_id != "Light"
colors = CUSTOM_SCHEMES[base_scheme_id.to_sym] colors = CUSTOM_SCHEMES[base_scheme_id.to_sym]
end end
colors || ColorScheme.base_colors colors || ColorScheme.base_colors
@ -148,7 +226,7 @@ class ColorScheme < ActiveRecord::Base
def resolved_colors def resolved_colors
resolved = ColorScheme.base_colors.dup resolved = ColorScheme.base_colors.dup
if base_scheme_id && base_scheme_id != "default" if base_scheme_id && base_scheme_id != "Light"
if scheme = CUSTOM_SCHEMES[base_scheme_id.to_sym] if scheme = CUSTOM_SCHEMES[base_scheme_id.to_sym]
scheme.each do |name, value| scheme.each do |name, value|
resolved[name] = value resolved[name] = value

View File

@ -3995,3 +3995,8 @@ en:
admin: "Admin" admin: "Admin"
moderator: "Moderator" moderator: "Moderator"
regular: "Regular User" regular: "Regular User"
previews:
topic_title: "Discussion topic"
share_button: "Share"
reply_button: "Reply"

View File

@ -3185,11 +3185,23 @@ en:
color_schemes: color_schemes:
base_theme_name: "Base" base_theme_name: "Base"
default: "Light Scheme" light: "Light Scheme"
dark: "Dark Scheme" dark: "Dark Scheme"
default_theme_name: "Default" neutral: "Neutral Scheme"
dark_theme_name: "Dark" grey_amber: "Grey Amber Scheme"
shades_of_blue: "Shades of Blue Scheme"
latte: "Latte Scheme"
summer: "Summer Scheme"
dark_rose: "Dark Rose Scheme"
default_theme_name: "Light"
light_theme_name: "Light" light_theme_name: "Light"
dark_theme_name: "Dark"
neutral_theme_name: "Neutral"
grey_amber_theme_name: "Grey Amber"
shades_of_blue_theme_name: "Shades of Blue"
latte_theme_name: "Latte"
summer_theme_name: "Summer"
dark_rose_theme_name: "Dark Rose"
about: "About" about: "About"
guidelines: "Guidelines" guidelines: "Guidelines"

View File

@ -2,9 +2,9 @@
if !Theme.exists? if !Theme.exists?
STDERR.puts "> Seeding dark and light themes" STDERR.puts "> Seeding dark and light themes"
name = I18n.t("wizard.step.colors.fields.theme_id.choices.dark.label") name = I18n.t("color_schemes.dark_theme_name")
dark_scheme = ColorScheme.find_by(base_scheme_id: "dark") dark_scheme = ColorScheme.find_by(base_scheme_id: "Dark")
dark_scheme ||= ColorScheme.create_from_base(name: name, via_wizard: true, base_scheme_id: "dark") dark_scheme ||= ColorScheme.create_from_base(name: name, via_wizard: true, base_scheme_id: "Dark")
name = I18n.t('color_schemes.dark_theme_name') name = I18n.t('color_schemes.dark_theme_name')
_dark_theme = Theme.create(name: name, user_id: -1, _dark_theme = Theme.create(name: name, user_id: -1,

View File

@ -118,39 +118,27 @@ class Wizard
@wizard.append_step('colors') do |step| @wizard.append_step('colors') do |step|
default_theme = Theme.find_by(id: SiteSetting.default_theme_id) default_theme = Theme.find_by(id: SiteSetting.default_theme_id)
scheme_id = default_theme&.color_scheme&.base_scheme_id || 'default' scheme_id = default_theme&.color_scheme&.base_scheme_id || 'Light'
themes = step.add_field(id: 'base_scheme_id', type: 'dropdown', required: true, value: scheme_id) themes = step.add_field(id: 'theme_previews', type: 'component', required: true, value: scheme_id)
ColorScheme.base_color_scheme_colors.each do |t| ColorScheme.base_color_scheme_colors.each do |t|
with_hash = t[:colors].dup with_hash = t[:colors].dup
with_hash.map { |k, v| with_hash[k] = "##{v}" } with_hash.map { |k, v| with_hash[k] = "##{v}" }
themes.add_choice(t[:id], data: { colors: with_hash }) themes.add_choice(t[:id], data: { colors: with_hash })
end end
step.add_field(id: 'theme_preview', type: 'component')
step.on_update do |updater| step.on_update do |updater|
scheme_name = updater.fields[:base_scheme_id] scheme_name = updater.fields[:theme_previews] || 'Light'
name = I18n.t("color_schemes.#{scheme_name.downcase.gsub(' ', '_')}_theme_name")
theme = nil theme = nil
scheme = ColorScheme.find_by(base_scheme_id: scheme_name, via_wizard: true)
scheme ||= ColorScheme.create_from_base(name: name, via_wizard: true, base_scheme_id: scheme_name)
themes = Theme.where(color_scheme_id: scheme.id).order(:id).to_a
theme = themes.find(&:default?)
theme ||= themes.first
if scheme_name == "dark" theme ||= Theme.create(name: name, user_id: @wizard.user.id, color_scheme_id: scheme.id)
scheme = ColorScheme.find_by(base_scheme_id: 'dark', via_wizard: true)
name = I18n.t("wizard.step.colors.fields.theme_id.choices.dark.label")
scheme ||= ColorScheme.create_from_base(name: name, via_wizard: true, base_scheme_id: "dark")
theme = Theme.find_by(color_scheme_id: scheme.id)
name = I18n.t('color_schemes.dark_theme_name')
theme ||= Theme.create(name: name, color_scheme_id: scheme.id, user_id: @wizard.user.id)
else
themes = Theme.where(color_scheme_id: nil).order(:id).to_a
theme = themes.find(&:default?)
theme ||= themes.first
name = I18n.t('color_schemes.light_theme_name')
theme ||= Theme.create(name: name, user_id: @wizard.user.id)
end
theme.set_default! theme.set_default!
end end
end end

View File

@ -151,12 +151,12 @@ describe Wizard::StepUpdater do
let!(:color_scheme) { Fabricate(:color_scheme, name: 'existing', via_wizard: true) } let!(:color_scheme) { Fabricate(:color_scheme, name: 'existing', via_wizard: true) }
it "updates the scheme" do it "updates the scheme" do
updater = wizard.create_updater('colors', base_scheme_id: 'dark') updater = wizard.create_updater('colors', theme_previews: 'Dark')
updater.update updater.update
expect(updater.success?).to eq(true) expect(updater.success?).to eq(true)
expect(wizard.completed_steps?('colors')).to eq(true) expect(wizard.completed_steps?('colors')).to eq(true)
theme = Theme.find_by(id: SiteSetting.default_theme_id) theme = Theme.find_by(id: SiteSetting.default_theme_id)
expect(theme.color_scheme.base_scheme_id).to eq('dark') expect(theme.color_scheme.base_scheme_id).to eq('Dark')
end end
end end
@ -167,14 +167,14 @@ describe Wizard::StepUpdater do
context 'dark theme' do context 'dark theme' do
it "creates the theme" do it "creates the theme" do
updater = wizard.create_updater('colors', base_scheme_id: 'dark', allow_dark_light_selection: true) updater = wizard.create_updater('colors', theme_previews: 'Dark', allow_dark_light_selection: true)
expect { updater.update }.to change { Theme.count }.by(1) expect { updater.update }.to change { Theme.count }.by(1)
theme = Theme.last theme = Theme.last
expect(theme.user_id).to eq(wizard.user.id) expect(theme.user_id).to eq(wizard.user.id)
expect(theme.color_scheme.base_scheme_id).to eq('dark') expect(theme.color_scheme.base_scheme_id).to eq('Dark')
end end
end end
@ -187,14 +187,14 @@ describe Wizard::StepUpdater do
theme = Theme.last theme = Theme.last
expect(theme.user_id).to eq(wizard.user.id) expect(theme.user_id).to eq(wizard.user.id)
expect(theme.color_scheme).to eq(nil) expect(theme.color_scheme).to eq(ColorScheme.find_by(name: 'Light'))
end end
end end
end end
context "without an existing scheme" do context "without an existing scheme" do
it "creates the scheme" do it "creates the scheme" do
updater = wizard.create_updater('colors', base_scheme_id: 'dark', allow_dark_light_selection: true) updater = wizard.create_updater('colors', theme_previews: 'Dark', allow_dark_light_selection: true)
updater.update updater.update
expect(updater.success?).to eq(true) expect(updater.success?).to eq(true)
expect(wizard.completed_steps?('colors')).to eq(true) expect(wizard.completed_steps?('colors')).to eq(true)