Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/marius-wieschollek/passwords-webextension.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorM. Wieschollek <passwords.public@mdns.eu>2021-02-28 12:50:11 +0300
committerGitHub <noreply@github.com>2021-02-28 12:50:11 +0300
commit2d04bf0f1f483b144e7e65a8ed8ef2b429916641 (patch)
tree53cb91195e9d1879964fb409c4c3eb3d9cd02de0
parentf37008c8d32c71bf6fb9a13b1bc0e49d23109a0a (diff)
parent1792c3e7f7d3b04b02cb8550c48927ccdda18a49 (diff)
Merge pull request #151 from flo-mic/master
Introduce custom settings for password recommendations
-rw-r--r--src/js/Controller/Setting/Get.js4
-rw-r--r--src/js/Controller/Setting/Reset.js4
-rw-r--r--src/js/Controller/Setting/Set.js24
-rw-r--r--src/js/Manager/MiningManager.js8
-rw-r--r--src/js/Manager/RecommendationManager.js60
-rw-r--r--src/js/Search/Query/Field/FieldFactory.js16
-rw-r--r--src/js/Search/Query/Field/FieldStartsWith.js42
-rw-r--r--src/js/Settings/MasterSettingsProvider.js38
-rw-r--r--src/platform/generic/_locales/de/messages.json42
-rw-r--r--src/platform/generic/_locales/en/messages.json42
-rw-r--r--src/vue/Components/Options/Settings.vue78
11 files changed, 325 insertions, 33 deletions
diff --git a/src/js/Controller/Setting/Get.js b/src/js/Controller/Setting/Get.js
index 9d9cc63..cea64a7 100644
--- a/src/js/Controller/Setting/Get.js
+++ b/src/js/Controller/Setting/Get.js
@@ -20,7 +20,9 @@ export default class Get extends AbstractController {
'server.default',
'theme.current',
'theme.custom',
- 'debug.localisation.enabled'
+ 'debug.localisation.enabled',
+ 'search.recommendation.mode',
+ 'search.recommendation.maxRows'
];
}
diff --git a/src/js/Controller/Setting/Reset.js b/src/js/Controller/Setting/Reset.js
index bd64e4e..3d05c9c 100644
--- a/src/js/Controller/Setting/Reset.js
+++ b/src/js/Controller/Setting/Reset.js
@@ -17,7 +17,9 @@ export default class Reset extends AbstractController {
'server.default',
'theme.current',
'theme.custom',
- 'debug.localisation.enabled'
+ 'debug.localisation.enabled',
+ 'search.recommendation.mode',
+ 'search.recommendation.maxRows'
];
}
diff --git a/src/js/Controller/Setting/Set.js b/src/js/Controller/Setting/Set.js
index 6e211c8..7522560 100644
--- a/src/js/Controller/Setting/Set.js
+++ b/src/js/Controller/Setting/Set.js
@@ -36,6 +36,10 @@ export default class Set extends AbstractController {
await this._setDefaultServer(value);
} else if(setting === 'theme.current') {
await this._setCurrentTheme(value);
+ } else if(setting === 'search.recommendation.mode') {
+ await this._setSearchRecommendationMode(value);
+ } else if(setting === 'search.recommendation.maxRows') {
+ await this._setSearchRecommendationMaxRows(Number(value));
} else if(this._booleanSettings.indexOf(setting) !== -1) {
await this._setBoolean(setting, value);
} else {
@@ -83,6 +87,26 @@ export default class Set extends AbstractController {
/**
*
+ * @param {String} value
+ * @return {Promise<void>}
+ * @private
+ */
+ async _setSearchRecommendationMode(value) {
+ await SettingsService.set('search.recommendation.mode', value);
+ }
+
+ /**
+ *
+ * @param {Number} value
+ * @return {Promise<void>}
+ * @private
+ */
+ async _setSearchRecommendationMaxRows(value) {
+ await SettingsService.set('search.recommendation.maxRows', value);
+ }
+
+ /**
+ *
* @param {String} setting
* @param {Boolean} value
* @return {Promise<void>}
diff --git a/src/js/Manager/MiningManager.js b/src/js/Manager/MiningManager.js
index 8647e12..ccb2eaf 100644
--- a/src/js/Manager/MiningManager.js
+++ b/src/js/Manager/MiningManager.js
@@ -3,6 +3,7 @@ import MiningItem from '@js/Models/Queue/MiningItem';
import ServerManager from '@js/Manager/ServerManager';
import ErrorManager from '@js/Manager/ErrorManager';
import TabManager from '@js/Manager/TabManager';
+import RecommendationManager from '@js/Manager/RecommendationManager';
import SearchQuery from '@js/Search/Query/SearchQuery';
import SearchIndex from '@js/Search/Index/SearchIndex';
import NotificationService from '@js/Services/NotificationService';
@@ -93,6 +94,8 @@ class MiningManager {
if(basePassword !== null) {
task.setTaskField('id', basePassword.getId())
.setTaskField('label', basePassword.getLabel())
+ .setTaskField('url', basePassword.getUrl())
+ .setTaskField('hidden', basePassword.getHidden())
.setTaskNew(false);
}
}
@@ -221,7 +224,8 @@ class MiningManager {
items = query
.where(
query.field('password').equals(data.password.value),
- query.field('username').equals(data.user.value)
+ query.field('username').equals(data.user.value),
+ RecommendationManager.getFilterQuery(query, Url(data.url))
)
.type('password')
.hidden(tab.tab.incognito)
@@ -258,7 +262,7 @@ class MiningManager {
items = query
.where(
query.field('username').equals(data.user.value),
- query.field('host').contains(url.host)
+ RecommendationManager.getFilterQuery(query, Url(data.url))
)
.type('password')
.hidden(tab.tab.incognito)
diff --git a/src/js/Manager/RecommendationManager.js b/src/js/Manager/RecommendationManager.js
index 3b9af73..018f4b8 100644
--- a/src/js/Manager/RecommendationManager.js
+++ b/src/js/Manager/RecommendationManager.js
@@ -3,6 +3,7 @@ import Url from 'url-parse';
import TabManager from '@js/Manager/TabManager';
import SearchIndex from '@js/Search/Index/SearchIndex';
import EventQueue from '@js/Event/EventQueue';
+import SettingsService from '@js/Services/SettingsService';
class RecommendationManager {
@@ -34,6 +35,23 @@ class RecommendationManager {
TabManager.tabChanged.on(this._tabEvent);
TabManager.urlChanged.on(this._tabEvent);
SearchIndex.listen.on(this._searchEvent);
+ this.initRecommendationOptions();
+ }
+
+ initRecommendationOptions() {
+ this.options = { searchQuery: "host", maxResults: 8 }
+ SettingsService.getValue('search.recommendation.mode')
+ .then((value) => {
+ if(value) {
+ this.options.searchQuery = value;
+ }
+ });
+ SettingsService.getValue('search.recommendation.maxRows')
+ .then((value) => {
+ if(value) {
+ this.options.maxRows = Number(value);
+ }
+ });
}
/**
@@ -65,13 +83,10 @@ class RecommendationManager {
let query = new SearchQuery('or');
query
- .where(
- query.field('host').equals(url.host),
- query.field('url').contains(url.pathname.length > 1 ? url.host + url.pathname:url.host)
- )
+ .where(this.getFilterQuery(query, url))
.type('password')
.score(0.3)
- .limit(8)
+ .limit(this.options.maxRows)
.sortBy('favorite')
.sortBy('uses')
.sortBy('shared')
@@ -84,6 +99,39 @@ class RecommendationManager {
}
/**
+ * @param {SearchQuery} query
+ * @param {String} url
+ */
+ getFilterQuery(query, url) {
+ let mode = this.options.searchQuery;
+ if(mode === 'domain') {
+ return query.field('host').contains(this.getSearchDomainFromHost(url.host));
+ } else if(mode === 'host') {
+ return query.field('host').startsWith(url.host.split(':')[0]);
+ } else if(mode === 'hostport') {
+ return query.field('host').equals(url.host);
+ } else {
+ return query.field('url').equals(url.protocol + "//" + url.host + (url.pathname.length > 1 ? url.pathname: ""));
+ }
+ }
+
+
+ /**
+ * @param {String} host
+ */
+ getSearchDomainFromHost(host) {
+ var regIPAdress = /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/;
+ if (regIPAdress.test(host)) {
+ return host;
+ }
+ else {
+ var domain = host.split(':')[0];
+ if(domain === domain.split('.')[0]) return domain;
+ return domain.split('.').reverse()[1] + "." + domain.split('.').reverse()[0];
+ }
+ }
+
+ /**
*
* @param {Object} tab
* @private
@@ -113,4 +161,4 @@ class RecommendationManager {
}
}
-export default new RecommendationManager(); \ No newline at end of file
+export default new RecommendationManager();
diff --git a/src/js/Search/Query/Field/FieldFactory.js b/src/js/Search/Query/Field/FieldFactory.js
index a37263d..fbece20 100644
--- a/src/js/Search/Query/Field/FieldFactory.js
+++ b/src/js/Search/Query/Field/FieldFactory.js
@@ -1,6 +1,7 @@
import FieldEquals from '@js/Search/Query/Field/FieldEquals';
import FieldContains from '@js/Search/Query/Field/FieldContains';
import FieldMatches from '@js/Search/Query/Field/FieldMatches';
+import FieldStartsWith from '@js/Search/Query/Field/FieldStartsWith';
import FieldIn from '@js/Search/Query/Field/FieldIn';
import FieldNotEquals from '@js/Search/Query/Field/FieldNotEquals';
import FieldNotIn from '@js/Search/Query/Field/FieldNotIn';
@@ -61,6 +62,21 @@ export default class FieldFactory {
/**
*
+ * @param {String} value
+ * @param {Number} [weight=null]
+ * @param {String} [name=null]
+ * @return {FieldContains}
+ */
+ startsWith(value, weight = null, name = null) {
+ if(!name) {
+ name = this._name;
+ }
+
+ return new FieldStartsWith(name, value, weight);
+ }
+
+ /**
+ *
* @param {String[]} value
* @param {String} [name=null]
* @return {FieldIn}
diff --git a/src/js/Search/Query/Field/FieldStartsWith.js b/src/js/Search/Query/Field/FieldStartsWith.js
new file mode 100644
index 0000000..c9c5b71
--- /dev/null
+++ b/src/js/Search/Query/Field/FieldStartsWith.js
@@ -0,0 +1,42 @@
+import AbstractField from '@js/Search/Query/Field/AbstractField';
+
+export default class FieldStartsWith extends AbstractField {
+
+ /**
+ *
+ * @param {String} name
+ * @param {String} value
+ * @param {Number} weight
+ */
+ constructor(name, value, weight = 1) {
+ super(name, value);
+
+ if(weight <= 0) weight = 1;
+ this._weight = weight;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ evaluate(item) {
+ let values = this._getFieldValues(item);
+
+ if(!values) return {passed: false};
+
+ let search = this._value.toLowerCase(),
+ checks = 0,
+ matches = 0;
+
+ for(let value of values) {
+ checks++;
+ if(value.startsWith(search)) {
+ matches += value.split(search).length - 1;
+ }
+ }
+
+ if(this._weight !== 1) matches *= this._weight;
+ if(matches > 0) return {matches, checks, passed: true};
+
+ return {checks, passed: false};
+ }
+} \ No newline at end of file
diff --git a/src/js/Settings/MasterSettingsProvider.js b/src/js/Settings/MasterSettingsProvider.js
index bd68303..05c9b02 100644
--- a/src/js/Settings/MasterSettingsProvider.js
+++ b/src/js/Settings/MasterSettingsProvider.js
@@ -76,22 +76,32 @@ class MasterSettingsProvider {
],
'debug.localisation.enabled' : [
'local.localisation.enabled'
- ]
+ ],
+ 'search.recommendation.maxRows': [
+ 'sync.search.recommendation.maxRows',
+ 'local.search.recommendation.maxRows'
+ ],
+ 'search.recommendation.mode': [
+ 'sync.search.recommendation.mode',
+ 'local.search.recommendation.mode'
+ ],
};
this._defaults = {
- 'theme.custom' : null,
- 'theme.current' : 'light',
- 'server.default' : null,
- 'paste.popup.close' : true,
- 'paste.form.submit' : true,
- 'paste.compromised.warning' : true,
- 'paste.autofill' : false,
- 'paste.basic-auth' : false,
- 'popup.related.search' : true,
- 'password.folder.private' : null,
- 'notification.password.new' : true,
- 'notification.password.update': true,
- 'debug.localisation.enabled' : true
+ 'theme.custom' : null,
+ 'theme.current' : 'light',
+ 'server.default' : null,
+ 'paste.popup.close' : true,
+ 'paste.form.submit' : true,
+ 'paste.compromised.warning' : true,
+ 'paste.autofill' : false,
+ 'paste.basic-auth' : false,
+ 'popup.related.search' : true,
+ 'password.folder.private' : null,
+ 'notification.password.new' : true,
+ 'notification.password.update' : true,
+ 'debug.localisation.enabled' : true,
+ 'search.recommendation.mode' : 'host',
+ 'search.recommendation.maxRows': 8
};
}
diff --git a/src/platform/generic/_locales/de/messages.json b/src/platform/generic/_locales/de/messages.json
index cdf0d4c..61c00be 100644
--- a/src/platform/generic/_locales/de/messages.json
+++ b/src/platform/generic/_locales/de/messages.json
@@ -1176,5 +1176,47 @@
"DebugNoErrors" : {
"message" : "Keine Fehlerberichte vorhanden",
"description": "Message shown when no error reports exist instead of the error logs in the error logs section in the extension settings in the debug tab"
+ },
+ "RecommendationSettings" : {
+ "message" : "Passwort Empfehlung",
+ "description": "Label of the section password recommendations in the extension settings."
+ },
+ "RecommendationSettingsHelp" : {
+ "message" : "Beim Ändern dieser Einstellungen muss die Erweiterung neu gestartet werden. Entweder durch deaktivieren und aktivieren der Erweiterung oder mit einem Browser Neustart.",
+ "description": "Label of the help text for section password recommendations in the extension settings."
+ },
+ "SettingsSearchRecommendationOption" : {
+ "message" : "Suche Passwort Empfehlung auf Basis von dieser Option.",
+ "description": "Label of the setting in the extension settings to define how password recommendations are searched."
+ },
+ "LabelSearchRecommendationDomain" : {
+ "message" : "Domain",
+ "description": "Find password recommendations by domain. So on page mail.example.com you will see all passwords for the domain and sumdomains of example.com."
+ },
+ "LabelSearchRecommendationHost" : {
+ "message" : "Host",
+ "description": "Find password recommendations by host. So on page mail.example.com you will see all passwords for the sumdomain mail e.g. (mail.example.com or mail.example.com:8443)."
+ },
+ "LabelSearchRecommendationHostPort" : {
+ "message" : "Host + Port",
+ "description": "Find password recommendations for the specific server and port."
+ },
+ "LabelSearchRecommendationExact" : {
+ "message" : "Exakte URL",
+ "description": "Find only passwords where th url matches exact to the current browser url."
+ },
+ "SettingsSearchRecommendationMaxRows" : {
+ "message" : "Maximale Anzahl an Ergebnissen für die Passwort Empfehlung.",
+ "description": "Label of the setting in the extension settings to define the maximum number of results for the password recommendation."
+ },
+ "SearchRecommendationMaxRowsNumber" : {
+ "message" : "$ROW$",
+ "description" : "Number of results that should be displayed for password recommendations.",
+ "placeholders": {
+ "row": {
+ "content": "$1",
+ "example": "One of 1, 5, 10, 15 or 20"
+ }
+ }
}
} \ No newline at end of file
diff --git a/src/platform/generic/_locales/en/messages.json b/src/platform/generic/_locales/en/messages.json
index e3d3d45..c855735 100644
--- a/src/platform/generic/_locales/en/messages.json
+++ b/src/platform/generic/_locales/en/messages.json
@@ -1190,5 +1190,47 @@
"DebugNoErrors" : {
"message" : "No errors in log",
"description": "Message shown when no error reports exist instead of the error logs in the error logs section in the extension settings in the debug tab"
+ },
+ "RecommendationSettings" : {
+ "message" : "Password recommendations",
+ "description": "Label of the section password recommendations in the extension settings."
+ },
+ "RecommendationSettingsHelp" : {
+ "message" : "If you change the settings below you will need to restart the extension. This can be done by disabling/enabling the extension or with a browser restart.",
+ "description": "Label of the help text for section password recommendations in the extension settings."
+ },
+ "SettingsSearchRecommendationOption" : {
+ "message" : "Search passwords based on the following option.",
+ "description": "Label of the setting in the extension settings to define how password recommendations are searched."
+ },
+ "LabelSearchRecommendationDomain" : {
+ "message" : "Domain",
+ "description": "Find password recommendations by domain. So on page mail.example.com you will see all passwords for the domain and sumdomains of example.com."
+ },
+ "LabelSearchRecommendationHost" : {
+ "message" : "Host",
+ "description": "Find password recommendations by host. So on page mail.example.com you will see all passwords for the sumdomain mail e.g. (mail.example.com or mail.example.com:8443)."
+ },
+ "LabelSearchRecommendationHostPort" : {
+ "message" : "Host + Port",
+ "description": "Find password recommendations for the specific server and port."
+ },
+ "LabelSearchRecommendationExact" : {
+ "message" : "Exact url",
+ "description": "Find only passwords where th url matches exact to the current browser url."
+ },
+ "SettingsSearchRecommendationMaxRows" : {
+ "message" : "Maximum number of results for the password recommendation.",
+ "description": "Label of the setting in the extension settings to define the maximum number of results for the password recommendation."
+ },
+ "SearchRecommendationMaxRowsNumber" : {
+ "message" : "$ROW$",
+ "description" : "Number of results that should be displayed for password recommendations.",
+ "placeholders": {
+ "row": {
+ "content": "$1",
+ "example": "One of 1, 5, 10, 15 or 20"
+ }
+ }
}
}
diff --git a/src/vue/Components/Options/Settings.vue b/src/vue/Components/Options/Settings.vue
index f6824a5..1eade48 100644
--- a/src/vue/Components/Options/Settings.vue
+++ b/src/vue/Components/Options/Settings.vue
@@ -39,6 +39,17 @@
<slider-field id="popup-related-search" v-model="relatedSearch"/>
<translate tag="label" for="popup-related-search" say="SettingsPopupRelatedSearch"/>
</div>
+
+ <translate tag="h3" say="RecommendationSettings"/>
+ <translate tag="p" say="RecommendationSettingsHelp"/>
+ <div class="setting">
+ <translate tag="label" for="search-recommendation-option" say="SettingsSearchRecommendationOption"/>
+ <select-field id="search-recommendation-option" :options="recommendationOptions" v-model="recSearchMode"/>
+ </div>
+ <div class="setting">
+ <translate tag="label" for="search-recommendation-maxRows" say="SettingsSearchRecommendationMaxRows"/>
+ <select-field id="search-recommendation-maxRows" :options="recommendationMaxRows" v-model="recSearchRows"/>
+ </div>
</div>
</template>
@@ -48,20 +59,23 @@
import SettingsService from '@js/Services/SettingsService';
import ToastService from '@js/Services/ToastService';
import SliderField from "@vue/Components/Form/SliderField";
+ import SelectField from "@vue/Components/Form/SelectField";
import HelpText from "@vue/Components/Options/Setting/HelpText";
export default {
- components: {HelpText, SliderField, Translate},
+ components: {HelpText, SliderField, SelectField, Translate},
data() {
return {
- autoclose : false,
- autosubmit : false,
- autofill : false,
- basicAuth : false,
- compromised : false,
- notifyPwNew : false,
- relatedSearch : false,
- notifyPwUpdate: false
+ autoclose : false,
+ autosubmit : false,
+ autofill : false,
+ basicAuth : false,
+ compromised : false,
+ notifyPwNew : false,
+ relatedSearch : false,
+ notifyPwUpdate : false,
+ recSearchMode : 'host',
+ recSearchRows : 8
};
},
@@ -69,6 +83,37 @@
this.loadData();
},
+ computed: {
+ recommendationOptions() {
+ return [
+ {
+ id : 'domain',
+ label: 'LabelSearchRecommendationDomain'
+ },
+ {
+ id : 'host',
+ label: 'LabelSearchRecommendationHost'
+ },
+ {
+ id : 'hostport',
+ label: 'LabelSearchRecommendationHostPort'
+ },
+ {
+ id : 'exact',
+ label: 'LabelSearchRecommendationExact'
+ }
+ ];
+ },
+ recommendationMaxRows() {
+ var i = 1;
+ var result = [];
+ for(i =1; i <= 20; i++) {
+ result.push({id: i, label: ['SearchRecommendationMaxRowsNumber', i]});
+ }
+ return result;
+ }
+ },
+
methods: {
loadData() {
this.getSetting('paste.popup.close', 'autoclose');
@@ -79,6 +124,8 @@
this.getSetting('popup.related.search', 'relatedSearch');
this.getSetting('notification.password.new', 'notifyPwNew');
this.getSetting('notification.password.update', 'notifyPwUpdate');
+ this.getSetting('search.recommendation.mode', 'recSearchMode');
+ this.getSetting('search.recommendation.maxRows', 'recSearchRows');
},
async getSetting(name, variable) {
try {
@@ -138,6 +185,16 @@
if(oldValue !== null && value !== oldValue) {
this.setSetting('notification.password.update', value);
}
+ },
+ recSearchMode(value, oldValue) {
+ if(oldValue !== null && value !== oldValue) {
+ this.setSetting('search.recommendation.mode', value);
+ }
+ },
+ recSearchRows(value, oldValue) {
+ if(oldValue !== null && value !== oldValue) {
+ this.setSetting('search.recommendation.maxRows', value);
+ }
}
}
};
@@ -149,6 +206,9 @@
h3 {
margin : 1.5rem 1rem .5rem;
}
+ p {
+ margin : 1.5rem 1rem .5rem;
+ }
.setting {
padding : 0.5rem 1rem;