diff --git a/.gitignore b/.gitignore index 060ca6e9..2ad05012 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,6 @@ npm-debug.log electron/dist electron/pub -/.idea +**/.idea /config.json /src/component-index.js diff --git a/README.md b/README.md index d4b778b9..27138203 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ released version of Riot: 1. Enter the URL into your browser and log into Riot! Releases are signed by PGP, and can be checked against the public key -at https://riot.im/packages/keys/riot-master.asc +at https://riot.im/packages/keys/riot.asc Note that Chrome does not allow microphone or webcam access for sites served over http (except localhost), so for working VoIP you will need to serve Riot @@ -62,7 +62,7 @@ to build. 1. If you're using the `develop` branch, install the develop versions of the dependencies, as the released ones will be too old: ``` - scripts/fetch-develop-deps.sh + scripts/fetch-develop.deps.sh ``` Whenever you git pull on riot-web you will also probably need to force an update to these dependencies - the simplest way is to re-run the script, but you can also diff --git a/electron_app/src/electron-main.js b/electron_app/src/electron-main.js index 99e14b74..ce5ac384 100644 --- a/electron_app/src/electron-main.js +++ b/electron_app/src/electron-main.js @@ -228,6 +228,17 @@ electron.app.on('ready', () => { } }); + if (process.platform === 'win32') { + // Handle forward/backward mouse buttons in Windows + mainWindow.on('app-command', (e, cmd) => { + if (cmd === 'browser-backward' && mainWindow.webContents.canGoBack()) { + mainWindow.webContents.goBack(); + } else if (cmd === 'browser-forward' && mainWindow.webContents.canGoForward()) { + mainWindow.webContents.goForward(); + } + }); + } + webContentsHandler(mainWindow.webContents); mainWindowState.manage(mainWindow); }); diff --git a/karma.conf.js b/karma.conf.js index d834987e..3b415b1a 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -84,13 +84,23 @@ module.exports = function (config) { // available preprocessors: // https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { - '{src,test}/**/*.js': ['webpack'], + '{src,test}/**/*.js': ['webpack', 'sourcemap'], }, // test results reporter to use - // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['progress', 'junit'], + reporters: ['logcapture', 'spec', 'junit', 'summary'], + + specReporter: { + suppressErrorSummary: false, // do print error summary + suppressFailed: false, // do print information about failed tests + suppressPassed: false, // do print information about passed tests + showSpecTiming: true, // print the time elapsed for each spec + }, + + client: { + captureLogs: true, + }, // web server port port: 9876, diff --git a/package.json b/package.json index 2b1d6e82..bb35ce89 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "build:res": "node scripts/copy-res.js", "build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js", "build:compile": "npm run reskindex && babel --source-maps -d lib src", - "build:bundle": "cross-env NODE_ENV=production webpack -p --progress", - "build:bundle:dev": "webpack --optimize-occurence-order --progress", + "build:bundle": "cross-env NODE_ENV=production webpack -p --progress --bail", + "build:bundle:dev": "webpack --optimize-occurence-order --progress --bail", "build:electron": "npm run clean && npm run build && npm run install:electron && build -wml --ia32 --x64", "build": "npm run reskindex && npm run build:res && npm run build:bundle", "build:dev": "npm run reskindex && npm run build:res && npm run build:bundle:dev", @@ -57,7 +57,7 @@ "bluebird": "^3.5.0", "browser-request": "^0.3.3", "classnames": "^2.1.2", - "draft-js": "^0.8.1", + "draft-js": "^0.11.0-alpha", "extract-text-webpack-plugin": "^0.9.1", "favico.js": "^0.3.10", "filesize": "3.5.6", @@ -114,11 +114,15 @@ "fs-extra": "^0.30.0", "html-webpack-plugin": "^2.24.0", "json-loader": "^0.5.3", - "karma": "^0.13.22", + "karma": "^1.7.0", "karma-chrome-launcher": "^0.2.3", "karma-cli": "^0.1.2", "karma-junit-reporter": "^0.4.1", + "karma-logcapture-reporter": "0.0.1", "karma-mocha": "^0.2.2", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "0.0.31", + "karma-summary-reporter": "^1.3.3", "karma-webpack": "^1.7.0", "matrix-mock-request": "^1.2.0", "matrix-react-test-utils": "^0.2.0", diff --git a/scripts/fetch-develop.deps.sh b/scripts/fetch-develop.deps.sh index 4fa1a4a2..e2d40341 100755 --- a/scripts/fetch-develop.deps.sh +++ b/scripts/fetch-develop.deps.sh @@ -49,42 +49,47 @@ function dodep() { [ "$curbranch" != 'develop' ] && clone $org $repo develop } || return $? - ( - cd $repo - echo "$repo set to branch "`git rev-parse --abbrev-ref HEAD` - ) + echo "$repo set to branch "`git -C "$repo" rev-parse --abbrev-ref HEAD` mkdir -p node_modules rm -r "node_modules/$repo" 2>/dev/null || true ln -sv "../$repo" node_modules/ + + ( + cd $repo + npm install + ) } +############################## + echo -en 'travis_fold:start:matrix-js-sdk\r' echo 'Setting up matrix-js-sdk' dodep matrix-org matrix-js-sdk -( - cd node_modules/matrix-js-sdk - npm install -) echo -en 'travis_fold:end:matrix-js-sdk\r' +############################## + echo -en 'travis_fold:start:matrix-react-sdk\r' echo 'Setting up matrix-react-sdk' dodep matrix-org matrix-react-sdk -mkdir -p node_modules/matrix-react-sdk/node_modules +# replace the version of js-sdk that got pulled into react-sdk with a symlink +# to our version. Make sure to do this *after* doing 'npm i' in react-sdk, +# otherwise npm helpfully moves another-json from matrix-js-sdk/node_modules +# into matrix-react-sdk/node_modules. +# +# (note this matches the instructions in the README.) +rm -r node_modules/matrix-react-sdk/node_modules/matrix-js-sdk ln -s ../../matrix-js-sdk node_modules/matrix-react-sdk/node_modules/ -( - cd node_modules/matrix-react-sdk - npm install -) - echo -en 'travis_fold:end:matrix-react-sdk\r' +############################## + # Link the reskindex binary in place: if we used npm link, # npm would do this for us, but we don't because we'd have # to define the npm prefix somewhere so it could put the diff --git a/scripts/jenkins.sh b/scripts/jenkins.sh index 4f2e9405..7b5b4c8e 100755 --- a/scripts/jenkins.sh +++ b/scripts/jenkins.sh @@ -8,8 +8,11 @@ nvm use 6 set -x -# check out corresponding branches of dependencies -`dirname $0`/fetch-develop.deps.sh +# check out corresponding branches of dependencies. +# +# clone the deps with depth 1: we know we will only ever need that one +# commit. +`dirname $0`/fetch-develop.deps.sh --depth 1 npm install diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index ea3aa5a2..cd9ac565 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -82,7 +82,7 @@ module.exports = React.createClass({ return; } const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to get protocol list from Home Server', '', ErrorDialog, { title: _t('Failed to get protocol list from Home Server'), description: _t('The Home Server may be too old to support third party networks'), }); @@ -178,7 +178,7 @@ module.exports = React.createClass({ this.setState({ loading: false }); console.error("Failed to get publicRooms: %s", JSON.stringify(err)); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to get public room list', '', ErrorDialog, { title: _t('Failed to get public room list'), description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')) }); @@ -206,7 +206,7 @@ module.exports = React.createClass({ desc = _t('Remove %(name)s from the directory?', {name: name}); } - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Remove from Directory', '', QuestionDialog, { title: _t('Remove from Directory'), description: desc, onFinished: (should_delete) => { @@ -227,7 +227,7 @@ module.exports = React.createClass({ modal.close(); this.refreshRoomList(); console.error("Failed to " + step + ": " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, { title: _t('Error'), description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')) }); @@ -316,7 +316,7 @@ module.exports = React.createClass({ const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null; if (!fields) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, { title: _t('Unable to join network'), description: _t('Riot does not know how to join a room on this network'), }); @@ -327,14 +327,14 @@ module.exports = React.createClass({ this.showRoomAlias(resp[0].alias); } else { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Room not found', '', ErrorDialog, { title: _t('Room not found'), description: _t('Couldn\'t find a matching Matrix room'), }); } }, (e) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, { title: _t('Fetching third party location failed'), description: _t('Unable to look up room ID from server'), }); diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 4844f0ed..7c6b1622 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -514,7 +514,7 @@ var RoomSubList = React.createClass({ }).catch(function(err) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to add tag " + self.props.tagName + " to room" + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { title: _t('Failed to add tag %(tagName)s to room', {tagName: self.props.tagName}), description: ((err && err.message) ? err.message : _t('Operation failed')), }); diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 2064cede..d6d9302f 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -72,7 +72,7 @@ module.exports = React.createClass({ onViewSourceClick: function() { const ViewSource = sdk.getComponent('structures.ViewSource'); - Modal.createDialog(ViewSource, { + Modal.createTrackedDialog('View Event Source', '', ViewSource, { content: this.props.mxEvent.event, }, 'mx_Dialog_viewsource'); this.closeMenu(); @@ -80,7 +80,7 @@ module.exports = React.createClass({ onViewClearSourceClick: function() { const ViewSource = sdk.getComponent('structures.ViewSource'); - Modal.createDialog(ViewSource, { + Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, { // FIXME: _clearEvent is private content: this.props.mxEvent._clearEvent, }, 'mx_Dialog_viewsource'); @@ -89,7 +89,7 @@ module.exports = React.createClass({ onRedactClick: function() { const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog"); - Modal.createDialog(ConfirmRedactDialog, { + Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, { onFinished: (proceed) => { if (!proceed) return; @@ -99,7 +99,7 @@ module.exports = React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); // display error message stating you couldn't delete this. const code = e.errcode || e.statusCode; - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, { title: _t('Error'), description: _t('You cannot delete this message. (%(code)s)', {code: code}) }); @@ -134,10 +134,9 @@ module.exports = React.createClass({ }, onQuoteClick: function() { - console.log(this.props.mxEvent); dis.dispatch({ action: 'quote', - event: this.props.mxEvent, + text: this.props.eventTileOps.getInnerText(), }); this.closeMenu(); }, @@ -153,6 +152,7 @@ module.exports = React.createClass({ let unhidePreviewButton; let permalinkButton; let externalURLButton; + let quoteButton; if (eventStatus === 'not_sent') { resendButton = ( @@ -221,11 +221,13 @@ module.exports = React.createClass({ ); - const quoteButton = ( -
{releaseNotes}, button: _t("Update"), @@ -54,7 +54,7 @@ export default React.createClass({ displayChangelog: function() { const ChangelogDialog = sdk.getComponent('dialogs.ChangelogDialog'); - Modal.createDialog(ChangelogDialog, { + Modal.createTrackedDialog('Display Changelog', '', ChangelogDialog, { version: this.props.version, newVersion: this.props.newVersion, onFinished: (update) => { diff --git a/src/components/views/globals/PasswordNagBar.js b/src/components/views/globals/PasswordNagBar.js index 873cefe1..bf71fa2c 100644 --- a/src/components/views/globals/PasswordNagBar.js +++ b/src/components/views/globals/PasswordNagBar.js @@ -25,7 +25,7 @@ import { _t, _tJsx } from 'matrix-react-sdk/lib/languageHandler'; export default React.createClass({ onUpdateClicked: function() { const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog'); - Modal.createDialog(SetPasswordDialog, { + Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog, { onFinished: (passwordChanged) => { if (!passwordChanged) { return; diff --git a/src/components/views/rooms/DNDRoomTile.js b/src/components/views/rooms/DNDRoomTile.js index cd5e54f0..430906d2 100644 --- a/src/components/views/rooms/DNDRoomTile.js +++ b/src/components/views/rooms/DNDRoomTile.js @@ -89,7 +89,7 @@ var roomTileSource = { }, (err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to set direct chat tag " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { title: _t('Failed to set direct chat tag'), description: ((err && err.message) ? err.message : _t('Operation failed')), }); @@ -114,7 +114,7 @@ var roomTileSource = { }).catch(function(err) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to remove tag " + prevTag + " from room: " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { title: _t('Failed to remove tag %(tagName)s from room', {tagName: prevTag}), description: ((err && err.message) ? err.message : _t('Operation failed')), }); @@ -133,7 +133,7 @@ var roomTileSource = { MatrixClientPeg.get().setRoomTag(item.room.roomId, newTag, newOrder).catch(function(err) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to add tag " + newTag + " to room: " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), description: ((err && err.message) ? err.message : _t('Operation failed')), }); diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 451a28b8..a58b5d58 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -131,7 +131,7 @@ module.exports = React.createClass({ this._refreshFromServer(); }, (error) => { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Error saving email notification preferences', '', ErrorDialog, { title: _t('Error saving email notification preferences'), description: _t('An error occurred whilst saving your email notification preferences.'), }); @@ -175,7 +175,7 @@ module.exports = React.createClass({ } var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); - Modal.createDialog(TextInputDialog, { + Modal.createTrackedDialog('Keywords Dialog', '', TextInputDialog, { title: _t('Keywords'), description: _t('Enter keywords separated by a comma:'), button: _t('OK'), @@ -241,7 +241,7 @@ module.exports = React.createClass({ }, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to change settings: " + error); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to change settings', '', ErrorDialog, { title: _t('Failed to change settings'), description: ((error && error.message) ? error.message : _t('Operation failed')), onFinished: self._refreshFromServer @@ -311,7 +311,7 @@ module.exports = React.createClass({ }, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Can't update user notification settings: " + error); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Can\'t update user notifcation settings', '', ErrorDialog, { title: _t('Can\'t update user notification settings'), description: ((error && error.message) ? error.message : _t('Operation failed')), onFinished: self._refreshFromServer @@ -353,7 +353,7 @@ module.exports = React.createClass({ var onError = function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to update keywords: " + error); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to update keywords', '', ErrorDialog, { title: _t('Failed to update keywords'), description: ((error && error.message) ? error.message : _t('Operation failed')), onFinished: self._refreshFromServer diff --git a/src/skins/vector/css/matrix-react-sdk/structures/login/_Login.scss b/src/skins/vector/css/matrix-react-sdk/structures/login/_Login.scss index 805c817a..7faab6e7 100644 --- a/src/skins/vector/css/matrix-react-sdk/structures/login/_Login.scss +++ b/src/skins/vector/css/matrix-react-sdk/structures/login/_Login.scss @@ -265,3 +265,16 @@ limitations under the License. margin: 3px; vertical-align: top; } + +.mx_Login_language { + margin-left: auto; + margin-right: auto; + min-width: 60%; +} + +.mx_Login_language_div { + display: flex; + margin-top: 12px; + margin-bottom: 12px; +} + diff --git a/src/skins/vector/css/matrix-react-sdk/views/avatars/_BaseAvatar.scss b/src/skins/vector/css/matrix-react-sdk/views/avatars/_BaseAvatar.scss index 106f0cde..320efda0 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/avatars/_BaseAvatar.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/avatars/_BaseAvatar.scss @@ -20,6 +20,7 @@ limitations under the License. .mx_BaseAvatar_initial { position: absolute; + left: 0px; color: $avatar-initial-color; text-align: center; speak: none; diff --git a/src/skins/vector/css/matrix-react-sdk/views/elements/_MemberEventListSummary.scss b/src/skins/vector/css/matrix-react-sdk/views/elements/_MemberEventListSummary.scss index 1025052b..02ecb5d8 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/elements/_MemberEventListSummary.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/elements/_MemberEventListSummary.scss @@ -50,11 +50,22 @@ limitations under the License. } .mx_MatrixChat_useCompactLayout { + .mx_MemberEventListSummary { + font-size: 13px; + .mx_EventTile_line { + line-height: 20px; + } + } + .mx_MemberEventListSummary_line { line-height: 22px; } .mx_MemberEventListSummary_toggle { - margin-top: 2px; + margin-top: 3px; + } + + .mx_TextualEvent.mx_MemberEventListSummary_summary { + font-size: 13px; } } diff --git a/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss b/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss index 55faab84..8825c98e 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss @@ -8,6 +8,7 @@ display: inline-block; height: 20px; line-height: 20px; + padding-left: 5px; } /* More specific to override `.markdown-body a` color */ @@ -35,9 +36,8 @@ .mx_UserPill .mx_BaseAvatar, .mx_RoomPill .mx_BaseAvatar { position: relative; + left: -3px; top: 2px; - margin-left: 2px; - margin-right: 2px; } .mx_Markdown_BOLD { diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss index 73c59c6e..75064d93 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss @@ -188,7 +188,7 @@ form.mx_Custom_Widget_Form div { } .mx_AppPermissionWarningImage img { - width: 150px; + width: 100px; } .mx_AppPermissionWarningText { @@ -199,6 +199,7 @@ form.mx_Custom_Widget_Form div { .mx_AppPermissionWarningTextLabel { font-weight: bold; + display: block; } .mx_AppPermissionWarningTextURL { diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_EventTile.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_EventTile.scss index 57d4a180..e9c62d3a 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_EventTile.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_EventTile.scss @@ -403,7 +403,15 @@ limitations under the License. } .mx_EventTile.mx_EventTile_info { + // same as the padding for non-compact .mx_EventTile.mx_EventTile_info padding-top: 0px; + font-size: 13px; + .mx_EventTile_line { + line-height: 20px; + } + .mx_EventTile_avatar { + top: 4px; + } } .mx_EventTile .mx_SenderProfile { @@ -411,6 +419,7 @@ limitations under the License. } .mx_EventTile.mx_EventTile_emote { + // add a bit more space for emotes so that avatars don't collide padding-top: 8px; .mx_EventTile_avatar { top: 2px; @@ -438,10 +447,6 @@ limitations under the License. top: 2px; } - .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { - top: 5px; - } - .mx_EventTile_e2eIcon { top: 7px; } @@ -455,12 +460,21 @@ limitations under the License. } .mx_EventTile_continuation .mx_EventTile_readAvatars, - .mx_EventTile_info .mx_EventTile_readAvatars, .mx_EventTile_emote .mx_EventTile_readAvatars { top: 5px; } + .mx_EventTile_info .mx_EventTile_readAvatars { + top: 4px; + } + .mx_RoomView_MessageList h2 { margin-top: 6px; } + + .mx_EventTile_content .markdown-body { + p, ul, ol, dl, blockquote, pre, table { + margin-bottom: 4px; // 1/4 of the non-compact margin-bottom + } + } }