diff --git a/js/actions/edition_list_actions.js b/js/actions/edition_list_actions.js index 6f9881ee..a52e32b9 100644 --- a/js/actions/edition_list_actions.js +++ b/js/actions/edition_list_actions.js @@ -17,23 +17,31 @@ class EditionListActions { ); } - fetchEditionList(pieceId, page, pageSize, orderBy, orderAsc, filterBy) { - if((!orderBy && typeof orderAsc === 'undefined') || !orderAsc) { + fetchEditionList({ pieceId, page, pageSize, orderBy, orderAsc, filterBy, maxEdition }) { + if ((!orderBy && typeof orderAsc === 'undefined') || !orderAsc) { orderBy = 'edition_number'; orderAsc = true; } // Taken from: http://stackoverflow.com/a/519157/1263876 - if((typeof page === 'undefined' || !page) && (typeof pageSize === 'undefined' || !pageSize)) { + if ((typeof page === 'undefined' || !page) && (typeof pageSize === 'undefined' || !pageSize)) { page = 1; pageSize = 10; } + let itemsToFetch = pageSize; + // If we only want to fetch up to a specified edition, fetch all pages up to it + // as one page and adjust afterwards + if (typeof maxEdition === 'number') { + itemsToFetch = Math.ceil(maxEdition / pageSize) * pageSize; + page = 1; + } + return Q.Promise((resolve, reject) => { EditionListFetcher - .fetch(pieceId, page, pageSize, orderBy, orderAsc, filterBy) + .fetch({ pieceId, page, itemsToFetch, orderBy, orderAsc, filterBy }) .then((res) => { - if(res && !res.editions) { + if (res && !res.editions) { throw new Error('Piece has no editions to fetch.'); } @@ -44,8 +52,9 @@ class EditionListActions { orderBy, orderAsc, filterBy, - 'editionListOfPiece': res.editions, - 'count': res.count + maxEdition, + count: res.count, + editionListOfPiece: res.editions }); resolve(res); }) @@ -54,7 +63,6 @@ class EditionListActions { reject(err); }); }); - } } diff --git a/js/actions/piece_list_actions.js b/js/actions/piece_list_actions.js index 7ef9cb59..c52ef5e2 100644 --- a/js/actions/piece_list_actions.js +++ b/js/actions/piece_list_actions.js @@ -15,7 +15,7 @@ class PieceListActions { ); } - fetchPieceList(page, pageSize, search, orderBy, orderAsc, filterBy) { + fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }) { // To prevent flickering on a pagination request, // we overwrite the piecelist with an empty list before // pieceListCount === -1 defines the loading state @@ -34,7 +34,7 @@ class PieceListActions { // afterwards, we can load the list return Q.Promise((resolve, reject) => { PieceListFetcher - .fetch(page, pageSize, search, orderBy, orderAsc, filterBy) + .fetch({ page, pageSize, search, orderBy, orderAsc, filterBy }) .then((res) => { this.actions.updatePieceList({ page, diff --git a/js/app.js b/js/app.js index dc8204cf..226409ec 100644 --- a/js/app.js +++ b/js/app.js @@ -1,6 +1,7 @@ 'use strict'; import 'babel/polyfill'; +import 'classlist-polyfill'; import React from 'react'; import { Router, Redirect } from 'react-router'; diff --git a/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js b/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js index 27657e04..2bb8b2d0 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js @@ -19,9 +19,10 @@ import { getLangText } from '../../utils/lang_utils'; let AccordionListItemEditionWidget = React.createClass({ propTypes: { - className: React.PropTypes.string, piece: React.PropTypes.object.isRequired, toggleCreateEditionsDialog: React.PropTypes.func.isRequired, + + className: React.PropTypes.string, onPollingSuccess: React.PropTypes.func }, @@ -50,14 +51,15 @@ let AccordionListItemEditionWidget = React.createClass({ * Calls the store to either show or hide the editionListTable */ toggleTable() { - let pieceId = this.props.piece.id; - let isEditionListOpen = this.state.isEditionListOpenForPieceId[pieceId] ? this.state.isEditionListOpenForPieceId[pieceId].show : false; + const { piece: { id: pieceId } } = this.props; + const { filterBy, isEditionListOpenForPieceId } = this.state; + const isEditionListOpen = isEditionListOpenForPieceId[pieceId] ? isEditionListOpenForPieceId[pieceId].show : false; - if(isEditionListOpen) { + if (isEditionListOpen) { EditionListActions.toggleEditionList(pieceId); } else { EditionListActions.toggleEditionList(pieceId); - EditionListActions.fetchEditionList(pieceId, null, null, null, null, this.state.filterBy); + EditionListActions.fetchEditionList({ pieceId, filterBy }); } }, diff --git a/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js b/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js index 586b75a2..3b0bb02e 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js @@ -66,20 +66,28 @@ let AccordionListItemTableEditions = React.createClass({ }, filterSelectedEditions() { - let selectedEditions = this.state.editionList[this.props.parentId] - .filter((edition) => edition.selected); - return selectedEditions; + return this.state + .editionList[this.props.parentId] + .filter((edition) => edition.selected); }, loadFurtherEditions() { + const { parentId: pieceId } = this.props; + const { page, pageSize, orderBy, orderAsc, filterBy } = this.state.editionList[pieceId]; + // trigger loading animation this.setState({ showMoreLoading: true }); - let editionList = this.state.editionList[this.props.parentId]; - EditionListActions.fetchEditionList(this.props.parentId, editionList.page + 1, editionList.pageSize, - editionList.orderBy, editionList.orderAsc, editionList.filterBy); + EditionListActions.fetchEditionList({ + pieceId, + pageSize, + orderBy, + orderAsc, + filterBy, + page: page + 1 + }); }, render() { const { className, parentId } = this.props; diff --git a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js index a8cab166..f6712d37 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -88,11 +88,12 @@ let AccordionListItemWallet = React.createClass({ }, onPollingSuccess(pieceId) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); EditionListActions.toggleEditionList(pieceId); - let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); + const notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, diff --git a/js/components/ascribe_buttons/create_editions_button.js b/js/components/ascribe_buttons/create_editions_button.js index 08fb76ce..c78b0d63 100644 --- a/js/components/ascribe_buttons/create_editions_button.js +++ b/js/components/ascribe_buttons/create_editions_button.js @@ -28,6 +28,12 @@ let CreateEditionsButton = React.createClass({ EditionListStore.listen(this.onChange); }, + componentDidUpdate() { + if(this.props.piece.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') { + this.startPolling(); + } + }, + componentWillUnmount() { EditionListStore.unlisten(this.onChange); clearInterval(this.state.pollingIntervalIndex); @@ -37,28 +43,24 @@ let CreateEditionsButton = React.createClass({ this.setState(state); }, - componentDidUpdate() { - if(this.props.piece.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') { - this.startPolling(); - } - }, - startPolling() { // start polling until editions are defined let pollingIntervalIndex = setInterval(() => { // requests, will try to merge the filterBy parameter with other parameters (mergeOptions). // Therefore it can't but null but instead has to be an empty object - EditionListActions.fetchEditionList(this.props.piece.id, null, null, null, null, {}) - .then((res) => { - - clearInterval(this.state.pollingIntervalIndex); - this.props.onPollingSuccess(this.props.piece.id, res.editions[0].num_editions); - - }) - .catch((err) => { - /* Ignore and keep going */ - }); + EditionListActions + .fetchEditionList({ + pieceId: this.props.piece.id, + filterBy: {} + }) + .then((res) => { + clearInterval(this.state.pollingIntervalIndex); + this.props.onPollingSuccess(this.props.piece.id, res.editions[0].num_editions); + }) + .catch((err) => { + /* Ignore and keep going */ + }); }, 5000); this.setState({ diff --git a/js/components/ascribe_detail/edition_action_panel.js b/js/components/ascribe_detail/edition_action_panel.js index 92c51c32..71bf38fe 100644 --- a/js/components/ascribe_detail/edition_action_panel.js +++ b/js/components/ascribe_detail/edition_action_panel.js @@ -79,9 +79,10 @@ let EditionActionPanel = React.createClass({ }, refreshCollection() { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); - EditionListActions.refreshEditionList({pieceId: this.props.edition.parent}); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); + EditionListActions.refreshEditionList({ pieceId: this.props.edition.parent }); }, handleSuccess(response) { diff --git a/js/components/ascribe_detail/piece_container.js b/js/components/ascribe_detail/piece_container.js index cd1559a2..f7ef024f 100644 --- a/js/components/ascribe_detail/piece_container.js +++ b/js/components/ascribe_detail/piece_container.js @@ -144,15 +144,18 @@ let PieceContainer = React.createClass({ }, handleEditionCreationSuccess() { + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + PieceActions.updateProperty({ key: 'num_editions', value: 0 }); - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); + this.toggleCreateEditionsDialog(); }, handleDeleteSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); // since we're deleting a piece, we just need to close // all editions dialogs and not reload them @@ -181,6 +184,8 @@ let PieceContainer = React.createClass({ }, handlePollingSuccess(pieceId, numEditions) { + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + // we need to refresh the num_editions property of the actual piece we're looking at PieceActions.updateProperty({ key: 'num_editions', @@ -191,8 +196,7 @@ let PieceContainer = React.createClass({ // btw.: It's not sufficient to just set num_editions to numEditions, since a single accordion // list item also uses the firstEdition property which we can only get from the server in that case. // Therefore we need to at least refetch the changed piece from the server or on our case simply all - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); const notification = new GlobalNotificationModel(getLangText('Editions successfully created'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); diff --git a/js/components/global_action.js b/js/components/global_action.js deleted file mode 100644 index 80df0c75..00000000 --- a/js/components/global_action.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -import React from 'react'; - -let GlobalAction = React.createClass({ - propTypes: { - requestActions: React.PropTypes.object - }, - - render() { - let pieceActions = null; - if (this.props.requestActions && this.props.requestActions.pieces){ - pieceActions = this.props.requestActions.pieces.map((item) => { - return ( -
- {item} -
); - }); - } - let editionActions = null; - if (this.props.requestActions && this.props.requestActions.editions){ - editionActions = Object.keys(this.props.requestActions.editions).map((pieceId) => { - return this.props.requestActions.editions[pieceId].map((item) => { - return ( -
- {item} -
); - }); - }); - } - - if (pieceActions || editionActions) { - return ( -
- {pieceActions} - {editionActions} -
); - } - return null; - } -}); - -export default GlobalAction; \ No newline at end of file diff --git a/js/components/piece_list.js b/js/components/piece_list.js index 68ba5c33..666d1b54 100644 --- a/js/components/piece_list.js +++ b/js/components/piece_list.js @@ -196,13 +196,14 @@ let PieceList = React.createClass({ this.state.pieceList .forEach((piece) => { // but only if they're actually open - if(this.state.isEditionListOpenForPieceId[piece.id].show) { + const isEditionListOpenForPiece = this.state.isEditionListOpenForPieceId[piece.id]; + + if (isEditionListOpenForPiece && isEditionListOpenForPiece.show) { EditionListActions.refreshEditionList({ pieceId: piece.id, filterBy }); } - }); }); @@ -212,15 +213,15 @@ let PieceList = React.createClass({ }, applyOrderBy(orderBy) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, page, pageSize, search } = this.state; + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); }, loadPieceList({ page, filterBy = this.state.filterBy, search = this.state.search }) { + const { orderAsc, pageSize } = this.state; const orderBy = this.state.orderBy || this.props.orderBy; - return PieceListActions.fetchPieceList(page, this.state.pageSize, search, - orderBy, this.state.orderAsc, filterBy); + return PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); }, fetchSelectedPieceEditionList() { @@ -246,8 +247,9 @@ let PieceList = React.createClass({ }, handleAclSuccess() { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderBy, orderAsc, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); this.fetchSelectedPieceEditionList() .forEach((pieceId) => { diff --git a/js/components/register_piece.js b/js/components/register_piece.js index b69ed7c2..19a340ba 100644 --- a/js/components/register_piece.js +++ b/js/components/register_piece.js @@ -65,26 +65,21 @@ let RegisterPiece = React.createClass( { this.setState(state); }, - handleSuccess(response){ + handleSuccess(response) { + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + const notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); // once the user was able to register a piece successfully, we need to make sure to keep // the piece list up to date - PieceListActions.fetchPieceList( - this.state.page, - this.state.pageSize, - this.state.searchTerm, - this.state.orderBy, - this.state.orderAsc, - this.state.filterBy - ); + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); this.history.push(`/pieces/${response.piece.id}`); }, getSpecifyEditions() { - if(this.state.whitelabel && this.state.whitelabel.acl_create_editions || Object.keys(this.state.whitelabel).length === 0) { + if (this.state.whitelabel && this.state.whitelabel.acl_create_editions || Object.keys(this.state.whitelabel).length === 0) { return ( merge current objects with the new ones from the server */ - for (let i = 0; i < editionListOfPiece.length; i++) { - - // if editionList for a specific piece does not exist yet, - // just initialize a new array - if(!this.editionList[pieceId]) { - this.editionList[pieceId] = []; - } - - // this is the index formula for accessing an edition of a specific - // page - let storeEditionIndex = (page - 1) * pageSize + i; - let editionsForPieces = this.editionList[pieceId]; + editionListOfPiece.forEach((updatedEdition, index) => { + // this is the index formula for accessing an edition starting from a specific page + const storeEditionIndex = (page - 1) * pageSize + index; // if edition already exists, just merge - if(editionsForPieces[storeEditionIndex]) { - editionsForPieces[storeEditionIndex] = React.addons.update(editionsForPieces[storeEditionIndex], {$merge: editionListOfPiece[i]}); + if (pieceEditionList[storeEditionIndex]) { + pieceEditionList[storeEditionIndex] = React.addons.update(pieceEditionList[storeEditionIndex], { $merge: updatedEdition }); } else { // if does not exist, assign - editionsForPieces[storeEditionIndex] = editionListOfPiece[i]; + pieceEditionList[storeEditionIndex] = updatedEdition; + } + }); + + // Remove editions after specified max by finding the index of the first + // edition larger than the max edition and using that to cut off the rest of the list + if (typeof maxEdition === 'number') { + const largerThanMaxIndex = pieceEditionList.findIndex(edition => edition.edition_number > maxEdition); + + if (largerThanMaxIndex !== -1) { + // The API defines inflexible page buckets based on the page number + // and page size, so we cannot just arbitrarily cut off the end of + // a page and expect get the rest of it on the next pagination request. + // Hence, we use the max edition index as a guide for which page to + // cut off to so as to always provide complete pages. + page = Math.ceil(largerThanMaxIndex / pageSize); + + // We only want to cut off the list if there are more editions than + // there should be (ie. we're not already at the end of the editions) + const totalPageSize = page * pageSize; + if (pieceEditionList.length > totalPageSize) { + pieceEditionList.length = totalPageSize; + } } } + const lastEdition = pieceEditionList[pieceEditionList.length - 1]; + /** * page, pageSize, orderBy, orderAsc and count are specific to a single list of editions * therefore they need to be saved in relation to their parent-piece. * * Default values for both are set in the editon_list_actions. */ - this.editionList[pieceId].page = page; - this.editionList[pieceId].pageSize = pageSize; - this.editionList[pieceId].orderBy = orderBy; - this.editionList[pieceId].orderAsc = orderAsc; - this.editionList[pieceId].count = count; - this.editionList[pieceId].filterBy = filterBy; + pieceEditionList.page = page; + pieceEditionList.pageSize = pageSize; + pieceEditionList.orderBy = orderBy; + pieceEditionList.orderAsc = orderAsc; + pieceEditionList.count = count; + pieceEditionList.filterBy = filterBy; + + if (pieceEditionList.maxSeen == null || lastEdition.edition_number > pieceEditionList.maxSeen) { + pieceEditionList.maxSeen = lastEdition.edition_number; + } + + this.editionList[pieceId] = pieceEditionList; } /** * We often just have to refresh the edition list for a certain pieceId, * this method provides exactly that functionality without any side effects */ - onRefreshEditionList({ pieceId, filterBy = {} }) { + onRefreshEditionList({ pieceId, filterBy }) { + const pieceEditionList = this.editionList[pieceId]; + // It may happen that the user enters the site logged in already // through /editions // If he then tries to delete a piece/edition and this method is called, // we'll not be able to refresh his edition list since its not yet there. // Therefore we can just return, since there is no data to be refreshed - if(!this.editionList[pieceId]) { + if (!this.editionList[pieceId]) { return; } - let prevEditionListLength = this.editionList[pieceId].length; - let prevEditionListPage = this.editionList[pieceId].page; - let prevEditionListPageSize = this.editionList[pieceId].pageSize; - - // we can also refresh the edition list using filterBy, - // if we decide not to do that then the old filter will just be applied. - if(filterBy && Object.keys(filterBy).length <= 0) { - filterBy = this.editionList[pieceId].filterBy; - prevEditionListLength = 10; - prevEditionListPage = 1; - prevEditionListPageSize = 10; + if (typeof filterBy !== 'object') { + filterBy = pieceEditionList.filterBy; } + const { maxSeen, orderAsc, orderBy, pageSize } = pieceEditionList; + // to clear an array, david walsh recommends to just set it's length to zero // http://davidwalsh.name/empty-array - this.editionList[pieceId].length = 0; + pieceEditionList.length = 0; - // refetch editions with adjusted page size - EditionsListActions.fetchEditionList(pieceId, 1, prevEditionListLength, - this.editionList[pieceId].orderBy, - this.editionList[pieceId].orderAsc, - filterBy) - .then(() => { - // reset back to the normal pageSize and page - this.editionList[pieceId].page = prevEditionListPage; - this.editionList[pieceId].pageSize = prevEditionListPageSize; + EditionsListActions + .fetchEditionList({ + pieceId, + pageSize, + orderBy, + orderAsc, + filterBy, + maxSeen, + page: 1 }) - .catch((err) => { - console.logGlobal(err); - }); + .catch(console.logGlobal); } onSelectEdition({ pieceId, editionId, toValue }) { this.editionList[pieceId].forEach((edition) => { // Taken from: http://stackoverflow.com/a/519157/1263876 - if(typeof toValue !== 'undefined' && edition.id === editionId) { + if (typeof toValue !== 'undefined' && edition.id === editionId) { edition.selected = toValue; } else if(edition.id === editionId) { if(edition.selected) { @@ -132,7 +150,6 @@ class EditionListStore { } onToggleEditionList(pieceId) { - this.isEditionListOpenForPieceId[pieceId] = { show: this.isEditionListOpenForPieceId[pieceId] ? !this.isEditionListOpenForPieceId[pieceId].show : true }; @@ -141,7 +158,7 @@ class EditionListStore { // the merge fails, as the edition list is not refreshed when closed. // Therefore in the case of a filter application when closed, we need to reload the // edition list - if(!this.isEditionListOpenForPieceId[pieceId].show) { + if (!this.isEditionListOpenForPieceId[pieceId].show) { // to clear an array, david walsh recommends to just set it's length to zero // http://davidwalsh.name/empty-array diff --git a/js/utils/store_utils.js b/js/utils/store_utils.js deleted file mode 100644 index ef78619f..00000000 --- a/js/utils/store_utils.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict' - -export function onChangeOnce(component, store) { - const onChange = (state) => { - component.setState(state); - store.unlisten(onChange); - }; - - store.listen(onChange); -} diff --git a/package.json b/package.json index ee1456fb..c961e9c3 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "browserify": "^9.0.8", "browserify-shim": "^3.8.10", "camelcase": "^1.2.1", + "classlist-polyfill": "^1.0.2", "classnames": "^1.2.2", "compression": "^1.4.4", "decamelize": "^1.1.1", diff --git a/sass/ascribe_custom_style.scss b/sass/ascribe_custom_style.scss index 96b97783..55fe2868 100644 --- a/sass/ascribe_custom_style.scss +++ b/sass/ascribe_custom_style.scss @@ -447,9 +447,9 @@ fieldset[disabled] .btn-secondary.active { } .search-bar > .form-group > .input-group { - border: 1px solid rgba(0, 0, 0, 0); + border: 1px solid $ascribe-blue-border; &:hover, &:focus, &:active { - border-color: $ascribe-blue-border; + border-color: $ascribe-blue-border-active; } input::-webkit-input-placeholder { color: rgba($ascribe-dark-blue, 0.5); diff --git a/sass/ascribe_variables.scss b/sass/ascribe_variables.scss index c3263f4d..cdff14da 100644 --- a/sass/ascribe_variables.scss +++ b/sass/ascribe_variables.scss @@ -5,6 +5,7 @@ $ascribe-light-blue: #D3DEE4; $ascribe-white: #F8F8F8; $ascribe-pink: #D10074; $ascribe-blue-border: rgba(0, 60, 105, 0.1); +$ascribe-blue-border-active: rgba(0, 60, 105, 0.2); $ascribe-red-error: rgb(169, 68, 66);