--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -259,16 +259,17 @@ devtools.jar:
skin/images/security-state-weak.svg (themes/images/security-state-weak.svg)
skin/images/diff.svg (themes/images/diff.svg)
skin/images/import.svg (themes/images/import.svg)
skin/images/pane-collapse.svg (themes/images/pane-collapse.svg)
skin/images/pane-expand.svg (themes/images/pane-expand.svg)
skin/images/help.svg (themes/images/help.svg)
skin/images/read-only.svg (themes/images/read-only.svg)
skin/images/reveal.svg (themes/images/reveal.svg)
+ skin/images/settings.svg (themes/images/settings.svg)
# Debugger
skin/images/debugger/angular.svg (themes/images/debugger/angular.svg)
skin/images/debugger/arrow.svg (themes/images/debugger/arrow.svg)
skin/images/debugger/back.svg (themes/images/debugger/back.svg)
skin/images/debugger/blackBox.svg (themes/images/debugger/blackBox.svg)
skin/images/debugger/breakpoint.svg (themes/images/debugger/breakpoint.svg)
skin/images/debugger/close.svg (themes/images/debugger/close.svg)
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -119,24 +119,16 @@ responsive.deviceAdderSave=Save
# device. %4$S is the user agent of the device. %5$S is a boolean value
# noting whether touch input is supported.
responsive.deviceDetails=Size: %1$S x %2$S\nDPR: %3$S\nUA: %4$S\nTouch: %5$S
# LOCALIZATION NOTE (responsive.devicePixelRatioOption): UI option in a menu to configure
# the device pixel ratio. %1$S is the devicePixelRatio value of the device.
responsive.devicePixelRatioOption=DPR: %1$S
-# LOCALIZATION NOTE (responsive.reloadConditions.label): Label on button to open a menu
-# used to choose whether to reload the page automatically when certain actions occur.
-responsive.reloadConditions.label=Reload when…
-
-# LOCALIZATION NOTE (responsive.reloadConditions.title): Title on button to open a menu
-# used to choose whether to reload the page automatically when certain actions occur.
-responsive.reloadConditions.title=Choose whether to reload the page automatically when certain actions occur
-
# LOCALIZATION NOTE (responsive.reloadConditions.touchSimulation): Label on checkbox used
# to select whether to reload when touch simulation is toggled.
responsive.reloadConditions.touchSimulation=Reload when touch simulation is toggled
# LOCALIZATION NOTE (responsive.reloadConditions.userAgent): Label on checkbox used
# to select whether to reload when user agent is changed.
responsive.reloadConditions.userAgent=Reload when user agent is changed
--- a/devtools/client/netmonitor/src/components/Toolbar.js
+++ b/devtools/client/netmonitor/src/components/Toolbar.js
@@ -51,17 +51,17 @@ const ENABLE_PERSISTENT_LOGS_LABEL =
L10N.getStr("netmonitor.toolbar.enablePersistentLogs.label");
const DISABLE_CACHE_TOOLTIP = L10N.getStr("netmonitor.toolbar.disableCache.tooltip");
const DISABLE_CACHE_LABEL = L10N.getStr("netmonitor.toolbar.disableCache.label");
const NO_THROTTLING_LABEL = new LocalizationHelper(
"devtools/client/locales/network-throttling.properties"
).getStr("responsive.noThrottling");
// Menu
-loader.lazyRequireGetter(this, "showMenu", "devtools/client/netmonitor/src/utils/menu", true);
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
// Throttling
const Types = require("devtools/client/shared/components/throttling/types");
const throttlingProfiles = require("devtools/client/shared/components/throttling/profiles");
const { changeNetworkThrottling } = require("devtools/client/shared/components/throttling/actions");
/**
--- a/devtools/client/netmonitor/src/utils/moz.build
+++ b/devtools/client/netmonitor/src/utils/moz.build
@@ -10,15 +10,14 @@ DIRS += [
DevToolsModules(
'filter-autocomplete-provider.js',
'filter-predicates.js',
'filter-text-utils.js',
'format-utils.js',
'headers-provider.js',
'l10n.js',
'mdn-utils.js',
- 'menu.js',
'open-request-in-tab.js',
'prefs.js',
'request-utils.js',
'sort-predicates.js',
'sort-utils.js'
)
--- a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -12,17 +12,17 @@ const {
getUrlQuery,
getUrlBaseName,
parseQueryString,
} = require("../utils/request-utils");
loader.lazyRequireGetter(this, "Curl", "devtools/client/shared/curl", true);
loader.lazyRequireGetter(this, "saveAs", "devtools/client/shared/file-saver", true);
loader.lazyRequireGetter(this, "copyString", "devtools/shared/platform/clipboard", true);
-loader.lazyRequireGetter(this, "showMenu", "devtools/client/netmonitor/src/utils/menu", true);
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
loader.lazyRequireGetter(this, "openRequestInTab", "devtools/client/netmonitor/src/utils/firefox/open-request-in-tab", true);
loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
class RequestListContextMenu {
constructor(props) {
this.props = props;
}
--- a/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
@@ -1,15 +1,15 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
+const { showMenu } = require("devtools/client/shared/components/menu/utils");
const { HEADERS } = require("../constants");
const { L10N } = require("../utils/l10n");
const stringMap = HEADERS
.filter((header) => header.hasOwnProperty("label"))
.reduce((acc, { name, label }) => Object.assign(acc, { [name]: label }), {});
const subMenuMap = HEADERS
--- a/devtools/client/netmonitor/webpack.config.js
+++ b/devtools/client/netmonitor/webpack.config.js
@@ -68,17 +68,18 @@ const webpackConfig = {
"node_modules",
],
alias: {
"Services": "devtools-modules/src/Services",
"react": path.join(__dirname, "node_modules/react"),
"devtools/client/framework/devtools": path.join(__dirname, "../../client/shared/webpack/shims/framework-devtools-shim"),
"devtools/client/framework/menu": "devtools-modules/src/menu",
- "devtools/client/netmonitor/src/utils/menu": "devtools-contextmenu",
+
+ "devtools/client/shared/components/menu/utils": "devtools-contextmenu",
"devtools/client/shared/vendor/react": "react",
"devtools/client/shared/vendor/react-dom": "react-dom",
"devtools/client/shared/vendor/react-redux": "react-redux",
"devtools/client/shared/vendor/redux": "redux",
"devtools/client/shared/vendor/reselect": "reselect",
"devtools/client/shared/vendor/jszip": "jszip",
--- a/devtools/client/responsive.html/actions/screenshot.js
+++ b/devtools/client/responsive.html/actions/screenshot.js
@@ -7,17 +7,17 @@
"use strict";
const {
TAKE_SCREENSHOT_START,
TAKE_SCREENSHOT_END,
} = require("./index");
const { getFormatStr } = require("../utils/l10n");
-const { getToplevelWindow } = require("../utils/window");
+const { getTopLevelWindow } = require("../utils/window");
const e10s = require("../utils/e10s");
const Services = require("Services");
const CAMERA_AUDIO_URL = "resource://devtools/client/themes/audio/shutter.wav";
const animationFrame = () => new Promise(resolve => {
window.requestAnimationFrame(resolve);
});
@@ -35,17 +35,17 @@ function getFileName() {
function createScreenshotFor(node) {
const mm = node.frameLoader.messageManager;
return e10s.request(mm, "RequestScreenshot");
}
function saveToFile(data, filename) {
- const chromeWindow = getToplevelWindow(window);
+ const chromeWindow = getTopLevelWindow(window);
const chromeDocument = chromeWindow.document;
// append .png extension to filename if it doesn't exist
filename = filename.replace(/\.png$|$/i, ".png");
chromeWindow.saveURL(data, filename, null,
true, true,
chromeDocument.documentURIObject, chromeDocument);
--- a/devtools/client/responsive.html/components/Browser.js
+++ b/devtools/client/responsive.html/components/Browser.js
@@ -9,17 +9,17 @@
const Services = require("Services");
const flags = require("devtools/shared/flags");
const { PureComponent } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const e10s = require("../utils/e10s");
const message = require("../utils/message");
-const { getToplevelWindow } = require("../utils/window");
+const { getTopLevelWindow } = require("../utils/window");
const FRAME_SCRIPT = "resource://devtools/client/responsive.html/browser/content.js";
class Browser extends PureComponent {
/**
* This component is not allowed to depend directly on frequently changing data (width,
* height). Any changes in props would cause the <iframe> to be removed and added again,
* throwing away the current state of the page.
@@ -109,17 +109,17 @@ class Browser extends PureComponent {
// since it still needs to do async work before the content is actually
// resized to match.
e10s.on(mm, "OnContentResize", onContentResize);
const ready = e10s.once(mm, "ChildScriptReady");
mm.loadFrameScript(FRAME_SCRIPT, true);
await ready;
- const browserWindow = getToplevelWindow(window);
+ const browserWindow = getTopLevelWindow(window);
const requiresFloatingScrollbars =
!browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
await e10s.request(mm, "Start", {
requiresFloatingScrollbars,
// Tests expect events on resize to wait for various size changes
notifyOnResize: flags.testing,
});
rename from devtools/client/responsive.html/components/ReloadConditions.js
rename to devtools/client/responsive.html/components/SettingsMenu.js
--- a/devtools/client/responsive.html/components/ReloadConditions.js
+++ b/devtools/client/responsive.html/components/SettingsMenu.js
@@ -1,50 +1,73 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const ToggleMenu = createFactory(require("./ToggleMenu"));
-
const { getStr } = require("../utils/l10n");
const Types = require("../types");
-class ReloadConditions extends PureComponent {
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
+
+class SettingsMenu extends PureComponent {
static get propTypes() {
return {
reloadConditions: PropTypes.shape(Types.reloadConditions).isRequired,
onChangeReloadCondition: PropTypes.func.isRequired,
};
}
- render() {
+ constructor(props) {
+ super(props);
+ this.onToggleSettingMenu = this.onToggleSettingMenu.bind(this);
+ }
+
+ onToggleSettingMenu(event) {
const {
reloadConditions,
onChangeReloadCondition,
} = this.props;
- return ToggleMenu({
- id: "reload-conditions-menu",
- items: [
- {
- id: "touchSimulation",
- label: getStr("responsive.reloadConditions.touchSimulation"),
- checked: reloadConditions.touchSimulation,
+ const menuItems = [
+ {
+ id: "touchSimulation",
+ checked: reloadConditions.touchSimulation,
+ label: getStr("responsive.reloadConditions.touchSimulation"),
+ type: "checkbox",
+ click: () => {
+ onChangeReloadCondition("touchSimulation", !reloadConditions.touchSimulation);
+ },
+ },
+ {
+ id: "userAgent",
+ checked: reloadConditions.userAgent,
+ label: getStr("responsive.reloadConditions.userAgent"),
+ type: "checkbox",
+ click: () => {
+ onChangeReloadCondition("userAgent", !reloadConditions.userAgent);
},
- {
- id: "userAgent",
- label: getStr("responsive.reloadConditions.userAgent"),
- checked: reloadConditions.userAgent,
- },
- ],
- label: getStr("responsive.reloadConditions.label"),
- title: getStr("responsive.reloadConditions.title"),
- onChange: onChangeReloadCondition,
+ },
+ ];
+
+ showMenu(menuItems, {
+ button: event.target,
+ useTopLevelWindow: true,
});
}
+
+ render() {
+ return (
+ dom.button({
+ id: "settings-button",
+ className: "devtools-button",
+ onClick: this.onToggleSettingMenu,
+ })
+ );
+ }
}
-module.exports = ReloadConditions;
+module.exports = SettingsMenu;
deleted file mode 100644
--- a/devtools/client/responsive.html/components/ToggleMenu.js
+++ /dev/null
@@ -1,130 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { PureComponent } = require("devtools/client/shared/vendor/react");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-
-const MenuItem = {
- id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- checked: PropTypes.bool,
-};
-
-class ToggleMenu extends PureComponent {
- static get propTypes() {
- return {
- id: PropTypes.string,
- items: PropTypes.arrayOf(PropTypes.shape(MenuItem)).isRequired,
- label: PropTypes.string,
- title: PropTypes.string,
- onChange: PropTypes.func.isRequired,
- };
- }
-
- constructor(props) {
- super(props);
-
- this.state = {
- isOpen: false,
- };
-
- this.onItemChange = this.onItemChange.bind(this);
- this.onToggleOpen = this.onToggleOpen.bind(this);
- }
-
- onItemChange({ target }) {
- const {
- onChange,
- } = this.props;
-
- // Close menu after changing an item
- this.setState({
- isOpen: false,
- });
-
- const id = target.name;
- onChange(id, target.checked);
- }
-
- onToggleOpen() {
- const {
- isOpen,
- } = this.state;
-
- this.setState({
- isOpen: !isOpen,
- });
- }
-
- render() {
- const {
- id: menuID,
- items,
- label: toggleLabel,
- title,
- } = this.props;
-
- const {
- isOpen,
- } = this.state;
-
- const {
- onItemChange,
- onToggleOpen,
- } = this;
-
- const menuItems = items.map(({ id, label, checked }) => {
- const inputID = `devtools-menu-item-${id}`;
-
- return dom.div(
- {
- className: "devtools-menu-item",
- key: id,
- },
- dom.input({
- type: "checkbox",
- id: inputID,
- name: id,
- checked,
- onChange: onItemChange,
- }),
- dom.label({
- htmlFor: inputID,
- }, label)
- );
- });
-
- let menuClass = "devtools-menu";
- if (isOpen) {
- menuClass += " opened";
- }
- const menu = dom.div(
- {
- className: menuClass,
- },
- menuItems
- );
-
- let buttonClass = "devtools-toggle-menu";
- buttonClass += " toolbar-dropdown toolbar-button devtools-button";
- if (isOpen || items.some(({ checked }) => checked)) {
- buttonClass += " selected";
- }
- return dom.div(
- {
- id: menuID,
- className: buttonClass,
- title,
- onClick: onToggleOpen,
- },
- toggleLabel,
- menu
- );
- }
-}
-
-module.exports = ToggleMenu;
--- a/devtools/client/responsive.html/components/Toolbar.js
+++ b/devtools/client/responsive.html/components/Toolbar.js
@@ -6,17 +6,17 @@
const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const DevicePixelRatioSelector = createFactory(require("./DevicePixelRatioSelector"));
const DeviceSelector = createFactory(require("./DeviceSelector"));
const NetworkThrottlingSelector = createFactory(require("devtools/client/shared/components/throttling/NetworkThrottlingSelector"));
-const ReloadConditions = createFactory(require("./ReloadConditions"));
+const SettingsMenu = createFactory(require("./SettingsMenu"));
const ViewportDimension = createFactory(require("./ViewportDimension"));
const { getStr } = require("../utils/l10n");
const Types = require("../types");
class Toolbar extends PureComponent {
static get propTypes() {
return {
@@ -121,17 +121,17 @@ class Toolbar extends PureComponent {
{ id: "toolbar-end-controls" },
dom.button({
id: "screenshot-button",
className: "toolbar-button devtools-button",
title: getStr("responsive.screenshot"),
onClick: onScreenshot,
disabled: screenshot.isCapturing,
}),
- ReloadConditions({
+ SettingsMenu({
reloadConditions,
onChangeReloadCondition,
}),
dom.div({ className: "devtools-separator" }),
dom.button({
id: "exit-button",
className: "toolbar-button devtools-button",
title: getStr("responsive.exit"),
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -6,15 +6,14 @@
DevToolsModules(
'App.js',
'Browser.js',
'DeviceAdder.js',
'DeviceModal.js',
'DevicePixelRatioSelector.js',
'DeviceSelector.js',
- 'ReloadConditions.js',
'ResizableViewport.js',
- 'ToggleMenu.js',
+ 'SettingsMenu.js',
'Toolbar.js',
'ViewportDimension.js',
'Viewports.js',
)
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -130,47 +130,16 @@ select > option:hover {
select > option.divider {
border-top: 1px solid var(--theme-splitter-color);
height: 0px;
padding: 0;
font-size: 0px;
}
/**
- * Toggle Menu
- */
-
-.devtools-toggle-menu {
- position: relative;
-}
-
-.devtools-toggle-menu .devtools-menu {
- display: none;
- flex-direction: column;
- align-items: start;
- position: absolute;
- right: 0;
- top: 100%;
- z-index: 1;
- padding: 5px;
- border-radius: 2px;
- background-color: var(--theme-toolbar-background);
- box-shadow: var(--rdm-box-shadow);
-}
-
-.devtools-toggle-menu .devtools-menu.opened {
- display: flex;
-}
-
-.devtools-toggle-menu .devtools-menu-item {
- display: flex;
- align-items: center;
-}
-
-/**
* Common background for dropdowns like select and toggle menu
*/
.toolbar-dropdown,
.toolbar-dropdown.devtools-button,
.toolbar-dropdown.devtools-button:hover:not(:empty):not(:disabled):not(.checked) {
background-color: var(--theme-toolbar-background);
background-image: var(--viewport-selection-arrow);
@@ -227,16 +196,20 @@ select > option.divider {
#touch-simulation-button::before {
background-image: url("./images/touch-events.svg");
}
#screenshot-button::before {
background-image: url("./images/screenshot.svg");
}
+#settings-button::before {
+ background-image: url("chrome://devtools/skin/images/settings.svg");
+}
+
#exit-button::before {
background-image: url("chrome://devtools/skin/images/close.svg");
}
#screenshot-button:disabled {
filter: var(--theme-icon-checked-filter);
opacity: 1 !important;
}
--- a/devtools/client/responsive.html/utils/window.js
+++ b/devtools/client/responsive.html/utils/window.js
@@ -4,20 +4,20 @@
"use strict";
const Services = require("Services");
/**
* Returns the `nsIDOMWindow` toplevel window for any child/inner window
*/
-function getToplevelWindow(window) {
+function getTopLevelWindow(window) {
return window.docShell.rootTreeItem.domWindow;
}
-exports.getToplevelWindow = getToplevelWindow;
+exports.getTopLevelWindow = getTopLevelWindow;
function getDOMWindowUtils(window) {
return window.windowUtils;
}
exports.getDOMWindowUtils = getDOMWindowUtils;
/**
* Check if the given browser window has finished the startup.
--- a/devtools/client/shared/components/menu/moz.build
+++ b/devtools/client/shared/components/menu/moz.build
@@ -1,11 +1,12 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
- 'MenuButton.js',
- 'MenuItem.js',
- 'MenuList.js',
+ 'MenuButton.js',
+ 'MenuItem.js',
+ 'MenuList.js',
+ 'utils.js',
)
rename from devtools/client/netmonitor/src/utils/menu.js
rename to devtools/client/shared/components/menu/utils.js
--- a/devtools/client/netmonitor/src/utils/menu.js
+++ b/devtools/client/shared/components/menu/utils.js
@@ -2,24 +2,33 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Menu = require("devtools/client/framework/menu");
const MenuItem = require("devtools/client/framework/menu-item");
+loader.lazyRequireGetter(this, "getTopLevelWindow", "devtools/client/responsive.html/utils/window", true);
+
/**
* Helper function for opening context menu.
*
- * @param {Array} items List of menu items.
+ * @param {Array} items
+ * List of menu items.
* @param {Object} options:
- * @property {Number} screenX coordinate of the menu on the screen
- * @property {Number} screenY coordinate of the menu on the screen
- * @property {Object} button parent used to open the menu
+ * @property {Element} button
+ * Button element used to open the menu.
+ * @property {Number} screenX
+ * Screen x coordinate of the menu on the screen.
+ * @property {Number} screenY
+ * Screen y coordinate of the menu on the screen.
+ * @property {Boolean} useTopLevelWindow
+ * Whether or not the top level window needs to be fetched. This option is used
+ * by RDM.
*/
function showMenu(items, options) {
if (items.length === 0) {
return;
}
// Build the menu object from provided menu items.
const menu = new Menu();
@@ -50,14 +59,21 @@ function showMenu(items, options) {
if (options.button) {
const button = options.button;
const rect = button.getBoundingClientRect();
const defaultView = button.ownerDocument.defaultView;
screenX = rect.left + defaultView.mozInnerScreenX;
screenY = rect.bottom + defaultView.mozInnerScreenY;
}
- menu.popup(screenX, screenY, { doc: window.parent.document });
+ let doc;
+ if (options.useTopLevelWindow) {
+ doc = getTopLevelWindow(window).document;
+ } else {
+ doc = window.parent.document;
+ }
+
+ menu.popup(screenX, screenY, { doc });
}
module.exports = {
showMenu,
};
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/settings.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <path fill="context-fill" fill-rule="evenodd" clip-rule="evenodd" d="M12 22a1 1 0 0 1-1-1v-3.083a5.966 5.966 0 0 1-2.477-1.026l-2.18 2.18a1 1 0 0 1-1.414-1.414l2.18-2.18A5.967 5.967 0 0 1 6.083 13H3a1 1 0 1 1 0-2h3.083a5.968 5.968 0 0 1 1.026-2.477l-2.18-2.18A1 1 0 0 1 6.343 4.93l2.18 2.18A5.968 5.968 0 0 1 11 6.083V3a1 1 0 1 1 2 0v3.083a5.967 5.967 0 0 1 2.476 1.026l2.18-2.18a1 1 0 1 1 1.415 1.414l-2.18 2.18A5.966 5.966 0 0 1 17.917 11H21a1 1 0 1 1 0 2h-3.083a5.966 5.966 0 0 1-1.026 2.476l2.18 2.18a1 1 0 0 1-1.414 1.415l-2.18-2.18A5.966 5.966 0 0 1 13 17.917V21a1 1 0 0 1-1 1zM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0z"></path>
+</svg>