mirror of
https://github.com/codeninjasllc/discourse.git
synced 2025-02-17 04:01:29 -05:00
Theming: a UI to choose some base colors that are applied to all the site css. CSS compiled outside of asset pipeline.
This commit is contained in:
parent
c97de2c449
commit
c4d3aa3d47
46 changed files with 596 additions and 310 deletions
|
@ -2,6 +2,8 @@
|
||||||
An input field for a color.
|
An input field for a color.
|
||||||
|
|
||||||
@param hexValue is a reference to the color's hex value.
|
@param hexValue is a reference to the color's hex value.
|
||||||
|
@param brightnessValue is a number from 0 to 255 representing the brightness of the color. See ColorSchemeColor.
|
||||||
|
@params valid is a boolean indicating if the input field is a valid color.
|
||||||
|
|
||||||
@class Discourse.ColorInputComponent
|
@class Discourse.ColorInputComponent
|
||||||
@extends Ember.Component
|
@extends Ember.Component
|
||||||
|
@ -13,10 +15,12 @@ Discourse.ColorInputComponent = Ember.Component.extend({
|
||||||
|
|
||||||
hexValueChanged: function() {
|
hexValueChanged: function() {
|
||||||
var hex = this.get('hexValue');
|
var hex = this.get('hexValue');
|
||||||
if (hex && (hex.length === 3 || hex.length === 6) && this.get('brightnessValue')) {
|
if (this.get('valid')) {
|
||||||
this.$('input').attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';');
|
this.$('input').attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';');
|
||||||
|
} else {
|
||||||
|
this.$('input').attr('style', '');
|
||||||
}
|
}
|
||||||
}.observes('hexValue', 'brightnessValue'),
|
}.observes('hexValue', 'brightnessValue', 'valid'),
|
||||||
|
|
||||||
didInsertElement: function() {
|
didInsertElement: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
|
@ -8,11 +8,10 @@
|
||||||
**/
|
**/
|
||||||
Discourse.AdminCustomizeColorsController = Ember.ArrayController.extend({
|
Discourse.AdminCustomizeColorsController = Ember.ArrayController.extend({
|
||||||
|
|
||||||
filter: null,
|
|
||||||
onlyOverridden: false,
|
onlyOverridden: false,
|
||||||
|
|
||||||
baseColorScheme: function() {
|
baseColorScheme: function() {
|
||||||
return this.get('model').findBy('id', 1);
|
return this.get('model').findBy('is_base', true);
|
||||||
}.property('model.@each.id'),
|
}.property('model.@each.id'),
|
||||||
|
|
||||||
baseColors: function() {
|
baseColors: function() {
|
||||||
|
@ -28,15 +27,10 @@ Discourse.AdminCustomizeColorsController = Ember.ArrayController.extend({
|
||||||
this.set('selectedItem', null);
|
this.set('selectedItem', null);
|
||||||
},
|
},
|
||||||
|
|
||||||
filterContent: Discourse.debounce(function() {
|
filterContent: function() {
|
||||||
if (!this.get('selectedItem')) { return; }
|
if (!this.get('selectedItem')) { return; }
|
||||||
|
|
||||||
var filter;
|
if (!this.get('onlyOverridden')) {
|
||||||
if (this.get('filter')) {
|
|
||||||
filter = this.get('filter').toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((filter === undefined || filter.length < 1) && !this.get('onlyOverridden')) {
|
|
||||||
this.set('colors', this.get('selectedItem.colors'));
|
this.set('colors', this.get('selectedItem.colors'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -44,18 +38,12 @@ Discourse.AdminCustomizeColorsController = Ember.ArrayController.extend({
|
||||||
var matches = Em.A(), self = this, baseColor;
|
var matches = Em.A(), self = this, baseColor;
|
||||||
|
|
||||||
_.each(this.get('selectedItem.colors'), function(color){
|
_.each(this.get('selectedItem.colors'), function(color){
|
||||||
if (filter === undefined || filter.length < 1 || color.get('name').toLowerCase().indexOf(filter) > -1) {
|
baseColor = self.get('baseColors').get(color.get('name'));
|
||||||
if (self.get('onlyOverridden')) {
|
if (color.get('hex') !== baseColor.get('hex')) matches.pushObject(color);
|
||||||
baseColor = self.get('baseColors').get(color.get('name'));
|
|
||||||
if (color.get('hex') === baseColor.get('hex') && color.get('opacity') === baseColor.get('opacity')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
matches.pushObject(color);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.set('colors', matches);
|
this.set('colors', matches);
|
||||||
}, 250).observes('filter', 'onlyOverridden'),
|
}.observes('onlyOverridden'),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
selectColorScheme: function(colorScheme) {
|
selectColorScheme: function(colorScheme) {
|
||||||
|
@ -64,6 +52,7 @@ Discourse.AdminCustomizeColorsController = Ember.ArrayController.extend({
|
||||||
this.set('colors', colorScheme.get('colors'));
|
this.set('colors', colorScheme.get('colors'));
|
||||||
colorScheme.set('savingStatus', null);
|
colorScheme.set('savingStatus', null);
|
||||||
colorScheme.set('selected', true);
|
colorScheme.set('selected', true);
|
||||||
|
this.filterContent();
|
||||||
},
|
},
|
||||||
|
|
||||||
newColorScheme: function() {
|
newColorScheme: function() {
|
||||||
|
@ -71,10 +60,7 @@ Discourse.AdminCustomizeColorsController = Ember.ArrayController.extend({
|
||||||
newColorScheme.set('name', I18n.t('admin.customize.colors.new_name'));
|
newColorScheme.set('name', I18n.t('admin.customize.colors.new_name'));
|
||||||
this.pushObject(newColorScheme);
|
this.pushObject(newColorScheme);
|
||||||
this.send('selectColorScheme', newColorScheme);
|
this.send('selectColorScheme', newColorScheme);
|
||||||
},
|
this.set('onlyOverridden', false);
|
||||||
|
|
||||||
clearFilter: function() {
|
|
||||||
this.set('filter', null);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
undo: function(color) {
|
undo: function(color) {
|
||||||
|
|
|
@ -27,7 +27,7 @@ Discourse.ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
||||||
copy: function() {
|
copy: function() {
|
||||||
var newScheme = Discourse.ColorScheme.create({name: this.get('name'), enabled: false, can_edit: true, colors: Em.A()});
|
var newScheme = Discourse.ColorScheme.create({name: this.get('name'), enabled: false, can_edit: true, colors: Em.A()});
|
||||||
_.each(this.get('colors'), function(c){
|
_.each(this.get('colors'), function(c){
|
||||||
newScheme.colors.pushObject(Discourse.ColorSchemeColor.create({name: c.get('name'), hex: c.get('hex'), opacity: c.get('opacity')}));
|
newScheme.colors.pushObject(Discourse.ColorSchemeColor.create({name: c.get('name'), hex: c.get('hex')}));
|
||||||
});
|
});
|
||||||
return newScheme;
|
return newScheme;
|
||||||
},
|
},
|
||||||
|
@ -40,7 +40,7 @@ Discourse.ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
||||||
}.property('name', 'enabled', 'colors.@each.changed', 'saving'),
|
}.property('name', 'enabled', 'colors.@each.changed', 'saving'),
|
||||||
|
|
||||||
disableSave: function() {
|
disableSave: function() {
|
||||||
return !this.get('changed') || this.get('saving');
|
return !this.get('changed') || this.get('saving') || _.any(this.get('colors'), function(c) { return !c.get('valid'); });
|
||||||
}.property('changed'),
|
}.property('changed'),
|
||||||
|
|
||||||
newRecord: function() {
|
newRecord: function() {
|
||||||
|
@ -48,6 +48,8 @@ Discourse.ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
||||||
}.property('id'),
|
}.property('id'),
|
||||||
|
|
||||||
save: function() {
|
save: function() {
|
||||||
|
if (this.get('is_base') || this.get('disableSave')) return;
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
this.set('savingStatus', I18n.t('saving'));
|
this.set('savingStatus', I18n.t('saving'));
|
||||||
this.set('saving',true);
|
this.set('saving',true);
|
||||||
|
@ -57,7 +59,7 @@ Discourse.ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
||||||
data.colors = [];
|
data.colors = [];
|
||||||
_.each(this.get('colors'), function(c) {
|
_.each(this.get('colors'), function(c) {
|
||||||
if (!self.id || c.get('changed')) {
|
if (!self.id || c.get('changed')) {
|
||||||
data.colors.pushObject({name: c.get('name'), hex: c.get('hex'), opacity: c.get('opacity')});
|
data.colors.pushObject({name: c.get('name'), hex: c.get('hex')});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -104,8 +106,8 @@ Discourse.ColorScheme.reopenClass({
|
||||||
id: colorScheme.id,
|
id: colorScheme.id,
|
||||||
name: colorScheme.name,
|
name: colorScheme.name,
|
||||||
enabled: colorScheme.enabled,
|
enabled: colorScheme.enabled,
|
||||||
can_edit: colorScheme.can_edit,
|
is_base: colorScheme.is_base,
|
||||||
colors: colorScheme.colors.map(function(c) { return Discourse.ColorSchemeColor.create({name: c.name, hex: c.hex, opacity: c.opacity}); })
|
colors: colorScheme.colors.map(function(c) { return Discourse.ColorSchemeColor.create({name: c.name, hex: c.hex}); })
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
colorSchemes.set('loading', false);
|
colorSchemes.set('loading', false);
|
||||||
|
|
|
@ -15,30 +15,24 @@ Discourse.ColorSchemeColor = Discourse.Model.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
startTrackingChanges: function() {
|
startTrackingChanges: function() {
|
||||||
this.set('originals', {
|
this.set('originals', {hex: this.get('hex') || 'FFFFFF'});
|
||||||
hex: this.get('hex') || 'FFFFFF',
|
|
||||||
opacity: this.get('opacity') || '100'
|
|
||||||
});
|
|
||||||
this.notifyPropertyChange('hex'); // force changed property to be recalculated
|
this.notifyPropertyChange('hex'); // force changed property to be recalculated
|
||||||
},
|
},
|
||||||
|
|
||||||
changed: function() {
|
changed: function() {
|
||||||
if (!this.originals) return false;
|
if (!this.originals) return false;
|
||||||
|
if (this.get('hex') !== this.originals['hex']) return true;
|
||||||
if (this.get('hex') !== this.originals['hex'] || this.get('opacity').toString() !== this.originals['opacity'].toString()) {
|
return false;
|
||||||
return true;
|
}.property('hex'),
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}.property('hex', 'opacity'),
|
|
||||||
|
|
||||||
undo: function() {
|
undo: function() {
|
||||||
if (this.originals) {
|
if (this.originals) this.set('hex', this.originals['hex']);
|
||||||
this.set('hex', this.originals['hex']);
|
|
||||||
this.set('opacity', this.originals['opacity']);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
translatedName: function() {
|
||||||
|
return I18n.t('admin.customize.colors.' + this.get('name'));
|
||||||
|
}.property('name'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
brightness returns a number between 0 (darkest) to 255 (brightest).
|
brightness returns a number between 0 (darkest) to 255 (brightest).
|
||||||
Undefined if hex is not a valid color.
|
Undefined if hex is not a valid color.
|
||||||
|
@ -61,11 +55,7 @@ Discourse.ColorSchemeColor = Discourse.Model.extend({
|
||||||
}
|
}
|
||||||
}.observes('hex'),
|
}.observes('hex'),
|
||||||
|
|
||||||
opacityChanged: function() {
|
valid: function() {
|
||||||
if (this.get('opacity')) {
|
return this.get('hex').match(/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/) !== null;
|
||||||
var o = this.get('opacity').toString().replace(/[^\d.]/g, "");
|
}.property('hex')
|
||||||
if (parseInt(o,10) > 100) { o = o.substr(0,o.length-1); }
|
|
||||||
this.set('opacity', o);
|
|
||||||
}
|
|
||||||
}.observes('opacity')
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,6 @@
|
||||||
**/
|
**/
|
||||||
Discourse.AdminCustomizeIndexRoute = Discourse.Route.extend({
|
Discourse.AdminCustomizeIndexRoute = Discourse.Route.extend({
|
||||||
redirect: function() {
|
redirect: function() {
|
||||||
this.transitionTo('adminCustomize.css_html');
|
this.transitionTo('adminCustomize.colors');
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -4,9 +4,9 @@
|
||||||
<h3>{{i18n admin.customize.colors.long_title}}</h3>
|
<h3>{{i18n admin.customize.colors.long_title}}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{{#each model}}
|
{{#each model}}
|
||||||
{{#if can_edit}}
|
{{#unless is_base}}
|
||||||
<li><a {{action selectColorScheme this}} {{bind-attr class="selected:active"}}>{{description}}</a></li>
|
<li><a {{action selectColorScheme this}} {{bind-attr class="selected:active"}}>{{description}}</a></li>
|
||||||
{{/if}}
|
{{/unless}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
<button {{action newColorScheme}} class='btn'>{{i18n admin.customize.new}}</button>
|
<button {{action newColorScheme}} class='btn'>{{i18n admin.customize.new}}</button>
|
||||||
|
@ -36,27 +36,22 @@
|
||||||
{{i18n admin.site_settings.show_overriden}}
|
{{i18n admin.site_settings.show_overriden}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class='controls'>
|
|
||||||
{{textField value=filter placeholderKey="type_to_filter"}}
|
|
||||||
<button {{action clearFilter}} class="btn">{{i18n admin.site_settings.clear_filter}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#if colors.length}}
|
||||||
<table class="table colors">
|
<table class="table colors">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th class="hex">{{i18n admin.customize.color}}</th>
|
<th class="hex">{{i18n admin.customize.color}}</th>
|
||||||
<th class="opacity">{{i18n admin.customize.opacity}}</th>
|
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{#each colors}}
|
{{#each colors}}
|
||||||
<tr {{bind-attr class="changed"}}>
|
<tr {{bind-attr class="changed valid:valid:invalid"}}>
|
||||||
<td class="name">{{name}}</td>
|
<td class="name" {{bind-attr title="name"}}>{{translatedName}}</td>
|
||||||
<td class="hex">{{color-input hexValue=hex brightnessValue=brightness}}</td>
|
<td class="hex">{{color-input hexValue=hex brightnessValue=brightness valid=valid}}</td>
|
||||||
<td class="opacity">{{textField class="opacity-input" value=opacity maxlength="3"}}</td>
|
|
||||||
<td class="changed">
|
<td class="changed">
|
||||||
<button {{bind-attr class=":btn :undo changed::invisible"}} {{action undo this}}>undo</button>
|
<button {{bind-attr class=":btn :undo changed::invisible"}} {{action undo this}}>undo</button>
|
||||||
</td>
|
</td>
|
||||||
|
@ -64,6 +59,9 @@
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<p>{{i18n search.no_results}}</p>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -62,7 +62,8 @@ Discourse.addInitializer(function() {
|
||||||
if (!$(this).data('orig')) {
|
if (!$(this).data('orig')) {
|
||||||
$(this).data('orig', this.href);
|
$(this).data('orig', this.href);
|
||||||
}
|
}
|
||||||
this.href = $(this).data('orig') + "&hash=" + me.hash;
|
var orig = $(this).data('orig');
|
||||||
|
this.href = orig + (orig.indexOf('?') >= 0 ? "&hash=" : "?hash=") + me.hash;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
@import "common/foundation/helpers";
|
@import "common/foundation/helpers";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.admin-contents table {
|
.admin-contents table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
tr {text-align: left;}
|
tr {text-align: left;}
|
||||||
|
@ -434,14 +435,18 @@ section.details {
|
||||||
.colors {
|
.colors {
|
||||||
thead th { border: none; }
|
thead th { border: none; }
|
||||||
td.hex { width: 100px; }
|
td.hex { width: 100px; }
|
||||||
|
td.changed { width: 300px; }
|
||||||
.hex-input { width: 80px; }
|
.hex-input { width: 80px; }
|
||||||
td.opacity { width: 50px; }
|
.hex { text-align: center; }
|
||||||
.opacity-input { width: 30px; }
|
|
||||||
.hex, .opacity { text-align: center; }
|
|
||||||
|
|
||||||
.changed .name {
|
.changed .name {
|
||||||
color: scale-color($highlight, $lightness: -50%);
|
color: scale-color($highlight, $lightness: -50%);
|
||||||
}
|
}
|
||||||
|
.invalid .hex input {
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
border-color: $danger;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,18 +77,18 @@ body {
|
||||||
.clear-transitions {
|
.clear-transitions {
|
||||||
@include transition(none !important);
|
@include transition(none !important);
|
||||||
}
|
}
|
||||||
form {
|
|
||||||
.tip {
|
.tip {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
&.good {
|
&.good {
|
||||||
color: $success;
|
color: $success;
|
||||||
}
|
}
|
||||||
&.bad {
|
&.bad {
|
||||||
color: $danger;
|
color: $danger;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#wmd-input {
|
#wmd-input {
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
$primary: #333333 !default;
|
$primary: #333333 !default;
|
||||||
$secondary: #ffffff !default;
|
$secondary: #ffffff !default;
|
||||||
$tertiary: #0088cc !default;
|
$tertiary: #0088cc !default;
|
||||||
$highlight: #ffff4d !default;
|
$header_background: #ffffff !default;
|
||||||
$danger: #e45735 !default;
|
$header_primary: #333333 !default;
|
||||||
$success: #009900 !default;
|
$highlight: #ffff4d !default;
|
||||||
$love: #fa6c8d !default;
|
$danger: #e45735 !default;
|
||||||
|
$success: #009900 !default;
|
||||||
|
$love: #fa6c8d !default;
|
||||||
|
|
|
@ -26,7 +26,6 @@ $base-font-size: 14px !default;
|
||||||
$base-line-height: 19px !default;
|
$base-line-height: 19px !default;
|
||||||
$base-font-family: Helvetica, Arial, sans-serif !default;
|
$base-font-family: Helvetica, Arial, sans-serif !default;
|
||||||
|
|
||||||
@import "colors";
|
/* These files don't actually exist. They're injected by DiscourseSassImporter. */
|
||||||
|
@import "theme_variables";
|
||||||
/* This file doesn't actually exist, it is injected by DiscourseSassImporter. */
|
|
||||||
@import "plugins_variables";
|
@import "plugins_variables";
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
@import "common";
|
@import "common";
|
||||||
@import "desktop/*";
|
|
||||||
|
/* @import "desktop/*"; I don't know why this doesn't work. */
|
||||||
|
|
||||||
|
@import "desktop/compose";
|
||||||
|
@import "desktop/discourse";
|
||||||
|
@import "desktop/header";
|
||||||
|
@import "desktop/login";
|
||||||
|
@import "desktop/modal";
|
||||||
|
@import "desktop/poster_expansion";
|
||||||
|
@import "desktop/topic-list";
|
||||||
|
@import "desktop/topic-post";
|
||||||
|
@import "desktop/topic";
|
||||||
|
@import "desktop/upload";
|
||||||
|
@import "desktop/user";
|
||||||
|
|
||||||
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
|
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
box-shadow: 0 2px 4px -2px rgba($primary, .25);
|
box-shadow: 0 2px 4px -2px rgba($header_primary, .25);
|
||||||
background-color: $secondary;
|
background-color: $header_background;
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
.docked & {
|
.docked & {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
.current-username {
|
.current-username {
|
||||||
float: left;
|
float: left;
|
||||||
a {
|
a {
|
||||||
color: $primary;
|
color: $header_primary;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
display:block;
|
display:block;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
.icon {
|
.icon {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
color: scale-color($primary, $lightness: 50%);
|
color: scale-color($header_primary, $lightness: 50%);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-top: 1px solid transparent;
|
border-top: 1px solid transparent;
|
||||||
|
|
|
@ -528,12 +528,10 @@ iframe {
|
||||||
width: 78%;
|
width: 78%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
.topic-statuses {
|
.topic-statuses {
|
||||||
i { color: $primary;
|
i { color: $header_primary; }
|
||||||
}
|
.unpinned { color: $header_primary; }
|
||||||
.unpinned { color: $primary;}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.topic-link {color: $primary;}
|
.topic-link { color: $header_primary; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
@import "common";
|
@import "common";
|
||||||
@import "mobile/*";
|
/* @import "mobile/*"; I don't know why this doesn't work */
|
||||||
|
|
||||||
|
@import "mobile/compose";
|
||||||
|
@import "mobile/discourse";
|
||||||
|
@import "mobile/faqs";
|
||||||
|
@import "mobile/header";
|
||||||
|
@import "mobile/login";
|
||||||
|
@import "mobile/modal";
|
||||||
|
@import "mobile/topic-list";
|
||||||
|
@import "mobile/topic-post";
|
||||||
|
@import "mobile/topic";
|
||||||
|
@import "mobile/upload";
|
||||||
|
@import "mobile/user";
|
||||||
|
|
||||||
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
|
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
background-color: $secondary;
|
background-color: $header_background;
|
||||||
box-shadow: 0 0 3px rgba($primary, .6);
|
box-shadow: 0 0 3px rgba($header_primary, .6);
|
||||||
|
|
||||||
.docked & {
|
.docked & {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
float: left;
|
float: left;
|
||||||
display: none;
|
display: none;
|
||||||
a {
|
a {
|
||||||
color: darken($primary, 40%);
|
color: darken($header_primary, 40%);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
.icon {
|
.icon {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
color: scale-color($primary, $lightness: 50%);
|
color: scale-color($header_primary, $lightness: 50%);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -3,16 +3,25 @@ class Admin::ColorSchemesController < Admin::AdminController
|
||||||
before_filter :fetch_color_scheme, only: [:update, :destroy]
|
before_filter :fetch_color_scheme, only: [:update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render_serialized(ColorScheme.current_version.order('id ASC').all.to_a, ColorSchemeSerializer)
|
render_serialized([ColorScheme.base] + ColorScheme.current_version.order('id ASC').all.to_a, ColorSchemeSerializer)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
color_scheme = ColorScheme.create(color_scheme_params)
|
color_scheme = ColorScheme.create(color_scheme_params)
|
||||||
render json: color_scheme, root: false
|
if color_scheme.valid?
|
||||||
|
render json: color_scheme, root: false
|
||||||
|
else
|
||||||
|
render_json_error(color_scheme)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
render json: ColorSchemeRevisor.revise(@color_scheme, color_scheme_params), root: false
|
color_scheme = ColorSchemeRevisor.revise(@color_scheme, color_scheme_params)
|
||||||
|
if color_scheme.valid?
|
||||||
|
render json: color_scheme, root: false
|
||||||
|
else
|
||||||
|
render_json_error(color_scheme)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -28,6 +37,6 @@ class Admin::ColorSchemesController < Admin::AdminController
|
||||||
end
|
end
|
||||||
|
|
||||||
def color_scheme_params
|
def color_scheme_params
|
||||||
params.permit(color_scheme: [:enabled, :name, colors: [:name, :hex, :opacity]])[:color_scheme]
|
params.permit(color_scheme: [:enabled, :name, colors: [:name, :hex]])[:color_scheme]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
class ColorScheme < ActiveRecord::Base
|
class ColorScheme < ActiveRecord::Base
|
||||||
|
|
||||||
|
attr_accessor :is_base
|
||||||
|
|
||||||
has_many :color_scheme_colors, -> { order('id ASC') }, dependent: :destroy
|
has_many :color_scheme_colors, -> { order('id ASC') }, dependent: :destroy
|
||||||
|
|
||||||
alias_method :colors, :color_scheme_colors
|
alias_method :colors, :color_scheme_colors
|
||||||
|
@ -8,22 +10,41 @@ class ColorScheme < ActiveRecord::Base
|
||||||
|
|
||||||
after_destroy :destroy_versions
|
after_destroy :destroy_versions
|
||||||
|
|
||||||
def self.enabled
|
validates_associated :color_scheme_colors
|
||||||
current_version.find_by(enabled: true) || find(1)
|
|
||||||
|
BASE_COLORS_FILE = "#{Rails.root}/app/assets/stylesheets/common/foundation/colors.scss"
|
||||||
|
|
||||||
|
@mutex = Mutex.new
|
||||||
|
|
||||||
|
def self.base_colors
|
||||||
|
@mutex.synchronize do
|
||||||
|
return @base_colors if @base_colors
|
||||||
|
@base_colors = {}
|
||||||
|
File.readlines(BASE_COLORS_FILE).each do |line|
|
||||||
|
matches = /\$([\w]+):\s*#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})(?:[;]|\s)/.match(line.strip)
|
||||||
|
@base_colors[matches[1]] = matches[2] if matches
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@base_colors
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_edit?
|
def self.enabled
|
||||||
self.id != 1 # base theme shouldn't be edited, except by seed data
|
current_version.find_by(enabled: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.base
|
||||||
|
return @base_color if @base_color
|
||||||
|
@base_color = new(name: I18n.t('color_schemes.base_theme_name'), enabled: false)
|
||||||
|
@base_color.colors = base_colors.map { |name, hex| {name: name, hex: hex} }
|
||||||
|
@base_color.is_base = true
|
||||||
|
@base_color
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
def colors=(arr)
|
def colors=(arr)
|
||||||
@colors_by_name = nil
|
@colors_by_name = nil
|
||||||
arr.each do |c|
|
arr.each do |c|
|
||||||
self.color_scheme_colors << ColorSchemeColor.new(
|
self.color_scheme_colors << ColorSchemeColor.new( name: c[:name], hex: c[:hex] )
|
||||||
name: c[:name],
|
|
||||||
hex: c[:hex],
|
|
||||||
opacity: c[:opacity].to_i
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -36,7 +57,7 @@ class ColorScheme < ActiveRecord::Base
|
||||||
|
|
||||||
def colors_hashes
|
def colors_hashes
|
||||||
color_scheme_colors.map do |c|
|
color_scheme_colors.map do |c|
|
||||||
{name: c.name, hex: c.hex, opacity: c.opacity}
|
{name: c.name, hex: c.hex}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,101 +1,7 @@
|
||||||
class ColorSchemeColor < ActiveRecord::Base
|
class ColorSchemeColor < ActiveRecord::Base
|
||||||
belongs_to :color_scheme
|
belongs_to :color_scheme
|
||||||
|
|
||||||
BASE_COLORS = {
|
validates :hex, format: { with: /\A([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\z/ }
|
||||||
primary_border_color: "e6e6e6",
|
|
||||||
secondary_border_color: "f5f5f5",
|
|
||||||
tertiary_border_color: "ffffff",
|
|
||||||
highlight_border_color: "ffff4d",
|
|
||||||
emphasis_border_color: "00aaff",
|
|
||||||
warning_border_color: "f0a28f",
|
|
||||||
success_border_color: "009900",
|
|
||||||
primary_background_color: "ffffff",
|
|
||||||
secondary_background_color: "333333",
|
|
||||||
tertiary_background_color: "4d4d4d",
|
|
||||||
moderator_background_color: "ffffe5",
|
|
||||||
emphasis_background_color: "e5f6ff",
|
|
||||||
success_background_color: "99ff99",
|
|
||||||
warning_background_color: "f6c7bc",
|
|
||||||
highlight_background_color: "ffffc2",
|
|
||||||
like_background_color: "fee7ed",
|
|
||||||
composer_background_color: "e6e6e6",
|
|
||||||
notification_badge_background_color: "8c8c8c",
|
|
||||||
primary_text_color: "333333",
|
|
||||||
secondary_text_color: "999999",
|
|
||||||
tertiary_text_color: "ffffff",
|
|
||||||
warning_text_color: "e45735",
|
|
||||||
success_text_color: "009900",
|
|
||||||
emphasis_text_color: "00aaff",
|
|
||||||
highlight_text_color: "ffff4d",
|
|
||||||
like_color: "fa6c8d",
|
|
||||||
primary_shadow_color: "333333",
|
|
||||||
secondary_shadow_color: "ffffff",
|
|
||||||
warning_shadow_color: "f0a28f",
|
|
||||||
success_shadow_color: "009900",
|
|
||||||
highlight: "ffff4d",
|
|
||||||
header_item_highlight: "e5f6ff",
|
|
||||||
link_color: "0088cc",
|
|
||||||
secondary_link_color: "ffffff",
|
|
||||||
"muted-link-color" => "8c8c8c",
|
|
||||||
"muted-important-link-color" => "8c8c8c",
|
|
||||||
"link-color-visited" => "0088cc",
|
|
||||||
"link-color-hover" => "0088cc",
|
|
||||||
"link-color-active" => "0088cc",
|
|
||||||
heatmap_high: "ea7c62",
|
|
||||||
heatmap_med: "e45735",
|
|
||||||
heatmap_low: "cb3d1b",
|
|
||||||
coldmap_high: "33bbff",
|
|
||||||
coldmap_med: "00aaff",
|
|
||||||
coldmap_low: "0088cc",
|
|
||||||
"btn-default-color" => "333333",
|
|
||||||
"btn-default-background-color" => "b3b3b3",
|
|
||||||
"btn-default-background-color-hover" => "f5f5f5",
|
|
||||||
"btn-primary-border-color" => "0088cc",
|
|
||||||
"btn-primary-background-color" => "00aaff",
|
|
||||||
"btn-primary-background-color-dark" => "00aaff",
|
|
||||||
"btn-primary-background-color-light" => "99ddff",
|
|
||||||
"btn-danger-border-color" => "cb3d1b",
|
|
||||||
"btn-danger-background-color" => "e45735",
|
|
||||||
"btn-danger-background-color-dark" => "cb3d1b",
|
|
||||||
"btn-danger-background-color-light" => "e45735",
|
|
||||||
"btn-success-background" => "00b300",
|
|
||||||
"nav-pills-color" => "333333",
|
|
||||||
"nav-pills-color-hover" => "e45735",
|
|
||||||
"nav-pills-border-color-hover" => "f9dad2",
|
|
||||||
"nav-pills-background-color-hover" => "f9dad2",
|
|
||||||
"nav-pills-color-active" => "e45735",
|
|
||||||
"nav-pills-border-color-active" => "cb3d1b",
|
|
||||||
"nav-pills-background-color-active" => "e45735",
|
|
||||||
"nav-stacked-color" => "333333",
|
|
||||||
"nav-stacked-border-color" => "cccccc",
|
|
||||||
"nav-stacked-background-color" => "f5f5f5",
|
|
||||||
"nav-stacked-divider-color" => "cccccc",
|
|
||||||
"nav-stacked-chevron-color" => "b3b3b3",
|
|
||||||
"nav-stacked-border-color-active" => "e45735",
|
|
||||||
"nav-stacked-background-color-active" => "e45735",
|
|
||||||
"nav-button-color-hover" => "333333",
|
|
||||||
"nav-button-background-color-hover" => "cccccc",
|
|
||||||
"nav-button-color-active" => "333333",
|
|
||||||
"nav-button-background-color-active" => "cccccc",
|
|
||||||
"modal-header-color" => "e45735",
|
|
||||||
"modal-header-border-color" => "b3b3b3",
|
|
||||||
"modal-close-button-color" => "b3b3b3",
|
|
||||||
"nav-like-button-color-hover" => "fa6c8d",
|
|
||||||
"nav-like-button-background-color-hover" => "fed9e1",
|
|
||||||
"nav-like-button-color-active" => "f83b67",
|
|
||||||
"nav-like-button-background-color-active" => "fed9e1",
|
|
||||||
"topic-list-border-color" => "b3b3b3",
|
|
||||||
"topic-list-th-color" => "8c8c8c",
|
|
||||||
"topic-list-th-border-color" => "b3b3b3",
|
|
||||||
"topic-list-th-background-color" => "f5f5f5",
|
|
||||||
"topic-list-td-color" => "8c8c8c",
|
|
||||||
"topic-list-td-border-color" => "cccccc",
|
|
||||||
"topic-list-star-color" => "cccccc",
|
|
||||||
"topic-list-starred-color" => "e45735",
|
|
||||||
"quote-background" => "f5f5f5",
|
|
||||||
topicMenuColor: "333333",
|
|
||||||
bookmarkColor: "00aaff"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
require_dependency 'discourse_sass_importer'
|
require_dependency 'sass/discourse_sass_compiler'
|
||||||
|
|
||||||
class SiteCustomization < ActiveRecord::Base
|
class SiteCustomization < ActiveRecord::Base
|
||||||
ENABLED_KEY = '7e202ef2-56d7-47d5-98d8-a9c8d15e57dd'
|
ENABLED_KEY = '7e202ef2-56d7-47d5-98d8-a9c8d15e57dd'
|
||||||
|
@ -14,29 +14,7 @@ class SiteCustomization < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def compile_stylesheet(scss)
|
def compile_stylesheet(scss)
|
||||||
env = Rails.application.assets
|
DiscourseSassCompiler.compile(scss, 'custom')
|
||||||
|
|
||||||
# In production Rails.application.assets is a Sprockets::Index
|
|
||||||
# instead of Sprockets::Environment, there is no cleaner way
|
|
||||||
# to get the environment from the index.
|
|
||||||
if env.is_a?(Sprockets::Index)
|
|
||||||
env = env.instance_variable_get('@environment')
|
|
||||||
end
|
|
||||||
|
|
||||||
context = env.context_class.new(env, "custom.scss", "app/assets/stylesheets/custom.scss")
|
|
||||||
|
|
||||||
::Sass::Engine.new(scss, {
|
|
||||||
syntax: :scss,
|
|
||||||
cache: false,
|
|
||||||
read_cache: false,
|
|
||||||
style: :compressed,
|
|
||||||
filesystem_importer: DiscourseSassImporter,
|
|
||||||
sprockets: {
|
|
||||||
context: context,
|
|
||||||
environment: context.environment
|
|
||||||
}
|
|
||||||
}).render
|
|
||||||
|
|
||||||
rescue => e
|
rescue => e
|
||||||
puts e.backtrace.join("\n") unless Sass::SyntaxError === e
|
puts e.backtrace.join("\n") unless Sass::SyntaxError === e
|
||||||
|
|
||||||
|
@ -49,13 +27,7 @@ class SiteCustomization < ActiveRecord::Base
|
||||||
begin
|
begin
|
||||||
self.send("#{stylesheet_attr}_baked=", compile_stylesheet(self.send(stylesheet_attr)))
|
self.send("#{stylesheet_attr}_baked=", compile_stylesheet(self.send(stylesheet_attr)))
|
||||||
rescue Sass::SyntaxError => e
|
rescue Sass::SyntaxError => e
|
||||||
error = e.sass_backtrace_str("custom stylesheet")
|
self.send("#{stylesheet_attr}_baked=", DiscourseSassCompiler.error_as_css(e, "custom stylesheet"))
|
||||||
error.gsub!("\n", '\A ')
|
|
||||||
error.gsub!("'", '\27 ')
|
|
||||||
|
|
||||||
self.send("#{stylesheet_attr}_baked=",
|
|
||||||
"footer { white-space: pre; }
|
|
||||||
footer:after { content: '#{error}' }")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class ColorSchemeColorSerializer < ApplicationSerializer
|
class ColorSchemeColorSerializer < ApplicationSerializer
|
||||||
attributes :name, :hex, :opacity
|
attributes :name, :hex
|
||||||
|
|
||||||
def hex
|
def hex
|
||||||
object.hex # otherwise something crazy is returned
|
object.hex # otherwise something crazy is returned
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
class ColorSchemeSerializer < ApplicationSerializer
|
class ColorSchemeSerializer < ApplicationSerializer
|
||||||
attributes :id, :name, :enabled, :can_edit
|
attributes :id, :name, :enabled, :is_base
|
||||||
|
|
||||||
has_many :colors, serializer: ColorSchemeColorSerializer, embed: :objects
|
has_many :colors, serializer: ColorSchemeColorSerializer, embed: :objects
|
||||||
|
|
||||||
def can_edit
|
def base
|
||||||
object.can_edit?
|
object.is_base || false
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -16,7 +16,7 @@ class ColorSchemeRevisor
|
||||||
def revise
|
def revise
|
||||||
ColorScheme.transaction do
|
ColorScheme.transaction do
|
||||||
if @params[:enabled]
|
if @params[:enabled]
|
||||||
ColorScheme.update_all enabled: false
|
ColorScheme.where('id != ?', @color_scheme.id).update_all enabled: false
|
||||||
end
|
end
|
||||||
|
|
||||||
@color_scheme.name = @params[:name]
|
@color_scheme.name = @params[:name]
|
||||||
|
@ -25,7 +25,7 @@ class ColorSchemeRevisor
|
||||||
|
|
||||||
if @params[:colors]
|
if @params[:colors]
|
||||||
new_version = @params[:colors].any? do |c|
|
new_version = @params[:colors].any? do |c|
|
||||||
(existing = @color_scheme.colors_by_name[c[:name]]).nil? or existing.hex != c[:hex] or existing.opacity != c[:opacity]
|
(existing = @color_scheme.colors_by_name[c[:name]]).nil? or existing.hex != c[:hex]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ class ColorSchemeRevisor
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@color_scheme.save!
|
@color_scheme.save
|
||||||
@color_scheme.clear_colors_cache
|
@color_scheme.clear_colors_cache
|
||||||
end
|
end
|
||||||
@color_scheme
|
@color_scheme
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
<%- unless SiteCustomization.override_default_style(session[:preview_style]) %>
|
<%- unless SiteCustomization.override_default_style(session[:preview_style]) %>
|
||||||
<% if mobile_view? %>
|
<%= DiscourseStylesheets.stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %>
|
||||||
<%= stylesheet_link_tag "mobile" %>
|
|
||||||
<% else %>
|
|
||||||
<%= stylesheet_link_tag "desktop" %>
|
|
||||||
<% end %>
|
|
||||||
<%- end %>
|
<%- end %>
|
||||||
|
|
||||||
<%- if staff? %>
|
<%- if staff? %>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
require_dependency 'discourse_sass_importer'
|
require_dependency 'sass/discourse_stylesheets'
|
||||||
|
require_dependency 'sass/discourse_sass_importer'
|
||||||
|
require_dependency 'sass/discourse_safe_sass_importer'
|
||||||
|
|
||||||
Sprockets.send(:remove_const, :SassImporter)
|
Sprockets.send(:remove_const, :SassImporter)
|
||||||
Sprockets::SassImporter = DiscourseSassImporter
|
Sprockets::SassImporter = DiscourseSassImporter
|
||||||
|
|
|
@ -1466,6 +1466,15 @@ en:
|
||||||
copy_name_prefix: "Copy of"
|
copy_name_prefix: "Copy of"
|
||||||
delete_confirm: "Delete this color scheme?"
|
delete_confirm: "Delete this color scheme?"
|
||||||
under_construction: "NOTE: Changes made here will do nothing! This feature is under construction!"
|
under_construction: "NOTE: Changes made here will do nothing! This feature is under construction!"
|
||||||
|
primary: 'primary'
|
||||||
|
secondary: 'secondary'
|
||||||
|
tertiary: 'tertiary'
|
||||||
|
header_background: "header background"
|
||||||
|
header_primary: "header primary"
|
||||||
|
highlight: 'highlight'
|
||||||
|
danger: 'danger'
|
||||||
|
success: 'success'
|
||||||
|
love: 'love'
|
||||||
|
|
||||||
|
|
||||||
email:
|
email:
|
||||||
|
|
|
@ -211,6 +211,10 @@ en:
|
||||||
common: "is one of the 10000 most common passwords. Please use a more secure password."
|
common: "is one of the 10000 most common passwords. Please use a more secure password."
|
||||||
ip_address:
|
ip_address:
|
||||||
signup_not_allowed: "Signup is not allowed from this account."
|
signup_not_allowed: "Signup is not allowed from this account."
|
||||||
|
color_scheme_color:
|
||||||
|
attributes:
|
||||||
|
hex:
|
||||||
|
invalid: "is not a valid color"
|
||||||
|
|
||||||
user_profile:
|
user_profile:
|
||||||
no_info_me: "<div class='missing-profile'>the About Me field of your profile is currently blank, <a href='/users/%{username_lower}/preferences/about-me'>would you like to fill it out?</a></div>"
|
no_info_me: "<div class='missing-profile'>the About Me field of your profile is currently blank, <a href='/users/%{username_lower}/preferences/about-me'>would you like to fill it out?</a></div>"
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
ColorScheme.seed do |s|
|
|
||||||
s.id = 1
|
|
||||||
s.name = I18n.t("color_schemes.base_theme_name")
|
|
||||||
s.enabled = false
|
|
||||||
end
|
|
||||||
|
|
||||||
ColorSchemeColor::BASE_COLORS.each_with_index do |color, i|
|
|
||||||
ColorSchemeColor.seed do |c|
|
|
||||||
c.id = i+1
|
|
||||||
c.name = color[0]
|
|
||||||
c.hex = color[1]
|
|
||||||
c.color_scheme_id = 1
|
|
||||||
end
|
|
||||||
end
|
|
9
db/migrate/20140506200235_remove_seed_color_scheme.rb
Normal file
9
db/migrate/20140506200235_remove_seed_color_scheme.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
class RemoveSeedColorScheme < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
execute "DELETE FROM color_schemes WHERE id = 1"
|
||||||
|
execute "DELETE FROM color_scheme_colors WHERE color_scheme_id = 1"
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
class RemoveOpacityFromColorSchemeColors < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
remove_column :color_scheme_colors, :opacity
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
add_column :color_scheme_colors, :opacity, :integer, null: false, default: 100
|
||||||
|
end
|
||||||
|
end
|
|
@ -24,6 +24,11 @@ class Autospec::ReloadCss
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.run_on_change(paths)
|
def self.run_on_change(paths)
|
||||||
|
if paths.any? { |p| p =~ /\.(css|s[ac]ss)/ }
|
||||||
|
s = DiscourseStylesheets.new(:desktop) # TODO: what about mobile?
|
||||||
|
s.compile
|
||||||
|
paths << "public" + s.stylesheet_relpath_no_digest
|
||||||
|
end
|
||||||
paths.map! do |p|
|
paths.map! do |p|
|
||||||
hash = nil
|
hash = nil
|
||||||
fullpath = "#{Rails.root}/#{p}"
|
fullpath = "#{Rails.root}/#{p}"
|
||||||
|
|
32
lib/sass/discourse_safe_sass_importer.rb
Normal file
32
lib/sass/discourse_safe_sass_importer.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
require_dependency 'sass/discourse_sass_importer'
|
||||||
|
|
||||||
|
# This custom importer is used to import stylesheets but excludes plugins and theming.
|
||||||
|
# It's used as a fallback when compilation of stylesheets fails.
|
||||||
|
|
||||||
|
class DiscourseSafeSassImporter < DiscourseSassImporter
|
||||||
|
def special_imports
|
||||||
|
super.merge({
|
||||||
|
"plugins" => [],
|
||||||
|
"plugins_mobile" => [],
|
||||||
|
"plugins_desktop" => [],
|
||||||
|
"plugins_variables" => []
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def find(name, options)
|
||||||
|
if name == "theme_variables"
|
||||||
|
# Load the default variables
|
||||||
|
contents = ""
|
||||||
|
special_imports[name].each do |css_file|
|
||||||
|
contents << File.read(css_file)
|
||||||
|
end
|
||||||
|
Sass::Engine.new(contents, options.merge(
|
||||||
|
filename: "#{name}.scss",
|
||||||
|
importer: self,
|
||||||
|
syntax: :scss
|
||||||
|
))
|
||||||
|
else
|
||||||
|
super(name, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
55
lib/sass/discourse_sass_compiler.rb
Normal file
55
lib/sass/discourse_sass_compiler.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
require_dependency 'sass/discourse_sass_importer'
|
||||||
|
|
||||||
|
class DiscourseSassCompiler
|
||||||
|
|
||||||
|
def self.compile(scss, target, opts={})
|
||||||
|
self.new(scss, target).compile(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Takes a Sass::SyntaxError and generates css that will show the
|
||||||
|
# error at the bottom of the page.
|
||||||
|
def self.error_as_css(sass_error, label)
|
||||||
|
error = sass_error.sass_backtrace_str(label)
|
||||||
|
error.gsub!("\n", '\A ')
|
||||||
|
error.gsub!("'", '\27 ')
|
||||||
|
|
||||||
|
"footer { white-space: pre; }
|
||||||
|
footer:after { content: '#{error}' }"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(scss, target)
|
||||||
|
@scss = scss
|
||||||
|
@target = target
|
||||||
|
end
|
||||||
|
|
||||||
|
# Compiles the given scss and output the css as a string.
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# safe: (boolean) if true, theme and plugin stylesheets will not be included. Default is false.
|
||||||
|
def compile(opts={})
|
||||||
|
env = Rails.application.assets
|
||||||
|
|
||||||
|
# In production Rails.application.assets is a Sprockets::Index
|
||||||
|
# instead of Sprockets::Environment, there is no cleaner way
|
||||||
|
# to get the environment from the index.
|
||||||
|
if env.is_a?(Sprockets::Index)
|
||||||
|
env = env.instance_variable_get('@environment')
|
||||||
|
end
|
||||||
|
|
||||||
|
context = env.context_class.new(env, "#{@target}.scss", "app/assets/stylesheets/#{@target}.scss")
|
||||||
|
|
||||||
|
::Sass::Engine.new(@scss, {
|
||||||
|
syntax: :scss,
|
||||||
|
cache: false,
|
||||||
|
read_cache: false,
|
||||||
|
style: Rails.env.production? ? :compressed : :expanded,
|
||||||
|
filesystem_importer: opts[:safe] ? DiscourseSafeSassImporter : DiscourseSassImporter,
|
||||||
|
sprockets: {
|
||||||
|
context: context,
|
||||||
|
environment: context.environment
|
||||||
|
}
|
||||||
|
}).render
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -31,7 +31,8 @@ class DiscourseSassImporter < Sass::Importers::Filesystem
|
||||||
"plugins" => DiscoursePluginRegistry.stylesheets,
|
"plugins" => DiscoursePluginRegistry.stylesheets,
|
||||||
"plugins_mobile" => DiscoursePluginRegistry.mobile_stylesheets,
|
"plugins_mobile" => DiscoursePluginRegistry.mobile_stylesheets,
|
||||||
"plugins_desktop" => DiscoursePluginRegistry.desktop_stylesheets,
|
"plugins_desktop" => DiscoursePluginRegistry.desktop_stylesheets,
|
||||||
"plugins_variables" => DiscoursePluginRegistry.sass_variables
|
"plugins_variables" => DiscoursePluginRegistry.sass_variables,
|
||||||
|
"theme_variables" => [ColorScheme::BASE_COLORS_FILE]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -45,21 +46,41 @@ class DiscourseSassImporter < Sass::Importers::Filesystem
|
||||||
|
|
||||||
def find(name, options)
|
def find(name, options)
|
||||||
if special_imports.has_key? name
|
if special_imports.has_key? name
|
||||||
stylesheets = special_imports[name]
|
if name == "theme_variables"
|
||||||
contents = ""
|
contents = ""
|
||||||
stylesheets.each do |css_file|
|
if color_scheme = ColorScheme.enabled
|
||||||
if css_file =~ /\.scss$/
|
ColorScheme.base_colors.each do |name, base_hex|
|
||||||
contents << "@import '#{css_file}';"
|
override = color_scheme.colors_by_name[name]
|
||||||
|
contents << "$#{name}: ##{override ? override.hex : base_hex} !default;\n"
|
||||||
|
end
|
||||||
else
|
else
|
||||||
contents << File.read(css_file)
|
special_imports[name].each do |css_file|
|
||||||
|
contents << File.read(css_file)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
depend_on(css_file)
|
|
||||||
|
Sass::Engine.new(contents, options.merge(
|
||||||
|
filename: "#{name}.scss",
|
||||||
|
importer: self,
|
||||||
|
syntax: :scss
|
||||||
|
))
|
||||||
|
else
|
||||||
|
stylesheets = special_imports[name]
|
||||||
|
contents = ""
|
||||||
|
stylesheets.each do |css_file|
|
||||||
|
if css_file =~ /\.scss$/
|
||||||
|
contents << "@import '#{css_file}';"
|
||||||
|
else
|
||||||
|
contents << File.read(css_file)
|
||||||
|
end
|
||||||
|
depend_on(css_file)
|
||||||
|
end
|
||||||
|
Sass::Engine.new(contents, options.merge(
|
||||||
|
filename: "#{name}.scss",
|
||||||
|
importer: self,
|
||||||
|
syntax: :scss
|
||||||
|
))
|
||||||
end
|
end
|
||||||
Sass::Engine.new(contents, options.merge(
|
|
||||||
filename: "#{name}.scss",
|
|
||||||
importer: self,
|
|
||||||
syntax: :scss
|
|
||||||
))
|
|
||||||
elsif name =~ GLOB
|
elsif name =~ GLOB
|
||||||
nil # globs must be relative
|
nil # globs must be relative
|
||||||
else
|
else
|
95
lib/sass/discourse_stylesheets.rb
Normal file
95
lib/sass/discourse_stylesheets.rb
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
require_dependency 'sass/discourse_sass_compiler'
|
||||||
|
|
||||||
|
class DiscourseStylesheets
|
||||||
|
|
||||||
|
CACHE_PATH = 'uploads/stylesheet-cache'
|
||||||
|
|
||||||
|
@lock = Mutex.new
|
||||||
|
|
||||||
|
def self.stylesheet_link_tag(target = :desktop)
|
||||||
|
builder = self.new(target)
|
||||||
|
@lock.synchronize do
|
||||||
|
builder.compile unless File.exists?(builder.stylesheet_fullpath)
|
||||||
|
builder.ensure_digestless_file
|
||||||
|
%[<link href="#{Rails.env.production? ? builder.stylesheet_relpath : builder.stylesheet_relpath_no_digest + '?body=1'}" media="screen" rel="stylesheet" />].html_safe
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.compile(target = :desktop)
|
||||||
|
@lock.synchronize do
|
||||||
|
builder = self.new(target)
|
||||||
|
builder.compile
|
||||||
|
builder.stylesheet_filename
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(target = :desktop)
|
||||||
|
@target = target
|
||||||
|
end
|
||||||
|
|
||||||
|
def compile
|
||||||
|
scss = File.read("#{Rails.root}/app/assets/stylesheets/#{@target}.scss")
|
||||||
|
css = begin
|
||||||
|
DiscourseSassCompiler.compile(scss, @target)
|
||||||
|
rescue Sass::SyntaxError => e
|
||||||
|
Rails.logger.error "Stylesheet failed to compile for '#{@target}'! Recompiling without plugins and theming."
|
||||||
|
Rails.logger.error e.sass_backtrace_str("#{@target} stylesheet")
|
||||||
|
DiscourseSassCompiler.compile(scss + DiscourseSassCompiler.error_as_css(e, "#{@target} stylesheet"), @target, safe: true)
|
||||||
|
end
|
||||||
|
FileUtils.mkdir_p(cache_fullpath)
|
||||||
|
File.open(stylesheet_fullpath, "w") do |f|
|
||||||
|
f.puts css
|
||||||
|
end
|
||||||
|
css
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_digestless_file
|
||||||
|
unless File.exist?(stylesheet_fullpath_no_digest) && File.mtime(stylesheet_fullpath) == File.mtime(stylesheet_fullpath_no_digest)
|
||||||
|
FileUtils.cp(stylesheet_fullpath, stylesheet_fullpath_no_digest)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_fullpath
|
||||||
|
"#{Rails.root}/public/#{CACHE_PATH}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def stylesheet_fullpath
|
||||||
|
"#{cache_fullpath}/#{stylesheet_filename}"
|
||||||
|
end
|
||||||
|
def stylesheet_fullpath_no_digest
|
||||||
|
"#{cache_fullpath}/#{stylesheet_filename_no_digest}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def stylesheet_relpath
|
||||||
|
"/#{CACHE_PATH}/#{stylesheet_filename}"
|
||||||
|
end
|
||||||
|
def stylesheet_relpath_no_digest
|
||||||
|
"/#{CACHE_PATH}/#{stylesheet_filename_no_digest}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def stylesheet_filename
|
||||||
|
"#{@target}_#{digest}.css"
|
||||||
|
end
|
||||||
|
def stylesheet_filename_no_digest
|
||||||
|
"#{@target}.css"
|
||||||
|
end
|
||||||
|
|
||||||
|
def digest
|
||||||
|
@digest ||= begin
|
||||||
|
# Watch for file changes unless in production env.
|
||||||
|
# In production, file changes only happen during deploy, followed by assets:precompile.
|
||||||
|
last_file_updated = if Rails.env.production?
|
||||||
|
0
|
||||||
|
else
|
||||||
|
[ Dir.glob("#{Rails.root}/app/assets/stylesheets/**/*.*css").map {|x| File.mtime(x) }.max,
|
||||||
|
Dir.glob("#{Rails.root}/plugins/**/assets/stylesheets/**/*.*css").map {|x| File.mtime(x) }.max ].max.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
theme = (cs = ColorScheme.enabled) ? "#{cs.id}-#{cs.version}" : 0
|
||||||
|
|
||||||
|
# digest encodes the things that trigger a recompile
|
||||||
|
Digest::SHA1.hexdigest("#{RailsMultisite::ConnectionManagement.current_db}-#{theme}-#{last_file_updated}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -44,4 +44,16 @@ task 'assets:precompile:before' do
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
task 'assets:precompile' => 'assets:precompile:before'
|
task 'assets:precompile:css' => 'environment' do
|
||||||
|
RailsMultisite::ConnectionManagement.each_connection do |db|
|
||||||
|
puts "Compiling css for #{db}"
|
||||||
|
[:desktop, :mobile].each do |target|
|
||||||
|
puts DiscourseStylesheets.compile(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
task 'assets:precompile' => 'assets:precompile:before' do
|
||||||
|
# Run after assets:precompile
|
||||||
|
Rake::Task["assets:precompile:css"].invoke
|
||||||
|
end
|
||||||
|
|
30
spec/components/discourse_sass_compiler_spec.rb
Normal file
30
spec/components/discourse_sass_compiler_spec.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require_dependency 'sass/discourse_sass_compiler'
|
||||||
|
|
||||||
|
describe DiscourseSassCompiler do
|
||||||
|
|
||||||
|
let(:test_scss) { "body { p {color: blue;} }\n@import 'common/foundation/variables';\n@import 'plugins';" }
|
||||||
|
|
||||||
|
describe '#compile' do
|
||||||
|
it "compiles scss" do
|
||||||
|
DiscoursePluginRegistry.stubs(:stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"])
|
||||||
|
css = described_class.compile(test_scss, "test")
|
||||||
|
css.should include("color")
|
||||||
|
css.should include('my-plugin-thing')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises error for invalid scss" do
|
||||||
|
expect {
|
||||||
|
described_class.compile("this isn't valid scss", "test")
|
||||||
|
}.to raise_error(Sass::SyntaxError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't load theme or plugins in safe mode" do
|
||||||
|
ColorScheme.expects(:enabled).never
|
||||||
|
DiscoursePluginRegistry.stubs(:stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"])
|
||||||
|
css = described_class.compile(test_scss, "test", safe: true)
|
||||||
|
css.should_not include('my-plugin-thing')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
32
spec/components/discourse_stylesheets_spec.rb
Normal file
32
spec/components/discourse_stylesheets_spec.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require_dependency 'sass/discourse_stylesheets'
|
||||||
|
|
||||||
|
describe DiscourseStylesheets do
|
||||||
|
|
||||||
|
describe "compile" do
|
||||||
|
it "can compile desktop bundle" do
|
||||||
|
DiscoursePluginRegistry.stubs(:stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"])
|
||||||
|
builder = described_class.new(:desktop)
|
||||||
|
builder.compile.should include('my-plugin-thing')
|
||||||
|
FileUtils.rm builder.stylesheet_fullpath
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can compile mobile bundle" do
|
||||||
|
DiscoursePluginRegistry.stubs(:mobile_stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"])
|
||||||
|
builder = described_class.new(:mobile)
|
||||||
|
builder.compile.should include('my-plugin-thing')
|
||||||
|
FileUtils.rm builder.stylesheet_fullpath
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can fallback when css is bad" do
|
||||||
|
DiscoursePluginRegistry.stubs(:stylesheets).returns([
|
||||||
|
"#{Rails.root}/spec/fixtures/scss/my_plugin.scss",
|
||||||
|
"#{Rails.root}/spec/fixtures/scss/broken.scss"
|
||||||
|
])
|
||||||
|
builder = described_class.new(:desktop)
|
||||||
|
builder.compile.should_not include('my-plugin-thing')
|
||||||
|
FileUtils.rm builder.stylesheet_fullpath
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -11,8 +11,8 @@ describe Admin::ColorSchemesController do
|
||||||
name: 'Such Design',
|
name: 'Such Design',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
colors: [
|
colors: [
|
||||||
{name: '$primary_background_color', hex: 'FFBB00', opacity: '100'},
|
{name: 'primary', hex: 'FFBB00'},
|
||||||
{name: '$secondary_background_color', hex: '888888', opacity: '70'}
|
{name: 'secondary', hex: '888888'}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
} }
|
} }
|
||||||
|
@ -40,6 +40,14 @@ describe Admin::ColorSchemesController do
|
||||||
xhr :post, :create, valid_params
|
xhr :post, :create, valid_params
|
||||||
::JSON.parse(response.body)['id'].should be_present
|
::JSON.parse(response.body)['id'].should be_present
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "returns failure with invalid params" do
|
||||||
|
params = valid_params
|
||||||
|
params[:color_scheme][:colors][0][:hex] = 'cool color please'
|
||||||
|
xhr :post, :create, valid_params
|
||||||
|
response.should_not be_success
|
||||||
|
::JSON.parse(response.body)['errors'].should be_present
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "update" do
|
describe "update" do
|
||||||
|
@ -56,6 +64,16 @@ describe Admin::ColorSchemesController do
|
||||||
xhr :put, :update, valid_params.merge(id: existing.id)
|
xhr :put, :update, valid_params.merge(id: existing.id)
|
||||||
::JSON.parse(response.body)['id'].should be_present
|
::JSON.parse(response.body)['id'].should be_present
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "returns failure with invalid params" do
|
||||||
|
color_scheme = Fabricate(:color_scheme)
|
||||||
|
params = valid_params.merge(id: color_scheme.id)
|
||||||
|
params[:color_scheme][:colors][0][:name] = color_scheme.colors.first.name
|
||||||
|
params[:color_scheme][:colors][0][:hex] = 'cool color please'
|
||||||
|
xhr :put, :update, params
|
||||||
|
response.should_not be_success
|
||||||
|
::JSON.parse(response.body)['errors'].should be_present
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "destroy" do
|
describe "destroy" do
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
Fabricator(:color_scheme_color) do
|
Fabricator(:color_scheme_color) do
|
||||||
color_scheme
|
color_scheme
|
||||||
name { sequence(:name) {|i| "$color_#{i}" } }
|
name { sequence(:name) {|i| "color_#{i}" } }
|
||||||
hex "333333"
|
hex "333333"
|
||||||
opacity 100
|
|
||||||
end
|
end
|
||||||
|
|
1
spec/fixtures/scss/broken.scss
vendored
Normal file
1
spec/fixtures/scss/broken.scss
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
this isn't valid scss, so it will fail to compile
|
3
spec/fixtures/scss/my_plugin.scss
vendored
Normal file
3
spec/fixtures/scss/my_plugin.scss
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.my-plugin-thing {
|
||||||
|
color: blue
|
||||||
|
}
|
18
spec/models/color_scheme_color_spec.rb
Normal file
18
spec/models/color_scheme_color_spec.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe ColorSchemeColor do
|
||||||
|
def test_invalid_hex(hex)
|
||||||
|
c = described_class.new(hex: hex)
|
||||||
|
c.should_not be_valid
|
||||||
|
c.errors[:hex].should be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it "validates hex value" do
|
||||||
|
['fff', 'ffffff', '333333', '333', '0BeeF0'].each do |hex|
|
||||||
|
described_class.new(hex: hex).should be_valid
|
||||||
|
end
|
||||||
|
['fffff', 'ffff', 'ff', 'f', '00000', '00', 'cheese', '#666666', '#666', '555 666'].each do |hex|
|
||||||
|
test_invalid_hex(hex)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,10 +2,28 @@ require 'spec_helper'
|
||||||
|
|
||||||
describe ColorScheme do
|
describe ColorScheme do
|
||||||
|
|
||||||
|
describe '#base_colors' do
|
||||||
|
it 'parses the colors.scss file and returns a hash' do
|
||||||
|
File.stubs(:readlines).with(described_class::BASE_COLORS_FILE).returns([
|
||||||
|
'$primary: #333333 !default;',
|
||||||
|
'$secondary: #ffffff !default; ',
|
||||||
|
'$highlight: #ffff4d;',
|
||||||
|
' $danger:#e45735 !default;',
|
||||||
|
])
|
||||||
|
|
||||||
|
colors = described_class.base_colors
|
||||||
|
colors.should be_a(Hash)
|
||||||
|
colors['primary'].should == '333333'
|
||||||
|
colors['secondary'].should == 'ffffff'
|
||||||
|
colors['highlight'].should == 'ffff4d'
|
||||||
|
colors['danger'].should == 'e45735'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
let(:valid_params) { {name: "Best Colors Evar", enabled: true, colors: valid_colors} }
|
let(:valid_params) { {name: "Best Colors Evar", enabled: true, colors: valid_colors} }
|
||||||
let(:valid_colors) { [
|
let(:valid_colors) { [
|
||||||
{name: '$primary_background_color', hex: 'FFBB00', opacity: '100'},
|
{name: '$primary_background_color', hex: 'FFBB00'},
|
||||||
{name: '$secondary_background_color', hex: '888888', opacity: '70'}
|
{name: '$secondary_background_color', hex: '888888'}
|
||||||
]}
|
]}
|
||||||
|
|
||||||
describe "new" do
|
describe "new" do
|
||||||
|
@ -31,8 +49,8 @@ describe ColorScheme do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#enabled" do
|
describe "#enabled" do
|
||||||
it "returns the base color scheme when there is no enabled record" do
|
it "returns nil when there is no enabled record" do
|
||||||
described_class.enabled.id.should == 1
|
described_class.enabled.should be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns the enabled color scheme" do
|
it "returns the enabled color scheme" do
|
||||||
|
|
|
@ -155,12 +155,12 @@ describe SiteCustomization do
|
||||||
|
|
||||||
it 'should compile scss' do
|
it 'should compile scss' do
|
||||||
c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '$black: #000; #a { color: $black; }', header: '')
|
c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '$black: #000; #a { color: $black; }', header: '')
|
||||||
c.stylesheet_baked.should == "#a{color:#000}\n"
|
["#a{color:#000;}", "#a{color:black;}"].should include(c.stylesheet_baked.gsub(' ', '').gsub("\n", ''))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should compile mobile scss' do
|
it 'should compile mobile scss' do
|
||||||
c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '', header: '', mobile_stylesheet: '$black: #000; #a { color: $black; }', mobile_header: '')
|
c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '', header: '', mobile_stylesheet: '$black: #000; #a { color: $black; }', mobile_header: '')
|
||||||
c.mobile_stylesheet_baked.should == "#a{color:#000}\n"
|
["#a{color:#000;}", "#a{color:black;}"].should include(c.mobile_stylesheet_baked.gsub(' ', '').gsub("\n", ''))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should allow including discourse styles' do
|
it 'should allow including discourse styles' do
|
||||||
|
|
|
@ -25,14 +25,23 @@ describe ColorSchemeRevisor do
|
||||||
color_scheme.reload.should_not be_enabled
|
color_scheme.reload.should_not be_enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can change colors" do
|
def test_color_change(color_scheme_arg, expected_enabled)
|
||||||
described_class.revise(color_scheme, valid_params.merge(colors: [
|
described_class.revise(color_scheme_arg, valid_params.merge(colors: [
|
||||||
{name: color.name, hex: 'BEEF99', opacity: 99}
|
{name: color.name, hex: 'BEEF99'}
|
||||||
]))
|
]))
|
||||||
color_scheme.reload
|
color_scheme_arg.reload
|
||||||
color_scheme.colors.size.should == 1
|
color_scheme_arg.enabled.should == expected_enabled
|
||||||
color_scheme.colors.first.hex.should == 'BEEF99'
|
color_scheme_arg.colors.size.should == 1
|
||||||
color_scheme.colors.first.opacity.should == 99
|
color_scheme_arg.colors.first.hex.should == 'BEEF99'
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can change colors of a color scheme that's not enabled" do
|
||||||
|
test_color_change(color_scheme, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can change colors of the enabled color scheme" do
|
||||||
|
color_scheme.update_attribute(:enabled, true)
|
||||||
|
test_color_change(color_scheme, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "disables other color scheme before enabling" do
|
it "disables other color scheme before enabling" do
|
||||||
|
@ -42,6 +51,17 @@ describe ColorSchemeRevisor do
|
||||||
color_scheme.reload.enabled.should == true
|
color_scheme.reload.enabled.should == true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "doesn't make changes when a color is invalid" do
|
||||||
|
expect {
|
||||||
|
cs = described_class.revise(color_scheme, valid_params.merge(colors: [
|
||||||
|
{name: color.name, hex: 'OOPS'}
|
||||||
|
]))
|
||||||
|
cs.should_not be_valid
|
||||||
|
cs.errors.should be_present
|
||||||
|
}.to_not change { color_scheme.reload.version }
|
||||||
|
color_scheme.colors.first.hex.should == color.hex
|
||||||
|
end
|
||||||
|
|
||||||
describe "versions" do
|
describe "versions" do
|
||||||
it "doesn't create a new version if colors is not given" do
|
it "doesn't create a new version if colors is not given" do
|
||||||
expect {
|
expect {
|
||||||
|
@ -50,25 +70,23 @@ describe ColorSchemeRevisor do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "creates a new version if colors have changed" do
|
it "creates a new version if colors have changed" do
|
||||||
old_hex, old_opacity = color.hex, color.opacity
|
old_hex = color.hex
|
||||||
expect {
|
expect {
|
||||||
described_class.revise(color_scheme, valid_params.merge(colors: [
|
described_class.revise(color_scheme, valid_params.merge(colors: [
|
||||||
{name: color.name, hex: 'BEEF99', opacity: 99}
|
{name: color.name, hex: 'BEEF99'}
|
||||||
]))
|
]))
|
||||||
}.to change { color_scheme.reload.version }.by(1)
|
}.to change { color_scheme.reload.version }.by(1)
|
||||||
old_version = ColorScheme.find_by(versioned_id: color_scheme.id, version: (color_scheme.version - 1))
|
old_version = ColorScheme.find_by(versioned_id: color_scheme.id, version: (color_scheme.version - 1))
|
||||||
old_version.should_not be_nil
|
old_version.should_not be_nil
|
||||||
old_version.colors.count.should == color_scheme.colors.count
|
old_version.colors.count.should == color_scheme.colors.count
|
||||||
old_version.colors_by_name[color.name].hex.should == old_hex
|
old_version.colors_by_name[color.name].hex.should == old_hex
|
||||||
old_version.colors_by_name[color.name].opacity.should == old_opacity
|
|
||||||
color_scheme.colors_by_name[color.name].hex.should == 'BEEF99'
|
color_scheme.colors_by_name[color.name].hex.should == 'BEEF99'
|
||||||
color_scheme.colors_by_name[color.name].opacity.should == 99
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't create a new version if colors have not changed" do
|
it "doesn't create a new version if colors have not changed" do
|
||||||
expect {
|
expect {
|
||||||
described_class.revise(color_scheme, valid_params.merge(colors: [
|
described_class.revise(color_scheme, valid_params.merge(colors: [
|
||||||
{name: color.name, hex: color.hex, opacity: color.opacity}
|
{name: color.name, hex: color.hex}
|
||||||
]))
|
]))
|
||||||
}.to_not change { color_scheme.reload.version }
|
}.to_not change { color_scheme.reload.version }
|
||||||
end
|
end
|
||||||
|
@ -85,10 +103,10 @@ describe ColorSchemeRevisor do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when there are previous versions' do
|
context 'when there are previous versions' do
|
||||||
let(:new_color_params) { {name: color.name, hex: 'BEEF99', opacity: 99} }
|
let(:new_color_params) { {name: color.name, hex: 'BEEF99'} }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
@prev_hex, @prev_opacity = color.hex, color.opacity
|
@prev_hex = color.hex
|
||||||
described_class.revise(color_scheme, valid_params.merge(colors: [ new_color_params ]))
|
described_class.revise(color_scheme, valid_params.merge(colors: [ new_color_params ]))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -99,9 +117,7 @@ describe ColorSchemeRevisor do
|
||||||
}.to change { color_scheme.reload.version }.by(-1)
|
}.to change { color_scheme.reload.version }.by(-1)
|
||||||
color_scheme.colors.size.should == 1
|
color_scheme.colors.size.should == 1
|
||||||
color_scheme.colors.first.hex.should == @prev_hex
|
color_scheme.colors.first.hex.should == @prev_hex
|
||||||
color_scheme.colors.first.opacity.should == @prev_opacity
|
|
||||||
color_scheme.colors_by_name[new_color_params[:name]].hex.should == @prev_hex
|
color_scheme.colors_by_name[new_color_params[:name]].hex.should == @prev_hex
|
||||||
color_scheme.colors_by_name[new_color_params[:name]].opacity.should == @prev_opacity
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "destroys the old version's record" do
|
it "destroys the old version's record" do
|
||||||
|
|
Loading…
Reference in a new issue