This is an automated email from the git hooks/post-receive script.
richard pushed a commit to branch tor-browser-102.3.0esr-12.0-2 in repository tor-browser.
commit f14573b550ba5738f69e57b92ff18442381b01c0 Author: Alex Catarineu acat@torproject.org AuthorDate: Wed May 29 20:04:37 2019 +0200
Bring back old Firefox onboarding
Revert "Bug 1462415 - Delete onboarding system add-on r=Standard8,k88hudson"
This reverts commit f7ffd78b62541d44d0102f8051d2f4080bdbc432.
Revert "Bug 1498378 - Actually remove the old onboarding add-on's prefs r=Gijs"
This reverts commit 057fe36fc6f3e93e265505c7dcc703a0941778e2.
Bug 28822: Convert onboarding to webextension
Partially revert 1564367 (controlCenter in UITour.jsm) --- browser/app/profile/firefox.js | 16 + browser/components/BrowserGlue.jsm | 11 - browser/components/uitour/UITour.jsm | 42 + browser/extensions/moz.build | 3 +- .../extensions/onboarding/OnboardingTelemetry.jsm | 610 +++++++ .../extensions/onboarding/OnboardingTourType.jsm | 56 + browser/extensions/onboarding/README.md | 87 + browser/extensions/onboarding/api.js | 260 +++ browser/extensions/onboarding/background.js | 8 + .../extensions/onboarding/content/Onboarding.jsm | 1873 ++++++++++++++++++++ .../onboarding/content/img/figure_addons.svg | 1 + .../onboarding/content/img/figure_customize.svg | 561 ++++++ .../onboarding/content/img/figure_default.svg | 1 + .../onboarding/content/img/figure_library.svg | 689 +++++++ .../onboarding/content/img/figure_performance.svg | 1 + .../onboarding/content/img/figure_private.svg | 1 + .../onboarding/content/img/figure_screenshots.svg | 191 ++ .../onboarding/content/img/figure_singlesearch.svg | 1 + .../onboarding/content/img/figure_sync.svg | 1 + .../onboarding/content/img/icons_addons.svg | 1 + .../onboarding/content/img/icons_customize.svg | 1 + .../onboarding/content/img/icons_default.svg | 1 + .../onboarding/content/img/icons_library.svg | 1 + .../onboarding/content/img/icons_performance.svg | 1 + .../onboarding/content/img/icons_private.svg | 1 + .../onboarding/content/img/icons_screenshots.svg | 1 + .../onboarding/content/img/icons_singlesearch.svg | 1 + .../onboarding/content/img/icons_sync.svg | 1 + .../onboarding/content/img/icons_tour-complete.svg | 17 + .../onboarding/content/img/watermark.svg | 1 + .../onboarding/content/onboarding-tour-agent.js | 114 ++ .../extensions/onboarding/content/onboarding.css | 589 ++++++ .../extensions/onboarding/content/onboarding.js | 49 + browser/extensions/onboarding/data_events.md | 154 ++ browser/extensions/onboarding/jar.mn | 14 + .../onboarding/locales/en-US/onboarding.properties | 126 ++ .../{moz.build => onboarding/locales/jar.mn} | 8 +- .../extensions/{ => onboarding/locales}/moz.build | 3 +- browser/extensions/onboarding/manifest.json | 26 + browser/extensions/onboarding/moz.build | 26 + browser/extensions/onboarding/schema.json | 1 + .../onboarding/test/browser/.eslintrc.js | 5 + .../extensions/onboarding/test/browser/browser.ini | 18 + .../browser/browser_onboarding_accessibility.js | 121 ++ .../test/browser/browser_onboarding_keyboard.js | 205 +++ .../browser/browser_onboarding_notification.js | 79 + .../browser/browser_onboarding_notification_2.js | 114 ++ .../browser/browser_onboarding_notification_3.js | 135 ++ .../browser/browser_onboarding_notification_4.js | 114 ++ .../browser/browser_onboarding_notification_5.js | 32 + ...arding_notification_click_auto_complete_tour.js | 62 + .../browser_onboarding_select_default_tour.js | 112 ++ .../test/browser/browser_onboarding_skip_tour.js | 65 + .../test/browser/browser_onboarding_tours.js | 163 ++ .../test/browser/browser_onboarding_tourset.js | 102 ++ .../test/browser/browser_onboarding_uitour.js | 247 +++ browser/extensions/onboarding/test/browser/head.js | 387 ++++ .../extensions/onboarding/test/unit/.eslintrc.js | 5 + browser/extensions/onboarding/test/unit/head.js | 58 + .../test/unit/test-onboarding-tour-type.js | 155 ++ .../extensions/onboarding/test/unit/xpcshell.ini | 5 + browser/installer/package-manifest.in | 1 + browser/locales/Makefile.in | 2 + browser/locales/filter.py | 1 + browser/locales/l10n.ini | 1 + browser/locales/l10n.toml | 4 + extensions/permissions/PermissionManager.cpp | 6 +- tools/lint/codespell.yml | 1 + 68 files changed, 7730 insertions(+), 20 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 16060fa72eb0..70e71c0b993e 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -2156,6 +2156,22 @@ pref("browser.sessionstore.restore_tabs_lazily", true);
pref("browser.suppress_first_window_animation", true);
+// Preferences for Photon onboarding system extension +pref("browser.onboarding.enabled", true); +// Mark this as an upgraded profile so we don't offer the initial new user onboarding tour. +pref("browser.onboarding.tourset-version", 2); +pref("browser.onboarding.state", "default"); +// On the Activity-Stream page, the snippet's position overlaps with our notification. +// So use `browser.onboarding.notification.finished` to let the AS page know +// if our notification is finished and safe to show their snippet. +pref("browser.onboarding.notification.finished", false); +pref("browser.onboarding.notification.mute-duration-on-first-session-ms", 300000); // 5 mins +pref("browser.onboarding.notification.max-life-time-per-tour-ms", 432000000); // 5 days +pref("browser.onboarding.notification.max-life-time-all-tours-ms", 1209600000); // 14 days +pref("browser.onboarding.notification.max-prompt-count-per-tour", 8); +pref("browser.onboarding.newtour", "performance,private,screenshots,addons,customize,default"); +pref("browser.onboarding.updatetour", "performance,library,screenshots,singlesearch,customize,sync"); + // Preference that allows individual users to disable Screenshots. pref("extensions.screenshots.disabled", false);
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm index b95b30bedbe8..99b289bd934a 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -3541,17 +3541,6 @@ BrowserGlue.prototype = { } }
- if (currentUIVersion < 76) { - // Clear old onboarding prefs from profile (bug 1462415) - let onboardingPrefs = Services.prefs.getBranch("browser.onboarding."); - if (onboardingPrefs) { - let onboardingPrefsArray = onboardingPrefs.getChildList(""); - for (let item of onboardingPrefsArray) { - Services.prefs.clearUserPref("browser.onboarding." + item); - } - } - } - if (currentUIVersion < 77) { // Remove currentset from all the toolbars let toolbars = [ diff --git a/browser/components/uitour/UITour.jsm b/browser/components/uitour/UITour.jsm index 6fdb9b93879e..0395969faa96 100644 --- a/browser/components/uitour/UITour.jsm +++ b/browser/components/uitour/UITour.jsm @@ -821,6 +821,14 @@ var UITour = { ["ViewShowing", this.onAppMenuSubviewShowing], ], }, + { + name: "controlCenter", + node: aWindow.gIdentityHandler._identityPopup, + events: [ + ["popuphidden", this.onPanelHidden], + ["popuphiding", this.onControlCenterHiding], + ], + }, ]; for (let panel of panels) { // Ensure the menu panel is hidden and clean up panel listeners after calling hideMenu. @@ -1408,6 +1416,31 @@ var UITour = { } else if (aMenuName == "bookmarks") { let menuBtn = aWindow.document.getElementById("bookmarks-menu-button"); openMenuButton(menuBtn); + } else if (aMenuName == "controlCenter") { + let popup = aWindow.gIdentityHandler._identityPopup; + + // Add the listener even if the panel is already open since it will still + // only get registered once even if it was UITour that opened it. + popup.addEventListener("popuphiding", this.onControlCenterHiding); + popup.addEventListener("popuphidden", this.onPanelHidden); + + popup.setAttribute("noautohide", "true"); + this.clearAvailableTargetsCache(); + + if (popup.state == "open") { + if (aOpenCallback) { + aOpenCallback(); + } + return; + } + + this.recreatePopup(popup); + + // Open the control center + if (aOpenCallback) { + popup.addEventListener("popupshown", aOpenCallback, { once: true }); + } + aWindow.document.getElementById("identity-box").click(); } else if (aMenuName == "pocket") { let button = aWindow.document.getElementById("save-to-pocket-button"); if (!button) { @@ -1454,6 +1487,9 @@ var UITour = { } else if (aMenuName == "bookmarks") { let menuBtn = aWindow.document.getElementById("bookmarks-menu-button"); closeMenuButton(menuBtn); + } else if (aMenuName == "controlCenter") { + let panel = aWindow.gIdentityHandler._identityPopup; + panel.hidePopup(); } else if (aMenuName == "urlbar") { aWindow.gURLBar.view.close(); } @@ -1532,6 +1568,12 @@ var UITour = { UITour._hideAnnotationsForPanel(aEvent, false, UITour.targetIsInAppMenu); },
+ onControlCenterHiding(aEvent) { + UITour._hideAnnotationsForPanel(aEvent, true, aTarget => { + return aTarget.targetName.startsWith("controlCenter-"); + }); + }, + onPanelHidden(aEvent) { aEvent.target.removeAttribute("noautohide"); UITour.recreatePopup(aEvent.target); diff --git a/browser/extensions/moz.build b/browser/extensions/moz.build index 2df11e89dd48..39bbc2937271 100644 --- a/browser/extensions/moz.build +++ b/browser/extensions/moz.build @@ -4,5 +4,4 @@ # 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/.
-DIRS += [ -] +DIRS += ["onboarding"] diff --git a/browser/extensions/onboarding/OnboardingTelemetry.jsm b/browser/extensions/onboarding/OnboardingTelemetry.jsm new file mode 100644 index 000000000000..96e07f58de82 --- /dev/null +++ b/browser/extensions/onboarding/OnboardingTelemetry.jsm @@ -0,0 +1,610 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["OnboardingTelemetry"]; + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetters(this, { + PingCentre: "resource:///modules/PingCentre.jsm", +}); +XPCOMUtils.defineLazyServiceGetter( + this, + "gUUIDGenerator", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator" +); + +// Validate the content has non-empty string +function hasString(str) { + return typeof str == "string" && !!str.length; +} + +// Validate the content is an empty string +function isEmptyString(str) { + return typeof str == "string" && str === ""; +} + +// Validate the content is an interger +function isInteger(i) { + return Number.isInteger(i); +} + +// Validate the content is a positive interger +function isPositiveInteger(i) { + return Number.isInteger(i) && i > 0; +} + +// Validate the number is -1 +function isMinusOne(num) { + return num === -1; +} + +// Validate the category value is within the list +function isValidCategory(category) { + return [ + "logo-interactions", + "onboarding-interactions", + "overlay-interactions", + "notification-interactions", + ].includes(category); +} + +// Validate the page value is within the list +function isValidPage(page) { + return ["about:newtab", "about:home", "about:welcome"].includes(page); +} + +// Validate the tour_type value is within the list +function isValidTourType(type) { + return ["new", "update"].includes(type); +} + +// Validate the bubble state value is within the list +function isValidBubbleState(str) { + return ["bubble", "dot", "hide"].includes(str); +} + +// Validate the logo state value is within the list +function isValidLogoState(str) { + return ["logo", "watermark"].includes(str); +} + +// Validate the notification state value is within the list +function isValidNotificationState(str) { + return ["show", "hide", "finished"].includes(str); +} + +// Validate the column must be defined per ping +function definePerPing(column) { + return function() { + throw new Error( + `Must define the '${column}' validator per ping because it is not the same for all pings` + ); + }; +} + +// Basic validators for session pings +// client_id, locale are added by PingCentre, IP is added by server +// so no need check these column here +const BASIC_SESSION_SCHEMA = { + addon_version: hasString, + category: isValidCategory, + page: isValidPage, + parent_session_id: hasString, + root_session_id: hasString, + session_begin: isInteger, + session_end: isInteger, + session_id: hasString, + tour_type: isValidTourType, + type: hasString, +}; + +// Basic validators for event pings +// client_id, locale are added by PingCentre, IP is added by server +// so no need check these column here +const BASIC_EVENT_SCHEMA = { + addon_version: hasString, + bubble_state: definePerPing("bubble_state"), + category: isValidCategory, + current_tour_id: definePerPing("current_tour_id"), + logo_state: definePerPing("logo_state"), + notification_impression: definePerPing("notification_impression"), + notification_state: definePerPing("notification_state"), + page: isValidPage, + parent_session_id: hasString, + root_session_id: hasString, + target_tour_id: definePerPing("target_tour_id"), + timestamp: isInteger, + tour_type: isValidTourType, + type: hasString, + width: isPositiveInteger, +}; + +/** + * We send 2 kinds (firefox-onboarding-event2, firefox-onboarding-session2) of pings to ping centre + * server (they call it `topic`). The `internal` state in `topic` field means this event is used internaly to + * track states and will not send out any message. + * + * To save server space and make query easier, we track session begin and end but only send pings + * when session end. Therefore the server will get single "onboarding/overlay/notification-session" + * event which includes both `session_begin` and `session_end` timestamp. + * + * We send `session_begin` and `session_end` timestamps instead of `session_duration` diff because + * of analytics engineer's request. + */ +const EVENT_WHITELIST = { + // track when a notification appears. + "notification-appear": { + topic: "firefox-onboarding-event2", + category: "notification-interactions", + parent: "notification-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isValidBubbleState, + current_tour_id: hasString, + logo_state: isValidLogoState, + notification_impression: isPositiveInteger, + notification_state: isValidNotificationState, + target_tour_id: isEmptyString, + }), + }, + // track when a user clicks close notification button + "notification-close-button-click": { + topic: "firefox-onboarding-event2", + category: "notification-interactions", + parent: "notification-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isValidBubbleState, + current_tour_id: hasString, + logo_state: isValidLogoState, + notification_impression: isPositiveInteger, + notification_state: isValidNotificationState, + target_tour_id: hasString, + }), + }, + // track when a user clicks notification's Call-To-Action button + "notification-cta-click": { + topic: "firefox-onboarding-event2", + category: "notification-interactions", + parent: "notification-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isValidBubbleState, + current_tour_id: hasString, + logo_state: isValidLogoState, + notification_impression: isPositiveInteger, + notification_state: isValidNotificationState, + target_tour_id: hasString, + }), + }, + // track the start and end time of the notification session + "notification-session": { + topic: "firefox-onboarding-session2", + category: "notification-interactions", + parent: "onboarding-session", + validators: BASIC_SESSION_SCHEMA, + }, + // track the start of a notification + "notification-session-begin": { topic: "internal" }, + // track the end of a notification + "notification-session-end": { topic: "internal" }, + // track when a user clicks the Firefox logo + "onboarding-logo-click": { + topic: "firefox-onboarding-event2", + category: "logo-interactions", + parent: "onboarding-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isValidBubbleState, + current_tour_id: isEmptyString, + logo_state: isValidLogoState, + notification_impression: isMinusOne, + notification_state: isValidNotificationState, + target_tour_id: isEmptyString, + }), + }, + // track when the onboarding is not visisble due to small screen in the 1st load + "onboarding-noshow-smallscreen": { + topic: "firefox-onboarding-event2", + category: "onboarding-interactions", + parent: "onboarding-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isEmptyString, + current_tour_id: isEmptyString, + logo_state: isEmptyString, + notification_impression: isMinusOne, + notification_state: isEmptyString, + target_tour_id: isEmptyString, + }), + }, + // init onboarding session with session_key, page url, and tour_type + "onboarding-register-session": { topic: "internal" }, + // track the start and end time of the onboarding session + "onboarding-session": { + topic: "firefox-onboarding-session2", + category: "onboarding-interactions", + parent: "onboarding-session", + validators: BASIC_SESSION_SCHEMA, + }, + // track onboarding start time (when user loads about:home or about:newtab) + "onboarding-session-begin": { topic: "internal" }, + // track onboarding end time (when user unloads about:home or about:newtab) + "onboarding-session-end": { topic: "internal" }, + // track when a user clicks the close overlay button + "overlay-close-button-click": { + topic: "firefox-onboarding-event2", + category: "overlay-interactions", + parent: "overlay-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isEmptyString, + current_tour_id: hasString, + logo_state: isEmptyString, + notification_impression: isMinusOne, + notification_state: isEmptyString, + target_tour_id: hasString, + }), + }, + // track when a user clicks outside the overlay area to end the tour + "overlay-close-outside-click": { + topic: "firefox-onboarding-event2", + category: "overlay-interactions", + parent: "overlay-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isEmptyString, + current_tour_id: hasString, + logo_state: isEmptyString, + notification_impression: isMinusOne, + notification_state: isEmptyString, + target_tour_id: hasString, + }), + }, + // track when a user clicks overlay's Call-To-Action button + "overlay-cta-click": { + topic: "firefox-onboarding-event2", + category: "overlay-interactions", + parent: "overlay-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isEmptyString, + current_tour_id: hasString, + logo_state: isEmptyString, + notification_impression: isMinusOne, + notification_state: isEmptyString, + target_tour_id: hasString, + }), + }, + // track when a tour is shown in the overlay + "overlay-current-tour": { + topic: "firefox-onboarding-event2", + category: "overlay-interactions", + parent: "overlay-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isEmptyString, + current_tour_id: hasString, + logo_state: isEmptyString, + notification_impression: isMinusOne, + notification_state: isEmptyString, + target_tour_id: isEmptyString, + }), + }, + // track when an overlay is opened and disappeared because the window is resized too small + "overlay-disapear-resize": { + topic: "firefox-onboarding-event2", + category: "overlay-interactions", + parent: "overlay-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isEmptyString, + current_tour_id: isEmptyString, + logo_state: isEmptyString, + notification_impression: isMinusOne, + notification_state: isEmptyString, + target_tour_id: isEmptyString, + }), + }, + // track when a user clicks a navigation button in the overlay + "overlay-nav-click": { + topic: "firefox-onboarding-event2", + category: "overlay-interactions", + parent: "overlay-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isEmptyString, + current_tour_id: hasString, + logo_state: isEmptyString, + notification_impression: isMinusOne, + notification_state: isEmptyString, + target_tour_id: hasString, + }), + }, + // track the start and end time of the overlay session + "overlay-session": { + topic: "firefox-onboarding-session2", + category: "overlay-interactions", + parent: "onboarding-session", + validators: BASIC_SESSION_SCHEMA, + }, + // track the start of an overlay session + "overlay-session-begin": { topic: "internal" }, + // track the end of an overlay session + "overlay-session-end": { topic: "internal" }, + // track when a user clicks 'Skip Tour' button in the overlay + "overlay-skip-tour": { + topic: "firefox-onboarding-event2", + category: "overlay-interactions", + parent: "overlay-session", + validators: Object.assign({}, BASIC_EVENT_SCHEMA, { + bubble_state: isEmptyString, + current_tour_id: hasString, + logo_state: isEmptyString, + notification_impression: isMinusOne, + notification_state: isEmptyString, + target_tour_id: isEmptyString, + }), + }, +}; + +const ONBOARDING_ID = "onboarding"; + +let OnboardingTelemetry = { + sessionProbe: null, + eventProbe: null, + state: { + sessions: {}, + }, + + init(startupData) { + this.sessionProbe = new PingCentre({ + topic: "firefox-onboarding-session2", + }); + this.eventProbe = new PingCentre({ topic: "firefox-onboarding-event2" }); + this.state.addon_version = startupData.version; + }, + + // register per tab session data + registerNewOnboardingSession(data) { + let { page, session_key, tour_type } = data; + if (this.state.sessions[session_key]) { + return; + } + // session_key and page url are must have + if (!session_key || !page || !tour_type) { + throw new Error( + "session_key, page url, and tour_type are required for onboarding-register-session" + ); + } + let onboarding_session_id = gUUIDGenerator.generateUUID().toString(); + this.state.sessions[session_key] = { + onboarding_session_id, + overlay_session_id: "", + notification_session_id: "", + page, + tour_type, + }; + }, + + process(data) { + let { type, session_key } = data; + if (type === "onboarding-register-session") { + this.registerNewOnboardingSession(data); + return; + } + + if (!this.state.sessions[session_key]) { + throw new Error(`${type} should pass valid session_key`); + } + + switch (type) { + case "onboarding-session-begin": + if (!this.state.sessions[session_key].onboarding_session_id) { + throw new Error( + `should fire onboarding-register-session event before ${type}` + ); + } + this.state.sessions[session_key].onboarding_session_begin = Date.now(); + return; + case "onboarding-session-end": + data = Object.assign({}, data, { + type: "onboarding-session", + }); + this.state.sessions[session_key].onboarding_session_end = Date.now(); + break; + case "overlay-session-begin": + this.state.sessions[ + session_key + ].overlay_session_id = gUUIDGenerator.generateUUID().toString(); + this.state.sessions[session_key].overlay_session_begin = Date.now(); + return; + case "overlay-session-end": + data = Object.assign({}, data, { + type: "overlay-session", + }); + this.state.sessions[session_key].overlay_session_end = Date.now(); + break; + case "notification-session-begin": + this.state.sessions[ + session_key + ].notification_session_id = gUUIDGenerator.generateUUID().toString(); + this.state.sessions[ + session_key + ].notification_session_begin = Date.now(); + return; + case "notification-session-end": + data = Object.assign({}, data, { + type: "notification-session", + }); + this.state.sessions[session_key].notification_session_end = Date.now(); + break; + } + let topic = EVENT_WHITELIST[data.type] && EVENT_WHITELIST[data.type].topic; + if (!topic) { + throw new Error( + `ping-centre doesn't know ${type} after processPings, only knows ${Object.keys( + EVENT_WHITELIST + )}` + ); + } + this._sendPing(topic, data); + }, + + // send out pings by topic + _sendPing(topic, data) { + if (topic === "internal") { + throw new Error( + `internal ping ${data.type} should be processed within processPings` + ); + } + + let { addon_version } = this.state; + let { + bubble_state = "", + current_tour_id = "", + logo_state = "", + notification_impression = -1, + notification_state = "", + session_key, + target_tour_id = "", + type, + width, + } = data; + let { + notification_session_begin, + notification_session_end, + notification_session_id, + onboarding_session_begin, + onboarding_session_end, + onboarding_session_id, + overlay_session_begin, + overlay_session_end, + overlay_session_id, + page, + tour_type, + } = this.state.sessions[session_key]; + let { category, parent } = EVENT_WHITELIST[type]; + let parent_session_id; + let payload; + let session_begin; + let session_end; + let session_id; + let root_session_id = onboarding_session_id; + + // assign parent_session_id + switch (parent) { + case "onboarding-session": + parent_session_id = onboarding_session_id; + break; + case "overlay-session": + parent_session_id = overlay_session_id; + break; + case "notification-session": + parent_session_id = notification_session_id; + break; + } + if (!parent_session_id) { + throw new Error( + `Unable to find the ${parent} parent session for the event ${type}` + ); + } + + switch (topic) { + case "firefox-onboarding-session2": + switch (type) { + case "onboarding-session": + session_id = onboarding_session_id; + session_begin = onboarding_session_begin; + session_end = onboarding_session_end; + delete this.state.sessions[session_key]; + break; + case "overlay-session": + session_id = overlay_session_id; + session_begin = overlay_session_begin; + session_end = overlay_session_end; + break; + case "notification-session": + session_id = notification_session_id; + session_begin = notification_session_begin; + session_end = notification_session_end; + break; + } + if (!session_id || !session_begin || !session_end) { + throw new Error( + `should fire ${type}-begin and ${type}-end event before ${type}` + ); + } + + payload = { + addon_version, + category, + page, + parent_session_id, + root_session_id, + session_begin, + session_end, + session_id, + tour_type, + type, + }; + this._validatePayload(payload); + this.sessionProbe && + this.sessionProbe.sendPing(payload, { filter: ONBOARDING_ID }); + break; + case "firefox-onboarding-event2": + let timestamp = Date.now(); + payload = { + addon_version, + bubble_state, + category, + current_tour_id, + logo_state, + notification_impression, + notification_state, + page, + parent_session_id, + root_session_id, + target_tour_id, + timestamp, + tour_type, + type, + width, + }; + this._validatePayload(payload); + this.eventProbe && + this.eventProbe.sendPing(payload, { filter: ONBOARDING_ID }); + break; + } + }, + + // validate data sanitation and make sure correct ping params are sent + _validatePayload(payload) { + let type = payload.type; + let { validators } = EVENT_WHITELIST[type]; + if (!validators) { + throw new Error(`Event ${type} without validators should not be sent.`); + } + let validatorKeys = Object.keys(validators); + // Not send with undefined column + if (Object.keys(payload).length > validatorKeys.length) { + throw new Error( + `Event ${type} want to send more columns than expect, should not be sent.` + ); + } + let results = {}; + let failed = false; + // Per column validation + for (let key of validatorKeys) { + if (payload[key] !== undefined) { + results[key] = validators[key](payload[key]); + if (!results[key]) { + failed = true; + } + } else { + results[key] = false; + failed = true; + } + } + if (failed) { + throw new Error( + `Event ${type} contains incorrect data: ${JSON.stringify( + results + )}, should not be sent.` + ); + } + }, +}; diff --git a/browser/extensions/onboarding/OnboardingTourType.jsm b/browser/extensions/onboarding/OnboardingTourType.jsm new file mode 100644 index 000000000000..0d005a36eb96 --- /dev/null +++ b/browser/extensions/onboarding/OnboardingTourType.jsm @@ -0,0 +1,56 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["OnboardingTourType"]; + +ChromeUtils.defineModuleGetter( + this, + "Services", + "resource://gre/modules/Services.jsm" +); + +var OnboardingTourType = { + /** + * Determine the current tour type (new user tour or update user tour). + * The function checks 2 criterias + * - TOURSET_VERSION: current onboarding tourset version + * - PREF_SEEN_TOURSET_VERSION: the user seen tourset version + * As the result the function will set the right current tour type in the tour type pref (PREF_TOUR_TYPE) for later use. + */ + check() { + const PREF_TOUR_TYPE = "browser.onboarding.tour-type"; + const PREF_SEEN_TOURSET_VERSION = "browser.onboarding.seen-tourset-version"; + const TOURSET_VERSION = Services.prefs.getIntPref( + "browser.onboarding.tourset-version" + ); + + if (!Services.prefs.prefHasUserValue(PREF_SEEN_TOURSET_VERSION)) { + // User has never seen an onboarding tour, present the user with the new user tour. + Services.prefs.setStringPref(PREF_TOUR_TYPE, "new"); + } else if ( + Services.prefs.getIntPref(PREF_SEEN_TOURSET_VERSION) < TOURSET_VERSION + ) { + // show the update user tour when tour set version is larger than the seen tourset version + Services.prefs.setStringPref(PREF_TOUR_TYPE, "update"); + // Reset all the notification-related prefs because tours update. + Services.prefs.setBoolPref( + "browser.onboarding.notification.finished", + false + ); + Services.prefs.clearUserPref( + "browser.onboarding.notification.prompt-count" + ); + Services.prefs.clearUserPref( + "browser.onboarding.notification.last-time-of-changing-tour-sec" + ); + Services.prefs.clearUserPref( + "browser.onboarding.notification.tour-ids-queue" + ); + Services.prefs.clearUserPref("browser.onboarding.state"); + } + Services.prefs.setIntPref(PREF_SEEN_TOURSET_VERSION, TOURSET_VERSION); + }, +}; diff --git a/browser/extensions/onboarding/README.md b/browser/extensions/onboarding/README.md new file mode 100644 index 000000000000..c63be42b7181 --- /dev/null +++ b/browser/extensions/onboarding/README.md @@ -0,0 +1,87 @@ +# Onboarding + +System addon to provide the onboarding overlay for user-friendly tours. + +## How to show the onboarding tour + +Open `about:config` page and filter with `onboarding` keyword. Then set following preferences: + +``` +browser.onboarding.disabled = false +browser.onboarding.tour-set = "new" // for new user tour, or "update" for update user tour +``` +And make sure the value of `browser.onboarding.tourset-verion` and `browser.onboarding.seen-tourset-verion` are the same. + +## How to show the onboarding notification + +Besides above settings, notification will wait 5 minutes before showing the first notification on a new profile or the updated user profile (to not put too much information to the user at once). + +To manually remove the mute duration, set pref `browser.onboarding.notification.mute-duration-on-first-session-ms` to `0` and notification will be shown at the next time you open `about:home`, `about:newtab`, or `about:welcome`. + +## How to show the snippets + +Snippets (the remote notification that handled by activity stream) will only be shown after onboarding notifications are all done. You can set preference `browser.onboarding.notification.finished` to `true` to disable onboarding notification and accept snippets right away. + +## Architecture + +![](https://i.imgur.com/7RK89Zw.png) + +During booting from `bootstrap.js`, `OnboardingTourType.jsm` will check the onboarding tour type (`new` and `update` are supported types) and set required initial states into preferences. + +Everytime `about:home`, `about:newtab`, or `about:welcome` page is opened, `onboarding.js` is injected into that page via [frame scripts](https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Message_Man...). + +Then in `onboarding.js`, all tours are defined inside of `onboardingTourset` dictionary. `getTourIDList` function will load tours from proper preferences. (Check `How to change the order of tours` section for more detail). + +When user clicks the action button in each tour, We use [UITour](http://bedrock.readthedocs.io/en/latest/uitour.html) to highlight the correspondent browser UI element. The UITour client is bundled in onboarding addon via `jar.mn`. + +## Landing rules + +We would apply some rules: + +* To avoid conflict with the origin page, all styles and ids should be formatted as `onboarding-*`. +* For consistency and easier filtering, all strings in `locales` should be formatted as `onboarding.*`. +* For consistency, all related preferences should be formatted as `browser.onboarding.*`. +* For accessibility, images that are for presentation only should have `role="presentation"` attribute. + +## How to change the order of tours + +Edit `browser/app/profile/firefox.js` and modify `browser.onboarding.newtour` for the new user tour or `browser.onboarding.updatetour` for the update user tour. You can change the tour list and the order by concate `tourIds` with `,` sign. You can find available `tourId` from `onboardingTourset` in `onboarding.js`. + +## How to pump tour set version after update tours + +We only update the tourset version when we have different **update** tourset. Update the new tourset **does not** require update the tourset version. + +The tourset version is used to track the last major tourset change version. The `tourset-version` pref store the major tourset version (ex: `1`) but not the current browser version. When browser update to the next version (ex: 58, 59) the tourset pref is still `1` if we didn't do any major tourset update. + +Once the tour set version is updated (ex: `2`), onboarding overlay should show the update tour to the updated user (ex: update from v56 -> v57), even when user has watched the previous tours or preferred to hide the previous tours. + +Edit `browser/app/profile/firefox.js` and set `browser.onboarding.tourset-version` as `[tourset version]` (in integer format). + +For example, if we update the tourset in v60 and decide to show all update users the tour, we set `browser.onboarding.tourset-version` as `3`. + +## Icon states + +Onboarding module has two states for its overlay icon: `default` and `watermark`. +By default, it shows `default` state. +When either tours or notifications are all completed, the icon changes to the `watermark` state. +The icon state is stored in `browser.onboarding.state`. +When `tourset-version` is updated, or when we detect the `tour-type` is changed to `update`, icon state will be changed back to the `default` state. + +## Customizable preferences + +Here are current support preferences that allow to customize the Onboarding's behavior. + +| PREF | DESCRIPTION | DEFAULT | +|-----|-------------|:-----:| +| `browser.onboarding.enabled` | disable onboarding experience entirely | true +| `browser.onboarding.notification.finished` | Decide if we want to hide the notification permanently. | false +| `browser.onboarding.notification.mute-duration-on-first-session-ms` |Notification mute duration. It also effect when the speech bubble is hidden and turned into the blue dot | 300000 (5 Min) +| `browser.onboarding.notification.max-life-time-all-tours-ms` | Notification tours will all hide after this period | 1209600000 (10 Days) +| `browser.onboarding.notification.max-life-time-per-tours-ms` | Per Notification tours will hide and show the next tour after this period | 432000000 (5 Days) +| `browser.onboarding.notification.max-prompt-count-per-tour` | Each tour can only show the specific times in notification bar if user didn't interact with the tour notification. | 8 +| `browser.onboarding.newtour` | The tourset of new user tour. | performance,private,screenshots,addons,customize,default +| `browser.onboarding.newtour.tooltip` | The string id which is shown in the new user tour's speech bubble. The preffered length is 2 lines. Should use `%S` to denote Firefox (brand short name) in string, or use `%1$S` if the name shows more than 1 time. | `onboarding.overlay-icon-tooltip2` +| `browser.onboarding.updatetour` | The tourset of new user tour. | performance,library,screenshots,singlesearch,customize,sync +| `browser.onboarding.updatetour.tooltip` | The string id which is shown in the update user tour's speech bubble. The preffered length is 2 lines. Should use `%S` to denote Firefox (brand short name) in string, or use `%1$S` if the name shows shows more than 1 time. | `onboarding.overlay-icon-tooltip-updated2` +| `browser.onboarding.default-icon-src` | The default icon url. Should be svg or at least 64x64 | `chrome://branding/content/icon64.png` +| `browser.onboarding.watermark-icon-src` | The watermark icon url. Should be svg or at least 64x64 | `resource://onboarding/img/watermark.svg` diff --git a/browser/extensions/onboarding/api.js b/browser/extensions/onboarding/api.js new file mode 100644 index 000000000000..f514530ea6e8 --- /dev/null +++ b/browser/extensions/onboarding/api.js @@ -0,0 +1,260 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ +/* globals APP_STARTUP, ADDON_INSTALL */ +"use strict"; + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetters(this, { + OnboardingTourType: "resource://onboarding/modules/OnboardingTourType.jsm", + OnboardingTelemetry: "resource://onboarding/modules/OnboardingTelemetry.jsm", + Services: "resource://gre/modules/Services.jsm", + UIState: "resource://services-sync/UIState.jsm", +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "resProto", + "@mozilla.org/network/protocol;1?name=resource", + "nsISubstitutingProtocolHandler" +); + +const RESOURCE_HOST = "onboarding"; + +const { PREF_STRING, PREF_BOOL, PREF_INT } = Ci.nsIPrefBranch; + +const BROWSER_READY_NOTIFICATION = "browser-delayed-startup-finished"; +const BROWSER_SESSION_STORE_NOTIFICATION = "sessionstore-windows-restored"; +const PREF_WHITELIST = [ + ["browser.onboarding.enabled", PREF_BOOL], + ["browser.onboarding.state", PREF_STRING], + ["browser.onboarding.notification.finished", PREF_BOOL], + ["browser.onboarding.notification.prompt-count", PREF_INT], + ["browser.onboarding.notification.last-time-of-changing-tour-sec", PREF_INT], + ["browser.onboarding.notification.tour-ids-queue", PREF_STRING], +]; + +[ + "onboarding-tour-addons", + "onboarding-tour-customize", + "onboarding-tour-default-browser", + "onboarding-tour-library", + "onboarding-tour-performance", + "onboarding-tour-private-browsing", + "onboarding-tour-screenshots", + "onboarding-tour-singlesearch", + "onboarding-tour-sync", +].forEach(tourId => + PREF_WHITELIST.push([ + `browser.onboarding.tour.${tourId}.completed`, + PREF_BOOL, + ]) +); + +let waitingForBrowserReady = true; +let startupData; + +/** + * Set pref. Why no `getPrefs` function is due to the privilege level. + * We cannot set prefs inside a framescript but can read. + * For simplicity and efficiency, we still read prefs inside the framescript. + * + * @param {Array} prefs the array of prefs to set. + * The array element carries info to set pref, should contain + * - {String} name the pref name, such as `browser.onboarding.state` + * - {*} value the value to set + **/ +function setPrefs(prefs) { + prefs.forEach(pref => { + let prefObj = PREF_WHITELIST.find(([name]) => name == pref.name); + if (!prefObj) { + return; + } + + let [name, type] = prefObj; + + switch (type) { + case PREF_BOOL: + Services.prefs.setBoolPref(name, pref.value); + break; + case PREF_INT: + Services.prefs.setIntPref(name, pref.value); + break; + case PREF_STRING: + Services.prefs.setStringPref(name, pref.value); + break; + default: + throw new TypeError( + `Unexpected type (${type}) for preference ${name}.` + ); + } + }); +} + +/** + * syncTourChecker listens to and maintains the login status inside, and can be + * queried at any time once initialized. + */ +let syncTourChecker = { + _registered: false, + _loggedIn: false, + + isLoggedIn() { + return this._loggedIn; + }, + + observe(subject, topic) { + const state = UIState.get(); + if (state.status == UIState.STATUS_NOT_CONFIGURED) { + this._loggedIn = false; + } else { + this.setComplete(); + } + }, + + init() { + if (!Services.prefs.getBoolPref("identity.fxaccounts.enabled")) { + return; + } + // Check if we've already logged in at startup. + const state = UIState.get(); + if (state.status != UIState.STATUS_NOT_CONFIGURED) { + this.setComplete(); + } + this.register(); + }, + + register() { + if (this._registered) { + return; + } + Services.obs.addObserver(this, "sync-ui-state:update"); + this._registered = true; + }, + + setComplete() { + this._loggedIn = true; + Services.prefs.setBoolPref( + "browser.onboarding.tour.onboarding-tour-sync.completed", + true + ); + }, + + unregister() { + if (!this._registered) { + return; + } + Services.obs.removeObserver(this, "sync-ui-state:update"); + this._registered = false; + }, + + uninit() { + this.unregister(); + }, +}; + +/** + * Listen and process events from content. + */ +function initContentMessageListener() { + Services.mm.addMessageListener("Onboarding:OnContentMessage", msg => { + switch (msg.data.action) { + case "set-prefs": + setPrefs(msg.data.params); + break; + case "get-login-status": + msg.target.messageManager.sendAsyncMessage( + "Onboarding:ResponseLoginStatus", + { + isLoggedIn: syncTourChecker.isLoggedIn(), + } + ); + break; + case "ping-centre": + try { + OnboardingTelemetry.process(msg.data.params.data); + } catch (e) { + Cu.reportError(e); + } + break; + } + }); +} + +/** + * onBrowserReady - Continues startup of the add-on after browser is ready. + */ +function onBrowserReady() { + waitingForBrowserReady = false; + + OnboardingTourType.check(); + OnboardingTelemetry.init(startupData); + Services.mm.loadFrameScript("resource://onboarding/onboarding.js", true); + initContentMessageListener(); +} + +/** + * observe - nsIObserver callback to handle various browser notifications. + */ +function observe(subject, topic, data) { + switch (topic) { + case BROWSER_READY_NOTIFICATION: + Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION); + onBrowserReady(); + break; + case BROWSER_SESSION_STORE_NOTIFICATION: + Services.obs.removeObserver(observe, BROWSER_SESSION_STORE_NOTIFICATION); + // Postpone Firefox account checking until "before handling user events" + // phase to meet performance criteria. The reason we don't postpone the + // whole onBrowserReady here is because in that way we will miss onload + // events for onboarding.js. + Services.tm.idleDispatchToMainThread(() => syncTourChecker.init()); + break; + } +} + +this.onboarding = class extends ExtensionAPI { + onStartup() { + resProto.setSubstitutionWithFlags( + RESOURCE_HOST, + Services.io.newURI("chrome/content/", null, this.extension.rootURI), + resProto.ALLOW_CONTENT_ACCESS + ); + + if (this.extension.rootURI instanceof Ci.nsIJARURI) { + this.manifest = this.extension.rootURI.JARFile.QueryInterface( + Ci.nsIFileURL + ).file; + } else if (this.extension.rootURI instanceof Ci.nsIFileURL) { + this.manifest = this.extension.rootURI.file; + } + + if (this.manifest) { + Components.manager.addBootstrappedManifestLocation(this.manifest); + } else { + Cu.reportError( + "Cannot find onboarding chrome.manifest for registring translated strings" + ); + } + + // Only start Onboarding when the browser UI is ready + if (Services.startup.startingUp) { + Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION); + Services.obs.addObserver(observe, BROWSER_SESSION_STORE_NOTIFICATION); + } else { + onBrowserReady(); + syncTourChecker.init(); + } + } + + onShutdown() { + resProto.setSubstitution(RESOURCE_HOST, null); + + // Stop waiting for browser to be ready + if (waitingForBrowserReady) { + Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION); + } + syncTourChecker.uninit(); + } +}; diff --git a/browser/extensions/onboarding/background.js b/browser/extensions/onboarding/background.js new file mode 100644 index 000000000000..efe296ff2278 --- /dev/null +++ b/browser/extensions/onboarding/background.js @@ -0,0 +1,8 @@ +/* eslint-env webextensions */ + +"use strict"; + +browser.runtime.onUpdateAvailable.addListener(details => { + // By listening to but ignoring this event, any updates will + // be delayed until the next browser restart. +}); diff --git a/browser/extensions/onboarding/content/Onboarding.jsm b/browser/extensions/onboarding/content/Onboarding.jsm new file mode 100644 index 000000000000..ad40b8dac8d9 --- /dev/null +++ b/browser/extensions/onboarding/content/Onboarding.jsm @@ -0,0 +1,1873 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["Onboarding"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const ONBOARDING_CSS_URL = "resource://onboarding/onboarding.css"; +const BUNDLE_URI = "chrome://onboarding/locale/onboarding.properties"; +const UITOUR_JS_URI = "resource://onboarding/lib/UITour-lib.js"; +const TOUR_AGENT_JS_URI = "resource://onboarding/onboarding-tour-agent.js"; +const BRAND_SHORT_NAME = Services.strings + .createBundle("chrome://branding/locale/brand.properties") + .GetStringFromName("brandShortName"); +const PROMPT_COUNT_PREF = "browser.onboarding.notification.prompt-count"; +const NOTIFICATION_FINISHED_PREF = "browser.onboarding.notification.finished"; +const ONBOARDING_DIALOG_ID = "onboarding-overlay-dialog"; +const ONBOARDING_MIN_WIDTH_PX = 960; +const SPEECH_BUBBLE_MIN_WIDTH_PX = 1365; +const SPEECH_BUBBLE_NEWTOUR_STRING_ID = "onboarding.overlay-icon-tooltip2"; +const SPEECH_BUBBLE_UPDATETOUR_STRING_ID = + "onboarding.overlay-icon-tooltip-updated2"; +const ICON_STATE_WATERMARK = "watermark"; +const ICON_STATE_DEFAULT = "default"; + +/** + * Helper function to create the tour description UI element. + */ +function createOnboardingTourDescription(div, title, description) { + let doc = div.ownerDocument; + let section = doc.createElement("section"); + section.className = "onboarding-tour-description"; + + let h1 = doc.createElement("h1"); + h1.setAttribute("data-l10n-id", title); + section.appendChild(h1); + + let p = doc.createElement("p"); + p.setAttribute("data-l10n-id", description); + section.appendChild(p); + + div.appendChild(section); + return section; +} + +/** + * Helper function to create the tour content UI element. + */ +function createOnboardingTourContent(div, imageSrc) { + let doc = div.ownerDocument; + let section = doc.createElement("section"); + section.className = "onboarding-tour-content"; + + let img = doc.createElement("img"); + img.setAttribute("src", imageSrc); + img.setAttribute("role", "presentation"); + section.appendChild(img); + + div.appendChild(section); + return section; +} + +/** + * Helper function to create the tour button UI element. + */ +function createOnboardingTourButton( + div, + buttonId, + l10nId, + buttonElementTagName = "button" +) { + let doc = div.ownerDocument; + let aside = doc.createElement("aside"); + aside.className = "onboarding-tour-button-container"; + + let button = doc.createElement(buttonElementTagName); + button.id = buttonId; + button.className = "onboarding-tour-action-button"; + button.setAttribute("data-l10n-id", l10nId); + aside.appendChild(button); + + div.appendChild(aside); + return aside; +} + +/** + * Add any number of tours, key is the tourId, value should follow the format below + * "tourId": { // The short tour id which could be saved in pref + * // The unique tour id + * id: "onboarding-tour-addons", + * // (optional) mark tour as complete instantly when the user enters the tour + * instantComplete: false, + * // The string id of tour name which would be displayed on the navigation bar + * tourNameId: "onboarding.tour-addon", + * // The method returing strings used on tour notification + * getNotificationStrings(bundle): + * - title: // The string of tour notification title + * - message: // The string of tour notification message + * - button: // The string of tour notification action button title + * // Return a div appended with elements for this tours. + * // Each tour should contain the following 3 sections in the div: + * // .onboarding-tour-description, .onboarding-tour-content, .onboarding-tour-button-container. + * // If there was a .onboarding-tour-action-button present and was clicked, tour would be marked as completed. + * getPage() {}, + * }, + **/ +var onboardingTourset = { + private: { + id: "onboarding-tour-private-browsing", + tourNameId: "onboarding.tour-private-browsing", + getNotificationStrings(bundle) { + return { + title: bundle.GetStringFromName( + "onboarding.notification.onboarding-tour-private-browsing.title" + ), + message: bundle.GetStringFromName( + "onboarding.notification.onboarding-tour-private-browsing.message2" + ), + button: bundle.GetStringFromName("onboarding.button.learnMore"), + }; + }, + getPage(win) { + let div = win.document.createElement("div"); + + createOnboardingTourDescription( + div, + "onboarding.tour-private-browsing.title2", + "onboarding.tour-private-browsing.description3" + ); + createOnboardingTourContent( + div, + "resource://onboarding/img/figure_private.svg" + ); + createOnboardingTourButton( + div, + "onboarding-tour-private-browsing-button", + "onboarding.tour-private-browsing.button" + ); + + return div; + }, + }, + addons: { + id: "onboarding-tour-addons", + tourNameId: "onboarding.tour-addons", + getNotificationStrings(bundle) { + return { + title: bundle.GetStringFromName( + "onboarding.notification.onboarding-tour-addons.title" + ), + message: bundle.formatStringFromName( + "onboarding.notification.onboarding-tour-addons.message", + [BRAND_SHORT_NAME], + 1 + ), + button: bundle.GetStringFromName("onboarding.button.learnMore"), + }; + }, + getPage(win) { + let div = win.document.createElement("div"); + + createOnboardingTourDescription( + div, + "onboarding.tour-addons.title2", + "onboarding.tour-addons.description2" + ); + createOnboardingTourContent( + div, + "resource://onboarding/img/figure_addons.svg" + ); + createOnboardingTourButton( + div, + "onboarding-tour-addons-button", + "onboarding.tour-addons.button" + ); + + return div; + }, + }, + customize: { + id: "onboarding-tour-customize", + tourNameId: "onboarding.tour-customize", + getNotificationStrings(bundle) { + return { + title: bundle.GetStringFromName( + "onboarding.notification.onboarding-tour-customize.title" + ), + message: bundle.formatStringFromName( + "onboarding.notification.onboarding-tour-customize.message", + [BRAND_SHORT_NAME], + 1 + ), + button: bundle.GetStringFromName("onboarding.button.learnMore"), + }; + }, + getPage(win) { + let div = win.document.createElement("div"); + + createOnboardingTourDescription( + div, + "onboarding.tour-customize.title2", + "onboarding.tour-customize.description2" + ); + createOnboardingTourContent( + div, + "resource://onboarding/img/figure_customize.svg" + ); + createOnboardingTourButton( + div, + "onboarding-tour-customize-button", + "onboarding.tour-customize.button" + ); + + return div; + }, + }, + default: { + id: "onboarding-tour-default-browser", + instantComplete: true, + tourNameId: "onboarding.tour-default-browser", + getNotificationStrings(bundle) { + return { + title: bundle.formatStringFromName( + "onboarding.notification.onboarding-tour-default-browser.title", + [BRAND_SHORT_NAME], + 1 + ), + message: bundle.formatStringFromName( + "onboarding.notification.onboarding-tour-default-browser.message", + [BRAND_SHORT_NAME], + 1 + ), + button: bundle.GetStringFromName("onboarding.button.learnMore"), + }; + }, + getPage(win, bundle) { + let div = win.document.createElement("div"); + let setFromBackGround = bundle.formatStringFromName( + "onboarding.tour-default-browser.win7.button", + [BRAND_SHORT_NAME], + 1 + ); + let setFromPanel = bundle.GetStringFromName( + "onboarding.tour-default-browser.button" + ); + let isDefaultMessage = bundle.GetStringFromName( + "onboarding.tour-default-browser.is-default.message" + ); + let isDefault2ndMessage = bundle.formatStringFromName( + "onboarding.tour-default-browser.is-default.2nd-message", + [BRAND_SHORT_NAME], + 1 + ); + + createOnboardingTourDescription( + div, + "onboarding.tour-default-browser.title2", + "onboarding.tour-default-browser.description2" + ); + createOnboardingTourContent( + div, + "resource://onboarding/img/figure_default.svg" + ); + + let aside = win.document.createElement("aside"); + aside.className = "onboarding-tour-button-container"; + div.appendChild(aside); + + let button = win.document.createElement("button"); + button.id = "onboarding-tour-default-browser-button"; + button.className = "onboarding-tour-action-button"; + button.setAttribute("data-bg", setFromBackGround); + button.setAttribute("data-panel", setFromPanel); + aside.appendChild(button); + + let isDefaultBrowserMsg = win.document.createElement("div"); + isDefaultBrowserMsg.id = "onboarding-tour-is-default-browser-msg"; + isDefaultBrowserMsg.className = "onboarding-hidden"; + aside.appendChild(isDefaultBrowserMsg); + isDefaultBrowserMsg.append(isDefaultMessage); + + let br = win.document.createElement("br"); + isDefaultBrowserMsg.appendChild(br); + isDefaultBrowserMsg.append(isDefault2ndMessage); + + div.addEventListener("beforeshow", () => { + win.document.dispatchEvent( + new Event("Agent:CanSetDefaultBrowserInBackground") + ); + }); + return div; + }, + }, + sync: { + id: "onboarding-tour-sync", + instantComplete: true, + tourNameId: "onboarding.tour-sync2", + getNotificationStrings(bundle) { + return { + title: bundle.GetStringFromName( + "onboarding.notification.onboarding-tour-sync.title" + ), + message: bundle.GetStringFromName( + "onboarding.notification.onboarding-tour-sync.message" + ), + button: bundle.GetStringFromName("onboarding.button.learnMore"), + }; + }, + getPage(win, bundle) { + const STATE_LOGOUT = "logged-out"; + const STATE_LOGIN = "logged-in"; + let div = win.document.createElement("div"); + div.dataset.loginState = STATE_LOGOUT; + // The email validation pattern used in the form comes from IETF rfc5321, + // which is identical to server-side checker of Firefox Account. See + // discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1378770#c6 + // for detail. + let emailRegex = + "^[\w.!#$%&’*+\/=?^`{|}~-]{1,64}@[a-z\d](?:[a-z\d-]{0,253}[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]{0,253}[a-z\d])?)+$"; + + let description = createOnboardingTourDescription( + div, + "onboarding.tour-sync.title2", + "onboarding.tour-sync.description2" + ); + + description.querySelector("h1").className = "show-on-logged-out"; + description.querySelector("p").className = "show-on-logged-out"; + + let h1LoggedIn = win.document.createElement("h1"); + h1LoggedIn.setAttribute( + "data-l10n-id", + "onboarding.tour-sync.logged-in.title" + ); + h1LoggedIn.className = "show-on-logged-in"; + description.appendChild(h1LoggedIn); + + let pLoggedIn = win.document.createElement("p"); + pLoggedIn.setAttribute( + "data-l10n-id", + "onboarding.tour-sync.logged-in.description" + ); + pLoggedIn.className = "show-on-logged-in"; + description.appendChild(pLoggedIn); + + let content = win.document.createElement("section"); + content.className = "onboarding-tour-content"; + div.appendChild(content); + + let form = win.document.createElement("form"); + form.className = "show-on-logged-out"; + content.appendChild(form); + + let h3 = win.document.createElement("h3"); + h3.setAttribute("data-l10n-id", "onboarding.tour-sync.form.title"); + form.appendChild(h3); + + let p = win.document.createElement("p"); + p.setAttribute("data-l10n-id", "onboarding.tour-sync.form.description"); + form.appendChild(p); + + let input = win.document.createElement("input"); + input.id = "onboarding-tour-sync-email-input"; + input.setAttribute("required", "true"); + input.setAttribute("type", "email"); + input.placeholder = bundle.GetStringFromName( + "onboarding.tour-sync.email-input.placeholder" + ); + input.pattern = emailRegex; + form.appendChild(input); + + let br = win.document.createElement("br"); + form.appendChild(br); + + let button = win.document.createElement("button"); + button.id = "onboarding-tour-sync-button"; + button.className = "onboarding-tour-action-button"; + button.setAttribute("data-l10n-id", "onboarding.tour-sync.button"); + form.appendChild(button); + + let img = win.document.createElement("img"); + img.setAttribute("src", "resource://onboarding/img/figure_sync.svg"); + img.setAttribute("role", "presentation"); + content.appendChild(img); + + let aside = win.document.createElement("aside"); + aside.className = "onboarding-tour-button-container show-on-logged-in"; + div.appendChild(aside); + + let connectDeviceButton = win.document.createElement("button"); + connectDeviceButton.id = "onboarding-tour-sync-connect-device-button"; + connectDeviceButton.className = "onboarding-tour-action-button"; + connectDeviceButton.setAttribute( + "data-l10n-id", + "onboarding.tour-sync.connect-device.button" + ); + aside.appendChild(connectDeviceButton); + + div.addEventListener("beforeshow", () => { + function loginStatusListener(msg) { + removeMessageListener( + "Onboarding:ResponseLoginStatus", + loginStatusListener + ); + div.dataset.loginState = msg.data.isLoggedIn + ? STATE_LOGIN + : STATE_LOGOUT; + } + this.sendMessageToChrome("get-login-status"); + this.mm.addMessageListener( + "Onboarding:ResponseLoginStatus", + loginStatusListener + ); + }); + + return div; + }, + }, + library: { + id: "onboarding-tour-library", + tourNameId: "onboarding.tour-library", + getNotificationStrings(bundle) { + return { + title: bundle.GetStringFromName( + "onboarding.notification.onboarding-tour-library.title" + ), + message: bundle.formatStringFromName( + "onboarding.notification.onboarding-tour-library.message", + [BRAND_SHORT_NAME], + 1 + ), + button: bundle.GetStringFromName("onboarding.button.learnMore"), + }; + }, + getPage(win) { + let div = win.document.createElement("div"); + + createOnboardingTourDescription( + div, + "onboarding.tour-library.title", + "onboarding.tour-library.description2" + ); + createOnboardingTourContent( + div, + "resource://onboarding/img/figure_library.svg" + ); + createOnboardingTourButton( + div, + "onboarding-tour-library-button", + "onboarding.tour-library.button2" + ); + + return div; + }, + }, + singlesearch: { + id: "onboarding-tour-singlesearch", + tourNameId: "onboarding.tour-singlesearch", + getNotificationStrings(bundle) { + return { + title: bundle.GetStringFromName( + "onboarding.notification.onboarding-tour-singlesearch.title" + ), + message: bundle.GetStringFromName( + "onboarding.notification.onboarding-tour-singlesearch.message" + ), + button: bundle.GetStringFromName("onboarding.button.learnMore"), + }; + }, + getPage(win, bundle) { + let div = win.document.createElement("div"); + + createOnboardingTourDescription( + div, + "onboarding.tour-singlesearch.title", + "onboarding.tour-singlesearch.description" + ); + createOnboardingTourContent( + div, + "resource://onboarding/img/figure_singlesearch.svg" + ); + createOnboardingTourButton( + div, + "onboarding-tour-singlesearch-button", + "onboarding.tour-singlesearch.button" + ); + + return div; + }, + }, + performance: { + id: "onboarding-tour-performance", + instantComplete: true, + tourNameId: "onboarding.tour-performance", + getNotificationStrings(bundle) { + return { + title: bundle.GetStringFromName( + "onboarding.notification.onboarding-tour-performance.title" + ), + message: bundle.formatStringFromName( + "onboarding.notification.onboarding-tour-performance.message", + [BRAND_SHORT_NAME], + 1 + ), + button: bundle.GetStringFromName("onboarding.button.learnMore"), + }; + }, + getPage(win, bundle) { + let div = win.document.createElement("div"); + + createOnboardingTourDescription( + div, + "onboarding.tour-performance.title", + "onboarding.tour-performance.description" + ); + createOnboardingTourContent( + div, + "resource://onboarding/img/figure_performance.svg" + ); + + return div; + }, + }, + screenshots: { + id: "onboarding-tour-screenshots", + tourNameId: "onboarding.tour-screenshots", + getNotificationStrings(bundle) { + return { + title: bundle.GetStringFromName( + "onboarding.notification.onboarding-tour-screenshots.title" + ), + message: bundle.formatStringFromName( + "onboarding.notification.onboarding-tour-screenshots.message", + [BRAND_SHORT_NAME], + 1 + ), + button: bundle.GetStringFromName("onboarding.button.learnMore"), + }; + }, + getPage(win, bundle) { + let div = win.document.createElement("div"); + // Screenshot tour opens the screenshot page directly, see below a#onboarding-tour-screenshots-button. + // The screenshots page should be responsible for highlighting the Screenshots button + + createOnboardingTourDescription( + div, + "onboarding.tour-screenshots.title", + "onboarding.tour-screenshots.description" + ); + createOnboardingTourContent( + div, + "resource://onboarding/img/figure_screenshots.svg" + ); + + let aside = createOnboardingTourButton( + div, + "onboarding-tour-screenshots-button", + "onboarding.tour-screenshots.button", + "a" + ); + + let button = aside.querySelector("a"); + button.setAttribute("href", "https://screenshots.firefox.com/#tour"); + button.setAttribute("target", "_blank"); + + return div; + }, + }, +}; + +/** + * The script won't be initialized if we turned off onboarding by + * setting "browser.onboarding.enabled" to false. + */ +class Onboarding { + constructor(mm, contentWindow) { + this.mm = mm; + this.init(contentWindow); + } + + /** + * @param {String} action the action to ask the chrome to do + * @param {Array | Object} params the parameters for the action + */ + sendMessageToChrome(action, params) { + this.mm.sendAsyncMessage("Onboarding:OnContentMessage", { + action, + params, + }); + } + + /** + * Template code for talking to `PingCentre` + * @param {Object} data the payload for the telemetry + */ + telemetry(data) { + this.sendMessageToChrome("ping-centre", { data }); + } + + registerNewTelemetrySession(data) { + this.telemetry( + Object.assign(data, { + type: "onboarding-register-session", + }) + ); + } + + async init(contentWindow) { + this._window = contentWindow; + // session_key is used for telemetry to track the current tab. + // The number will renew after reloading the page. + this._session_key = Date.now(); + this._tours = []; + this._tourType = Services.prefs.getStringPref( + "browser.onboarding.tour-type", + "update" + ); + + let tourIds = this._getTourIDList(); + tourIds.forEach(tourId => { + if (onboardingTourset[tourId]) { + this._tours.push(onboardingTourset[tourId]); + } + }); + + if (this._tours.length === 0) { + return; + } + + // We want to create and append elements after CSS is loaded so + // no flash of style changes and no additional reflow. + await this._loadCSS(); + this._bundle = Services.strings.createBundle(BUNDLE_URI); + + this._loadJS(UITOUR_JS_URI); + + this.uiInitialized = false; + let doc = this._window.document; + if (doc.hidden) { + // When the preloaded-browser feature is on, + // it would preload a hidden about:newtab in the background. + // We don't want to show onboarding experience in that hidden state. + let onVisible = () => { + if (!doc.hidden) { + doc.removeEventListener("visibilitychange", onVisible); + this._startUI(); + } + }; + doc.addEventListener("visibilitychange", onVisible); + } else { + this._startUI(); + } + } + + _startUI() { + this.registerNewTelemetrySession({ + page: this._window.location.href, + session_key: this._session_key, + tour_type: this._tourType, + }); + + this._window.addEventListener("beforeunload", this); + this._window.addEventListener("unload", this); + this._window.addEventListener("resize", this); + this._resizeTimerId = this._window.requestIdleCallback(() => + this._resizeUI() + ); + // start log the onboarding-session when the tab is visible + this.telemetry({ + type: "onboarding-session-begin", + session_key: this._session_key, + }); + } + + _resizeUI() { + this._windowWidth = this._window.document.body.getBoundingClientRect().width; + if (this._windowWidth < ONBOARDING_MIN_WIDTH_PX) { + // Don't show the overlay UI before we get to a better, responsive design. + this.destroy(); + return; + } + + this._initUI(); + if ( + this._isFirstSession && + this._windowWidth >= SPEECH_BUBBLE_MIN_WIDTH_PX + ) { + this._overlayIcon.classList.add("onboarding-speech-bubble"); + } else { + this._overlayIcon.classList.remove("onboarding-speech-bubble"); + } + } + + _initUI() { + if (this.uiInitialized) { + return; + } + this.uiInitialized = true; + this._tourItems = []; + this._tourPages = []; + + let { body } = this._window.document; + this._overlayIcon = this._renderOverlayButton(); + this._overlayIcon.addEventListener("click", this); + this._overlayIcon.addEventListener("keypress", this); + body.insertBefore(this._overlayIcon, body.firstChild); + + this._overlay = this._renderOverlay(); + this._overlay.addEventListener("click", this); + this._overlay.addEventListener("keydown", this); + this._overlay.addEventListener("keypress", this); + body.appendChild(this._overlay); + + this._loadJS(TOUR_AGENT_JS_URI); + + this._initPrefObserver(); + this._onIconStateChange( + Services.prefs.getStringPref( + "browser.onboarding.state", + ICON_STATE_DEFAULT + ) + ); + + // Doing tour notification takes some effort. Let's do it on idle. + this._window.requestIdleCallback(() => this.showNotification()); + } + + _getTourIDList() { + let tours = Services.prefs.getStringPref( + `browser.onboarding.${this._tourType}tour`, + "" + ); + return tours + .split(",") + .filter(tourId => { + if ( + tourId === "sync" && + !Services.prefs.getBoolPref("identity.fxaccounts.enabled") + ) { + return false; + } + return tourId !== ""; + }) + .map(tourId => tourId.trim()); + } + + _initPrefObserver() { + if (this._prefsObserved) { + return; + } + + this._prefsObserved = new Map(); + this._prefsObserved.set("browser.onboarding.state", () => { + this._onIconStateChange( + Services.prefs.getStringPref( + "browser.onboarding.state", + ICON_STATE_DEFAULT + ) + ); + }); + this._tours.forEach(tour => { + let tourId = tour.id; + this._prefsObserved.set( + `browser.onboarding.tour.${tourId}.completed`, + () => { + this.markTourCompletionState(tourId); + this._checkWatermarkByTours(); + } + ); + }); + for (let [name, callback] of this._prefsObserved) { + Services.prefs.addObserver(name, callback); + } + } + + _checkWatermarkByTours() { + let tourDone = this._tours.every(tour => this.isTourCompleted(tour.id)); + if (tourDone) { + this.sendMessageToChrome("set-prefs", [ + { + name: "browser.onboarding.state", + value: ICON_STATE_WATERMARK, + }, + ]); + } + } + + _clearPrefObserver() { + if (this._prefsObserved) { + for (let [name, callback] of this._prefsObserved) { + Services.prefs.removeObserver(name, callback); + } + this._prefsObserved = null; + } + } + + /** + * Find a tour that should be selected. It is either a first tour that was not + * yet complete or the first one in the tab list. + */ + get _firstUncompleteTour() { + return ( + this._tours.find(tour => !this.isTourCompleted(tour.id)) || this._tours[0] + ); + } + + /* + * Return currently showing tour navigation item + */ + get _activeTourId() { + // We are doing lazy load so there might be no items. + if (!this._tourItems) { + return ""; + } + + let tourItem = this._tourItems.find(item => + item.classList.contains("onboarding-active") + ); + return tourItem ? tourItem.id : ""; + } + + /** + * Return current logo state as "logo" or "watermark". + */ + get _logoState() { + return this._overlayIcon.classList.contains("onboarding-watermark") + ? "watermark" + : "logo"; + } + + /** + * Return current speech bubble state as "bubble", "dot" or "hide". + */ + get _bubbleState() { + let state; + if (this._overlayIcon.classList.contains("onboarding-watermark")) { + state = "hide"; + } else if ( + this._overlayIcon.classList.contains("onboarding-speech-bubble") + ) { + state = "bubble"; + } else { + state = "dot"; + } + return state; + } + + /** + * Return current notification state as "show", "hide" or "finished". + */ + get _notificationState() { + if (this._notificationCachedState === "finished") { + return this._notificationCachedState; + } + + if (Services.prefs.getBoolPref(NOTIFICATION_FINISHED_PREF, false)) { + this._notificationCachedState = "finished"; + } else if (this._notification) { + this._notificationCachedState = "show"; + } else { + // we know it is in the hidden state if there's no notification bar + this._notificationCachedState = "hide"; + } + + return this._notificationCachedState; + } + + /** + * Return current notification prompt count. + */ + get _notificationPromptCount() { + return Services.prefs.getIntPref(PROMPT_COUNT_PREF, 0); + } + + /** + * Return current screen width and round it up to the nearest 50 pixels. + * Collecting rounded values reduces the risk that this could be used to + * derive a unique user identifier + */ + get _windowWidthRounded() { + return Math.round(this._windowWidth / 50) * 50; + } + + handleClick(target) { + let { id, classList } = target; + // Only containers receive pointer events in onboarding tour tab list, + // actual semantic tab is their first child. + if (classList.contains("onboarding-tour-item-container")) { + ({ id, classList } = target.firstChild); + } + + switch (id) { + case "onboarding-overlay-button-icon": + case "onboarding-overlay-button": + this.telemetry({ + type: "onboarding-logo-click", + bubble_state: this._bubbleState, + logo_state: this._logoState, + notification_state: this._notificationState, + session_key: this._session_key, + width: this._windowWidthRounded, + }); + this.showOverlay(); + this.gotoPage(this._firstUncompleteTour.id); + break; + case "onboarding-skip-tour-button": + this.hideNotification(); + this.hideOverlay(); + this.skipTour(); + break; + case "onboarding-overlay-close-btn": + // If the clicking target is directly on the outer-most overlay, + // that means clicking outside the tour content area. + // Let's toggle the overlay. + case "onboarding-overlay": + let eventName = + id === "onboarding-overlay-close-btn" + ? "overlay-close-button-click" + : "overlay-close-outside-click"; + this.telemetry({ + type: eventName, + current_tour_id: this._activeTourId, + session_key: this._session_key, + target_tour_id: this._activeTourId, + width: this._windowWidthRounded, + }); + this.hideOverlay(); + break; + case "onboarding-notification-close-btn": + let currentTourId = this._notificationBar.dataset.targetTourId; + // should trigger before notification-session event is sent + this.telemetry({ + type: "notification-close-button-click", + bubble_state: this._bubbleState, + current_tour_id: currentTourId, + logo_state: this._logoState, + notification_impression: this._notificationPromptCount, + notification_state: this._notificationState, + session_key: this._session_key, + target_tour_id: currentTourId, + width: this._windowWidthRounded, + }); + this.hideNotification(); + this._removeTourFromNotificationQueue(currentTourId); + break; + case "onboarding-notification-action-btn": + let tourId = this._notificationBar.dataset.targetTourId; + this.telemetry({ + type: "notification-cta-click", + bubble_state: this._bubbleState, + current_tour_id: tourId, + logo_state: this._logoState, + notification_impression: this._notificationPromptCount, + notification_state: this._notificationState, + session_key: this._session_key, + target_tour_id: tourId, + width: this._windowWidthRounded, + }); + this.showOverlay(); + this.gotoPage(tourId); + this._removeTourFromNotificationQueue(tourId); + break; + } + if (classList.contains("onboarding-tour-item")) { + this.telemetry({ + type: "overlay-nav-click", + current_tour_id: this._activeTourId, + session_key: this._session_key, + target_tour_id: id, + width: this._windowWidthRounded, + }); + this.gotoPage(id); + // Keep focus (not visible) on current item for potential keyboard + // navigation. + target.focus(); + } else if (classList.contains("onboarding-tour-action-button")) { + let activeTourId = this._activeTourId; + this.setToursCompleted([activeTourId]); + this.telemetry({ + type: "overlay-cta-click", + current_tour_id: activeTourId, + session_key: this._session_key, + target_tour_id: activeTourId, + width: this._windowWidthRounded, + }); + } + } + + /** + * Wrap keyboard focus within the dialog. + * When moving forward, focus on the first element when the current focused + * element is the last one. + * When moving backward, focus on the last element when the current focused + * element is the first one. + * Do nothing if focus is moving in the middle of the list of dialog's focusable + * elements. + * + * @param {DOMNode} current currently focused element + * @param {Boolean} back direction + * @return {DOMNode} newly focused element if any + */ + wrapMoveFocus(current, back) { + let elms = [ + ...this._dialog.querySelectorAll( + `button, input[type="checkbox"], input[type="email"], [tabindex="0"]` + ), + ]; + let next; + if (back) { + if (elms.indexOf(current) === 0) { + next = elms[elms.length - 1]; + next.focus(); + } + } else if (elms.indexOf(current) === elms.length - 1) { + next = elms[0]; + next.focus(); + } + return next; + } + + handleKeydown(event) { + let { target, key, shiftKey } = event; + + // Currently focused item could be tab container if previous navigation was done + // via mouse. + if (target.classList.contains("onboarding-tour-item-container")) { + target = target.firstChild; + } + let targetIndex; + switch (key) { + case "ArrowUp": + // Go to and focus on the previous tab if it's available. + targetIndex = this._tourItems.indexOf(target); + if (targetIndex > 0) { + let previous = this._tourItems[targetIndex - 1]; + this.handleClick(previous); + previous.focus(); + } + event.preventDefault(); + break; + case "ArrowDown": + // Go to and focus on the next tab if it's available. + targetIndex = this._tourItems.indexOf(target); + if (targetIndex > -1 && targetIndex < this._tourItems.length - 1) { + let next = this._tourItems[targetIndex + 1]; + this.handleClick(next); + next.focus(); + } + event.preventDefault(); + break; + case "Escape": + this.hideOverlay(); + break; + case "Tab": + let next = this.wrapMoveFocus(target, shiftKey); + // If focus was wrapped, prevent Tab key default action. + if (next) { + event.preventDefault(); + } + break; + default: + break; + } + event.stopPropagation(); + } + + handleKeypress(event) { + let { target, key } = event; + + if (target === this._overlayIcon) { + if ([" ", "Enter"].includes(key)) { + // Remember that the dialog was opened with a keyboard. + this._overlayIcon.dataset.keyboardFocus = true; + this.handleClick(target); + event.preventDefault(); + } + return; + } + + // Currently focused item could be tab container if previous navigation was done + // via mouse. + if (target.classList.contains("onboarding-tour-item-container")) { + target = target.firstChild; + } + switch (key) { + case " ": + case "Enter": + // Assume that the handle function should be identical for keyboard + // activation if there is a click handler for the target. + if (target.classList.contains("onboarding-tour-item")) { + this.handleClick(target); + target.focus(); + } + break; + default: + break; + } + event.stopPropagation(); + } + + handleEvent(evt) { + switch (evt.type) { + case "beforeunload": + // To make sure the telemetry pings are sent, + // we send "onboarding-session-end" ping as well as + // "overlay-session-end" and "notification-session-end" ping + // (by hiding the overlay and notificaiton) on beforeunload. + this.hideOverlay(); + this.hideNotification(); + this.telemetry({ + type: "onboarding-session-end", + session_key: this._session_key, + }); + break; + case "unload": + // Notice: Cannot do `destroy` on beforeunload, must do on unload. + // Otherwise, we would hit the docShell leak in the test. + // See Bug 1413830#c190 and Bug 1429652 for details. + this.destroy(); + break; + case "resize": + this._window.cancelIdleCallback(this._resizeTimerId); + this._resizeTimerId = this._window.requestIdleCallback(() => + this._resizeUI() + ); + break; + case "keydown": + this.handleKeydown(evt); + break; + case "keypress": + this.handleKeypress(evt); + break; + case "click": + this.handleClick(evt.target); + break; + default: + break; + } + } + + destroy() { + if (!this.uiInitialized) { + return; + } + this.uiInitialized = false; + + this._overlayIcon.dispatchEvent( + new this._window.CustomEvent("Agent:Destroy") + ); + + this._clearPrefObserver(); + this._overlayIcon.remove(); + if (this._overlay) { + // send overlay-session telemetry + this.hideOverlay(); + this._overlay.remove(); + } + if (this._notificationBar) { + // send notification-session telemetry + this.hideNotification(); + this._notificationBar.remove(); + } + this._tourItems = this._tourPages = this._overlayIcon = this._overlay = this._notificationBar = null; + } + + _onIconStateChange(state) { + switch (state) { + case ICON_STATE_DEFAULT: + this._overlayIcon.classList.remove("onboarding-watermark"); + break; + case ICON_STATE_WATERMARK: + this._overlayIcon.classList.add("onboarding-watermark"); + break; + } + return true; + } + + showOverlay() { + if (!this._tourItems.length) { + // Lazy loading until first toggle. + this._loadTours(this._tours); + } + + if ( + this._overlay && + !this._overlay.classList.contains("onboarding-opened") + ) { + this.hideNotification(); + this._overlay.classList.add("onboarding-opened"); + this.toggleModal(true); + this.telemetry({ + type: "overlay-session-begin", + session_key: this._session_key, + }); + } + } + + hideOverlay() { + if ( + this._overlay && + this._overlay.classList.contains("onboarding-opened") + ) { + this._overlay.classList.remove("onboarding-opened"); + this.toggleModal(false); + this.telemetry({ + type: "overlay-session-end", + session_key: this._session_key, + }); + } + } + + /** + * Set modal dialog state and properties for accessibility purposes. + * @param {Boolean} opened whether the dialog is opened or closed. + */ + toggleModal(opened) { + let { document: doc } = this._window; + if (opened) { + // Set aria-hidden to true for the rest of the document. + [...doc.body.children].forEach( + child => + child.id !== "onboarding-overlay" && + child.setAttribute("aria-hidden", true) + ); + // When dialog is opened with the keyboard, focus on the first + // uncomplete tour because it will be the selected tour. + if (this._overlayIcon.dataset.keyboardFocus) { + doc.getElementById(this._firstUncompleteTour.id).focus(); + } else { + // When the dialog is opened with the mouse, focus on the dialog + // itself to avoid visible keyboard focus styling. + this._dialog.focus(); + } + } else { + // Remove all set aria-hidden attributes. + [...doc.body.children].forEach(child => + child.removeAttribute("aria-hidden") + ); + // If dialog was opened with a keyboard, set the focus back to the overlay + // button. + if (this._overlayIcon.dataset.keyboardFocus) { + delete this._overlayIcon.dataset.keyboardFocus; + this._overlayIcon.focus(); + } else { + this._window.document.activeElement.blur(); + } + } + } + + /** + * Switch to proper tour. + * @param {String} tourId specify which tour should be switched. + */ + gotoPage(tourId) { + let targetPageId = `${tourId}-page`; + for (let page of this._tourPages) { + if (page.id === targetPageId) { + page.style.display = ""; + page.dispatchEvent(new this._window.CustomEvent("beforeshow")); + } else { + page.style.display = "none"; + } + } + for (let tab of this._tourItems) { + if (tab.id == tourId) { + tab.classList.add("onboarding-active"); + tab.setAttribute("aria-selected", true); + this.telemetry({ + type: "overlay-current-tour", + current_tour_id: tourId, + session_key: this._session_key, + width: this._windowWidthRounded, + }); + + // Some tours should complete instantly upon showing. + if (tab.getAttribute("data-instant-complete")) { + this.setToursCompleted([tourId]); + } + } else { + tab.classList.remove("onboarding-active"); + tab.setAttribute("aria-selected", false); + } + } + } + + isTourCompleted(tourId) { + return Services.prefs.getBoolPref( + `browser.onboarding.tour.${tourId}.completed`, + false + ); + } + + setToursCompleted(tourIds) { + let params = []; + tourIds.forEach(id => { + if (!this.isTourCompleted(id)) { + params.push({ + name: `browser.onboarding.tour.${id}.completed`, + value: true, + }); + } + }); + if (params.length) { + this.sendMessageToChrome("set-prefs", params); + } + } + + markTourCompletionState(tourId) { + // We are doing lazy load so there might be no items. + if (!this._tourItems || this._tourItems.length === 0) { + return; + } + + let completed = this.isTourCompleted(tourId); + let targetItem = this._tourItems.find(item => item.id == tourId); + let completedTextId = `onboarding-complete-${tourId}-text`; + // Accessibility: Text version of the auxiliary information about the tour + // item completion is provided via an invisible node with an aria-label that + // the tab is pointing to via aria-described by. + let completedText = targetItem.querySelector(`#${completedTextId}`); + if (completed) { + targetItem.classList.add("onboarding-complete"); + if (!completedText) { + completedText = this._window.document.createElement("span"); + completedText.id = completedTextId; + completedText.setAttribute( + "aria-label", + this._bundle.GetStringFromName("onboarding.complete") + ); + targetItem.appendChild(completedText); + targetItem.setAttribute("aria-describedby", completedTextId); + } + } else { + targetItem.classList.remove("onboarding-complete"); + targetItem.removeAttribute("aria-describedby"); + if (completedText) { + completedText.remove(); + } + } + } + + get _isFirstSession() { + // Should only directly return on the "false" case. Consider: + // 1. On the 1st session, `_firstSession` is true + // 2. During the 1st session, user resizes window so that the UI is destroyed + // 3. After the 1st mute session, user resizes window so that the UI is re-init + if (this._firstSession === false) { + return false; + } + this._firstSession = true; + + // There is a queue, which means we had prompted tour notifications before. Therefore this is not the 1st session. + if ( + Services.prefs.prefHasUserValue( + "browser.onboarding.notification.tour-ids-queue" + ) + ) { + this._firstSession = false; + } + + // When this is set to 0 on purpose, always judge as not the 1st session + if ( + Services.prefs.getIntPref( + "browser.onboarding.notification.mute-duration-on-first-session-ms" + ) === 0 + ) { + this._firstSession = false; + } + + return this._firstSession; + } + + _getLastTourChangeTime() { + return ( + 1000 * + Services.prefs.getIntPref( + "browser.onboarding.notification.last-time-of-changing-tour-sec", + 0 + ) + ); + } + + _muteNotificationOnFirstSession(lastTourChangeTime) { + if (!this._isFirstSession) { + return false; + } + + if (lastTourChangeTime <= 0) { + this.sendMessageToChrome("set-prefs", [ + { + name: + "browser.onboarding.notification.last-time-of-changing-tour-sec", + value: Math.floor(Date.now() / 1000), + }, + ]); + return true; + } + let muteDuration = Services.prefs.getIntPref( + "browser.onboarding.notification.mute-duration-on-first-session-ms" + ); + return Date.now() - lastTourChangeTime <= muteDuration; + } + + _isTimeForNextTourNotification(lastTourChangeTime) { + let maxCount = Services.prefs.getIntPref( + "browser.onboarding.notification.max-prompt-count-per-tour" + ); + if (this._notificationPromptCount >= maxCount) { + return true; + } + + let maxTime = Services.prefs.getIntPref( + "browser.onboarding.notification.max-life-time-per-tour-ms" + ); + if (lastTourChangeTime && Date.now() - lastTourChangeTime >= maxTime) { + return true; + } + + return false; + } + + _removeTourFromNotificationQueue(tourId) { + let params = []; + let queue = this._getNotificationQueue(); + params.push({ + name: "browser.onboarding.notification.tour-ids-queue", + value: queue.filter(id => id != tourId).join(","), + }); + params.push({ + name: "browser.onboarding.notification.last-time-of-changing-tour-sec", + value: 0, + }); + params.push({ + name: "browser.onboarding.notification.prompt-count", + value: 0, + }); + this.sendMessageToChrome("set-prefs", params); + } + + _getNotificationQueue() { + let queue = ""; + if ( + Services.prefs.prefHasUserValue( + "browser.onboarding.notification.tour-ids-queue" + ) + ) { + queue = Services.prefs.getStringPref( + "browser.onboarding.notification.tour-ids-queue" + ); + } else { + // For each tour, it only gets 2 chances to prompt with notification + // (each chance includes 8 impressions or 5-days max life time) + // if user never interact with it. + // Assume there are tour #0 ~ #5. Here would form the queue as + // "#0,#1,#2,#3,#4,#5,#0,#1,#2,#3,#4,#5". + // Then we would loop through this queue and remove prompted tour from the queue + // until the queue is empty. + let ids = this._tours.map(tour => tour.id).join(","); + queue = `${ids},${ids}`; + this.sendMessageToChrome("set-prefs", [ + { + name: "browser.onboarding.notification.tour-ids-queue", + value: queue, + }, + ]); + } + return queue ? queue.split(",") : []; + } + + showNotification() { + if (this._notificationState === "finished") { + return; + } + + let lastTime = this._getLastTourChangeTime(); + if (this._muteNotificationOnFirstSession(lastTime)) { + return; + } + + // After the notification mute on the 1st session, + // we don't want to show the speech bubble by default + this._overlayIcon.classList.remove("onboarding-speech-bubble"); + + let queue = this._getNotificationQueue(); + let totalMaxTime = Services.prefs.getIntPref( + "browser.onboarding.notification.max-life-time-all-tours-ms" + ); + if (lastTime && Date.now() - lastTime >= totalMaxTime) { + // Reach total max life time for all tour notifications. + // Clear the queue so that we would finish tour notifications below + queue = []; + } + + let startQueueLength = queue.length; + // See if need to move on to the next tour + if (queue.length && this._isTimeForNextTourNotification(lastTime)) { + queue.shift(); + } + // We don't want to prompt the completed tour. + while (queue.length && this.isTourCompleted(queue[0])) { + queue.shift(); + } + + if (!queue.length) { + this.sendMessageToChrome("set-prefs", [ + { + name: NOTIFICATION_FINISHED_PREF, + value: true, + }, + { + name: "browser.onboarding.notification.tour-ids-queue", + value: "", + }, + { + name: "browser.onboarding.state", + value: ICON_STATE_WATERMARK, + }, + ]); + return; + } + let targetTourId = queue[0]; + let targetTour = this._tours.find(tour => tour.id == targetTourId); + + // Show the target tour notification + this._notificationBar = this._renderNotificationBar(); + this._notificationBar.addEventListener("click", this); + this._notificationBar.dataset.targetTourId = targetTour.id; + let notificationStrings = targetTour.getNotificationStrings(this._bundle); + let actionBtn = this._notificationBar.querySelector( + "#onboarding-notification-action-btn" + ); + actionBtn.textContent = notificationStrings.button; + let tourTitle = this._notificationBar.querySelector( + "#onboarding-notification-tour-title" + ); + tourTitle.textContent = notificationStrings.title; + let tourMessage = this._notificationBar.querySelector( + "#onboarding-notification-tour-message" + ); + tourMessage.textContent = notificationStrings.message; + this._notificationBar.classList.add("onboarding-opened"); + this._window.document.body.appendChild(this._notificationBar); + + let params = []; + let promptCount = 1; + if (startQueueLength != queue.length) { + // We just change tour so update the time, the count and the queue + params.push({ + name: "browser.onboarding.notification.last-time-of-changing-tour-sec", + value: Math.floor(Date.now() / 1000), + }); + params.push({ + name: PROMPT_COUNT_PREF, + value: promptCount, + }); + params.push({ + name: "browser.onboarding.notification.tour-ids-queue", + value: queue.join(","), + }); + } else { + promptCount = this._notificationPromptCount + 1; + params.push({ + name: PROMPT_COUNT_PREF, + value: promptCount, + }); + } + this.sendMessageToChrome("set-prefs", params); + this.telemetry({ + type: "notification-session-begin", + session_key: this._session_key, + }); + // since set-perfs is async, pass promptCount directly to avoid gathering the wrong + // notification_impression. + this.telemetry({ + type: "notification-appear", + bubble_state: this._bubbleState, + current_tour_id: targetTourId, + logo_state: this._logoState, + notification_impression: promptCount, + notification_state: this._notificationState, + session_key: this._session_key, + width: this._windowWidthRounded, + }); + } + + hideNotification() { + if (this._notificationBar) { + if (this._notificationBar.classList.contains("onboarding-opened")) { + this._notificationBar.classList.remove("onboarding-opened"); + this.telemetry({ + type: "notification-session-end", + session_key: this._session_key, + }); + } + } + } + + _renderNotificationBar() { + let footer = this._window.document.createElement("footer"); + footer.id = "onboarding-notification-bar"; + footer.setAttribute("aria-live", "polite"); + footer.setAttribute( + "aria-labelledby", + "onboarding-notification-tour-title" + ); + + let section = this._window.document.createElement("section"); + section.id = "onboarding-notification-message-section"; + section.setAttribute("role", "presentation"); + footer.appendChild(section); + + let icon = this._window.document.createElement("div"); + icon.id = "onboarding-notification-tour-icon"; + icon.setAttribute("role", "presentation"); + section.appendChild(icon); + + let onboardingNotificationBody = this._window.document.createElement("div"); + onboardingNotificationBody.id = "onboarding-notification-body"; + onboardingNotificationBody.setAttribute("role", "presentation"); + section.appendChild(onboardingNotificationBody); + + let title = this._window.document.createElement("h1"); + title.id = "onboarding-notification-tour-title"; + onboardingNotificationBody.appendChild(title); + + let message = this._window.document.createElement("p"); + message.id = "onboarding-notification-tour-message"; + onboardingNotificationBody.appendChild(message); + + let actionButton = this._window.document.createElement("button"); + actionButton.id = "onboarding-notification-action-btn"; + actionButton.className = "onboarding-action-button"; + section.appendChild(actionButton); + + let closeButton = this._window.document.createElement("button"); + closeButton.id = "onboarding-notification-close-btn"; + closeButton.className = "onboarding-close-btn"; + footer.appendChild(closeButton); + + closeButton.setAttribute( + "title", + this._bundle.GetStringFromName( + "onboarding.notification-close-button-tooltip" + ) + ); + + return footer; + } + + skipTour() { + this.setToursCompleted(this._tours.map(tour => tour.id)); + this.sendMessageToChrome("set-prefs", [ + { + name: NOTIFICATION_FINISHED_PREF, + value: true, + }, + { + name: "browser.onboarding.state", + value: ICON_STATE_WATERMARK, + }, + ]); + this.telemetry({ + type: "overlay-skip-tour", + current_tour_id: this._activeTourId, + session_key: this._session_key, + width: this._windowWidthRounded, + }); + } + + _renderOverlay() { + let div = this._window.document.createElement("div"); + div.id = "onboarding-overlay"; + + this._dialog = this._window.document.createElement("div"); + this._dialog.setAttribute("role", "dialog"); + this._dialog.setAttribute("tabindex", "-1"); + this._dialog.setAttribute("aria-labelledby", "onboarding-header"); + this._dialog.id = ONBOARDING_DIALOG_ID; + div.appendChild(this._dialog); + + let header = this._window.document.createElement("header"); + header.id = "onboarding-header"; + header.textContent = this._bundle.GetStringFromName( + "onboarding.overlay-title2" + ); + this._dialog.appendChild(header); + + let nav = this._window.document.createElement("nav"); + this._dialog.appendChild(nav); + + let tourList = this._window.document.createElement("ul"); + tourList.id = "onboarding-tour-list"; + tourList.setAttribute("role", "tablist"); + nav.appendChild(tourList); + + let footer = this._window.document.createElement("footer"); + footer.id = "onboarding-footer"; + this._dialog.appendChild(footer); + + let button = this._window.document.createElement("button"); + button.id = "onboarding-overlay-close-btn"; + button.className = "onboarding-close-btn"; + button.setAttribute( + "title", + this._bundle.GetStringFromName("onboarding.overlay-close-button-tooltip") + ); + this._dialog.appendChild(button); + + // support show/hide skip tour button via pref + if ( + !Services.prefs.getBoolPref( + "browser.onboarding.skip-tour-button.hide", + false + ) + ) { + let skipButton = this._window.document.createElement("button"); + skipButton.id = "onboarding-skip-tour-button"; + skipButton.classList.add("onboarding-action-button"); + skipButton.textContent = this._bundle.GetStringFromName( + "onboarding.skip-tour-button-label" + ); + footer.appendChild(skipButton); + } + + return div; + } + + _renderOverlayButton() { + let button = this._window.document.createElement("button"); + // support customize speech bubble string via pref + let tooltipStringPrefId = ""; + let defaultTourStringId = ""; + if (this._tourType === "new") { + tooltipStringPrefId = "browser.onboarding.newtour.tooltip"; + defaultTourStringId = SPEECH_BUBBLE_NEWTOUR_STRING_ID; + } else { + tooltipStringPrefId = "browser.onboarding.updatetour.tooltip"; + defaultTourStringId = SPEECH_BUBBLE_UPDATETOUR_STRING_ID; + } + let tooltip = ""; + try { + let tooltipStringId = Services.prefs.getStringPref( + tooltipStringPrefId, + defaultTourStringId + ); + tooltip = this._bundle.formatStringFromName( + tooltipStringId, + [BRAND_SHORT_NAME], + 1 + ); + } catch (e) { + Cu.reportError(e); + // fallback to defaultTourStringId to proceed + tooltip = this._bundle.formatStringFromName( + defaultTourStringId, + [BRAND_SHORT_NAME], + 1 + ); + } + button.setAttribute("aria-label", tooltip); + button.id = "onboarding-overlay-button"; + button.setAttribute("aria-haspopup", true); + button.setAttribute("aria-controls", `${ONBOARDING_DIALOG_ID}`); + let defaultImg = this._window.document.createElement("img"); + defaultImg.id = "onboarding-overlay-button-icon"; + defaultImg.setAttribute("role", "presentation"); + defaultImg.src = Services.prefs.getStringPref( + "browser.onboarding.default-icon-src", + "chrome://branding/content/icon64.png" + ); + button.appendChild(defaultImg); + let watermarkImg = this._window.document.createElement("img"); + watermarkImg.id = "onboarding-overlay-button-watermark-icon"; + watermarkImg.setAttribute("role", "presentation"); + watermarkImg.src = Services.prefs.getStringPref( + "browser.onboarding.watermark-icon-src", + "resource://onboarding/img/watermark.svg" + ); + button.appendChild(watermarkImg); + return button; + } + + _loadTours(tours) { + let itemsFrag = this._window.document.createDocumentFragment(); + let pagesFrag = this._window.document.createDocumentFragment(); + for (let tour of tours) { + // Create tour navigation items dynamically + let li = this._window.document.createElement("li"); + // List item should have no semantics. It is just a container for an + // actual tab. + li.setAttribute("role", "presentation"); + li.className = "onboarding-tour-item-container"; + // Focusable but not tabbable. + li.tabIndex = -1; + + let tab = this._window.document.createElement("span"); + tab.id = tour.id; + tab.textContent = this._bundle.GetStringFromName(tour.tourNameId); + tab.className = "onboarding-tour-item"; + if (tour.instantComplete) { + tab.dataset.instantComplete = true; + } + tab.tabIndex = 0; + tab.setAttribute("role", "tab"); + + let tourPanelId = `${tour.id}-page`; + tab.setAttribute("aria-controls", tourPanelId); + + li.appendChild(tab); + itemsFrag.appendChild(li); + // Dynamically create tour pages + let div = tour.getPage.call(this, this._window, this._bundle); + + // Do a traverse for elements in the page that need to be localized. + let l10nElements = div.querySelectorAll("[data-l10n-id]"); + for (let i = 0; i < l10nElements.length; i++) { + let element = l10nElements[i]; + // We always put brand short name as the first argument for it's the + // only and frequently used arguments in our l10n case. Rewrite it if + // other arguments appear. + element.textContent = this._bundle.formatStringFromName( + element.dataset.l10nId, + [BRAND_SHORT_NAME], + 1 + ); + } + + div.id = tourPanelId; + div.classList.add("onboarding-tour-page"); + div.setAttribute("role", "tabpanel"); + div.setAttribute("aria-labelledby", tour.id); + div.style.display = "none"; + pagesFrag.appendChild(div); + // Cache elements in arrays for later use to avoid cost of querying elements + this._tourItems.push(tab); + this._tourPages.push(div); + + this.markTourCompletionState(tour.id); + } + + let ul = this._window.document.getElementById("onboarding-tour-list"); + ul.appendChild(itemsFrag); + let footer = this._window.document.getElementById("onboarding-footer"); + this._dialog.insertBefore(pagesFrag, footer); + } + + _loadCSS() { + // Returning a Promise so we can inform caller of loading complete + // by resolving it. + return new Promise(resolve => { + let doc = this._window.document; + let link = doc.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = ONBOARDING_CSS_URL; + link.addEventListener("load", resolve); + doc.head.appendChild(link); + }); + } + + _loadJS(uri) { + let doc = this._window.document; + let script = doc.createElement("script"); + script.type = "text/javascript"; + script.src = uri; + doc.head.appendChild(script); + } +} diff --git a/browser/extensions/onboarding/content/img/figure_addons.svg b/browser/extensions/onboarding/content/img/figure_addons.svg new file mode 100644 index 000000000000..b5f056737f11 --- /dev/null +++ b/browser/extensions/onboarding/content/img/figure_addons.svg @@ -0,0 +1 @@ +<svg width="295" height="199" viewBox="0 0 295 199" xmlns="http://www.w3.org/2000/svg"><title>addons</title><defs><linearGradient x1="-3335.765%" y1="-2236.632%" x2="5558.543%" y2="3780.103%" id="a"><stop stop-color="#CCFBFF" offset="0%"/><stop stop-color="#C9E4FF" offset="100%"/></linearGradient><linearGradient x1="-251.09%" y1="-799.657%" x2="413.095%" y2="1054.368%" id="b"><stop stop-color="#CCFBFF" offset="0%"/><stop stop-color="#C9E4FF" offset="100%"/></linearGradient><linearGradien [...] \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/figure_customize.svg b/browser/extensions/onboarding/content/img/figure_customize.svg new file mode 100644 index 000000000000..0c0cb30df5dc --- /dev/null +++ b/browser/extensions/onboarding/content/img/figure_customize.svg @@ -0,0 +1,561 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="295" height="238"> + <defs> + <linearGradient id="a" x1="-678.179817%" x2="218.03211%" y1="-1879.5122%" y2="503.09878%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="b" x1="-2438.15968%" x2="713.035484%" y1="-2346.83281%" y2="705.8875%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="c" x1="-1876.47349%" x2="477.431325%" y1="-2215.7169%" y2="536.030986%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="d" x1="-300.502319%" x2="326.878731%" y1="-277.869139%" y2="301.876261%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="e" x1="-556.386842%" x2="471.897895%" y1="-1050.94952%" y2="809.757143%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="f" x1="-2301.11875%" x2="1769.175%" y1="-4460.38%" y2="3354.584%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="g" x1="-14090.38%" x2="5447.03%" y1="-14085.94%" y2="5451.47%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="h" x1="-1245.88053%" x2="483.093805%" y1="-2962.82857%" y2="1024.39796%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="i" x1="-4762.32308%" x2="1072.27051%" y1="-2525.31233%" y2="591.799315%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="j" x1="-419.785061%" x2="175.867683%" y1="-263.047589%" y2="146.541719%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="k" x1="-13945.16%" x2="5592.25%" y1="-13931.16%" y2="5606.26%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="l" x1="-93.8791876%" x2="171.036409%" y1="-368.29%" y2="383.149231%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="m" x1="-105.119971%" x2="175.589943%" y1="-106.702736%" y2="160.566895%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="n" x1="-4526.45652%" x2="3968.06957%" y1="-3864.98889%" y2="3371.08889%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="o" x1="-1590.58053%" x2="2387.43252%" y1="-835.835705%" y2="1325.72397%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="p" x1="-1174.27536%" x2="1657.23333%" y1="-1275.87873%" y2="1781.26242%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="q" x1="-8557.56%" x2="10979.85%" y1="-4234.38%" y2="5534.325%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="r" x1="-949.737079%" x2="1245.47865%" y1="-1023.81277%" y2="1336.75514%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="s" x1="-850.555238%" x2="1010.15048%" y1="-759.279881%" y2="912.10717%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="t" x1="-2526.775%" x2="962.048214%" y1="-2513.94763%" y2="949.261152%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="u" x1="-953.117868%" x2="406.88755%" y1="-1083.71008%" y2="471.112383%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="v" x1="-1736.94827%" x2="671.463404%" y1="-2238.58822%" y2="855.656147%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="w" x1="-9592.54%" x2="9944.87%" y1="-9613.77%" y2="9923.64%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="x" x1="-546.9251%" x2="669.232184%" y1="-637.97868%" y2="716.339388%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="y" x1="-2626.25%" x2="2515.17368%" y1="-10166.57%" y2="9370.85%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="z" x1="-26076.58%" x2="9092.02%" y1="-26064.58%" y2="9104.02%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="A" x1="-11996.8348%" x2="3293.86087%" y1="-4084.84179%" y2="1164.20299%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="B" x1="-1988.44219%" x2="759.104687%" y1="-1576.81875%" y2="621.219375%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="C" x1="-4889.30185%" x2="1623.40185%" y1="-2351.25495%" y2="817.087387%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="D" x1="-2655.5559%" x2="951.48%" y1="-6714.61282%" y2="2302.97692%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="E" x1="-11418.996%" x2="2648.448%" y1="-28603.67%" y2="6564.93%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="F" x1="-1067.54883%" x2="792.163033%" y1="-899.682353%" y2="691.657014%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="G" x1="-3245.82558%" x2="2272.05861%" y1="-2753.32267%" y2="1935.824%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="H" x1="-835.133806%" x2="827.684161%" y1="-835.133806%" y2="827.684161%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="I" x1="-4541.82131%" x2="1223.52295%" y1="-2322.54576%" y2="657.84322%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="J" x1="-2057.47051%" x2="889.742903%" y1="-1738.77914%" y2="791.335971%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="K" x1="-1278.62667%" x2="1189.34526%" y1="-1278.9986%" y2="1188.97333%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="L" x1="-6112.0075%" x2="2680.1425%" y1="-6270.03333%" y2="2747.55641%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="M" x1="-1115.93023%" x2="572.391158%" y1="-1175.6355%" y2="582.7945%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="N" x1="-9656.07586%" x2="2471.02759%" y1="-9322.84667%" y2="2400.02%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="O" x1="-7887.73698%" x2="3321.17237%" y1="-6188.2325%" y2="2603.9175%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="P" x1="-984.783738%" x2="288.77261%" y1="-1902.68288%" y2="506.125342%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="Q" x1="-2522.67732%" x2="1102.95155%" y1="-5039.01837%" y2="2138.24694%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="R" x1="-5921.7225%" x2="2870.4275%" y1="-6075.45385%" y2="2942.1359%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="S" x1="-5881.53%" x2="2910.62%" y1="-5881.26%" y2="2910.89%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="T" x1="-5841.3375%" x2="2950.8125%" y1="-5841.4525%" y2="2950.6975%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="U" x1="-7423.23691%" x2="3785.67244%" y1="-5801.6425%" y2="2990.5075%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="V" x1="-4020.34%" x2="1003.74571%" y1="-2527.16182%" y2="669.983636%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="W" x1="-4517.96032%" x2="1064.35714%" y1="-5480.38654%" y2="1282.80577%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="X" x1="-3834.66828%" x2="2163.11753%" y1="-3992.49299%" y2="2248.99581%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="Y" x1="-132.800878%" x2="141.123835%" y1="-126.933901%" y2="145.268963%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="Z" x1="-8624.4%" x2="10913.01%" y1="-4751.06111%" y2="6103.05556%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="aa" x1="-20576.83%" x2="14591.77%" y1="-11391.2944%" y2="8146.81667%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ab" x1="-3210.85073%" x2="1716.38147%" y1="-3721.57455%" y2="1963.19067%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ac" x1="-964.539164%" x2="305.324758%" y1="-1877.16986%" y2="531.638356%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ad" x1="-5971.9075%" x2="2820.24%" y1="-7463.6%" y2="3526.5875%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ae" x1="-3626.20024%" x2="2128.73795%" y1="-3780.54791%" y2="2217.23789%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="af" x1="-3545.17742%" x2="2127.17742%" y1="-3793.28448%" y2="2270.26724%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ag" x1="-8571.16538%" x2="4955.21923%" y1="-4812.20217%" y2="2833.14565%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ah" x1="-921.592388%" x2="295.314187%" y1="-948.070803%" y2="335.454745%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ai" x1="-1521.4596%" x2="706.721231%" y1="-1247.46875%" y2="591.922626%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aj" x1="-678.258824%" x2="423.307164%" y1="-682.475952%" y2="429.068947%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ak" x1="-6036.96%" x2="2755.19%" y1="-6038.3275%" y2="2753.82%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="al" x1="-876.033667%" x2="359.821607%" y1="-805.490909%" y2="336.346753%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="am" x1="-6523.57663%" x2="4813.74946%" y1="-5038.58141%" y2="3749.13318%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="an" x1="-2645.94937%" x2="963.166315%" y1="-6683.46667%" y2="2334.12564%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ao" x1="-6631.98345%" x2="4705.34265%" y1="-5121.96932%" y2="3665.74527%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ap" x1="-1435.66843%" x2="1068.42563%" y1="-2846.04456%" y2="2010.54343%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aq" x1="-2633.78646%" x2="975.329221%" y1="-6654.88205%" y2="2362.70769%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ar" x1="-2206.3925%" x2="2189.6825%" y1="-2444.83034%" y2="2406.01103%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="as" x1="-5385.00363%" x2="1874.66412%" y1="-10484.884%" y2="3582.556%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="at" x1="-2391.91311%" x2="1397.1783%" y1="-5593.4125%" y2="3198.7375%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="au" x1="-2264.71662%" x2="1521.15732%" y1="-5306.3925%" y2="3485.7575%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="av" x1="-8124.26538%" x2="5402.11923%" y1="-4560.45%" y2="3084.89783%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aw" x1="-651.882139%" x2="479.56521%" y1="-1403.71323%" y2="934.962067%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ax" x1="-782.651586%" x2="579.099454%" y1="-1688.18577%" y2="1133.37245%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="ay" x1="-2808.00445%" x2="930.963547%" y1="-4874.39455%" y2="1519.89636%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="az" x1="-3080.27111%" x2="827.351111%" y1="-4651.45333%" y2="1209.98%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aA" x1="-17842.03%" x2="17326.57%" y1="-17824.13%" y2="17344.47%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aB" x1="-4927.80617%" x2="7466.4141%" y1="-2177.67416%" y2="3371.61183%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aC" x1="-20583.89%" x2="14584.71%" y1="-5842.07714%" y2="4206.09429%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aD" x1="-13953.96%" x2="21214.64%" y1="-2172.57143%" y2="3409.74603%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aE" x1="-13796.3%" x2="21372.3%" y1="-1986.00882%" y2="3185.84412%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aF" x1="-13888.17%" x2="21280.43%" y1="-2353.96379%" y2="3709.58793%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aG" x1="-9372.00909%" x2="6613.71818%" y1="-2958.36812%" y2="2138.53043%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aH" x1="-16384.5222%" x2="12067.4729%" y1="-4573.9%" y2="3418.96364%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aI" x1="-17462.5%" x2="5983.23333%" y1="-13777.5842%" y2="4732.21053%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aJ" x1="-7480.69%" x2="7500.95%" y1="-7483.33%" y2="7498.32%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aK" x1="-7021.27187%" x2="3968.91562%" y1="-20520.9909%" y2="11450.4636%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aL" x1="-9826.0913%" x2="5464.60435%" y1="-22671.15%" y2="12497.45%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aM" x1="-2964.13075%" x2="2873.3758%" y1="-3993.57709%" y2="3854.15587%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aN" x1="-2330.22879%" x2="2205.28384%" y1="-2914.60952%" y2="2667.70794%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aO" x1="-1407.98283%" x2="1424.97017%" y1="-1728.51863%" y2="1719.38333%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aP" x1="-1807.9102%" x2="1780.72245%" y1="-2740.56%" y2="2669.99385%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aQ" x1="-1472.82%" x2="1783.415%" y1="-4365.0426%" y2="5068.41814%"> + <stop stop-color="#FFFBCC" offset="0%"/> + <stop stop-color="#FFC9D5" offset="100%"/> + </linearGradient> + <linearGradient id="aR" x1="-511.087979%" x2="436.292949%" y1="-431.133333%" y2="359.905%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + <linearGradient id="aS" x1="-2336.83483%" x2="1396.15506%" y1="-7055.5%" y2="4019.03333%"> + <stop stop-color="#FFE900" offset="18.75%"/> + <stop stop-color="#FF0039" offset="100%"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd"> + <path d="M149.5 168.5c-.1 0-.1.1-.2.1l-3.3 1.5c-.2.1-.3.1-.5.2.7.3 1.4.5 2.2.5 1.6 0 3.1-.7 4.2-1.9 1-1.1 1.4-2.5 1.3-4-.1-.9-.3-1.7-.7-2.4l-1.6 4.4c-.3.6-.8 1.2-1.4 1.6zM178.7 206.1c-.1-.1-.2-.3-.2-.4l-2 2.7 3.1 1.1-.8-2.6c-.1-.2-.1-.5-.1-.8zM240.6 207.9h0zM168.5 200.6h-.2c-.2.2-.5.3-.7.4l-2.5.7.2.8c1.1.7 2 1.7 2.5 2.9l1 .4 3.7-5c.9-1.2 2.2-1.9 3.7-2l-.1-.3-2.5.7c-.2.1-.4.1-.6.1h-.2c-.2.2-.5.3-.7.4l-3.1.9c-.1-.1-.3 0-.5 0zM146.9 159.8c.1.1.2.1.3.2 0-.1.1-.2.1-.3-.1 0-.2 0-.4.1zM143. [...] + <path fill="#EDEDF0" fill-rule="nonzero" d="M227.5 226.4c.1.1.1.1.2.1 0 0-.1 0-.2-.1z"/> + <path fill="#EDEDF0" fill-rule="nonzero" d="M228.2 231c-1.2 0-2.4-.4-3.4-1.2-1.3-1.1-2.1-2-2.4-7.2-3.4 0-6.7.1-9.9.2.6 3.3.2 4.4-.7 5.6-1 1.4-2.6 2.2-4.3 2.2-2.9 0-5.3-2.1-9.6-7-15.1 1.3-25.3 3.8-25.3 6.6 0 4.3 23.1 7.7 51.6 7.7s51.6-3.4 51.6-7.7c0-3.6-16.7-6.7-39.3-7.5-2.3 5.7-5.1 8.3-8.3 8.3z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M158.9 75.5h13.4c.3 0 .6-.2.6-.6 0-.3-.2-.6-.6-.6h-13.4c-.3 0-.6.2-.6.6.1.4.3.6.6.6zM155.4 85.7c0-.3-.2-.6-.6-.6h-13.4c-.3 0-.6.2-.6.6 0 .3.2.6.6.6h13.4c.3-.1.6-.3.6-.6z"/> + <path fill="#FFF" fill-rule="nonzero" d="M134.3 114.7l.6-.4.4-.2c0-.7.1-1.3.2-2 0-.1.1-.2.1-.4-.4-.9-.8-2-1.2-3v6h-.1zM131.8 102.3c-.1-.3 0-.6.3-.7.3-.1.6 0 .7.3l.3.9V67h-13c.7 2.2 1.8 5.2 3.1 8.8.1.3 0 .6-.3.7h-.2c-.2 0-.4-.1-.5-.4-1.3-3.8-2.4-7-3.2-9.2h-3.4c1.1 3.8 3.1 10.1 5.8 18.2l-.1-.5c1.6 4.4 8.9 24.1 11.5 31l.4-.3v-9.5c-.6-1.4-1.1-2.7-1.4-3.5zM121.2 91.2c-3.9-10.9-6.6-19.6-7.9-24.2H7.1v98.7c0 .6 0 .9.1 1 .1 0 .4.1 1 .1h124c.6 0 .9 0 1-.1 0-.1.1-.4.1-1v-38.4l-1.6 1-.4.2c-.3.2- [...] + <path fill="#FFF" fill-rule="nonzero" d="M70.3 103.8c-5.6 0-10.2 4.6-10.2 10.2s4.6 10.2 10.2 10.2 10.2-4.6 10.2-10.2c.1-5.6-4.5-10.2-10.2-10.2zM137.7 124.4l-.9.6.9 2.1v-2.7zM135.3 121.7s0 .1 0 0l2.4-1.5v-.1l-2.4 1.6z"/> + <path fill="#FFF" fill-rule="nonzero" d="M134.8 126.3l-.5.3v39.1c0 1.9-.3 2.2-2.2 2.2H8.1c-1.9 0-2.2-.3-2.2-2.2V65.8h107c-.2-.8-.4-1.4-.4-1.8-.1-.6.3-1.2.9-1.3.6-.1 1.2.3 1.3.9.1.4.3 1.2.6 2.2h3.4l-.8-2.4c-.1-.3.1-.6.4-.7.3-.1.6.1.7.4 0 0 .3 1 .9 2.7h14.5v39.7c.6 1.5 1.3 3.1 1.8 4.4.4-.9.9-1.6 1.6-2.3V49.7c0-2.3-1.9-4.2-4.2-4.2H6.8c-2.3 0-4.2 1.9-4.2 4.2v118c0 2 1.8 3.7 3.9 3.7h127.3c1 0 1.9-.4 2.6-.9-.8-1.6-1.2-3.4-1.3-5.3 0-1.5.9-2.7 2.2-3.3-.8-.8-1.1-2-.7-3.1l1.1-2.9v-23.4c-1-2.1- [...] + <path fill="#FFF" fill-rule="nonzero" d="M129.1 125.6c-.1.1-.2.2-.4.2-.2.1-.4.1-.6.1.2 0 .4 0 .6-.1.2 0 .3-.1.4-.2l4.1-2.5v-.1l-4.1 2.6z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M137.7 120.2v.1l2.2-1.5M139 115.8c-.2-.5-.2-1-.3-1.5 0 .5.1 1 .3 1.5z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M133.8 171.4H6.5c-2.2 0-3.9-1.6-3.9-3.7v-118c0-2.3 1.9-4.2 4.2-4.2h126.6c2.3 0 4.2 1.9 4.2 4.2V107.6c.6-.7 1.4-1.3 2.2-1.8V81.2h27.6c.1-.2.2-.4.2-.6 0-.2.3-.2.3 0 .1.2.1.4.2.6h14.5c.6 0 1.1-.5 1.1-1.1 0-.6-.5-1.1-1.1-1.1h-42.8V49.7c0-3.6-2.9-6.5-6.5-6.5H6.8c-3.6 0-6.5 2.9-6.5 6.5v118c0 3.3 2.8 5.9 6.1 5.9h127.3c1.4 0 2.7-.5 3.7-1.2-.4-.6-.8-1.3-1.1-1.9-.7.5-1.6.9-2.5.9z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M137.7 127.1l-.9-2.1-1.9 1.2c.9 2 1.9 4.1 2.9 6.3V156l1.2-3.2c.2-.5.6-1 1-1.3v-14.1c2.6 5.5 5.2 11 7.4 15.2h.4c.7 0 1.4.1 2.1.2-3.1-6.1-6.1-12.1-8.7-17.9-.2-.5-.7-1.5-1.2-2.7V123l-2.2 1.4v2.7h-.1z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M134.3 65.8h-14.5c-.6-1.7-.9-2.7-.9-2.7-.1-.3-.4-.4-.7-.4-.3.1-.4.4-.4.7l.8 2.4h-3.4c-.3-1-.5-1.8-.6-2.2-.1-.6-.7-1-1.3-.9-.6.1-1 .7-.9 1.3.1.4.2 1 .4 1.8H6v99.8c0 1.9.3 2.2 2.2 2.2h124c1.9 0 2.2-.3 2.2-2.2v-39.1l-1.1.7v38.4c0 .6 0 .9-.1 1-.1 0-.4.1-1 .1H8.2c-.6 0-.9 0-1-.1 0-.1-.1-.4-.1-1V67h106.2c1.3 4.6 4 13.3 7.9 24.2h.1c2.5 7.2 7.1 19.3 9.6 25.7l2-1.2c-2.7-6.9-9.9-26.7-11.5-31l.1.5c-2.8-8.1-4.7-14.4-5.8-18.2h3.4c.8 2.2 1.8 5.4 3.2 9.2. [...] + <path fill="#D7D7DB" fill-rule="nonzero" d="M95.6 109.8h-7.1c-.4-2.1-1.2-4-2.3-5.8l5.1-5.1c.7-.9 1-2 .8-3.1-.2-1.1-.7-2.1-1.6-2.8-.7-.6-1.6-.8-2.5-.8-.9 0-1.8.3-2.6.9l-5.1 5.1c-1.8-1.1-3.7-1.8-5.8-2.3v-7.1c0-2.3-1.9-4.2-4.2-4.2-2.3 0-4.2 1.9-4.2 4.2v7.1c-2.1.4-4 1.2-5.8 2.3l-4.7-5.1c-.8-.8-2-1.3-3.1-1.3-1.2 0-2.3.5-3.1 1.3-.8.8-1.3 2-1.3 3.1 0 1.2.5 2.3 1.3 3.2l5.1 4.7c-1.1 1.8-1.8 3.7-2.3 5.8H45c-2.3 0-4.2 1.9-4.2 4.2 0 2.3 1.9 4.2 4.2 4.2h7.1c.4 2.1 1.2 4 2.3 5.8l-5 4.7c-1.9 1.4-2. [...] + <path fill="#F9F9FA" fill-rule="nonzero" d="M33.7 25.5h97.9c.6 0 1.1-.5 1.1-1.1 0-.6-.5-1.1-1.1-1.1h-22.8c-2-3.7-7.1-11.7-13.4-12.9-8.4-1.6-10 6.7-10 6.7S79.8 2.6 65.8 4.5c-6.5.9-9 4.2-9.8 7.8h.1c.3 0 .6.2.6.5 0 .4.1.7.1 1.1 0 .3-.2.6-.5.6h-.1c-.2 0-.4-.1-.5-.3-.1 1.9.1 3.8.5 5.3H57c-.1-.3-.2-.6-.4-1-.1-.3.1-.6.4-.7.3-.1.6.1.7.4.3 1 .6 1.7.6 1.7.1.2.1.4 0 .5-.1.2-.3.3-.5.3h-1.3c.4 1.5.9 2.5.9 2.7H33.7c-.6 0-1.1.5-1.1 1.1 0 .5.5 1 1.1 1z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M205.5 42.3c.1 0 .3-.1.4-.2.6-.7 1.5-1.1 2.6-1.4.3-.1.5-.4.4-.7-.1-.3-.4-.5-.7-.4-1.3.4-2.4.9-3.1 1.7-.2.2-.2.6 0 .8.1.2.3.2.4.2zM212.7 40.5c.4.1.7.2 1 .3h.2c.2 0 .5-.1.5-.4.1-.3-.1-.6-.4-.7-.4-.1-.8-.2-1.1-.3-.3-.1-.6.1-.7.4 0 .4.2.7.5.7zM238.3 50.7h3.3c.3 0 .6-.2.6-.6 0-.3-.2-.6-.6-.6h-3.3c-.3 0-.6.2-.6.6 0 .4.3.6.6.6zM221.2 46.7c.3-1 1.2-3.2 3.8-3.2.3 0 .7 0 1 .1 1.6.3 3.2 1.3 4.8 3 .2.2.6.2.8 0 .2-.2.2-.6 0-.8-1.8-1.9-3.6-3-5.4-3.4-.4-. [...] + <path fill="#F9F9FA" fill-rule="nonzero" d="M191.7 54.6h54.4c.6 0 1.1-.5 1.1-1.1 0-.6-.5-1.1-1.1-1.1h-12.9c-1.1-2.1-3.9-6.5-7.4-7.2-2.4-.5-3.8.5-4.6 1.6 0 .1-.1.2-.2.3-.6 1-.8 1.9-.8 1.9s-3.1-8-10.9-7c-5.7.8-6 5-5.4 7.8h.7s.1-.1.2-.1c.3-.1.6 0 .7.3l.1.1c.1.2.1.4 0 .5-.1.2-.3.3-.5.3h-.9c.2.9.5 1.4.5 1.5h-13.2.2c-.6 0-1.1.5-1.1 1.1-.1.6.4 1.1 1.1 1.1z"/> + <path fill="#EDEDF0" fill-rule="nonzero" d="M107.4 231.1c-4 0-5.8-2.5-6.2-4.6l-.1-.5c-7 .5-12.1 2.1-12.1 4 0 2.3 7.3 4.1 16.3 4.1s16.3-1.8 16.3-4.1c0-1.4-2.7-2.6-6.7-3.3-.2.7-.6 1.3-1 1.9-2 2.4-5.7 2.5-6.5 2.5z"/> + <path fill="#FFF" fill-rule="nonzero" d="M227.3 225.7c-.1-.3-.1-.6-.1-1 0 .4 0 .7.1 1zM228 226.5h-.1.1zM226.9 216v0zM199.5 218.8c.3.3.5.6.8.9-.3-.3-.6-.6-.8-.9z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M237.7 208.7c1-.3 1.9-.5 2.8-.8h.1c6.5-2 12.4-4.7 17.4-7.6-7.2 3.5-15 5-20 5.6 0 1-.1 1.9-.3 2.8z"/> + <path fill="#FFF" fill-rule="nonzero" d="M241.9 163c.1-.2.2-.4.2-.6 0 .2-.1.4-.2.6zM234.1 70.2c-.3 0-.5-.1-.8-.1-.3 0-.7 0-1 .1.3 0 .7-.1 1-.1.2 0 .5 0 .8.1zM232 70.2c-2.5.4-4.6 2.2-5.5 4.5.9-2.3 3-4 5.5-4.5zM219.1 84.9c0-.1 0-.1 0 0 0-.1 0-.1 0 0zM221.2 79.8c.5-.5 1.1-.9 1.7-1.3-.6.3-1.2.8-1.7 1.3zM226 76.7c-.4.3-.7.7-1 1-.7.1-1.4.4-2.1.7.6-.3 1.3-.6 2.1-.7.3-.3.7-.6 1-1zM207.3 226.3s0-.1 0 0h-.1c0-.1.1 0 .1 0zM263.6 172.3c-.4.1-.8.3-1.2.4.4-.1.8-.2 1.2-.4zM248.7 65.6h.8c-.3.1-.5 0- [...] + <path fill="#F9F9FA" fill-rule="nonzero" d="M235.8 218c-.5 1.4-1.1 3.2-2 4.9-1.5 3.1-3.5 5.9-5.8 5.9-.7 0-1.3-.2-1.8-.6-.6-.5-1.2-.9-1.4-5.4-.1-1.5-.1-3.5-.1-6.2-.5 0-1-.1-1.6-.2l-.7-.2c0 2.8 0 4.9.1 6.6.2 5.1 1 6.1 2.4 7.2 1 .8 2.1 1.2 3.4 1.2 3.2 0 6-2.7 8.4-8.1.6-1.3 1.2-2.8 1.7-4.4.7-2 1.3-4.8 1.9-8.2-.9.3-1.9.5-2.9.8-.5 2.6-1.1 4.9-1.6 6.7zM265 198.7c-2.4 1.6-5 3.1-7.8 4.7 1.6-.7 3.1-1.4 4.6-2.3h.2c3.7 0 7.1-.5 10.2-1.4-1.7-.2-3.3-.6-4.7-1.3-.8.1-1.6.2-2.5.3z"/> + <path fill="#FFF" fill-rule="nonzero" d="M284.9 173c.8-.7 1.8-1.2 2.9-1.2.4 0 .7 0 1 .1h.1c-.7-.6-1.5-1.1-2.4-1.2-.3-.1-.6-.1-.8-.1-.8 0-1.6.2-2.3.6l-.2.2c.6.6 1.2 1.1 1.7 1.6zM287.7 188c.3-.6.6-1.1.9-1.7-.4.3-.8.6-1.3.8.1.4.3.6.4.9z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M266.3 154.9c-1.2.6-2.1.9-2.7 1.2-.3.1-.6.2-.8.3-.2.1-.3.1-.3.1-.2.1-.4.1-.7.1-.6 0-1.2-.3-1.7-.7-6.4 3.3-13.7 6.8-16.1 7.9-.1.2-.2.4-.2.6.8-.1 1.7-.1 2.5-.1 3.5 0 6.8.6 9.7 1.8 2.7 1.1 5 2.5 7 4.3 6.4-2.4 7.9-7.8 7.9-8 .2-.9.9-1.5 1.8-1.7h.3c.8 0 1.5.4 1.9 1.1.1.2 1.7 2.9 2.3 7.1 1 .2 2 .6 2.9 1-.5-5.5-2.5-9.1-2.9-9.7-.9-1.4-2.4-2.3-4-2.3-.2 0-.5 0-.7.1-1.9.3-3.4 1.7-3.9 3.5 0 .1-1 3.6-5.1 5.7-1.9-1.4-4-2.7-6.4-3.7-1.3-.6-2.8-1-4.2-1.3 5.1 [...] + <path fill="#FFF" fill-rule="nonzero" d="M265.3 137.7h.5-.5zM246.3 166.4h-.3H246.3z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M284.3 126.3l1.2-1.8c4.6-2.2 7.4-7.2 6.8-12.3v-.2c1.4-2.3 2-5 1.7-7.7-.3-2.4-1.3-4.6-2.8-6.4-.3-2.1-.7-4.1-1.3-6.1v-1c0-4-2-7.7-5.3-9.9-2.8-4.1-6.5-7.8-10.6-10.6-1.8-4.4-6.2-7.3-11-7.3-1.4 0-2.9.3-4.3.8-.8-.2-1.6-.4-2.4-.5-2.1-1.6-4.7-2.5-7.3-2.5-.5 0-1 0-1.5.1-2.2.3-4.4 1.2-6.1 2.6-2.1.4-4.2 1.1-6.2 1.9-.6-.1-1.1-.1-1.7-.1-5.2 0-9.9 3.5-11.4 8.4-4.4 1.8-7.4 6.2-7.4 11.1v.2c-.2.5-.4 1-.6 1.4-.4.4-.8.7-1.2 1.2-.1-2.2-1.1-4.2-2.2-6.3-.2-.4-.4 [...] + <path fill="#FFF" fill-rule="nonzero" d="M287.7 101.9c-.4-.6-.9-1.1-1.4-1.5.6.4 1 .9 1.4 1.5zM286.9 111.2c.2.6.4 1.2.5 1.9 0 .2 0 .5.1.7 0-.2 0-.5-.1-.7-.1-.7-.3-1.3-.5-1.9zM289 106c0-.3 0-.6-.1-.9 0-.4-.1-.7-.2-1.1.1.3.2.7.2 1.1.1.3.1.6.1.9zM263.6 137.5c-.3-.1-.7-.1-1-.2h-.1.1c.3.1.6.1 1 .2zM259.6 140.2c.4-.1.7-.2 1.1-.4-.4.2-.7.4-1.1.4zM188.5 192.2v0zM218.2 88.3c-.2.4-.4.9-.5 1.3-.2.1-.3.1-.5.2.1-.1.3-.2.5-.2.1-.4.3-.9.5-1.3zM186 170.6c0-.1-.1-.1-.1-.2 0 .1 0 .2.1.2zM215.8 97.7c0-. [...] + <path fill="#F9F9FA" fill-rule="nonzero" d="M207.5 230.7c1.7 0 3.3-.8 4.3-2.2.9-1.2 1.3-2.3.7-5.6-.3-2-1.1-4.8-2.2-8.8 1 0 2 .1 3 .1h2.1l-3.8-1.1-4.8-.6c1.6 5.2 2.4 8.5 2.9 10.6.6 3.2.3 3.7-.2 4.3-.5.8-1.4 1.2-2.3 1.2-1.7 0-3.4-1.3-6.6-4.9-.8-.9-1.8-2-2.9-3.3-3.3-3.8-5.6-8.6-7.1-12.7-1-.5-2-1.1-2.9-1.7 1.6 4.8 4.3 11 8.4 15.8.6.8 1.3 1.5 1.8 2.1 4.4 4.8 6.7 6.8 9.6 6.8zM185.9 201.9c1.2.9 2.4 1.7 3.6 2.5l-.3-.9c-2.1-3-3.1-7-3-11.4-.4-.3-.8-.5-1.2-.8-.2-.2-.3-.5-.1-.8.2-.2.5-.3.8-.1.2. [...] + <path fill="#FFF" fill-rule="nonzero" d="M206.8 158.8c.5-.4 1-.9 1.6-1.3-.6.4-1.1.9-1.6 1.3z"/> + <path fill="url(#a)" fill-rule="nonzero" d="M237.4 200.4c-.1 1-.2 2-.2 2.9 4.1-.4 10.5-1.6 17.1-4.5 1.7-.8 3.3-1.6 4.7-2.6-.7-.2-1.4-.6-1.9-1.1-3.5 1.9-8.3 4-14.2 4.8-1.9.2-3.7.4-5.5.5z"/> + <path fill="url(#b)" fill-rule="nonzero" d="M268.8 169.3c1.6-.6 3.4-.9 5.1-.9.4 0 .7 0 1.1.1-.4-2.3-1.1-4-1.5-4.9-.1-.3-.3-.5-.3-.6v-.1s0 .1-.1.3c0 .1-.1.2-.1.3-.1.1-.1.3-.2.5-.7 1.2-1.8 3.3-4 5.3z"/> + <path fill="url(#c)" fill-rule="nonzero" d="M274.9 176.9c-.3 0-.6-.1-.9-.1-2.6 0-5 1.4-6.3 3.6-.3.5-.7 1-1.1 1.4l1.6.4c0-.1.1-.2.2-.3l.9-.7c.2-.2.6-.1.8.1.2.2.1.6-.1.8l-.5.4.9.2c.8.2 1.5.6 2 1.2.7-1.4 1.3-2.8 1.8-4.1.2-1 .5-1.9.7-2.9z"/> + <path fill="url(#d)" fill-rule="nonzero" d="M189.9 156.3c-2.8-1.8-6-2.6-9.1-2.6-5.6 0-11.1 2.8-14.3 7.8-5 7.9-2.7 18.3 5.2 23.3 2.8 1.8 6 2.6 9.1 2.6 2.1 0 4.2-.4 6.1-1.1.3-1.5.7-3.1 1.2-4.6-.5 0-1-.1-1.5-.4-1.5-.8-2.1-2.6-1.3-4s2.6-1.9 4.1-1.1c.3.2.5.3.7.5.6-1.3 1.3-2.6 2-3.9-3.2.4-4.2.5-4.7.5h-.6c-1-.1-2.3-.6-2.9-1.8-.5-.8-.7-2.3.5-4.3.5-.7 1.1-1.9 10.6-5.9-1.4-1.9-3-3.7-5.1-5zm-14.1 2.1c1.7 0 3.1 1.3 3.1 2.9 0 1.6-1.4 2.9-3.1 2.9-1.7 0-3.1-1.3-3.1-2.9 0-1.6 1.4-2.9 3.1-2.9zm-8.7 1 [...] + <path fill="url(#e)" fill-rule="nonzero" d="M204.7 160.7c-9.4 3.5-17.8 8.2-17.9 8.3-.1.1-.2.1-.3.1-.2 0-.4-.1-.5-.3-.2.3-.3.6-.3.9v.6c0 .1 0 .2.1.2 0 .1.1.1.1.2.4.5 1.2.5 1.2.5H188c.2 0 .4 0 .6-.1.3 0 .6-.1.9-.1h.2c.3 0 .6-.1.9-.1h.2c.4 0 .7-.1 1.1-.2h.1c.9-.1 2-.3 3.1-.5 2.9-4 5-6.2 5.1-6.4.2-.2.6-.2.8 0 .1.1.2.2.2.4 1.1-1.2 2.2-2.3 3.5-3.5z"/> + <path fill="url(#f)" fill-rule="nonzero" d="M187.9 167.1c-.1 0-.1.1-.2.1s-.1.1-.2.1c1.1-.6 2.7-1.4 4.8-2.5-1.8.9-3.4 1.7-4.4 2.3z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M213.3 204.7c-.7-.2-1.3-.7-1.7-1.2l-.4 1.3 1.9.5c.3-.1.7-.2 1-.3l-.8-.3z"/> + <path fill="#FFF" fill-rule="nonzero" d="M188.7 193.1c0 .1-.1.1-.1.1v1.6c0 .4.1.8.2 1.2 0 .2.1.4.1.5l.3 1.2c0 .2.1.3.1.5.1.4.3.8.4 1.2v.1c.8 1.6 2.9 4.5 8.2 5.4.3.1.5.3.4.6-.1.3-.3.5-.5.5h-.1c-2.7-.5-4.6-1.5-6-2.5l.3.9c.2.5.3 1 .5 1.5 1.4.7 2.7 1.2 4.1 1.7 2.4.8 4.8 1.4 7.2 1.9 0-.1-.1-.3-.1-.4 0-.1-.1-.3-.1-.4-.2-.5-.4-1-.5-1.5-.1-.3-.2-.6-.3-.8h.2c0-.6 0-1.1.2-1.7l1.2-4.1c-.7-.2-1.5-.4-2.2-.6-.3-.1-.5-.4-.4-.7.1-.3.4-.5.7-.4.7.2 1.5.4 2.2.6l4.1-14.3c.7-2.4 2.9-4 5.4-4 .5 0 1 .1 1.6 [...] + <path fill="url(#g)" fill-rule="nonzero" d="M203.6 209.1c0-.1 0-.1 0 0-.1-.2-.2-.3-.2-.4.1.1.1.2.2.4z"/> + <path fill="url(#h)" fill-rule="nonzero" d="M222.3 203.8l-1.9-.6c0 .1 0 .2-.1.3-.4 1.3-1.6 2.2-2.9 2.2-.3 0-.6 0-.9-.1l-2.4-.7c-.3.1-.7.2-1 .3l10 2.9 1.3-4.5c-.4.2-.8.3-1.3.3-.2.1-.5 0-.8-.1z"/> + <path fill="#EDEDF0" fill-rule="nonzero" d="M227.1 224.7c0 .4.1.7.1 1 0 .3.1.5.2.6 0 .1.1.1.1.1.1.1.1.1.2.1H228.3s.1 0 .1-.1c.1 0 .1-.1.2-.1l.1-.1c.1 0 .1-.1.2-.1l.1-.1.2-.2.1-.1c.1-.1.2-.2.2-.3l.1-.1c.1-.2.3-.3.4-.5v-.1c.1-.2.2-.3.4-.5 0-.1.1-.2.1-.2.1-.1.2-.3.3-.4.1-.1.1-.2.2-.3.1-.1.1-.2.2-.3-1.4 0-2.9-.1-4.3-.1v.9c.2.2.2.5.2.9z"/> + <path fill="url(#i)" fill-rule="nonzero" d="M230 212.6c-.5 1.6-1.6 2.8-3.1 3.5v7.5c0 .4.1.7.1 1.1 0 .4.1.7.1 1 0 .3.1.5.2.6 0 .1.1.1.1.1.1.1.1.1.2.1H228.2s.1 0 .1-.1c.1 0 .1-.1.2-.1l.1-.1c.1 0 .1-.1.2-.1l.1-.1.2-.2.1-.1c.1-.1.2-.2.2-.3l.1-.1c.1-.2.3-.3.4-.5v-.1c.1-.2.2-.3.4-.5 0-.1.1-.2.1-.2.1-.1.2-.3.3-.4.1-.1.1-.2.2-.3.1-.1.1-.2.2-.3 0 0 0-.1.1-.1.1-.1.1-.2.2-.3.1-.2.2-.3.2-.5.1-.1.1-.2.2-.4s.2-.4.2-.5c.1-.1.1-.3.2-.4.1-.2.2-.4.2-.6.1-.1.1-.3.2-.4.1-.2.2-.5.3-.7 0-.1.1-.2.1-.4.1-.4 [...] + <path fill="url(#j)" fill-rule="nonzero" d="M240.1 167.1c-.1.2-.3.3-.5.3h-.2c-.3-.1-.4-.4-.3-.7.4-.9.9-2.1 1.4-3.2-5.6 9-7.5 16.2-9.5 22.5l.9.3c1.4.4 2.6 1.4 3.3 2.7.7 1.3.9 2.8.5 4.3l-4.9 17.1c1.6-.3 3.2-.6 4.7-.9v-.1-.1c.1-.6.2-1.1.2-1.7v-.1c0-.2.1-.5.1-.7 0-.1-.1-.2 0-.3.5-4.5 1.9-23.7 1.9-23.9 0-.3.3-.5.6-.5s.5.3.5.6c0 .1-.7 9.5-1.3 16.7 1.7-.1 3.5-.2 5.3-.5 5.6-.8 10.3-2.8 13.7-4.7-.5-.9-.6-2-.4-3l1.9-7.3c.4-1.4 1.4-2.4 2.6-2.8-.9-1.2-1-2.9-.3-4.3.8-1.6 2-3.1 3.3-4.3-.4.1-.8.3-1 [...] + <path fill="url(#k)" fill-rule="nonzero" d="M202.7 206.4c.1.2.2.5.3.8 0-.3-.1-.5-.1-.8h-.2z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M194.4 210.8c.3.6.6 1.3 1 1.9 0 .1.1.1.1.2.3.6.7 1.3 1.1 1.9 0 .1.1.1.1.2l1.2 1.8.1.1c.5.6.9 1.3 1.5 1.9.3.3.5.6.8.9.1.1.1.2.2.2l.6.6c.1.1.2.2.2.3.2.2.3.4.5.5.1.1.2.2.2.3.2.2.3.3.4.5.1.1.2.2.2.3l.5.5.2.2.2.2.4.4.1.1.5.5.2.2.3.3.2.2.3.3c.1.1.1.1.2.1.1.1.2.1.3.2l.1.1c.1.1.2.1.3.2 0 0 .1 0 .1.1.1.1.2.1.3.2h.1c.1 0 .2.1.2.1h.6c.1 0 .2-.1.2-.2 0 0 .1-.1.1-.2v-.1-.3-.1c0-.2 0-.4-.1-.6v-.2c0-.2-.1-.4-.1-.6v-.2c0-.2-.1-.4-.1-.6v-.2-.1c-.1-.3-.1-.6- [...] + <path fill="url(#l)" fill-rule="nonzero" d="M241.8 136.3c-7.4 7.4-10.4 9.3-19.4 11-14.7 1.3-34.1-.7-39.2-3.2-5.6-2.7-12-17.9-12-18.1-.1-.5-.5-.8-1-.8-.4 2.5.9 6.1 2.9 9.7.3.6.7 1.2 1.1 1.8.6.9 1.2 1.8 1.8 2.6.4.6.8 1.1 1.2 1.6.4.5.8 1 1.3 1.5.2.2.4.5.6.7.4.4.8.8 1.3 1.2.2.2.4.3.6.5.8.6 1.6 1.1 2.3 1.4 6.8 2.5 16.4 4.1 26.3 4.1 1.7 0 3.4-.1 5.1-.2h.2c.1 0 12.1-1.3 17.8-3.6.3-.1.6 0 .7.3.1.3 0 .6-.3.7-3.8 1.5-10.1 2.6-14.2 3.2-.3.2-.6.3-1 .5 10.2 0 19.5-1.3 23.1-4.7 4.3-4 3.1-12.5.8-10.2z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M250.4 158.2c-3.6 1.7-6.5 3.1-7.6 3.6 2.1-1 4.8-2.2 7.6-3.6z"/> + <path fill="url(#m)" fill-rule="nonzero" d="M224.6 99.7c.7 0 1.4-.1 2.1-.1v-.2c0-.3.3-.6.6-.5l3.3.1c.3-1.1.7-1.7.8-1.7.2-.2.5-.3.8-.1.2.2.3.5.1.8-.1.1-1.4 1.9-.1 4.9.6 1.5 2.9 2.8 3.8 3.2.2.1.3.3.3.5 0 0 .4 4.6 3.5 8.4 3.4 4.2 8.4 5.7 8.5 5.7.2.1.4.2.4.4 0 0 .6 3.3 1.9 4.6.7.7 2.6 2.6 7.4 1.7.3-.1.6.1.6.4.1.3-.1.6-.4.6-1 .2-1.9.3-2.7.3-3.1 0-4.7-1.2-5.8-2.2-1.3-1.3-1.9-3.9-2.1-4.8-1.1-.4-4.5-1.7-7.4-4.6.6 1 1.2 1.9 2 2.9v.1c0 3.5 2.3 6.4 5.4 7.4.7 1.5 1.3 3.2 1.6 5.3h.1c.3-.1.6.1.7.4 [...] + <path fill="url(#n)" fill-rule="nonzero" d="M233 105.8c.5.6 1.1 1.2 1.8 1.6.1.3.3.7.5 1.1-.1-.6-.2-1.1-.3-1.4-.4-.3-1.2-.7-2-1.3z"/> + <path fill="url(#o)" fill-rule="nonzero" d="M202.5 90.4c1.3 1.2 3.4.6 4.4-.9v-.1c0-.1.3-5.4-2.2-7.4 0 0-.1 0-.1.1l-.2.2s-.1.1-.1.2c-.1.1-.1.2-.2.3 0 .1-.1.1-.1.2-.1.1-.1.2-.2.4 0 .1-.1.1-.1.2-.1.2-.2.3-.3.5 0 .1-.1.1-.1.2-.1.2-.2.5-.3.7 0 .1 0 .1-.1.2-.1.2-.2.4-.2.6 0 .1-.1.2-.1.3-.1.2-.1.3-.2.5 0 .1-.1.2-.1.3 0 .2-.1.3-.1.5 0 .1 0 .2-.1.3 0 .1-.1.3-.1.4V89.7c0 .1 0 .2.1.3v.2c0 .1.1.3.2.3.1-.2.1-.2.2-.1z"/> + <path fill="url(#p)" fill-rule="nonzero" d="M209.1 94.2V94c-.1-.5-.3-1.4-.7-2.6-.1-.3-.2-.5-.3-.7-.6 2.1-3.7 3.3-5.8 1.5 0 .4-.1.7-.1 1.1 0 .6 0 1.1.1 1.6s.2 1 .4 1.3c.1.1.1.2.2.2.1.1.3.2.4.3.1.1.3.1.4.2 2.1.7 4.6-.6 5.4-2.7z"/> + <path fill="url(#q)" fill-rule="nonzero" d="M204 98c0 .3.1.5.1.8-.1-.7-.2-1.3-.3-2 .1.4.1.8.2 1.2z"/> + <path fill="url(#r)" fill-rule="nonzero" d="M210.6 95.5c-.4 2.7-3.6 4.7-6.5 3.5v.2c.1.4.2.8.2 1.1.4 1.6 1 2.9 1.6 3.4 2.8.5 6.9-1.5 7.1-4.5v-.5c-.2-.6-.5-1.4-1.1-2.1-.4-.4-.9-.8-1.3-1.1z"/> + <path fill="url(#s)" fill-rule="nonzero" d="M214.2 112c-.1 0-.1-.1 0 0-.2-.4 0-.7.3-.8.2-.1 3.9-1.4 3.9-5.2 0-2.6-2.1-4.5-3.5-5.5.2 3.4-3.6 6.1-7 6 1.5 2.5 3.4 3 3.5 3 .6.1 1 .7.9 1.3-.1.5-.6.9-1.1.9.2.2.5.3.7.3.6.3 1.5.2 2.3 0z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M240.4 163.4c-.5 1.1-1 2.3-1.4 3.2-.1.3 0 .6.3.7h.2c.2 0 .4-.1.5-.3.7-1.7 1.6-3.7 2.1-4.6.1-.1.1-.3.2-.4.2-.1.3-.2.5-.2 1-.5 4-1.9 7.6-3.6 3-1.4 6.1-3 9-4.5 0-.3.1-.6.3-.9 0 0 0-.1.1-.2-5.5 2.5-17.4 8.1-18 8.5-.3.2-.8 1.2-1.4 2.3z"/> + <path fill="url(#t)" fill-rule="nonzero" d="M275.8 140c-.7.2-1.3.6-2 .9v.4c.4 1.9 1.8 3.4 3.5 4.2.5-.6.9-1.1 1.3-1.7.3-.5.6-.9.8-1.3-.8-.2-1.5-.6-2-1.1-.4-.5-.7-1-.9-1.5-.2-.1-.4 0-.7.1z"/> + <path fill="url(#u)" fill-rule="nonzero" d="M273.3 149.1c.1 0 .1-.1.2-.1l.6-.5c.2-.1.4-.3.6-.4.1-.1.2-.2.3-.2-.3-.2-.7-.4-1-.7-1.4-1.1-2.5-2.7-3-4.4-.1.1-.2.1-.3.2-.2.1-.4.3-.6.4l-.6.5c-.2.2-.4.3-.6.5-.6.5-1.2 1-1.7 1.5-.6.5-1.1 1-1.6 1.5-.9.9-1.8 1.9-2.6 2.9s-1.4 1.8-1.7 2.2c-.2.3-.3.5-.4.7l-.1.2c-.2.3-.2.7-.1 1 .1.4.3.7.6.8.3.2.7.2 1 .1 0 0 .1 0 .3-.1.2-.1.4-.1.7-.3.6-.2 1.4-.6 2.6-1.1h.1c-1.2-2.3-.6-5.1-.6-5.3.1-.6.7-1 1.3-.8.4.1.7.4.8.8 1.8-.4 4.1 0 5.8.6z"/> + <path fill="url(#v)" fill-rule="nonzero" d="M281.4 141.3c.9-.2 2.7-.9 4.2-3.5-1-.8-2.6.8-3.3-.6-.6-1.1.1-1.5-.4-2.1h-.6c-1.6 0-2.9.7-3.5 1.9-.6 1.1-.3 2.5.6 3.5.6.8 1.8 1.1 3 .8z"/> + <path fill="#FFF" fill-rule="nonzero" d="M267.5 148.5c.1.2.1.4 0 .6 0 0-.5 2.5.5 4.1.5.8 1.3 1.2 2.4 1.4 1.3.2 2.3 0 3.1-.6 1.2-.9 1.4-2.7 1.4-2.7 0-.4.3-.7.6-.9-.3-.3-.7-.5-1.1-.8-.3-.2-.7-.4-1.2-.5-1.6-.6-3.9-1-5.7-.6z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M148 139.7c-.3.1-.4.5-.3.7 2 4.3 4 8.2 6 12 .1.2.3.3.5.3.1 0 .2 0 .3-.1.3-.1.4-.5.2-.8-2-3.7-4-7.6-6-11.9-.1-.2-.4-.4-.7-.2zM160.4 163.3c.1 0 .2 0 .3-.1.3-.2.3-.5.2-.8-.6-.9-1.2-1.9-1.8-2.8-.1-.1-.2-.2-.4-.3.6 1.2 1 2.4 1.2 3.7.2.2.4.3.5.3zM143.3 129.9h-.2v.4l.4 1c.1.2.3.3.5.3h.2c.3-.1.4-.5.3-.7l-.4-1h-.8zM283.4 171.4c1.5-1.3 3.1-2.7 4.6-4.1.2-.2.2-.6 0-.8-.2-.2-.6-.2-.8 0-1.7 1.5-3.3 3-5 4.4.3.2.6.4.9.7.2-.1.2-.2.3-.2zM185 190.5c-.2.2-.1.6 [...] + <path fill="#FFFEFE" fill-rule="nonzero" d="M235.2 188.8c-.7-1.3-1.9-2.3-3.3-2.7l-.9-.3-15.3-4.4c-.5-.1-1-.2-1.6-.2-2.5 0-4.7 1.7-5.4 4l-4.1 14.3-.3 1.1-1.2 4.1c-.2.6-.2 1.1-.2 1.7 0 .3 0 .5.1.8.1.5.2 1 .5 1.5.1.1.1.2.1.3 0 0 0 .1.1.1.1.2.2.4.4.5.7 1 1.7 1.7 2.9 2.1l4.8 1.4 3.8 1.1 7 2 .7.2c.5.1 1 .2 1.6.2.8 0 1.5-.2 2.2-.5 1.5-.7 2.6-1.9 3.1-3.5l.7-2.3 4.9-17.1c.3-1.5.1-3.1-.6-4.4zm-1.7 3.7l-5.6 19.4c-.4 1.5-1.8 2.4-3.2 2.4-.3 0-.6 0-.9-.1l-16.2-4.7c-1.8-.5-2.8-2.4-2.3-4.2l5.6-19.4c [...] + <path fill="#FFFEFE" fill-rule="nonzero" d="M228.7 191.1l-12.9-3.7c-.2 0-.3-.1-.5-.1-.7 0-1.4.5-1.6 1.2l-4.7 16.2c-.3.9.3 1.8 1.2 2.1l12.9 3.7c.2 0 .3.1.5.1.7 0 1.4-.5 1.6-1.2l4.7-16.2c.2-.9-.4-1.9-1.2-2.1zm-14.4 7.2c.1-.4.4-.6.8-.6h.2l8.1 2.3c.4.1.7.6.6 1-.1.4-.4.6-.8.6h-.2l-8.1-2.3c-.5-.1-.7-.5-.6-1zm-.9 3.2c.1-.4.4-.6.8-.6h.2l3.2.9c.4.1.7.6.6 1-.1.4-.4.6-.8.6h-.2l-3.2-.9c-.5 0-.8-.5-.6-1zm9.7 6.7l-10-2.9-1.9-.5.4-1.3c.4.6 1 1 1.7 1.2l.9.2 2.4.7c.3.1.6.1.9.1 1.4 0 2.6-.9 2.9-2.2 0- [...] + <path fill="#FFFEFE" fill-rule="nonzero" d="M166.9 216.4c-.3-.9-1.1-1.5-2-1.5-.2 0-.4 0-.6.1-1.1.3-1.7 1.5-1.4 2.6.3.9 1.1 1.5 2 1.5.2 0 .4 0 .6-.1h.2c1-.3 1.6-1.4 1.3-2.4-.1-.1-.1-.2-.1-.2zM163.9 207.1c-.3-.8-1.1-1.3-1.9-1.3-.3 0-.5 0-.8.1-1.1.4-1.6 1.6-1.2 2.7.3.8 1.1 1.3 1.9 1.3.3 0 .5 0 .8-.1.1 0 .1 0 .2-.1 1-.4 1.5-1.5 1.1-2.5l-.1-.1zM136.1 109.9c-.3.6-.5 1.2-.6 1.8 0 .1-.1.2-.1.4-.1.7-.2 1.3-.2 2l-.4.2-.6.4-.9.6-.2.1-.4.3-2 1.2-2.7 1.7-2.3 1.4c-.3.1-.5.3-.7.5-1.8 1.4-2.5 3.9-1. [...] + <path fill="#D7D7DB" fill-rule="nonzero" d="M154.8 144.4l-2.2-4.7c-.1-.3-.5-.4-.7-.3-.3.1-.4.5-.3.7l2.2 4.7c.1.2.3.3.5.3.1 0 .2 0 .2-.1.3 0 .4-.4.3-.6zM161 185.3c.1.2.3.2.5.2.1 0 .2 0 .3-.1.3-.2.3-.5.1-.8l-2.5-3.5c-.2-.3-.5-.3-.8-.1-.3.2-.3.5-.1.8l2.5 3.5z"/> + <path fill="#E1E1E6" fill-rule="nonzero" d="M179 214.8l-3.8-1.3c-.2-.1-.3 0-.5.1-.1.1-.2.2-.2.3-.1.3.1.6.4.7l3.8 1.3h.2c.2 0 .5-.1.5-.4v-.4c-.1-.2-.2-.3-.4-.3zM179.7 181.2c.1 0 .3 0 .4-.1.2-.2.3-.5.1-.8l-2.7-3.2-1.6-1.9c-.2-.2-.5-.3-.8-.1-.2.2-.3.5-.1.8l.9 1.1 3.3 4c.2.1.4.2.5.2z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M268.1 159.5l-6 5.5c-.2.2-.2.6 0 .8.1.1.3.2.4.2.1 0 .3 0 .4-.1l6-5.5c.2-.2.2-.6 0-.8-.2-.3-.5-.3-.8-.1zM125.9 112.2c.1.2.3.4.5.4h.2c.3-.1.4-.4.3-.7l-1.7-4.7c-.1-.3-.4-.4-.7-.3-.3.1-.4.4-.3.7l1.7 4.6z"/> + <path fill="#FFF" fill-rule="nonzero" d="M98.9 188.6c.6-.9 1.6-1.5 2.5-1.7-1.1.2-2.2.8-2.9 1.8-.2.2-.3.5-.4.7h.2c.2-.2.3-.5.6-.8zM113 187.5c-.7-.2-1.3-.4-2-.3-1.6 0-3.3.8-4.3 2.1 0 .1.1.2.1.3 1.4-2 3.9-2.8 6.2-2.1zM95.5 213.7c.3.6.7 1.2 1.3 1.6.5.4 1.1.6 1.7.6l-.1-.1c-.6-.1-1.2-.3-1.7-.7-.6-.4-1-.9-1.2-1.4zM90.5 210.7c-2-1.5-2.9-4-2.4-6.3-.6 2.4.3 5 2.4 6.5 1.1.8 2.4 1.1 3.6 1.1.3 0 .7 0 1-.1v-.2c-1.6.4-3.2.1-4.6-1zM91.6 191.8c.4-.5.8-1 1.3-1.4-.6.4-1.2.9-1.6 1.6-1.8 2.5-1.4 5.9.8 7. [...] + <path fill="#FFF" fill-rule="nonzero" d="M114.5 211.4c.3.1.5.4.5.7-.4 1.9-1 4.2-2.5 5.8l-.1.1c-.5.6-1.2 1.1-2 1.4.9-.4 1.6-.9 2.1-1.5l.1-.1c1.4-1.6 2-3.9 2.4-5.8.1-.3-.1-.6-.5-.7h-.1c-.3 0-.5.2-.6.5v.1c.1-.3.4-.5.7-.5z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M121.5 195.5c.4-1.4.5-2.9.2-4.4-.4-2.7-1.9-5.1-4.2-6.8-1.8-1.3-3.9-2-6.1-2-1.4 0-2.7.3-4 .8-1.4-.9-3.1-1.3-4.8-1.3-2.4 0-4.7.9-6.4 2.5-3.3.1-6.5 1.8-8.4 4.5-1.9 2.7-2.5 6.2-1.6 9.3-.3.3-.6.7-.9 1.1-3.5 4.9-2.4 11.7 2.5 15.2 1.2.8 2.5 1.4 3.8 1.8.6 1 1.4 1.9 2.4 2.6 1.2.9 2.6 1.4 4 1.5.7.6 1.5 1 2.4 1.5l.7 4.2.1.5c.3 2.1 2.2 4.6 6.2 4.6.7 0 4.4-.1 6.5-2.6.5-.6.8-1.2 1-1.9.2-.8.3-1.6.2-2.4l-.3-2.1c.4-.3.8-.7 1.2-1.1l.2-.2c2.2-2.5 3.1-5.7 3.5- [...] + <path fill="url(#w)" fill-rule="nonzero" d="M108.2 214.3c.1 0 .2-.1.3-.1-.1 0-.2 0-.3.1z"/> + <path fill="url(#x)" fill-rule="nonzero" d="M110.3 225.1l-.9-5.4c.1 0 .2-.1.2-.1.2-.1.5-.1.7-.2.8-.3 1.5-.8 2-1.4l.1-.1c1.4-1.6 2.1-3.9 2.5-5.8.1-.3-.1-.6-.5-.7-.3-.1-.6.1-.7.4-.2 1.2-.6 2.6-1.1 3.7v-.1c0 .1 0 .1-.1.2-.2-.5-.4-1-.5-1.4-.1-.2-.1-.3-.2-.4-.1-.3-.4-.5-.7-.4-.1 0-.1.1-.2.1-.1.2-.2.4-.1.6 0 .1.1.3.2.5.2.4.5 1 .6 1.6.2.6.1.9 0 .9l-.1.1c-.4.5-1 .9-1.6 1.1-.2.1-.3.1-.5.2h-.1l-.5-3.3c-.9.3-1.8.4-2.2.4h-.4c-.3 0-.5-.3-.5-.6 0-.1.1-.2.2-.3-.7 0-1.3-.2-2-.4h.1l.5 3s-.1 0-.1-.1c- [...] + <path fill="#EDEDF0" fill-rule="nonzero" d="M107.5 226.5h.4c.7-.1 1.4-.3 1.8-.5-1.2-.1-2.5-.1-3.8-.1v.1c.2.4.8.5 1.6.5z"/> + <path fill="url(#y)" fill-rule="nonzero" d="M107.5 226.5h.4c.7-.1 1.4-.3 1.8-.5-1.2-.1-2.5-.1-3.8-.1v.1c.2.4.8.5 1.6.5z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M105.2 203.6c.8-.3 1.5-.5 2.3-.8-.8.3-1.6.6-2.3.8zM102.8 204.3c-.7.2-1.5.3-2 .5.6-.2 1.3-.3 2-.5zM98.8 214s0 .1 0 0c.2.7.6 1.3.9 1.7h.1c-.5-.5-.8-1-1-1.7zM107.4 202.8c.7-.3 1.4-.6 1.9-.8-.5.2-1.2.5-1.9.8zM99.6 212.7s.1 0 0 0c.1.1.1 0 0 0z"/> + <path fill="#FFF" fill-rule="nonzero" d="M105.8 214.7c.1-.1.3-.2.4-.2 0 0 1 .1 2-.3.1 0 .2-.1.3-.1h.1c.4-.2.8-.5 1.2-.8l.1-.1.4-.4.5-.5c.1-.1.1-.2.2-.3.6-.9 1-2 1.1-3h.6c2 0 4-1 5.3-2.8 1.9-2.7 1.4-6.4-1-8.5-.1-.1-.3-.2-.5-.4-.3-.2-.6-.4-1-.6-.1 0-.1-.1-.2-.1.2-.2.4-.4.5-.6 1.8-2.6 1.2-6.1-1.4-7.9-.4-.3-.9-.5-1.3-.7-2.2-.7-4.8.1-6.2 2.1 0-.1-.1-.2-.1-.3-.1.1-.1.2-.2.2-.3-.8-.9-1.5-1.6-2-.8-.6-1.7-.8-2.7-.8-.3 0-.5.1-.8.1-1 .3-1.9.8-2.5 1.7-.2.3-.4.6-.5.9h-.2c0 .1-.1.1-.1.2-.6-.2-1.2- [...] + <path fill="url(#z)" fill-rule="nonzero" d="M203.6 209.1c.1.2.1.3.1.5.1 0 .2 0 .3.1-.1-.3-.3-.4-.4-.6z"/> + <path fill="url(#A)" fill-rule="nonzero" d="M227 221.9v-1.3-1.4-1.4-.7-1c-.7.3-1.5.5-2.2.5 0 2.6 0 4.6.1 6.2h2.2c-.1-.3-.1-.6-.1-.9z"/> + <path fill="url(#B)" fill-rule="nonzero" d="M203.3 223.1l-.2-.2-.5-.5c-.1-.1-.2-.2-.2-.3-.1-.2-.3-.3-.4-.5-.1-.1-.2-.2-.2-.3-.2-.2-.3-.4-.5-.5-.1-.1-.2-.2-.2-.3l-.6-.6c-.1-.1-.1-.2-.2-.2-.3-.3-.5-.6-.8-.9-.5-.6-1-1.2-1.5-1.9l-.1-.1-1.2-1.8c0-.1-.1-.1-.1-.2-.4-.6-.7-1.2-1.1-1.9 0-.1-.1-.1-.1-.2-.3-.6-.7-1.3-1-1.9 0-.1 0-.1-.1-.2-.3-.6-.5-1.1-.7-1.7-1-.4-2-.9-3-1.4 1.6 4.2 3.9 8.9 7.1 12.7 1.1 1.3 2 2.3 2.9 3.3.9-.1 1.9-.1 2.8-.2 0 0 0-.1-.1-.2z"/> + <path fill="url(#C)" fill-rule="nonzero" d="M204.7 212.6c0 .1 0 .1.1.2.1.4.2.8.4 1.2v.1c.1.4.2.7.3 1.1 0 .1.1.2.1.3.1.4.2.7.3 1.1v.1l.3 1.2c0 .1.1.2.1.3.1.3.2.6.2.9 0 .1.1.2.1.3.1.4.2.7.3 1.1 0 .1 0 .1.1.2.1.3.1.6.2.8 0 .1 0 .2.1.3.1.3.1.6.2.9V223c.7 0 1.5-.1 2.3-.1-.4-2.1-1.3-5.4-2.9-10.6-.8-.1-1.6-.3-2.5-.4.1.3.2.5.3.7z"/> + <path fill="url(#D)" fill-rule="nonzero" d="M214.9 199.4l8.1 2.3h.2c.4 0 .7-.2.8-.6.1-.4-.1-.9-.6-1l-8.1-2.3h-.2c-.4 0-.7.2-.8.6-.1.4.1.8.6 1z"/> + <path fill="url(#E)" fill-rule="nonzero" d="M267.5 198.4l-1.2-.6c-.4.3-.9.6-1.3.9.9-.1 1.7-.2 2.5-.3z"/> + <path fill="url(#F)" fill-rule="nonzero" d="M151.3 155.4c-1.2-.4-2.4-.6-3.6-.6-2.5 0-4.9.9-6.8 2.5l1.2-3.3c.1-.3 0-.7-.4-.8h-.2c-.3 0-.5.2-.6.4l-1.1 2.8v4.4l5.2 2h.2c.3 0 .5-.2.6-.4.1-.3 0-.7-.4-.8l-3.7-1.4c1.5-1.8 3.7-2.7 5.8-2.7 1.8 0 3.6.6 5.1 1.9 3.2 2.8 3.6 7.7.7 10.9-1.5 1.8-3.7 2.7-5.8 2.7-1.8 0-3.6-.6-5.1-1.9-1.7-1.5-2.7-3.6-2.7-5.9v2.5c0 1.1-.3 2.2-.9 3 1.9 2.8 5 4.6 8.6 4.6h.1c5.7-.1 10.3-4.7 10.2-10.4.2-4.2-2.4-8-6.4-9.5z"/> + <path fill="url(#G)" fill-rule="nonzero" d="M144.1 167.6l.8.3.1.2 3.3-1.5c.2-.1.3-.3.3-.4l1.8-4.8c.1-.3 0-.7-.4-.8h-.2c-.3 0-.5.2-.6.4l-1.7 4.6-3.1 1.3c-.3 0-.4.4-.3.7z"/> + <path fill="url(#H)" fill-rule="nonzero" d="M163 112.5c.1.5.7.5.8 0 1.2-4.8 5-8.6 9.8-9.8.5-.1.5-.7 0-.8-4.8-1.2-8.6-5-9.8-9.8-.1-.5-.7-.5-.8 0-1.2 4.8-5 8.6-9.8 9.8-.5.1-.5.7 0 .8 4.8 1.2 8.5 5 9.8 9.8z"/> + <path fill="url(#I)" fill-rule="nonzero" d="M234.8 212.7c-.1.5-.2 1.1-.3 1.6v.1c-.1.5-.2 1-.4 1.5-.1.5-.3.9-.4 1.4-.1.4-.3.8-.4 1.1 0 .1-.1.3-.1.4-.1.2-.2.5-.3.7-.1.1-.1.3-.2.4-.1.2-.2.4-.2.6-.1.1-.1.3-.2.4-.1.2-.2.4-.2.5-.1.1-.1.3-.2.4-.1.2-.2.3-.2.5-.1.1-.1.2-.2.3 0 0 0 .1-.1.1.8 0 1.7 0 2.5.1.8-1.7 1.5-3.5 2-4.9.6-1.7 1.1-4 1.6-6.9l-2.4.6c-.1.3-.1.6-.2 1l-.1.1z"/> + <path fill="url(#J)" fill-rule="nonzero" d="M191.9 204.4l-.3-.9c1.4 1.1 3.3 2 6 2.5h.1c.3 0 .5-.2.5-.5.1-.3-.1-.6-.4-.6-5.2-1-7.3-3.8-8.2-5.4-.1-.4-.3-.8-.4-1.2 0-.1-.1-.3-.1-.5l-.3-1.2c0-.2-.1-.4-.1-.5-.1-.4-.1-.8-.2-1.2v-.6-1c-.1.1-.2.1-.3.1-.1 0-.2 0-.3-.1-.5-.4-1.1-.7-1.6-1.1-.1 4.4.9 8.4 3 11.4l.3.9c1 .6 1.9 1.1 2.9 1.6-.3-.6-.4-1.2-.6-1.7z"/> + <path fill="url(#K)" fill-rule="nonzero" d="M178.1 85.3c-.1-.3-.5-.3-.6 0-.8 3.2-3.4 5.8-6.6 6.6-.3.1-.3.5 0 .6 3.2.8 5.8 3.4 6.6 6.6.1.3.5.3.6 0 .8-3.2 3.4-5.8 6.6-6.6.3-.1.3-.5 0-.6-3.3-.9-5.8-3.4-6.6-6.6z"/> + <path fill="url(#L)" fill-rule="nonzero" d="M180.4 204.7l3.1-.9-.9-3-3.1.9"/> + <path fill="url(#M)" fill-rule="nonzero" d="M176.8 200.9c-.9 0-1.9.4-2.5 1.2l-4.6 6.2-3.6-1.3-.1-.5c-.4-1.2-1.3-2.2-2.5-2.6l-.7-2.3-3.1.9.5 1.7c-2 1-2.8 3.4-1.8 5.4.7 1.4 2.1 2.2 3.6 2.2.6 0 1.2-.1 1.8-.4.6-.3 1.2-.8 1.5-1.4l2.3.8-1.7 2.2c-.3-.1-.7-.1-1-.1-1.8 0-3.4 1.2-3.9 3-.6 2.1.7 4.3 2.9 4.9.3.1.7.1 1 .1 1.8 0 3.4-1.2 3.9-3 .2-.7.2-1.5-.1-2.2-.1-.3-.1-.5-.2-.8l10.3-13.5c-.6-.3-1.3-.5-2-.5zm-13.9 8.8c-.1 0-.1 0-.2.1-.2.1-.5.1-.8.1-.8 0-1.6-.5-1.9-1.3-.4-1.1.1-2.3 1.2-2.7.2-.1.5-. [...] + <path fill="url(#N)" fill-rule="nonzero" d="M274.7 179.9c.4-1.1 1.2-2 2.3-2.4-.4-.2-.8-.3-1.2-.4-.3-.1-.6-.1-.9-.2-.2 1-.4 1.9-.8 3h.6z"/> + <path fill="url(#O)" fill-rule="nonzero" d="M180.9 206.3l.9 3.1c1.7-.5 2.6-2.3 2.1-4l-3 .9z"/> + <path fill="url(#P)" fill-rule="nonzero" d="M284.7 187.9c-.4-.2-.7-.3-1-.3-.7 0-1.3.3-1.6.9-1.7 3.1-4.9 4.9-8.3 4.9-.8 0-1.5-.1-2.3-.3-2.9-.7-5.3-2.8-6.4-5.6l3.7 1c.2 0 .3.1.5.1.8 0 1.6-.6 1.8-1.4.3-1-.3-2-1.4-2.3l-7.3-1.9c-.2 0-.3-.1-.5-.1-.8 0-1.6.6-1.8 1.4l-1.9 7.3c-.3 1 .3 2 1.4 2.3.2 0 .3.1.5.1.8 0 1.6-.6 1.8-1.4l.5-2c2.4 4.4 6.9 6.9 11.5 6.9 2.1 0 4.2-.5 6.1-1.5 2.3-1.3 4.3-3.2 5.5-5.6.5-.9.2-2-.8-2.5z"/> + <path fill="url(#Q)" fill-rule="nonzero" d="M172.7 212.8l2.1.8c.1-.1.3-.1.5-.1l3.8 1.3c.2 0 .3.2.3.3 1.3 0 2.6-.9 3-2.2l-7.7-2.7-2 2.6z"/> + <path fill="url(#R)" fill-rule="nonzero" d="M176.1 196l-.9-3-3.1.9 1 3"/> + <path fill="url(#S)" fill-rule="nonzero" d="M171.5 197.4l-.9-3.1-3.1 1 1 3"/> + <path fill="url(#T)" fill-rule="nonzero" d="M166.9 198.8l-.9-3.1-3.1 1 1 3"/> + <path fill="url(#U)" fill-rule="nonzero" d="M162.3 200.2l-.9-3.1c-1.7.5-2.6 2.3-2.1 4l3-.9z"/> + <path fill="url(#V)" fill-rule="nonzero" d="M274.7 180c-.2 0-.4-.1-.6-.1-.4 1.3-1 2.7-1.8 4.1.9 1 1.3 2.4 1 3.8-.3 1.3-1.3 2.3-2.5 2.8l.9.3c3-2.5 5.1-4.5 6.1-5.5l-.3-.1c-1.1-.3-2-1-2.5-1.9-.6-1-.7-2.1-.4-3.1 0-.1.1-.2.1-.3z"/> + <path fill="url(#W)" fill-rule="nonzero" d="M274.9 191.2c2.2-.3 4.2-1.7 5.3-3.7v-.1c.3-.4.6-.8 1-1.1l-1-.3s0 .1-.1.1c0 .2-1.9 2.2-5.2 5.1z"/> + <path fill="url(#X)" fill-rule="nonzero" d="M187.6 158.4c-1.4-.9-3.2-.6-4 .7-.8 1.3-.3 3 1.1 3.9 1.4.9 3.2.6 4-.7.8-1.2.3-3-1.1-3.9z"/> + <path fill="url(#Y)" fill-rule="nonzero" d="M276.7 136.6c-.3.7-.4 1.4-.4 2.1l-.9.3c-1.2.5-2.5 1.1-3.7 1.8-.2.1-.4.3-.7.4-.4.2-.7.5-1.2.8-.2.1-.4.3-.6.4l-.6.5c-.1.1-.3.2-.4.3h-.8c-1.1.1-9.8 2-18 3.9.6-1.9 1.2-3.8 1.6-5.8 1.3 0 2.7-.1 4-.2 1 .9 2.3 1.3 3.7 1.3 2.1 0 3.9-1.1 4.9-2.7.5.1 1 .1 1.5.1 4.8 0 9.1-3 10.7-7.5 3-1 5.2-3.7 5.7-6.9.6-.9 1.2-1.8 1.8-2.8 4-1.5 6.6-5.7 6-10 0-.4-.1-.7-.2-1.1 1.5-1.9 2.1-4.4 1.8-6.8-.3-2.1-1.2-4.1-2.7-5.6-.3-2.4-.8-4.7-1.5-7 .1-.4.1-.9.1-1.3 0-3.3-1.7 [...] + <path fill="#FFF" fill-rule="nonzero" d="M219.1 84.8c0-.6.1-1.3.3-1.8-.2.5-.3 1.2-.3 1.8z"/> + <path fill="url(#Z)" fill-rule="nonzero" d="M219.1 84.8c0-.6.1-1.3.3-1.8-.2.5-.3 1.2-.3 1.8z"/> + <path fill="url(#aa)" fill-rule="nonzero" d="M219.1 84.8c0-.6.1-1.3.3-1.8-.2.5-.3 1.2-.3 1.8z"/> + <path fill="url(#ab)" fill-rule="nonzero" d="M178.6 177.5c-.4-.2-.8-.3-1.1-.4l2.7 3.2c.2.2.2.6-.1.8-.1.1-.2.1-.4.1s-.3-.1-.4-.2l-3.3-4c-.9.1-1.6.6-2 1.3-.2.3-.3.7-.3 1.1 1.2 1.3 2.4 2.6 3.6 3.7 1.4.3 2.7-.2 3.3-1.2.7-1.4-.2-3.4-2-4.4z"/> + <path fill="url(#ac)" fill-rule="nonzero" d="M267.8 172.1c-2.3 1.3-4.3 3.2-5.5 5.6-.5.9-.1 2.1.8 2.5h.1l.4.1c.2 0 .3.1.5.1.7 0 1.4-.4 1.7-1.1 1.7-3 4.9-4.8 8.2-4.8.8 0 1.6.1 2.4.3 2.9.7 5.3 2.8 6.4 5.6l-3.7-1c-.2 0-.3-.1-.5-.1-.8 0-1.6.6-1.8 1.4-.3 1 .3 2 1.4 2.3l7.3 1.9c.2 0 .3.1.5.1.8 0 1.6-.6 1.8-1.4l1.9-7.3c.3-1-.3-2-1.4-2.3-.2 0-.3-.1-.5-.1-.8 0-1.6.6-1.8 1.4l-.5 2c-2.4-4.4-6.9-6.9-11.5-6.9-2.2.2-4.3.7-6.2 1.7z"/> + <path fill="url(#ad)" fill-rule="nonzero" d="M180.7 194.6c-.4-1.4-1.7-2.3-3.1-2.3-.3 0-.6 0-.9.1l.9 3.1 3.1-.9z"/> + <path fill="url(#ae)" fill-rule="nonzero" d="M172.6 172.1c.8-1.4.2-3.2-1.3-4-1.5-.8-3.3-.3-4.1 1.1-.8 1.4-.2 3.2 1.3 4 1.5.8 3.3.3 4.1-1.1z"/> + <path fill="url(#af)" fill-rule="nonzero" d="M175.8 164.2c1.7 0 3.1-1.3 3.1-2.9 0-1.6-1.4-2.9-3.1-2.9-1.7 0-3.1 1.3-3.1 2.9 0 1.6 1.4 2.9 3.1 2.9z"/> + <path fill="url(#ag)" fill-rule="nonzero" d="M224.1 117.1c.4 0 .9.1 1.3.1 0-.1.1-.2.1-.3v-3c0-.7-.6-1.3-1.3-1.3-.7 0-1.3.6-1.3 1.3v3c0 .1 0 .2.1.3.2-.1.7-.1 1.1-.1z"/> + <path fill="url(#ah)" fill-rule="nonzero" d="M237.5 199.2c.6-7.2 1.2-16.6 1.3-16.7 0-.3-.2-.6-.5-.6s-.6.2-.6.5c0 .2-1.4 19.4-1.9 23.9v.3c0 .2-.1.5-.1.7v.1c-.1.6-.2 1.1-.2 1.7v.2c.8-.2 1.6-.4 2.3-.6.1-.9.3-1.8.4-2.8 5.1-.6 12.8-2.1 20-5.6 2.3-1.3 4.3-2.6 6.2-3.9-.5-.4-1-.8-1.5-1.3-.8.7-1.8 1.2-2.9 1.2-.3 0-.7 0-1-.1-1.4.9-3 1.8-4.7 2.6-6.6 3-13 4.1-17.1 4.5.1-.9.2-1.8.2-2.9 1.8-.1 3.6-.2 5.5-.5 5.9-.9 10.7-2.9 14.2-4.8-.2-.2-.4-.5-.6-.8 0 0 0-.1-.1-.2-3.4 1.9-8 3.8-13.7 4.7-1.8.2-3.5. [...] + <path fill="url(#ai)" fill-rule="nonzero" d="M264.9 127.4c1 0 2.1-.3 3.3-1.3 2.4-2 2.5-4.3 2.3-5.6 1.4-.2 2.8-.8 4.2-2.2 3.6-3.7 2.9-7.8 2.1-9.4-.3-.5-1-.8-1.5-.5-.5.3-.8 1-.5 1.5 0 0 1.7 3.4-1.7 6.8-2.9 2.9-5.8 1.1-6.1.9-.5-.4-1.2-.2-1.6.3-.4.5-.2 1.2.3 1.6.8.5 2.1 1.1 3.7 1.2.2.9.2 2.9-1.9 4.7-2.6 2.1-4.8.3-4.9.2-.2-.2-.6-.2-.8.1-.2.2-.2.6.1.8 0-.2 1.2.9 3 .9z"/> + <path fill="url(#aj)" fill-rule="nonzero" d="M249.6 126.6c1 1 2.7 2.2 5.8 2.2.8 0 1.7-.1 2.7-.3.3-.1.5-.3.4-.6-.1-.3-.3-.5-.6-.4-4.9.9-6.7-1-7.4-1.7-1.3-1.3-1.9-4.5-1.9-4.6 0-.2-.2-.4-.4-.4-.1 0-5.1-1.5-8.5-5.7-3.1-3.9-3.5-8.4-3.5-8.4 0-.2-.1-.4-.3-.5-.8-.4-3.2-1.7-3.8-3.2-1.3-3.1.1-4.9.1-4.9.2-.2.2-.6-.1-.8-.2-.2-.6-.2-.8.1 0 0-.5.6-.8 1.7l-3.3-.1c-.3 0-.6.2-.6.5v.2c.1.2.3.4.5.4l3.2.1c0 .9.1 2 .6 3.2.4.9 1.2 1.7 2 2.3.8.6 1.6 1.1 2.1 1.3 0 .3.1.8.3 1.4.4 1.8 1.3 4.7 3.5 7.3.4.5.8 1 [...] + <path fill="url(#ak)" fill-rule="nonzero" d="M179 200.2l3.1-1-.9-3-3.1.9"/> + <path fill="url(#al)" fill-rule="nonzero" d="M231.2 188.3l-16.2-4.7c-.3-.1-.6-.1-.9-.1-1.5 0-2.8 1-3.2 2.4l-5.6 19.4c-.5 1.8.5 3.7 2.3 4.2l16.2 4.7c.3.1.6.1.9.1 1.5 0 2.8-1 3.2-2.4l5.6-19.4c.5-1.8-.5-3.7-2.3-4.2zm-1.4 4.9l-4.7 16.2c-.2.7-.9 1.2-1.6 1.2-.2 0-.3 0-.5-.1l-12.9-3.7c-.9-.3-1.4-1.2-1.2-2.1l4.7-16.2c.2-.7.9-1.2 1.6-1.2.2 0 .3 0 .5.1l12.9 3.7c.9.2 1.5 1.2 1.2 2.1z"/> + <path fill="url(#am)" fill-rule="nonzero" d="M99.3 199.1c-.2-.6-.9-1-1.5-.7-.6.2-1 .9-.7 1.5l.8 2.4c.6-.6 1.4-.9 2.2-.8l-.8-2.4z"/> + <path fill="url(#an)" fill-rule="nonzero" d="M224.4 196.8l-8.1-2.3h-.2c-.4 0-.7.2-.8.6-.1.4.1.9.6 1l8.1 2.3h.2c.4 0 .7-.2.8-.6.1-.4-.2-.8-.6-1z"/> + <path fill="url(#ao)" fill-rule="nonzero" d="M108 198.9c.6-.6 1.4-.9 2.2-.8l-.8-2.4c-.2-.6-.9-1-1.5-.7-.6.2-1 .9-.7 1.5l.8 2.4z"/> + <path fill="url(#ap)" fill-rule="nonzero" d="M106.2 206.7c1-.5 2-1 3.1-.7.1-.1.3-.2.4-.4.5-.5.9-1.1 1.3-1.8.2-.4.4-.7.6-1.1.2-.4.3-.9.5-1.4l.1-.2v-.1c0-.1-.1-.2-.2-.1-.2.1-.5.1-.7.2-.3.1-.7.2-1 .3l-1.7.5c-1.2.3-2.3.7-3.5 1.1-1.1.4-2.3.8-3.4 1.2l-1.7.6c-.3.1-.6.3-1 .4-.2.1-.5.2-.7.3 0 0-.1 0-.1.1-.1.1 0 .2 0 .3l.2.1c.4.3.8.6 1.2.8.4.2.8.4 1.1.6.7.3 1.4.5 2.1.6.2 0 .4 0 .5.1.1-.1.1-.2.2-.2.7-.9 1.7-1 2.7-1.2zm-5.4-1.9c.6-.1 1.3-.3 2-.5-.7.2-1.4.3-2 .5zm6.6-2c.7-.3 1.4-.6 1.9-.8-.5.2-1. [...] + <path fill="url(#aq)" fill-rule="nonzero" d="M225.3 193.6l-8.1-2.3h-.2c-.4 0-.7.2-.8.6-.1.4.1.9.6 1l8.1 2.3h.2c.4 0 .7-.2.8-.6.1-.4-.2-.9-.6-1z"/> + <path fill="url(#ar)" fill-rule="nonzero" d="M164 84.3c-.2 0-.2.3 0 .3 1.8.5 3.3 1.9 3.7 3.7 0 .2.3.2.3 0 .5-1.8 1.9-3.3 3.7-3.7.2 0 .2-.3 0-.3-1.6-.4-2.9-1.6-3.5-3.1h-.7c-.6 1.5-1.9 2.7-3.5 3.1z"/> + <path fill="url(#as)" fill-rule="nonzero" d="M213.9 202.6l3.2.9h.2c.4 0 .7-.2.8-.6.1-.4-.1-.9-.6-1l-3.2-.9h-.2c-.4 0-.7.2-.8.6-.1.4.2.9.6 1z"/> + <path fill="url(#at)" fill-rule="nonzero" d="M227.9 119.2l-.3-.3c-.1-.1-.2-.2-.3-.2-.7-.5-1.8-1.1-3.2-1.1-2.9 0-4.4 2.2-4.5 2.3-.3.5-.2 1.2.3 1.5.5.3 1.2.2 1.5-.3 0 0 .9-1.3 2.6-1.3 1.1 0 1.9.5 2.3.9l.2.2.2.2c.2.3.6.5.9.5.2 0 .4-.1.6-.2.5-.3.7-1 .3-1.5.1 0-.1-.3-.6-.7z"/> + <path fill="url(#au)" fill-rule="nonzero" d="M196.8 121.5c.5.3 1.2.2 1.5-.3 0 0 .1-.2.4-.4.4-.4 1.2-.9 2.2-.9 1.7 0 2.6 1.3 2.6 1.3.2.3.6.5.9.5.2 0 .4-.1.6-.2.5-.3.7-1 .3-1.5-.1-.1-1.5-2.3-4.5-2.3-1.9 0-3.2 1-3.9 1.7l-.3.3c-.2.2-.3.4-.3.4-.2.4 0 1.1.5 1.4z"/> + <path fill="url(#av)" fill-rule="nonzero" d="M200.9 117.1c.4 0 .9.1 1.3.1 0-.1.1-.2.1-.3v-3c0-.7-.6-1.3-1.3-1.3-.7 0-1.3.6-1.3 1.3v3c0 .1 0 .2.1.3.3-.1.7-.1 1.1-.1z"/> + <path fill="url(#aw)" fill-rule="nonzero" d="M207.7 137.3l-1.5-.6c-.7-.3-1.5-.5-2.2-.8l-3.8-1.3-7.5-2.4c-.2-.1-.4-.1-.6-.2-1.1 1.2-2.3 2.4-2.8 2.7-.4.2-3.4-.5-3.5-.8-.1-.2.3-1.9.7-3.5-.5-.1-.9-.3-1.4-.4l-.6-.1c-.3 1.4-.7 3.1-.9 3.2-.4.3-2.9-1.2-3.1-1.5-.1-.2-.5-1.6-.7-2.9-.3-.1-.5-.1-.8-.2-.5-.1-1.1-.2-1.6-.4h-.2c-.2.1-.3.3-.3.5l.1.5c.3 1.1.7 2.2 1.2 3 .4.9.9 1.7 1.3 2.5.9 1.5 1.9 2.7 3 3.8.3.3.6.5.9.8.2-.1.4-.1.6-.1-.2 0-.4.1-.6.1 1.9 1.6 3.9 2.7 6 3.3h.2c2.2.6 4.4.8 6.9.5.4 0 .8-.1 [...] + <path fill="url(#ax)" fill-rule="nonzero" d="M231.2 80.2c.4 0 .6-.2.6-.5 0-.1.5-3 4.2-3.5 1.8-.3 2.9.5 3.5 1.2-3.5 2.2-4.1 5.7-3.9 7.3.1.6.6 1 1.1 1h.1c.6-.1 1-.6 1-1.2 0 0-.4-3.8 4-5.8 3.8-1.7 5.8 1 6.1 1.4.3.5 1 .6 1.5.3s.6-1 .3-1.5c-.5-.7-1.3-1.5-2.5-2.1.5-.9 1.6-2.1 3.8-2.4 3.3-.5 4.2 2.3 4.3 2.4.1.3.4.5.7.4.3-.1.5-.4.4-.7 0 0-1.3-3.8-5.5-3.2-2.8.4-4.1 2-4.7 3.1-1.5-.5-3.3-.5-5.3.4-.1.1-.3.1-.4.2-.8-1-2.2-2.1-4.7-1.7-4.5.6-5.1 4.4-5.2 4.4 0 .2.3.5.6.5z"/> + <path fill="#EDEDF0" fill-rule="nonzero" d="M207.7 223.7v.2c0 .2.1.4.1.6v.2c0 .2.1.4.1.6v.5c0 .1 0 .2-.1.2l-.2.2H207.3h-.1-.1c-.1 0-.1 0-.2-.1h-.1c-.1 0-.2-.1-.3-.2 0 0-.1 0-.1-.1-.1-.1-.2-.1-.3-.2l-.1-.1c-.1-.1-.2-.1-.3-.2-.1 0-.1-.1-.2-.1l-.3-.3-.2-.2-.3-.3-.2-.2-.5-.5-.1-.1-.4-.4c-1 .1-1.9.1-2.8.2 3.3 3.6 5 4.9 6.6 4.9.9 0 1.8-.4 2.3-1.2.4-.6.8-1.1.2-4.3-.8 0-1.5.1-2.3.1.1.4.1.6.2.8z"/> + <path fill="url(#ay)" fill-rule="nonzero" d="M207.7 223.7v.2c0 .2.1.4.1.6v.2c0 .2.1.4.1.6v.5c0 .1 0 .2-.1.2l-.2.2H207.3h-.1-.1c-.1 0-.1 0-.2-.1h-.1c-.1 0-.2-.1-.3-.2 0 0-.1 0-.1-.1-.1-.1-.2-.1-.3-.2l-.1-.1c-.1-.1-.2-.1-.3-.2-.1 0-.1-.1-.2-.1l-.3-.3-.2-.2-.3-.3-.2-.2-.5-.5-.1-.1-.4-.4c-1 .1-1.9.1-2.8.2 3.3 3.6 5 4.9 6.6 4.9.9 0 1.8-.4 2.3-1.2.4-.6.8-1.1.2-4.3-.8 0-1.5.1-2.3.1.1.4.1.6.2.8z"/> + <path fill="#EDEDF0" fill-rule="nonzero" d="M231.2 223.1c-.1.1-.1.2-.2.3-.1.2-.2.3-.3.4 0 .1-.1.2-.1.2-.1.2-.2.4-.4.5v.1c-.1.2-.3.4-.4.5 0 .1-.1.1-.1.1-.1.1-.2.2-.2.3 0 .1-.1.1-.1.1l-.2.2-.1.1c-.1.1-.1.1-.2.1l-.1.1c-.1 0-.1.1-.2.1 0 0-.1 0-.1.1h-.2-.1-.1-.1c-.1 0-.2-.1-.2-.1l-.1-.1c-.1-.1-.1-.3-.2-.6s-.1-.6-.1-1c0-.3-.1-.7-.1-1.1v-.9h-2.2c.2 4.5.8 4.9 1.4 5.4.5.4 1.2.6 1.8.6 2.3 0 4.3-2.8 5.8-5.9-.8 0-1.6 0-2.5-.1-.3.4-.4.5-.4.6z"/> + <path fill="url(#az)" fill-rule="nonzero" d="M231.2 223.1c-.1.1-.1.2-.2.3-.1.2-.2.3-.3.4 0 .1-.1.2-.1.2-.1.2-.2.4-.4.5v.1c-.1.2-.3.4-.4.5 0 .1-.1.1-.1.1-.1.1-.2.2-.2.3 0 .1-.1.1-.1.1l-.2.2-.1.1c-.1.1-.1.1-.2.1l-.1.1c-.1 0-.1.1-.2.1 0 0-.1 0-.1.1h-.2-.1-.1-.1c-.1 0-.2-.1-.2-.1l-.1-.1c-.1-.1-.1-.3-.2-.6s-.1-.6-.1-1c0-.3-.1-.7-.1-1.1v-.9h-2.2c.2 4.5.8 4.9 1.4 5.4.5.4 1.2.6 1.8.6 2.3 0 4.3-2.8 5.8-5.9-.8 0-1.6 0-2.5-.1-.3.4-.4.5-.4.6z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M167.7 80.6c-.1.2-.1.4-.2.6h.7c-.1-.2-.2-.4-.2-.6 0-.2-.3-.2-.3 0z"/> + <path fill="url(#aA)" fill-rule="nonzero" d="M167.7 80.6c-.1.2-.1.4-.2.6h.7c-.1-.2-.2-.4-.2-.6 0-.2-.3-.2-.3 0z"/> + <path fill="#FFF" fill-rule="nonzero" d="M118.4 58.7c.1.2.2.3.4.3h.1c.3-.1.4-.3.4-.6v-.1l-1.9-5.3c-.1-.3-.4-.4-.6-.3-.3.1-.4.4-.3.6l1.9 5.4z"/> + <path fill="url(#aB)" fill-rule="nonzero" d="M118.4 58.7c.1.2.2.3.4.3h.1c.3-.1.4-.3.4-.6v-.1l-1.9-5.3c-.1-.3-.4-.4-.6-.3-.3.1-.4.4-.3.6l1.9 5.4z"/> + <path fill="#FFF" fill-rule="nonzero" d="M134.3 121.9c.2-.2.5-.4.9-.2v.1l2.4-1.5v-3.5l-3.4 2.1v3h.1zM137.7 164.3c-.2.2-.4.6-.4.9 0 .9.1 1.8.4 2.6v-3.5z"/> + <path fill="url(#aC)" fill-rule="nonzero" d="M137.7 164.3c-.2.2-.4.6-.4.9 0 .9.1 1.8.4 2.6v-3.5z"/> + <path fill="#FFF" fill-rule="nonzero" d="M115.5 59c.3 0 .5-.2.5-.5v-5.3c0-.3-.2-.5-.5-.5s-.5.2-.5.5v5.3c0 .3.2.5.5.5z"/> + <path fill="url(#aD)" fill-rule="nonzero" d="M115.5 59c.3 0 .5-.2.5-.5v-5.3c0-.3-.2-.5-.5-.5s-.5.2-.5.5v5.3c0 .3.2.5.5.5z"/> + <path fill="#FFF" fill-rule="nonzero" d="M112.6 59c.3 0 .5-.2.5-.5v-5.8c0-.3-.2-.5-.5-.5s-.5.2-.5.5v5.8c0 .3.2.5.5.5z"/> + <path fill="url(#aE)" fill-rule="nonzero" d="M112.6 59c.3 0 .5-.2.5-.5v-5.8c0-.3-.2-.5-.5-.5s-.5.2-.5.5v5.8c0 .3.2.5.5.5z"/> + <path fill="#FFF" fill-rule="nonzero" d="M114 59c.3 0 .5-.2.5-.5v-4.8c0-.3-.2-.5-.5-.5s-.5.2-.5.5v4.8c0 .3.3.5.5.5z"/> + <path fill="url(#aF)" fill-rule="nonzero" d="M114 59c.3 0 .5-.2.5-.5v-4.8c0-.3-.2-.5-.5-.5s-.5.2-.5.5v4.8c0 .3.3.5.5.5z"/> + <path fill="#FFF" fill-rule="nonzero" d="M129.1 125.6l4.1-2.6v-.5c0-.1-.1-.1-.1-.2 0 0 .1 0 .1.1v-2.9l-5.6 3.6c-.8.3-1.1 1.2-.8 2 .2.6.8.9 1.3.9.2 0 .4 0 .6-.1.1-.1.2-.2.4-.3z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M139 115.8l-1.3.9v3.5l2.2-1.4v-8.3c-.5.7-.9 1.5-1.1 2.3-.1.5-.1 1-.1 1.4.1.6.1 1.1.3 1.6zM139.9 165.2c0-.7-.6-1.3-1.3-1.3-.4 0-.7.1-.9.4v3.5c.3 1.1.7 2.1 1.3 3 .6-.9.9-1.9.9-3v-2.6z"/> + <path fill="url(#aG)" fill-rule="nonzero" d="M139.9 165.2c0-.7-.6-1.3-1.3-1.3-.4 0-.7.1-.9.4v3.5c.3 1.1.7 2.1 1.3 3 .6-.9.9-1.9.9-3v-2.6z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M138.7 159.7c-.1.3 0 .7.4.8l.8.3v-4.4l-1.2 3.3z"/> + <path fill="url(#aH)" fill-rule="nonzero" d="M138.7 159.7c-.1.3 0 .7.4.8l.8.3v-4.4l-1.2 3.3z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M198 217c.5.6.9 1.3 1.5 1.9-.5-.7-1-1.3-1.5-1.9z"/> + <path fill="url(#aI)" fill-rule="nonzero" d="M198 217c.5.6.9 1.3 1.5 1.9-.5-.7-1-1.3-1.5-1.9z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M207.8 226l-.2.2c.1-.1.2-.1.2-.2z"/> + <path fill="url(#aJ)" fill-rule="nonzero" d="M207.8 226l-.2.2c.1-.1.2-.1.2-.2z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M224.1 117.6c1.4 0 2.5.5 3.2 1.1-.7-.5-1.8-1.1-3.2-1.1z"/> + <path fill="url(#aK)" fill-rule="nonzero" d="M224.1 117.6c1.4 0 2.5.5 3.2 1.1-.7-.5-1.8-1.1-3.2-1.1z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M224.1 119.9c1.1 0 1.9.5 2.3.9-.4-.4-1.2-.9-2.3-.9z"/> + <path fill="url(#aL)" fill-rule="nonzero" d="M224.1 119.9c1.1 0 1.9.5 2.3.9-.4-.4-1.2-.9-2.3-.9z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M134.3 121.9v-3.1l-1.1.7v2.9s-.1 0-.1-.1c0 .1.1.1.1.2v.5l2.1-1.3v-.1c-.5-.1-.8 0-1 .3z"/> + <path fill="url(#aM)" fill-rule="nonzero" d="M150.7 112.6l-4.5 2.7c-.3.2-.7.3-1 .3-.6 0-1.3-.3-1.6-.9-.4-.6-.4-1.3-.1-1.8.1-.4.4-.7.8-1l4.2-2.7c-.7-.5-1.5-.9-2.3-1.1-.4-.1-.8-.1-1.3-.1-2 0-3.8 1-4.9 2.5-.5.7-.9 1.5-1.1 2.3-.1.5-.1 1-.1 1.4 0 .5.1 1 .3 1.5v.1l-1.3.8-3.4 2.1-1.1.7-5.6 3.6c-.8.3-1.1 1.2-.8 2 .2.6.8.9 1.3.9.2 0 .4 0 .6-.1.1-.1.3-.1.4-.2l4.1-2.6 2.1-1.3 2.4-1.5 2.2-1.4.7-.5c.8.8 1.7 1.3 2.8 1.6.5.1.9.2 1.4.2 1.4 0 2.7-.5 3.7-1.3s1.8-2 2.1-3.4c.2-1 .2-1.9 0-2.8z"/> + <path fill="#FFFEFE" fill-rule="nonzero" d="M143.5 114.7c.4.6 1 .9 1.6.9.4 0 .7-.1 1-.3l4.5-2.7c.2.9.2 1.9 0 2.8-.3 1.4-1.1 2.6-2.1 3.4 1.1-.8 1.9-2.1 2.2-3.5.2-.9.2-1.9 0-2.8l-4.6 2.7c-.3.2-.7.3-1 .3-.6 0-1.3-.3-1.7-.9-.3-.5-.4-1.2-.2-1.7-.1.5-.1 1.2.3 1.8z"/> + <path fill="url(#aN)" fill-rule="nonzero" d="M143.5 114.7c.4.6 1 .9 1.6.9.4 0 .7-.1 1-.3l4.5-2.7c.2.9.2 1.9 0 2.8-.3 1.4-1.1 2.6-2.1 3.4 1.1-.8 1.9-2.1 2.2-3.5.2-.9.2-1.9 0-2.8l-4.6 2.7c-.3.2-.7.3-1 .3-.6 0-1.3-.3-1.7-.9-.3-.5-.4-1.2-.2-1.7-.1.5-.1 1.2.3 1.8z"/> + <path fill="#FFFEFE" fill-rule="nonzero" d="M126.8 125.1c-.3-.8 0-1.7.8-2l5.6-3.6 1.1-.7 3.4-2.1 1.3-.8v-.1l-11.5 7.3c-.8.3-1.1 1.3-.8 2 .3.6.8.9 1.4.9h.1c-.7 0-1.2-.3-1.4-.9z"/> + <path fill="url(#aO)" fill-rule="nonzero" d="M126.8 125.1c-.3-.8 0-1.7.8-2l5.6-3.6 1.1-.7 3.4-2.1 1.3-.8v-.1l-11.5 7.3c-.8.3-1.1 1.3-.8 2 .3.6.8.9 1.4.9h.1c-.7 0-1.2-.3-1.4-.9z"/> + <path fill="#FFFEFE" fill-rule="nonzero" d="M139.9 110.5c1.1-1.5 2.9-2.5 4.9-2.5.4 0 .8 0 1.3.1.8.2 1.6.6 2.3 1.1l.2-.1c-.7-.6-1.5-1-2.4-1.2-.4-.1-.8-.1-1.3-.1-2.8 0-5.4 2-6 4.9-.1.5-.1 1-.1 1.6 0-.5 0-1 .1-1.4.1-1 .5-1.7 1-2.4z"/> + <path fill="url(#aP)" fill-rule="nonzero" d="M139.9 110.5c1.1-1.5 2.9-2.5 4.9-2.5.4 0 .8 0 1.3.1.8.2 1.6.6 2.3 1.1l.2-.1c-.7-.6-1.5-1-2.4-1.2-.4-.1-.8-.1-1.3-.1-2.8 0-5.4 2-6 4.9-.1.5-.1 1-.1 1.6 0-.5 0-1 .1-1.4.1-1 .5-1.7 1-2.4z"/> + <path fill="url(#aQ)" fill-rule="nonzero" d="M106.4 207.6c.1 0 .1 0 0 0h.1c1-.3 1.9-.9 2.7-1.6-1.1-.3-2 .2-3.1.7-1 .2-2 .3-2.7 1l-.2.2c1.2.2 2.3 0 3.2-.3z"/> + <path fill="url(#aR)" fill-rule="nonzero" d="M118.3 196.1c.7-1.4.9-3 .6-4.6-.4-2.1-1.5-3.9-3.3-5.1-1.4-1-3-1.4-4.6-1.4-1.5 0-3 .5-4.2 1.3-.2-.2-.4-.3-.6-.5-1.2-.8-2.5-1.2-4-1.2-2.1 0-4 1-5.3 2.6h-.7c-2.7 0-5.2 1.4-6.8 3.6-1.8 2.6-1.9 5.9-.6 8.6-.7.5-1.2 1.1-1.7 1.8-1.3 1.8-1.8 4.1-1.4 6.3.4 2.2 1.6 4.1 3.5 5.4 1.2.9 2.6 1.4 4.1 1.5.4 1.1 1.2 2.1 2.2 2.8 1 .7 2.2 1.1 3.5 1.1 1 .9 2.2 1.6 3.7 2.2l1 5.5h2.3l-1.3-7.2c-1.3-.4-2.7-1-3.8-1.8-.4-.3-.7-.6-1-1h-.1l-.1-.1c-.4-.4-.7-1-.9-1.6v-.1 [...] + <path fill="#EDEDF0" fill-rule="nonzero" d="M107.9 226.5h-.4c-.8 0-1.5-.2-1.5-.6v-.1h-2.3l.1.5c.2 1.2 1.3 2.5 3.8 2.5 1.5 0 3.5-.5 4.6-1.8.2-.2.3-.5.4-.7l-2.7-.3c-.5.2-1.3.4-2 .5z"/> + <path fill="url(#aS)" fill-rule="nonzero" d="M107.9 226.5h-.4c-.8 0-1.5-.2-1.5-.6v-.1h-2.3l.1.5c.2 1.2 1.3 2.5 3.8 2.5 1.5 0 3.5-.5 4.6-1.8.2-.2.3-.5.4-.7l-2.7-.3c-.5.2-1.3.4-2 .5z"/> + <path d="M-30-28h352v303H-30z"/> + </g> +</svg> diff --git a/browser/extensions/onboarding/content/img/figure_default.svg b/browser/extensions/onboarding/content/img/figure_default.svg new file mode 100644 index 000000000000..c52e4b8500f7 --- /dev/null +++ b/browser/extensions/onboarding/content/img/figure_default.svg @@ -0,0 +1 @@ +<svg width="272" height="247" viewBox="0 0 272 247" xmlns="http://www.w3.org/2000/svg"><title>default-browser</title><defs><linearGradient x1="-12.708%" y1="-28.803%" x2="102.994%" y2="115.824%" id="a"><stop stop-color="#FFCCD7" offset="40.06%"/><stop stop-color="#EDBEE2" offset="100%"/></linearGradient><linearGradient x1="-78.121%" y1="-55.724%" x2="136.609%" y2="135.651%" id="b"><stop stop-color="#FFE900" offset="28.07%"/><stop stop-color="#FFCC07" offset="32.21%"/><stop stop-color="#F [...] \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/figure_library.svg b/browser/extensions/onboarding/content/img/figure_library.svg new file mode 100644 index 000000000000..aad20181b996 --- /dev/null +++ b/browser/extensions/onboarding/content/img/figure_library.svg @@ -0,0 +1,689 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="267" height="240"> + <defs> + <linearGradient id="a" x1="-287.251713%" x2="363.382118%" y1="-127.999431%" y2="247.172106%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="b" x1="-8347.28%" x2="11424.26%" y1="-8337.33%" y2="11434.21%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="c" x1="-2354.3122%" x2="2468.01463%" y1="-738.5544%" y2="843.1688%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="d" x1="-11316.73%" x2="8454.81%" y1="-5346.60952%" y2="4068.40952%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="e" x1="-156.148629%" x2="205.305484%" y1="-480.49483%" y2="430.938303%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="f" x1="-11777.11%" x2="7994.43%" y1="-1542.90541%" y2="1128.92432%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="g" x1="-1966.10678%" x2="1385.00169%" y1="-2646.49545%" y2="1847.03636%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="h" x1="-1259.26087%" x2="945.558937%" y1="-1283.95691%" y2="942.373333%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="i" x1="-4828.28387%" x2="3895.46452%" y1="-2550.56897%" y2="2112.12414%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="j" x1="-1420.34388%" x2="1159.68716%" y1="-3565.4194%" y2="2819.67133%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="k" x1="-6578.28%" x2="13193.26%" y1="-6566.33%" y2="13205.21%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="l" x1="-690.589109%" x2="1266.98911%" y1="-1068.60597%" y2="1882.37015%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="m" x1="-3693.78418%" x2="6240.18862%" y1="-1360.99327%" y2="2373.67085%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="n" x1="-51.4002563%" x2="99.3496099%" y1="-59.6430664%" y2="133.087695%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="o" x1="-47.4074974%" x2="121.810771%" y1="-106.87209%" y2="132.306567%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="p" x1="-701.943676%" x2="609.202314%" y1="-537.964802%" y2="487.22249%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="q" x1="-1074.53%" x2="834.91%" y1="-358.218519%" y2="348.981481%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="r" x1="-5230.64688%" x2="3222.21875%" y1="-2856.73793%" y2="1806.91207%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="s" x1="-1536.40601%" x2="955.898444%" y1="-3896.2795%" y2="2345.49035%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="t" x1="-2573.03736%" x2="4141.82528%" y1="-7694%" y2="12077.54%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="u" x1="-105.756%" x2="253.726545%" y1="-959.543678%" y2="1313.04713%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="v" x1="-113.495628%" x2="246.641894%" y1="-1951.93556%" y2="2441.74%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="w" x1="-203.741261%" x2="362.77851%" y1="-8794.04%" y2="10977.5%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="x" x1="-8901.65455%" x2="9072.47273%" y1="-4629.9%" y2="4785.11905%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="y" x1="-135.885507%" x2="273.463147%" y1="-6854.87692%" y2="8354%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="z" x1="-237.240755%" x2="222.496119%" y1="-950.902381%" y2="659.16369%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="A" x1="-323.294457%" x2="276.418625%" y1="-16784.12%" y2="10262.94%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="B" x1="-324.50885%" x2="273.863496%" y1="-16876.15%" y2="10170.29%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="C" x1="-8757.43409%" x2="-13250.9636%" y1="-25788.3267%" y2="-38969.3533%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="D" x1="-4977.81154%" x2="-7512.62308%" y1="-21732.3667%" y2="-32716.5611%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="E" x1="-778.197863%" x2="-1200.66709%" y1="-2873.70382%" y2="-4382.98244%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="F" x1="-3162.7925%" x2="-4810.42083%" y1="-25654.4533%" y2="-38835.4867%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="G" x1="-1053.32338%" x2="1514.40909%" y1="-4984.71765%" y2="6645.6%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="H" x1="-5039.72338%" x2="-7607.45714%" y1="-23040.7706%" y2="-34671.0941%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="I" x1="143.631333%" x2="-4.86%" y1="790.352632%" y2="-381.952632%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="J" x1="-2552.41333%" x2="-3870.516%" y1="-20494.2053%" y2="-30900.2789%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="K" x1="-1250.60304%" x2="-1918.56115%" y1="-38487.33%" y2="-58258.87%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="L" x1="-37598.9%" x2="-57370.44%" y1="-17879.1857%" y2="-27294.2048%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="M" x1="-882.727251%" x2="-1363.78637%" y1="-29434.6846%" y2="-44643.5692%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="N" x1="-268.313828%" x2="273.677355%" y1="-882.118713%" y2="699.481287%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="O" x1="-420.455862%" x2="943.098621%" y1="-4784.28571%" y2="9338.24286%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="P" x1="-587.656122%" x2="1429.84796%" y1="-3859.74375%" y2="8497.475%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="Q" x1="-597.567708%" x2="1461.96771%" y1="-6217.96%" y2="13553.58%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="R" x1="-989.3%" x2="1835.20571%" y1="-6563.19091%" y2="11410.9364%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="S" x1="-1683.03158%" x2="3520.00526%" y1="-4061.93125%" y2="8295.28125%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="T" x1="-289.56383%" x2="551.778298%" y1="-736.619802%" y2="1220.95842%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="U" x1="-8102.24%" x2="11669.3%" y1="-8112.37%" y2="11659.17%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="V" x1="-527.27218%" x2="959.309774%" y1="-7671.89%" y2="12099.65%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="W" x1="-563.298261%" x2="1155.96609%" y1="-4360.425%" y2="7996.7875%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="X" x1="-595.656881%" x2="1218.24587%" y1="-7031.95%" y2="12739.59%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="Y" x1="-4261.16471%" x2="7369.15294%" y1="-5186.16429%" y2="8936.36429%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="Z" x1="-7291.52%" x2="12480.03%" y1="-7323.1%" y2="12448.44%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aa" x1="-46.8866667%" x2="106.777333%" y1="-610.354545%" y2="437.354545%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="ab" x1="-954.992%" x2="1681.21333%" y1="-6801.97273%" y2="11172.1545%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="ac" x1="-53.1965517%" x2="108.827586%" y1="-138.8375%" y2="154.825%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="ad" x1="-2268.40345%" x2="4549.36897%" y1="-4153.9%" y2="8203.3125%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="ae" x1="-134.196822%" x2="349.214914%" y1="-7485.96%" y2="12285.58%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="af" x1="-203.129153%" x2="467.092542%" y1="-7412.3%" y2="12359.24%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="ag" x1="-8254.16%" x2="11517.38%" y1="-4829.67647%" y2="6800.64118%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="ah" x1="-261.207831%" x2="281.860241%" y1="-1137.19462%" y2="943.173846%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="ai" x1="-353.298433%" x2="352.892428%" y1="-15403.61%" y2="11643.5%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="aj" x1="-355.267885%" x2="350.914099%" y1="-15487.8%" y2="11558.97%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="ak" x1="-2084.69358%" x2="-3141.99572%" y1="-5548.86479%" y2="-8333.58732%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="al" x1="-2136.94011%" x2="-3223.28791%" y1="-39758.41%" y2="-59529.95%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="am" x1="-8671.43111%" x2="-13065.1111%" y1="-39159.26%" y2="-58930.8%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="an" x1="42.05%" x2="39.02%" y1="40.85%" y2="37.83%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="ao" x1="-1655.02189%" x2="-2503.58541%" y1="-18008.5045%" y2="-26995.5636%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="ap" x1="26.16%" x2="23.82%" y1="17.93%" y2="15.58%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aq" x1="-7321.04%" x2="-10915.8655%" y1="-26976.66%" y2="-40157.6867%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="ar" x1="-3806.45143%" x2="-5689.45619%" y1="-33702.4583%" y2="-50178.75%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="as" x1="-719.07449%" x2="1298.42959%" y1="-4375.10588%" y2="7255.21176%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="at" x1="-4193.87653%" x2="-6211.37959%" y1="-24406.3118%" y2="-36036.6294%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="au" x1="-524.679508%" x2="1095.93852%" y1="-4333.45%" y2="8023.7625%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="av" x1="-3315.91393%" x2="-4936.53115%" y1="-25616.6063%" y2="-37973.8188%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aw" x1="-1422.94082%" x2="2612.06735%" y1="-5115.85714%" y2="9006.67143%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="ax" x1="-8372.54082%" x2="-12407.5531%" y1="-29439.4643%" y2="-43561.9929%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="ay" x1="-2040.6303%" x2="3950.74545%" y1="-6860.53%" y2="12911.01%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="az" x1="-12359.7364%" x2="-18351.1091%" y1="-40913.58%" y2="-60685.12%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aA" x1="-1005.75152%" x2="1989.93788%" y1="-6296.96364%" y2="11677.1727%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aB" x1="-6165.30303%" x2="-9160.98939%" y1="-37254.2727%" y2="-55228.4%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aC" x1="-2871.84%" x2="5036.776%" y1="-4515.63125%" y2="7841.58125%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aD" x1="-16493.056%" x2="-24401.672%" y1="-25798.7875%" y2="-38156%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aE" x1="-4836.46667%" x2="8344.56%" y1="-7269.91%" y2="12501.63%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aF" x1="-27538.4933%" x2="-40719.52%" y1="-41322.96%" y2="-61094.5%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aG" x1="123.979381%" x2="7.09896907%" y1="645.125%" y2="-299.65%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="aH" x1="-4143.41443%" x2="-6181.71959%" y1="-33849.65%" y2="-50325.925%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aI" x1="110.22963%" x2="13.6574074%" y1="263.406667%" y2="-84.2533333%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="aJ" x1="-7493.57037%" x2="-11154.9667%" y1="-27110.28%" y2="-40291.3067%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aK" x1="-1314.06588%" x2="-1982.02331%" y1="-40374.36%" y2="-60145.89%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aL" x1="-39504.49%" x2="-59276.05%" y1="-23215.4176%" y2="-34845.7353%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aM" x1="-935.697066%" x2="-1419.10856%" y1="-40260.71%" y2="-60032.24%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aN" x1="-239.365731%" x2="302.59479%" y1="-1057.81832%" y2="1006.59618%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="aO" x1="-195.98196%" x2="188.238494%" y1="-262.20413%" y2="218.292299%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="aP" x1="-148.239568%" x2="156.504317%" y1="-236.10625%" y2="205.1375%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="aQ" x1="-684.479137%" x2="737.933813%" y1="-1012.53646%" y2="1046.99896%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + <linearGradient id="aR" x1="-802.736152%" x2="689.739334%" y1="-1056.80385%" y2="890.777014%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="aS" x1="-1124.88665%" x2="549.535228%" y1="-1423.71471%" y2="673.128094%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="aT" x1="-465.885211%" x2="339.528169%" y1="-152.931663%" y2="157.298039%"> + <stop stop-color="#FFE900" offset="28.07%"/> + <stop stop-color="#FFCC07" offset="32.21%"/> + <stop stop-color="#FF8119" offset="41.22%"/> + <stop stop-color="#FF0B36" offset="54.35%"/> + <stop stop-color="#FF0039" offset="55.5%"/> + <stop stop-color="#ED00B5" offset="85.24%"/> + </linearGradient> + <linearGradient id="aU" x1="-632.473239%" x2="759.889437%" y1="-217.098158%" y2="319.212821%"> + <stop stop-color="#FFCCD7" offset="40.06%"/> + <stop stop-color="#EDBEE2" offset="100%"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd"> + <path d="M150.1 145.9v-.2.2zM152.6 147.1c0 .9.3 1.9.9 2.8-.6-.9-.9-1.9-.9-2.8zM149.7 154.2c0-.2-.1-.5-.3-.6.2.2.3.4.3.6 0 0-.1.7.8 1.8-.9-1-.8-1.8-.8-1.8zM229.2 188.9c.4-1.5.7-3 .8-4.4 0-.5.1-1 .1-1.5 0 .5-.1 1-.1 1.5-.1 1.4-.4 2.9-.8 4.4zM103.1 216.7h.8l-.3-.3c-.1.2-.3.3-.5.3zM235.1 153.6v.2c.4.1.8.3 1.1.6.8.7 1 1.8.7 2.7.1-.2.1-.4.1-.6.1-1.3-.7-2.5-2-2.9v-.2l-.1-.9c-1.5-.1-3-.2-4.6-.4l-.3 3.3 5.1-1.8zM245.1 143.8c6.7-3.5 11.1-12.3 10.9-20.8.3 8.5-4.2 17.3-10.9 20.8-3.5 1.8-8.8 2.6- [...] + <path d="M239.9 150.3c-.8.1-1.6.2-2.4.2-.1-.1-.3-.1-.4-.1-2.1-.1-4.3-.2-6.4-.4v.1c2.1.2 4.2.4 6.3.5.1 0 .3 0 .4.1.8 0 1.6-.1 2.4-.2 6.9-.9 11-3.2 15.3-8.7 1.4-1.7 3-4.6 4.1-7.8-1.2 3.2-2.7 5.9-4.1 7.7-4.3 5.4-8.4 7.7-15.2 8.6zM104 200.6c0-.1 0-.1 0 0 0-.1 0-.1 0 0zM145.8 157.9l-.2-.3v-.1l.1.1M140.7 165.2h-.1l-.6.9v.1M252 110.6c-2.8-4.7-6.4-9.1-8.6-11.7 2.2 2.6 5.8 7 8.6 11.7zM206.9 117.5c-.2-.3-.5-.5-.7-.7-.6-.5-1.4-.7-2.1-.7 1 0 2.1.5 2.8 1.4.5.8 1.5 1 2.3.5.1-.1.2-.1.3-.2-.1.1-.2.1 [...] + <path d="M214.8 223.5v-.9c-1.3.2-2.6.4-3.8.6-4.1.6-8.2 1-12.4 1-6.3 0-12.6-.5-18.8-1.7 0 1.3 0 2.3-.1 3.3 4.7-.1 9.6-.2 14.7-.2 7.1 0 13.9.1 20.4.3v-2.4zM159.1 216.9c-.7-1.9-1.3-4.2-2-6.8h-1.3c-3.9-.1-7.9-.4-11.7-1.1v1.9h1c1 0 1.9 1 1.9 2.2v1.4c0 1.2-.8 2.1-1.7 2.2h.2l.4.3c.2-.2.4-.3.7-.3h.8c1.9.1 3.1.4 3.6.9 1.6 1.3 2.6 4.2 2.6 7.5 0 .5-.1 1.2-.2 1.9 3.1-.2 6.3-.4 9.8-.6-.6-1-1.1-2.1-1.6-3.2-.9-1.9-1.8-4.1-2.5-6.3zM235.4 114.9c-.2.1-.3.3-.3.4.1-.1.2-.2.3-.4.1 0 .1 0 0 0zM150.3 134.7 [...] + <path d="M152.3 147.3c-.2-3.1-.3-6.4-.4-9.8.2-1 .4-1.9.5-2.8 0-10.7.9-20.1 2.9-26v-.1c-2 5.9-2.9 15.3-2.9 26.1-.2.9-.3 1.8-.5 2.8v.2c0-.1 0-.2.1-.3 0 3.5.1 6.8.3 9.9 0 .1 0 .1 0 0zM236.4 182.5c-.4 15.2-3.8 25.4-5.2 29-.3 1.4-.7 2.7-1.1 3.8-1.6 4.5-3.5 8.5-5.2 11.1 1.7-2.6 3.7-6.6 5.3-11.2.4-1.1.7-2.4 1.1-3.8 1.4-3.6 4.8-13.7 5.2-29v-2.3s-.1 0-.1-.1v2.5zM149 153.6c.1-.6.3-1.3.6-1.9-.3.6-.6 1.2-.6 1.9h.2c-.1-.1-.2-.1-.2 0zM174.2 215.2v.2h-.1l.1-.2-.1.3c.1 2.1.1 4.1.1 6 0 1.7 0 3.2-.1 4 [...] + <path fill="#FFF" fill-rule="nonzero" d="M7.7 72.3v98.1c0 1 .1 1.2.1 1.2s.2.1 1.2.1h121.1l-.6-1.9c-.9-3.1.3-6.3 2.9-7.9V72.3H7.7zm45.8 65.5c0 1.8-1.5 3.3-3.3 3.3-1.8 0-3.3-1.5-3.3-3.3V98.4c0-1.8 1.5-3.3 3.3-3.3 1.8 0 3.3 1.5 3.3 3.3v39.4zm9.8 0c0 1.8-1.5 3.3-3.3 3.3-1.8 0-3.3-1.5-3.3-3.3V105c0-1.8 1.5-3.3 3.3-3.3 1.8 0 3.3 1.5 3.3 3.3v32.8zm9.9 0c0 1.8-1.5 3.3-3.3 3.3-1.8 0-3.3-1.5-3.3-3.3v-36.1c0-1.8 1.5-3.3 3.3-3.3 1.8 0 3.3 1.5 3.3 3.3v36.1zm20.8 3.1c-.4.1-.8.2-1.1.2-1.3 0-2.6-.8- [...] + <path fill="#FFF" fill-rule="nonzero" d="M127.3 173.2l1.8.1c.1-.2.3-.4.5-.5H9c-2 0-2.5-.4-2.5-2.5V71.2h127v90.2c.2-.1.4-.2.7-.2l3.6-1.1v-4.8c-1.3-2.3-1.2-5 0-7.1V55.4c0-2.3-1.9-4.2-4.2-4.2H6.5c-2.3 0-4.2 1.9-4.2 4.2v118.3c0 2 1.8 3.7 3.9 3.7h90c.3-.6.6-1.2 1.1-1.5.9-.7 2.6-.8 3.8-.8.5 0 .9.2 1.2.5l.5-.4h19.1c1.3-1.2 3-2 4.9-2h.5zm9.1-13.5l-.1-.2.1.2zm-.1-.3v.4l-.3-.9.3.5zm-9.6-101.2c1.6 0 2.9 1.3 2.9 2.9 0 1.6-1.3 2.9-2.9 2.9-1.6 0-2.9-1.3-2.9-2.9 0-1.6 1.3-2.9 2.9-2.9zm-9.2 0c1.6 0 [...] + <path fill="#D7D7DB" fill-rule="nonzero" d="M138.7 159.7c.3-.5.6-1.1.8-1.7l-1.7-2.8v4.7l.9-.2zM6.2 177.5c-2.2 0-3.9-1.6-3.9-3.7V55.4c0-2.3 1.9-4.2 4.2-4.2h127.1c2.3 0 4.2 1.9 4.2 4.2V148c.5-.9 1.3-1.7 2.2-2.3V55.4c0-3.6-2.9-6.5-6.5-6.5H6.5c-3.6 0-6.5 2.9-6.5 6.5v118.3c0 3.3 2.8 5.9 6.2 5.9h89.2c.2-.8.4-1.5.7-2.2H6.2v.1zM139 167.8l1.1-1.6v-.1l-1.1 1.7c-.2.1-.2.4 0 .5-.1-.1-.1-.3 0-.5z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M6.5 71.2v99.2c0 2 .4 2.5 2.5 2.5h120.5c.2-.2.4-.5.7-.7l-.1-.4H9c-1 0-1.2-.1-1.2-.1s-.1-.2-.1-1.2V72.3h124.7V162c.3-.2.7-.4 1.1-.6V71.2H6.5zM132.8 169.2c-.2-.7-.2-1.4 0-2.1-.3.6-.3 1.4 0 2.1l.7 2.3v-.1l-.7-2.2zM13.3 64c1.6 0 2.9-1.3 2.9-2.9 0-1.6-1.3-2.9-2.9-2.9-1.6 0-2.9 1.3-2.9 2.9 0 1.6 1.4 2.9 2.9 2.9zM22.6 64c1.6 0 2.9-1.3 2.9-2.9 0-1.6-1.3-2.9-2.9-2.9-1.6 0-2.9 1.3-2.9 2.9 0 1.6 1.3 2.9 2.9 2.9zM38.1 64.3H102c1.7 0 3.1-1.4 3.1-3.1V61c [...] + <path fill="#D7D7DB" fill-rule="nonzero" d="M60 101.6c-1.8 0-3.3 1.5-3.3 3.3v32.8c0 1.8 1.5 3.3 3.3 3.3 1.8 0 3.3-1.5 3.3-3.3v-32.8c0-1.8-1.4-3.3-3.3-3.3zM69.9 98.4c-1.8 0-3.3 1.5-3.3 3.3v36.1c0 1.8 1.5 3.3 3.3 3.3 1.8 0 3.3-1.5 3.3-3.3v-36.1c0-1.9-1.5-3.3-3.3-3.3zM50.2 95.1c-1.8 0-3.3 1.5-3.3 3.3v39.4c0 1.8 1.5 3.3 3.3 3.3 1.8 0 3.3-1.5 3.3-3.3V98.4c0-1.8-1.5-3.3-3.3-3.3zM82.8 100.5c-.6-1.7-2.5-2.6-4.2-2-1.7.6-2.6 2.5-2 4.2l13.1 36.1c.5 1.3 1.7 2.2 3.1 2.2.4 0 .8-.1 1.1-.2 1.7-.6 2. [...] + <path fill="#F9F9FA" fill-rule="nonzero" d="M40.9 25.6h97.9c.6 0 1.1-.5 1.1-1.1 0-.6-.5-1.1-1.1-1.1H116c-2-3.7-7.1-11.7-13.4-12.9-8.4-1.6-10 6.7-10 6.7S87 2.7 73 4.6c-6.5.9-9 4.2-9.8 7.8h.1c.3 0 .6.2.6.5 0 .4.1.7.1 1.1 0 .3-.2.6-.5.6h-.1c-.2 0-.4-.1-.5-.3-.1 1.9.1 3.8.5 5.3h.7c-.1-.3-.2-.6-.4-1-.1-.3.1-.6.4-.7.3-.1.6.1.7.4.3 1 .6 1.7.6 1.7.1.2.1.4 0 .5-.1.2-.3.3-.5.3h-1.3c.4 1.5.9 2.5.9 2.7H40.7c-.6 0-1.1.5-1.1 1.1.1.5.6 1 1.3 1z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M229.2 52.9c.3-1 1.2-3.2 3.8-3.2.3 0 .7 0 1 .1 1.6.3 3.2 1.3 4.8 3 .2.2.6.2.8 0 .2-.2.2-.6 0-.8-1.8-1.9-3.6-3-5.4-3.4-.4-.1-.8-.1-1.2-.1-3.5 0-4.6 3-4.9 4-.1.3.1.6.4.7h.2c.1 0 .2 0 .3-.1.1 0 .2-.1.2-.2zM213.5 48.4c.1 0 .3-.1.4-.2.6-.7 1.5-1.1 2.6-1.4.3-.1.5-.4.4-.7-.1-.3-.4-.5-.7-.4-1.3.4-2.4.9-3.1 1.7-.2.2-.2.6 0 .8.1.2.3.2.4.2zM246.3 56.9h3.3c.3 0 .6-.2.6-.6 0-.3-.2-.6-.6-.6h-3.3c-.3 0-.6.2-.6.6 0 .3.3.6.6.6zM220.7 46.6c.4.1.7.2 1 .3h.2c. [...] + <path fill="#F9F9FA" fill-rule="nonzero" d="M199.6 60.7h54.5c.6 0 1.1-.5 1.1-1.1 0-.6-.5-1.1-1.1-1.1h-12.9c-1.1-2.1-3.9-6.5-7.4-7.2-2.4-.5-3.8.5-4.6 1.6 0 .1-.1.2-.2.3-.6 1-.8 1.9-.8 1.9s-3.1-8-10.9-7c-5.8.8-6 5-5.4 7.8h.7s.1-.1.2-.1c.3-.1.6 0 .7.3l.1.1c.1.2.1.4 0 .5-.1.2-.3.3-.5.3h-.9c.2.9.5 1.4.5 1.5h-13.2.2c-.6 0-1.1.5-1.1 1.1-.1.6.4 1.1 1 1.1z"/> + <path fill="#EDEDF0" fill-rule="nonzero" d="M173.7 229.1c-.1.2-.1.4-.2.5 0 0-.1 0-.1.1.1 0 .2-.1.3-.1.3-.3.5-1.6.6-3.6h-.1c-.2 1.5-.3 2.6-.5 3.1zM173.1 232c.5 0 1-.1 1.5-.4-.4.2-.9.4-1.5.4-2.1 0-4.3-2.7-6.1-5.8h-.1c1.8 3.2 4 5.8 6.2 5.8z"/> + <path fill="#EDEDF0" fill-rule="nonzero" d="M231.1 226.7c-2.6 4.8-5.9 8.5-9.7 8.5-1.4 0-2.9-.5-4-1.4-1.7-1.4-2.5-2.9-2.6-7.8-6.4-.2-13.3-.3-20.4-.3-5 0-9.9.1-14.7.2-.2 5.1-1 6.6-2.7 7.9-1.1.9-2.5 1.4-4 1.4-4 0-6.8-3.6-8.7-6.8-.4-.6-.8-1.3-1.1-2-3.4.2-6.7.4-9.8.6-.3 2-1.1 4.5-2.2 5.4-1.1.9-3.1 1.1-4.5 1.1-.6 0-1-.3-1.4-.6l-.6.6H97.4c-1 0-1.9-.7-2.2-1.7l-.3-1.1v-.2c-5.9.7-9.4 1.5-9.4 2.4 0 2.1 17.6 3.7 39.4 3.7 3.5 0 6.8 0 10-.1 12.2 2.1 34.3 3.4 59.5 3.4 38.5 0 69.7-3.2 69.7-7.1 0-2.6 [...] + <path fill="#EDEDF0" fill-rule="nonzero" d="M219.5 231.3c.5.4 1.2.7 1.9.7.6 0 1.3-.2 1.9-.6-.6.4-1.2.6-1.8.6-.7 0-1.4-.3-2-.7-.6-.6-1.2-1.1-1.3-5.3h-.1c.2 4.3.8 4.8 1.4 5.3zM223.3 228.4c.5-.5 1-1.2 1.5-2-.5.8-1 1.4-1.5 2z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M102.1 188.2l-.3-.2c-.1.2-.3.3-.6.3h-.7c-1.6-.1-2.6-.3-3.1-.7l-.6-.6H83c-.6 0-1.1.5-1.1 1.1 0 .6.5 1.1 1.1 1.1h19.4c.1-.4.2-.7.5-.9h-.8v-.1zM156.8 189.1h.1c-.2-.8-.3-1.5-.4-2.2h-.1c.1.7.3 1.4.4 2.2zM27.3 194.1c.3 0 .6-.2.6-.6 0-.3-.2-.6-.6-.6H24c-.3 0-.6.2-.6.6 0 .3.2.6.6.6h3.3zM19.5 193h-1.1c-.3 0-.6.2-.6.6 0 .3.2.6.6.6h1.1c.3 0 .6-.2.6-.6 0-.3-.3-.6-.6-.6zM68.5 194.1c.3 0 .6-.2.6-.6 0-.3-.2-.6-.6-.6h-3.3c-.3 0-.6.2-.6.6 0 .3.2.6.6.6h3.3zM [...] + <path fill="#EDEDF0" fill-rule="nonzero" d="M65.7 230.4c-.3.9-.6 1.7-1.1 2.1-.8.8-2.3.9-3.4.9-.4 0-.8-.3-1-.6l-.5.5H24.5h-.1c2.2 1.6 12.1 2.7 24 2.7 13.5 0 24.4-1.5 24.4-3.4 0-.7-2.7-1.6-7.1-2.2z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M28.3 224.9h32.9c.1 0 .1 0 .2.1-.1-.4-.1-.7-.3-1H27.2v4.6h33.7c.2-.3.3-.7.4-1.1h-33c-.2 0-.4-.2-.4-.4s.2-.4.4-.4h32.9c.1 0 .2 0 .2.1v-1.1c-.1.1-.2.1-.3.1H28.3c-.2 0-.4-.2-.4-.4s.2-.5.4-.5z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M59.2 231.8l.8-.9.3.1c.3.1.5.3.6.5.1.1.2.3.3.3 1.2 0 2.1-.2 2.5-.6.6-.5 1.3-3.3 1.3-5.1 0-2.6-.7-4.6-1.4-5.2-.1-.1-.8-.3-1.9-.4-.1.4-.2.7-.7.8h-.3l-.9-.9H24.3c-.1 0-.3.1-.3.3v1.2c0 .2.1.3.3.3H62l.2.4c1.3 2.4.8 5.9-.4 7.3l-.2.2H24.3c-.1 0-.2 0-.2.1s-.1.2 0 .3l.2 1c0 .1.1.2.2.2h34.7v.1z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M59.7 233.4l.5-.5c.2.3.6.6 1 .6 1.1 0 2.6-.2 3.4-.9.4-.4.8-1.2 1.1-2.1.5-1.5.7-3.3.7-4.3 0-2.8-.8-5.4-1.9-6.5-.4-.4-1.3-.7-2.7-.8h-.6c-.3 0-.4.1-.5.3l-.3-.3h-36c-.9 0-1.7.9-1.7 2v1.2c0 1.1.7 2 1.7 2h.9v4.6h-.9c-.5 0-1 .3-1.3.8-.3.5-.4 1.1-.3 1.7l.2 1c.2.8.8 1.4 1.5 1.5h35.2v-.3zm-35.5-1.8l-.2-1v-.3c0-.1.1-.1.2-.1h37.2l.2-.2c1.3-1.4 1.7-4.9.4-7.3l-.2-.4H24.3c-.1 0-.3-.1-.3-.3v-1.2c0-.2.1-.3.3-.3h35.5l.9.9h.3c.4-.1.6-.5.7-.8 1.2.1 1.8.3 1.9.4 [...] + <path fill="#FFF" fill-rule="nonzero" d="M174.2 215.4v-.2M236.7 157.9c.2-.2.3-.4.3-.6.1-.2.1-.5.1-.7 0 .2-.1.4-.1.6-.1.2-.2.4-.3.7zM161.3 204.2v.1h.1v-.1h-.1zM173.7 229.1c-.1.1-.1.2-.2.3-.4.3-1 .1-1.7-.5-.1 0-.1-.1-.2-.2-.3-.2-.5-.5-.8-.9.9 1.1 1.7 1.8 2.3 1.8h.2s.1 0 .1-.1c.1 0 .2-.1.3-.4z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M233.4 212.2c-.3 1.4-.7 2.7-1.1 3.8-.5 1.4-4.6 12.7-9 15.4-.6.4-1.3.6-1.9.6-.7 0-1.3-.2-1.9-.7-.7-.5-1.3-1-1.3-5.3 0-1.7 0-4.1.2-7.4-6.5 1.5-13.1 2.3-19.7 2.4-7.4.1-14.9-.7-22.1-2.6.2 11.4-.6 12-1.5 12.8-.1.1-.3.2-.5.3-.5.3-1 .4-1.5.4-2.2 0-4.4-2.6-6.2-5.8-2.5-4.2-4.3-9.3-4.6-10.2-1-3-1.9-6.1-2.6-9.2-1.3.1-2.6.1-3.9.1-3.9-.1-7.9-.5-11.7-1.1v3.2c3.9.7 7.8 1 11.7 1.1h1.3c.7 2.7 1.3 5 2 6.8.8 2.2 1.6 4.3 2.6 6.3.5 1.1 1 2.1 1.6 3.2.4.7.7 1.3 1 [...] + <path fill="#F9F9FA" fill-rule="nonzero" d="M137.8 148.1c-1.2 2.1-1.3 4.8 0 7.1v.1l1.7 2.8c-.3.6-.6 1.1-.8 1.7l-.8.3-3.6 1.1c-.2.1-.5.2-.7.2-.4.2-.8.4-1.1.6-2.5 1.7-3.8 4.9-2.9 7.9l.6 1.9.1.4c-.2.3-.4.5-.7.7-.2.2-.3.4-.5.5l-1.8-.1h-.6c-1.9 0-3.6.8-4.9 2h10.3l1.8-2.1-.5-1.7-.7-2.2c-.2-.7-.2-1.5 0-2.2.3-1.1 1.2-2.1 2.4-2.5l5.8-1.8c.8-1.4 1.6-3 2.3-4.7l-2.6-4.3c-.5-.9-.7-1.9-.4-2.8.2-.9.8-1.8 1.7-2.3l1.1-.7 4.8-2.9c.9-3.3 1.7-6.9 2.2-10.6 0-12.6 1.1-21.9 3.3-27.6l-.2-.5c-.4-1.2.3-2.4 1. [...] + <path fill="#FFF" fill-rule="nonzero" d="M136.3 159.7v-.3l-.2-.5.2.9M155.9 106.8l-.3-.9.3 1"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M230.3 174.3c0-1-.1-2-.2-2.9-.1-.7-.2-1.4-.2-2.1-.3-.1-.6-.1-1-.2l-.4 4.5c.6.2 1.2.4 1.8.7zM242.5 116.3c-.7-.6-1.6-.9-2.6-1-.8 0-1.6.2-2.3.7.7.6 1.7.9 2.6.9.9 0 1.7-.2 2.3-.6z"/> + <path fill="#FFF" fill-rule="nonzero" d="M174.5 205.9c9.6 4.4 20.4 5.7 30.8 3.8 14.7-2.9 21.6-12.6 23.9-20.8.4-1.5.7-3 .8-4.4 0-.5.1-1 .1-1.5.2-2.5.2-5 .2-7.5v-.1c-.6-.3-1.3-.6-1.9-.8l-1.1 11.7c-.1.9-.8 1.5-1.7 1.5l-22.1 3.1c-.9.9-2.9 2.3-6.5 2.5h-.8c-3.3 0-5.3-1.4-6.2-2.3l-24.9-3.6c-.9 0-1.6-.7-1.6-1.5l-1.2-15.4c-2.4.6-4.8 1.4-7.1 2.3.3 2 .6 4.2 1 6.4.1.1.2.2.2.4l.2.9c.6 3.2 2.6 14.1 4.8 23.4v.1h.1v.1h.1c.8 3.7 1.7 7.3 2.9 10.9 2 5.6 4.5 10.3 6.4 12.7.3.3.6.6.8.9.1 0 .1.1.2.2.7.6 1. [...] + <path fill="#FAFAFA" fill-rule="nonzero" d="M152 137.6c.1 3.3.2 6.6.4 9.8l.3-.3v.1c-.1.9.3 1.9.8 2.9.8 1.4 2.1 2.7 3.2 3.7 1.8-1 3.3-1.2 4-1.2l-.3-4.3c0-.5.1-1 .5-1.3.3-.3.8-.5 1.3-.5h.3l1.4-6.1c.1-.4.4-.8.8-.8.4-.1 10.5-2.3 19.1 1.5 7.1 3.1 11.3 7.9 13.1 10.5 1.5-2.6 5.2-7.4 12.7-11 6.3-3.1 15.5-3.4 16.7-2.9.3.1.6.4.7.7.2.6 1.3 4.5 1.9 7.1h.2c.5 0 1 .1 1.3.5.2.2.4.5.4.8 5.4-.1 10.7-.9 14.2-2.7 6.7-3.5 11.1-12.3 10.9-20.7 0-1.2-.2-2.4-.4-3.6-.6-2.9-2-5.9-3.7-8.9-2.8-4.7-6.4-9.1-8.6-1 [...] + <path fill="#FFF" fill-rule="nonzero" d="M161.7 166.1c-3.6.4-6.2-.6-7.8-1.9.3 2.1.7 4.7 1.1 7.6 2.3-.9 4.7-1.7 7.2-2.3l-.1-.8c-.2-.9-.4-1.7-.4-2.6z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M136.4 159.7l-.1-.3"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M156.4 186.9H145c-.1.3-.2.5-.4.7h.8l.3.2c.1-.2.3-.3.6-.3h.7c1.6.1 2.6.3 3.1.7.3.2.5.5.8.8h6c-.2-.7-.4-1.4-.5-2.1zM150.2 182.9c0-.3-.2-.6-.6-.6h-7.5v1.1h7.5c.3 0 .6-.2.6-.5z"/> + <path fill="url(#a)" fill-rule="nonzero" d="M252 110.6c1.7 3 3.1 6 3.7 8.9.2 1.2.4 2.4.4 3.6.2 8.5-4.2 17.3-10.9 20.8-3.5 1.9-8.8 2.6-14.2 2.7v.5l-.3 2.9c2.1.2 4.2.4 6.4.4.1 0 .3 0 .4.1.8 0 1.6-.1 2.4-.2 6.9-.9 11-3.2 15.3-8.7 1.4-1.7 2.9-4.5 4.1-7.7 1.5-4.1 2.4-8.8 1.3-12.8-1.2-4.7-4.7-9.3-7.7-12.5-2.9-3.8-6-7.3-9.5-10.6-.1-.1-.3-.2-.5-.2-.1 0-.3.1-.4.1.2.3.5.6.8.9 2.3 2.7 5.9 7.1 8.7 11.8z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M242.6 98c.2.3.5.6.8.9-.3-.3-.6-.6-.8-.9z"/> + <path fill="url(#b)" fill-rule="nonzero" d="M242.6 98c.2.3.5.6.8.9-.3-.3-.6-.6-.8-.9z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M252 110.6c1.7 3 3.1 6 3.7 8.9.2 1.2.4 2.4.4 3.6 0-1.2-.2-2.4-.4-3.6-.6-2.9-2-6-3.7-8.9z"/> + <path fill="url(#c)" fill-rule="nonzero" d="M252 110.6c1.7 3 3.1 6 3.7 8.9.2 1.2.4 2.4.4 3.6 0-1.2-.2-2.4-.4-3.6-.6-2.9-2-6-3.7-8.9z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M229.9 169.3c.1.7.1 1.4.2 2.1 0-.8-.1-1.5-.2-2.1z"/> + <path fill="url(#d)" fill-rule="nonzero" d="M229.9 169.3c.1.7.1 1.4.2 2.1 0-.8-.1-1.5-.2-2.1z"/> + <path fill="#EDEDF0" fill-rule="nonzero" d="M220.4 226.2c0 1.8.2 3.1.5 3.3.1.1.3.2.5.2.5 0 1.2-.5 1.9-1.3.5-.5 1-1.2 1.5-2-1.4-.1-2.9-.2-4.4-.2z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M205.3 209.7c-10.4 1.9-21.2.6-30.8-3.8 9.6 4.4 20.3 5.8 30.8 3.8 14.7-2.8 21.6-12.5 23.9-20.8-2.3 8.3-9.2 17.9-23.9 20.8z"/> + <path fill="url(#e)" fill-rule="nonzero" d="M205.3 209.7c-10.4 1.9-21.2.6-30.8-3.8 9.6 4.4 20.3 5.8 30.8 3.8 14.7-2.8 21.6-12.5 23.9-20.8-2.3 8.3-9.2 17.9-23.9 20.8z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M230.1 183c.2-2.5.3-5 .2-7.4 0 2.4 0 4.9-.2 7.4z"/> + <path fill="url(#f)" fill-rule="nonzero" d="M230.1 183c.2-2.5.3-5 .2-7.4 0 2.4 0 4.9-.2 7.4z"/> + <path fill="url(#g)" fill-rule="nonzero" d="M236.4 180.1v-1c-1.9-1.3-3.9-2.4-5.9-3.4 0 .7.1 1.3.1 1.8 2 .8 3.9 1.4 5.8 2.6z"/> + <path fill="url(#h)" fill-rule="nonzero" d="M236.5 172.8c-.1-1.6-.1-3.1-.2-4.6-1.4 1-3.6 1.6-6.4 1.1.1.7.2 1.4.2 2.1.2 1.5.3 3 .4 4.3-.1 0-.1-.1-.2-.1 0 2.5 0 5-.2 7.4 0 .5-.1 1-.1 1.5-.1 1.4-.4 2.9-.8 4.4-2.3 8.3-9.2 18-23.9 20.8-10.4 1.9-21.2.6-30.8-3.8v2.9h.1c.3 0 .6.2.6.5l.3 6.4c2.9.9 10.8 3 23.3 3 7.3-.2 14.5-1.1 21.5-2.9 2.3-.9 4.4-2.2 6.3-3.8.2-.2.6-.2.8 0 .2.2.2.6 0 .8h-.1v.1c-1.9 1.7-4.1 3-6.4 4-.2 2.9-.3 5.7-.3 7.9v1.4c0 1.8.2 3.1.5 3.3.1.1.3.2.5.2.5 0 1.2-.5 1.9-1.3.5-.5 1 [...] + <path fill="url(#i)" fill-rule="nonzero" d="M202.7 108.7v3.5c0 .2 0 .4.1.6.4-.1.8-.1 1.2-.1.5 0 1.1.1 1.6.2.1-.2.2-.5.2-.7v-3.5c0-.9-.7-1.6-1.6-1.6-.8 0-1.5.7-1.5 1.6z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M202.8 112.8c-1.8.3-3.4 1.3-4.5 2.8-.4.7-.3 1.5.2 2.1.1.1.2.2.3.2.8.5 1.8.3 2.3-.5.4-.6 1-1 1.6-1.2h.1c.2-.1.4-.1.5-.1H203.9c.7 0 1.5.2 2.1.7.3.2.5.4.7.7.5.8 1.5 1 2.3.5.1-.1.2-.1.3-.2.6-.5.7-1.4.2-2.1-1-1.4-2.4-2.3-4-2.7-.5-.1-1.1-.2-1.6-.2-.3-.1-.7 0-1.1 0zm5.1 1.6c.1 0 .1.1.2.2s.3.2.4.4c.2.1.3.3.5.5.1.1.2.2.2.3l.3.3c.1.2.1.5.1.7v.1c0 .1-.1.2-.1.2 0 .1 0 .1-.1.2 0 .1-.1.1-.1.1l-.1.1h-.1-.1c-.1 0-.2.1-.2.1h-.1c-.4 0-.7-.1-1-.4-.8-1-2-1.7-3 [...] + <path fill="url(#j)" fill-rule="nonzero" d="M202.9 113.4c-.2 0-.4.1-.6.2-.2.1-.4.1-.5.2h-.1c-.2.1-.3.2-.5.2h-.1c-.2.1-.3.2-.5.3h-.1c-.3.2-.5.4-.8.7l-.1.1c-.2.2-.4.5-.6.7-.3.5-.2 1.2.3 1.5.4.3.9.2 1.2 0l.3-.3c.2-.2.4-.5.6-.7l.1-.1c.2-.1.4-.3.5-.4.1-.1.2-.1.4-.2.1-.1.3-.1.4-.2.2-.1.4-.1.6-.1H204c1.3 0 2.5.6 3.3 1.7.2.3.6.4 1 .4h.1c.1 0 .2 0 .2-.1.1 0 .1 0 .2-.1h.1l.1-.1.1-.1s.1-.1.1-.2.1-.1.1-.2v-.1c0-.2 0-.5-.1-.7l-.3-.3c-.1-.1-.1-.2-.2-.3-.1-.2-.3-.3-.5-.5-.1-.1-.3-.2-.4-.4-1.2-.8-3. [...] + <path fill="url(#k)" fill-rule="nonzero" d="M140 165.4v.7l.6-.9"/> + <path fill="#FFF" fill-rule="nonzero" d="M137.8 166.1l-1.9.6c-.8.2-1.2 1.1-1 1.8l1.1 3.7.2.6.1.2v.1l.1.4c-.5.6-1 1.1-1.4 1.7h2.4c.2-.4.3-.9.3-1.4v-7.7h.1z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M140 168.2l-.1.2c-.2.3-.5.3-.8.2l-.2-.2c-.1-.2-.1-.4 0-.6l1.1-1.6v-.7l-2.2.7v7.7c0 .5-.1 1-.3 1.4h2.3c.1-.5.2-.9.2-1.4v-5.7z"/> + <path fill="url(#l)" fill-rule="nonzero" d="M154.4 170.6c-3.5 1.5-6.8 3.4-9.9 5.6.1.1.1.2.2.4l.2.7c3.1-2 6.3-3.8 9.7-5.2-.1-.6-.1-1.1-.2-1.5z"/> + <path fill="url(#m)" fill-rule="nonzero" d="M156.8 189.1c-.2-.8-.3-1.5-.4-2.2-.8-4.1-1.3-6.9-1.3-6.9 0-.3.1-.5.4-.6.1-.1.2-.1.2-.1.2 0 .3 0 .4.1-.4-2.2-1.1-5.2-1.5-7.4.1-.1.2-.1.4-.2-.4-2.9-.8-5.5-1.1-7.7-1.5-1.3-2.1-2.8-2-3.7v-.1c-1.2-.6-2.1-1.4-2.8-2.1-1.4-1.5-1.7-3-1.7-3.2v-.4c.1-.4.5-.8.9-.8h.3l.2-.2c.1-.6.3-1.3.6-1.9.8-1.9 2.1-3.5 2.7-4.3-.1-3.2-.2-6.5-.3-9.9 0 .1 0 .2-.1.3l-.6 3c-.1.2-.1.5-.2.7-.3 1.1-.5 2.3-.9 3.5-.1.2-.1.4-.2.6v.2l-.1.2-.1.5h-.1l-1.5 4.1c-.1.2-.3.4-.6.3h-.1-. [...] + <path fill="#F9F9FA" fill-rule="nonzero" d="M149.2 153.6s0-.1 0 0c.2 0 .2 0 .3.1.2.1.3.3.3.6 0 0-.1.7.8 1.8.5.6 1.3 1.2 2.5 1.9.2-.9.7-1.7 1.5-2.5s1.5-1.3 2.3-1.7c-1.2-1.1-2.4-2.4-3.2-3.7-.6-.9-.9-1.9-.9-2.8v-.1l-.3.3c-.6.7-1.9 2.4-2.7 4.3-.3.6-.5 1.3-.6 1.9-.2-.2-.1-.2 0-.1z"/> + <path fill="url(#n)" fill-rule="nonzero" d="M136.3 173.1l-.3-.9-1.1-3.7c-.2-.8.2-1.6 1-1.8l1.9-.6 2.2-.7.6-.2h.1l-.7 1-1.1 1.6c-.1.2-.1.4 0 .5 0 .1.1.2.2.2.3.2.6.1.8-.2l.1-.2 2.4-3.5h.1c1.2-2.2 2.4-4.4 3.3-6.7v-.1l-.2-.3-.1-.2-.1-.1-2.9-4.7c-.4-.7-.2-1.6.5-2l4.7-2.9.3-.2.1-.1-1 2.8c-.1.3.1.6.3.7h.1c.2 0 .5-.1.6-.3l1.5-4.1h.1l.1-.5.1-.2v-.2c.1-.2.1-.4.2-.6.3-1.2.6-2.3.9-3.5.1-.2.1-.5.2-.7l.6-3v-.2c.2-.9.3-1.9.5-2.8 0-10.8.9-20.2 2.9-26.1v.1l.7 1.7c.1.2.3.3.5.4h.2c.3-.1.4-.4.3-.7l-1.2- [...] + <path fill="url(#o)" fill-rule="nonzero" d="M237.3 167.2c-.2.3-.6.7-1 1 0 .9.1 1.7.2 2.4.1.5 0 1.3 0 2.2 0 2-.2 4.6 0 6.1v3.5c-.4 15.3-3.8 25.4-5.2 29-.4 1.4-.7 2.6-1.1 3.8-1.6 4.6-3.6 8.6-5.3 11.2-.5.8-1.1 1.5-1.5 2-.7.8-1.4 1.3-1.9 1.3-.2 0-.3-.1-.5-.2-.3-.3-.5-1.5-.5-3.3v-1-.3-.1c0-2.2.2-5.1.3-7.9v-.1c2.4-.9 4.6-2.3 6.5-4h.1c.2-.2.2-.6 0-.8-.2-.2-.6-.2-.8 0-1.9 1.6-4 2.9-6.3 3.8-7.1 1.8-14.3 2.7-21.5 2.9-12.5 0-20.4-2.1-23.3-3l-.3-6.4c0-.3-.3-.5-.6-.5h-.1c-.1 0-.2.1-.3.2-.1.1-.1.2 [...] + <path fill="url(#p)" fill-rule="nonzero" d="M152 160.4c.2-.2 1.1.2 1-.1-.2-.8-.2-1.6 0-2.4-1.2-.7-2-1.3-2.5-1.9-.9-1-.8-1.8-.8-1.8 0-.2-.1-.4-.2-.6-.1 0-.1-.1-.2-.1H149c-.1 0-.2.1-.2.2h-.3c-.5.1-.8.4-.9.8v.4c0 .2.3 1.7 1.7 3.2.6 1 1.5 1.7 2.7 2.3z"/> + <path fill="url(#q)" fill-rule="nonzero" d="M161.9 166s-.1 0-.2.1c.1.9.2 1.8.4 2.6l-.2-2.7z"/> + <path fill="url(#r)" fill-rule="nonzero" d="M241.3 112.7c.1-.2.2-.5.2-.7v-3.5c0-.5-.3-1-.7-1.3-.3-.2-.6-.3-.9-.3-.9 0-1.6.7-1.6 1.6v3.5c0 .2 0 .4.1.6h.3c.3 0 .6-.1.9-.1.6 0 1.2.1 1.7.2z"/> + <path fill="url(#s)" fill-rule="nonzero" d="M235.1 117.3c.3.2.7.2 1 .1.2-.1.4-.2.6-.4.3-.4.7-.7 1.1-1 .7-.4 1.4-.7 2.3-.7 1 0 1.9.4 2.6 1 .3.2.5.4.7.7.4.5 1.1.6 1.6.2.4-.3.6-.9.3-1.4-.2-.4-.5-.7-.8-1-.8-.8-1.8-1.3-2.8-1.5-.8-.2-1.6-.2-2.4-.1-1 .1-1.9.5-2.7 1.1-.3.2-.6.4-.8.7l-.4.4-.4.4c-.6.5-.5 1.2.1 1.5z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M102.5 224.6c-.3 0-.5-.2-.5-.5s.2-.5.5-.5h44.2c.1 0 .2 0 .2.1-.1-.4-.2-.8-.4-1.2H101v5.2h45.2c.2-.3.4-.8.6-1.3H102.4c-.3 0-.5-.2-.5-.5s.2-.5.5-.5h44.2c.1 0 .2 0 .3.1.1-.4.1-.8 0-1.3-.1.1-.2.2-.3.2h-44.1v.2z"/> + <path fill="url(#t)" fill-rule="nonzero" d="M96.8 230.3c.9-.1 1.9-.2 2.9-.3v-.3h-2.6c-.1 0-.2 0-.3.1 0 .2-.1.3 0 .5z"/> + <path fill="url(#u)" fill-rule="nonzero" d="M151.8 225.1c0-2.9-1-5.2-1.9-6-.2-.1-1-.4-2.6-.5-.1.4-.3.9-.9.9l-.4.1-1.2-1H97.1c-.2 0-.3.2-.3.3v1.4c0 .2.2.3.3.3h15.6l34.2-.6.6.6h.1l.2.3 1.1 1.1-.2 1.1c.3 1.4.2 3-.2 4.2l3-.3c.2-.6.3-1.3.3-1.9z"/> + <path fill="#EDEDF0" fill-rule="nonzero" d="M142.6 230.1h-43v-.1c-1 .1-2 .2-2.9.3l.3 1c0 .2.2.3.3.3H144l1.1-1 .5.1c.4.1.6.3.8.6l.4.4c1.6 0 2.8-.3 3.3-.7.5-.4 1.1-2.1 1.5-3.8l-3 .3c-.2.5-.4 1-.6 1.4l-.2 1-5.2.2z"/> + <path fill="url(#v)" fill-rule="nonzero" d="M142.6 230.1h-43v-.1c-1 .1-2 .2-2.9.3l.3 1c0 .2.2.3.3.3H144l1.1-1 .5.1c.4.1.6.3.8.6l.4.4c1.6 0 2.8-.3 3.3-.7.5-.4 1.1-2.1 1.5-3.8l-3 .3c-.2.5-.4 1-.6 1.4l-.2 1-5.2.2z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M112.7 220.7h34.9l-.7-.6"/> + <path fill="url(#w)" fill-rule="nonzero" d="M112.7 220.7h34.9l-.7-.6"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M147.9 221l.1.1c.4.6.7 1.3.8 2l.2-1.1-1.1-1z"/> + <path fill="url(#x)" fill-rule="nonzero" d="M147.9 221l.1.1c.4.6.7 1.3.8 2l.2-1.1-1.1-1z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M99.7 230.1h43l5.1-.3.2-1c-.2.3-.4.5-.6.7l-.3.3H99.7v.3z"/> + <path fill="url(#y)" fill-rule="nonzero" d="M99.7 230.1h43l5.1-.3.2-1c-.2.3-.4.5-.6.7l-.3.3H99.7v.3z"/> + <path fill="url(#z)" fill-rule="nonzero" d="M95.2 231.8c.2 1 1.1 1.7 2.2 1.7h47.3l.6-.6c.3.3.8.6 1.4.6 1.5 0 3.4-.2 4.5-1.1 1.1-.9 1.9-3.4 2.2-5.4.1-.7.2-1.4.2-1.9 0-3.2-1-6.2-2.6-7.5-.6-.4-1.8-.7-3.6-.9h-.8c-.3 0-.6.1-.7.3l-.4-.3H97c-1.2 0-2.2 1-2.2 2.2v1.4c0 1.2 1 2.2 2.2 2.2h1.2v5.2H97c-.7 0-1.3.3-1.8.9-.4.5-.5 1.1-.4 1.7v.2l.4 1.3zm1.6-1.9c.1-.1.2-.1.3-.1h50l.3-.3c.2-.2.4-.4.6-.7.3-.4.5-.9.6-1.4.4-1.3.5-2.8.2-4.2-.2-.7-.4-1.4-.8-2l-.1-.1-.2-.3H97.2c-.2 0-.3-.2-.3-.3v-1.4c0-.2.2-. [...] + <path fill="url(#A)" fill-rule="nonzero" d="M147 223.8c-.1 0-.2-.1-.2-.1h-44.2c-.3 0-.5.2-.5.5s.2.5.5.5h44.2c.1 0 .3-.1.3-.2.1-.1.1-.2.1-.3 0-.2-.1-.3-.2-.4z"/> + <path fill="url(#B)" fill-rule="nonzero" d="M102.5 225.6c-.3 0-.5.2-.5.5s.2.5.5.5H146.9c.2-.1.3-.2.3-.4 0-.1-.1-.3-.2-.4-.1-.1-.2-.1-.3-.1h-44.2v-.1z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M103 210.8h38.8v-5.2h-38.5c-.8 1.1-1 3.5-.3 5.2z"/> + <path fill="url(#C)" fill-rule="nonzero" d="M134.5 203.4c-1.4-.5-2.8-.9-4.2-1.5h-.2l4.2 1.5h.2z"/> + <path fill="url(#D)" fill-rule="nonzero" d="M145.3 203.6c-2.5-.5-5.1-1-7.6-1.8h-.2c2.5.8 5.1 1.4 7.8 1.8z"/> + <path fill="url(#E)" fill-rule="nonzero" d="M103.2 213.9l.4-.1 1 1h40.6c.2 0 .3-.2.3-.3v-1.4c0-.2-.1-.3-.3-.3h-13.3l-29.1.6-.5-.6h-.1l-.2-.3-.9-1.1.1-1.1c-.4-2-.1-4.2.7-5.6l.1-1 4.4-.3h18.5c-.8-.4-1.7-.9-2.6-1.5h-17.1l-.9 1-.4-.1c-.3-.1-.5-.3-.7-.6-.1-.1-.2-.3-.3-.4-1.3 0-2.4.3-2.8.7-.7.6-1.4 3.8-1.4 5.9 0 2.9.8 5.2 1.6 6 .1.1.9.4 2.2.5 0-.5.2-.9.7-1z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M134.3 203.4c-1.4-.5-2.8-.9-4.2-1.5h-7.8c.9.6 1.8 1.1 2.6 1.5h9.4z"/> + <path fill="url(#F)" fill-rule="nonzero" d="M134.3 203.4c-1.4-.5-2.8-.9-4.2-1.5h-7.8c.9.6 1.8 1.1 2.6 1.5h9.4z"/> + <path fill="url(#G)" fill-rule="nonzero" d="M145.3 203.6c.1-.1.1-.2.1-.3l-.2-1.1c0-.2-.1-.3-.3-.3h-7.2c2.5.7 5 1.3 7.6 1.7z"/> + <path fill="url(#H)" fill-rule="nonzero" d="M145.3 203.6c.1-.1.1-.2.1-.3l-.2-1.1c0-.2-.1-.3-.3-.3h-7.2c2.5.7 5 1.3 7.6 1.7z"/> + <path fill="url(#I)" fill-rule="nonzero" d="M142.9 203.4v.3h2.2c.1 0 .1 0 .2-.1-2.6-.4-5.2-1-7.8-1.8h-7.2l4.2 1.5h8.4v.1z"/> + <path fill="url(#J)" fill-rule="nonzero" d="M142.9 203.4v.3h2.2c.1 0 .1 0 .2-.1-2.6-.4-5.2-1-7.8-1.8h-7.2l4.2 1.5h8.4v.1z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M131.8 212.7h-29.6l.5.7"/> + <path fill="url(#K)" fill-rule="nonzero" d="M131.8 212.7h-29.6l.5.7"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M101.9 212.4l-.1-.1c-.3-.6-.6-1.3-.7-2l-.1 1.1.9 1z"/> + <path fill="url(#L)" fill-rule="nonzero" d="M101.9 212.4l-.1-.1c-.3-.6-.6-1.3-.7-2l-.1 1.1.9 1z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M134.5 203.4h-28.1l-4.4.3-.1 1c.1-.3.3-.5.5-.7l.2-.3H143v-.3h-8.5z"/> + <path fill="url(#M)" fill-rule="nonzero" d="M134.5 203.4h-28.1l-4.4.3-.1 1c.1-.3.3-.5.5-.7l.2-.3H143v-.3h-8.5z"/> + <path fill="url(#N)" fill-rule="nonzero" d="M103 216.8h.2c.2 0 .4-.1.5-.3l.3.3H145.4c1-.1 1.7-1.1 1.7-2.2v-1.4c0-1.2-.9-2.2-1.9-2.2h-1v-5.5h1c.6 0 1.1-.3 1.5-.9.2-.3.3-.5.3-.8.1-.3.1-.7 0-1.1l-.2-1.1c-.1-.4-.3-.8-.5-1.1-.4-.1-.7-.3-1-.5l-.5.4h-40.1c-.2 0-.3 0-.5-.1-.4-.1-.7-.3-.9-.6h-.2c-1.2 0-2.9.2-3.9 1.1-1.3 1.3-2 5.5-2 7.4 0 3.2.9 6.2 2.2 7.5.5.4 1.5.7 3.1.9.1.1.3.2.5.2zm-2.8-2.5c-.8-.8-1.6-3.1-1.6-6 0-2.1.8-5.3 1.4-5.9.4-.4 1.5-.6 2.8-.7.1.1.3.3.3.4.2.3.4.5.7.6l.4.1.9-1H144.8c.1 [...] + <path fill="#FAFAFA" fill-rule="nonzero" d="M108.9 193.8c-.2 0-.4-.2-.4-.4s.2-.4.4-.4h37.5c.1 0 .1 0 .2.1l-.3-.9h-38.6v4.1H146c.2-.3.4-.6.5-1h-37.6c-.2 0-.4-.2-.4-.4s.2-.4.4-.4h37.5c.1 0 .2 0 .3.1 0-.3.1-.7 0-1-.1.1-.2.1-.3.1h-37.5v.1z"/> + <path fill="url(#O)" fill-rule="nonzero" d="M104.3 198.9c0 .1.1.2.3.2h13.9c-.4-.4-.9-.8-1.3-1.1h-10.7v-.3h-2.2c-.1 0-.2 0-.2.1-.1.1-.1.1-.1.2l.3.9z"/> + <path fill="url(#P)" fill-rule="nonzero" d="M104.2 189.1c-.1 0-.2.1-.2.2v1.1c0 .1.1.3.3.3h9.3c0-.6.1-1.1.2-1.6h-9.6z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M113.8 189.1h-9.5-.1 9.6z"/> + <path fill="url(#Q)" fill-rule="nonzero" d="M113.8 189.1h-9.5-.1 9.6z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M117.2 198c.4.4.8.8 1.3 1.1h5.7c-.7-.4-1.4-.7-2-1.1h-5z"/> + <path fill="url(#R)" fill-rule="nonzero" d="M117.2 198c.4.4.8.8 1.3 1.1h5.7c-.7-.4-1.4-.7-2-1.1h-5z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M113.8 189.1c-.1.5-.2 1.1-.2 1.6h3.4c.1-.6.2-1.1.4-1.6h-3.6z"/> + <path fill="url(#S)" fill-rule="nonzero" d="M113.8 189.1c-.1.5-.2 1.1-.2 1.6h3.4c.1-.6.2-1.1.4-1.6h-3.6z"/> + <path fill="url(#T)" fill-rule="nonzero" d="M142.9 198h-15.8c.9.4 1.7.8 2.6 1.1h14.4l.9-.8.4.1c.3.1.5.3.7.5.1.1.2.3.3.3 1.3 0 2.4-.2 2.8-.5.7-.5 1.4-2.9 1.4-4.6 0-2.3-.8-4-1.6-4.6-.1-.1-.8-.3-2-.4h-.2c-.1.3-.3.6-.7.7h-.3l-1-.7h-13.3c-.4.5-.7.9-1.1 1.4l16.2-.3.5.5h.1l.2.2.9.8-.1.9c.4 1.6.1 3.2-.7 4.3l-.1.8-4.5.3z"/> + <path fill="url(#T)" fill-rule="nonzero" d="M142.9 198h-15.8c.9.4 1.7.8 2.6 1.1h14.4l.9-.8.4.1c.3.1.5.3.7.5.1.1.2.3.3.3 1.3 0 2.4-.2 2.8-.5.7-.5 1.4-2.9 1.4-4.6 0-2.3-.8-4-1.6-4.6-.1-.1-.8-.3-2-.4h-.2c-.1.3-.3.6-.7.7h-.3l-1-.7h-13.3c-.4.5-.7.9-1.1 1.4l16.2-.3.5.5h.1l.2.2.9.8-.1.9c.4 1.6.1 3.2-.7 4.3l-.1.8-4.5.3z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M146.8 189.1h.2-.2z"/> + <path fill="url(#U)" fill-rule="nonzero" d="M146.8 189.1h.2-.2zM146.8 189.1h.2-.2z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M144.8 189.1h-13.3 13.3z"/> + <path fill="url(#V)" fill-rule="nonzero" d="M144.8 189.1h-13.3 13.3zM144.8 189.1h-13.3 13.3z"/> + <path fill="url(#W)" fill-rule="nonzero" d="M119.8 189.1c-.3.5-.5 1-.6 1.6l10.5-.2c.3-.5.7-.9 1-1.4h-10.9z"/> + <path fill="url(#W)" fill-rule="nonzero" d="M119.8 189.1c-.3.5-.5 1-.6 1.6l10.5-.2c.3-.5.7-.9 1-1.4h-10.9z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M130.8 189.1h-10.9 10.9z"/> + <path fill="url(#X)" fill-rule="nonzero" d="M130.8 189.1h-10.9 10.9zM130.8 189.1h-10.9 10.9z"/> + <path fill="url(#Y)" fill-rule="nonzero" d="M129.7 190.5h.6c.4-.5.7-.9 1.1-1.4h-.7c-.3.5-.6.9-1 1.4z"/> + <path fill="url(#Y)" fill-rule="nonzero" d="M129.7 190.5h.6c.4-.5.7-.9 1.1-1.4h-.7c-.3.5-.6.9-1 1.4z"/> + <path fill="url(#Y)" fill-rule="nonzero" d="M129.7 190.5h.6c.4-.5.7-.9 1.1-1.4h-.7c-.3.5-.6.9-1 1.4z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M131.5 189.1h-.7.7z"/> + <path fill="url(#Z)" fill-rule="nonzero" d="M131.5 189.1h-.7.7zM131.5 189.1h-.7.7zM131.5 189.1h-.7.7z"/> + <path fill="url(#aa)" fill-rule="nonzero" d="M122.2 198c.6.4 1.3.8 2 1.1h5.5c-.9-.4-1.8-.7-2.6-1.1h-4.9z"/> + <path fill="url(#ab)" fill-rule="nonzero" d="M122.2 198c.6.4 1.3.8 2 1.1h5.5c-.9-.4-1.8-.7-2.6-1.1h-4.9z"/> + <path fill="url(#ac)" fill-rule="nonzero" d="M119.8 189.1h-2.5c-.2.5-.3 1-.4 1.6h2.3c.1-.6.3-1.1.6-1.6z"/> + <path fill="url(#ad)" fill-rule="nonzero" d="M119.8 189.1h-2.5c-.2.5-.3 1-.4 1.6h2.3c.1-.6.3-1.1.6-1.6z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M117.2 198H142.9l4.4-.2.1-.8c-.1.2-.3.4-.5.5l-.2.2h-40.2v.3h10.7z"/> + <path fill="url(#ae)" fill-rule="nonzero" d="M117.2 198H142.9l4.4-.2.1-.8c-.1.2-.3.4-.5.5l-.2.2h-40.2v.3h10.7z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M130.3 190.5h-.6l-10.5.2h-1.6 29.5l-.5-.5"/> + <path fill="url(#af)" fill-rule="nonzero" d="M130.3 190.5h-.6l-10.5.2h-1.6 29.5l-.5-.5"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M147.4 191l.1.1c.3.5.6 1 .7 1.6l.1-.9-.9-.8z"/> + <path fill="url(#ag)" fill-rule="nonzero" d="M147.4 191l.1.1c.3.5.6 1 .7 1.6l.1-.9-.9-.8z"/> + <path fill="url(#ah)" fill-rule="nonzero" d="M104.1 200.5c.2 0 .3.1.5.1h40.1l.5-.4c.2.2.6.4 1 .5h.2c1.2 0 2.9-.2 3.8-.8 1.3-1 2-4.2 2-5.7 0-2-.6-3.8-1.4-5-.2-.3-.5-.6-.8-.8-.5-.3-1.5-.6-3.1-.7h-.7c-.3 0-.5.1-.6.3l-.3-.2h-.8c-.3.4-.8.6-1.4.6h-40.2c-.2.3-.4.6-.5.9v1.3c0 1 .8 1.7 1.9 1.7h1v4.1h-1c-.6 0-1.1.2-1.5.7-.4.4-.5 1-.3 1.5l.2.9c.1.3.2.5.4.7.2 0 .5.2 1 .3-.1 0-.1 0 0 0zm0-2.7c.1-.1.1-.1.2-.1H146.7l.2-.2.5-.5c.8-1.1 1.1-2.8.7-4.3-.1-.6-.4-1.1-.7-1.6l-.1-.1-.2-.2h-42.8c-.2 0-.3-.1- [...] + <path fill="url(#ai)" fill-rule="nonzero" d="M146.6 193.1c-.1 0-.1-.1-.2-.1h-37.5c-.2 0-.4.2-.4.4s.2.4.4.4h37.5c.1 0 .2 0 .3-.1.1-.1.1-.2.1-.3 0-.1-.1-.2-.2-.3z"/> + <path fill="url(#aj)" fill-rule="nonzero" d="M108.9 194.6c-.2 0-.4.2-.4.4s.2.4.4.4h37.6c.2 0 .3-.2.3-.3 0-.1-.1-.2-.1-.3-.1-.1-.2-.1-.3-.1h-37.5v-.1z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M101.1 183.6h38.6v-4.1h-38.4c-.6 1-.8 2.8-.2 4.1z"/> + <path fill="url(#ak)" fill-rule="nonzero" d="M98.4 186.4c.1.1.9.3 2.2.4.1-.3.3-.7.8-.7h.3l1 .8h11.9c.2-.5.5-1 .8-1.4l-14.6.2-.5-.5h-.1l-.2-.2-.9-.8.1-.9c-.3-1.2-.2-2.5.2-3.5H97c-.2.7-.3 1.4-.3 2 .1 2.2.9 4 1.7 4.6z"/> + <path fill="#FFF" fill-rule="nonzero" d="M120.7 176.7h-17.3l-.9.8h17.9c0-.3.1-.5.3-.8z"/> + <path fill="url(#al)" fill-rule="nonzero" d="M120.7 176.7h-17.3l-.9.8h17.9c0-.3.1-.5.3-.8z"/> + <path fill="#FFF" fill-rule="nonzero" d="M102 177.4c-.3-.1-.5-.3-.7-.5-.1-.1-.2-.3-.3-.3-1.3 0-2.4.2-2.8.5l-.3.3h4.5-.4z"/> + <path fill="url(#am)" fill-rule="nonzero" d="M102 177.4c-.3-.1-.5-.3-.7-.5-.1-.1-.2-.3-.3-.3-1.3 0-2.4.2-2.8.5l-.3.3h4.5-.4z"/> + <path fill="#FFF" fill-rule="nonzero" d="M133 177.5c.2-.3.5-.5.8-.8l-.8.8z"/> + <path fill="url(#an)" fill-rule="nonzero" d="M133 177.5c.2-.3.5-.5.8-.8l-.8.8z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M100 178.9l.1-.8 4.4-.2h15.6c0-.1.1-.2.2-.4H97.9c-.3.5-.7 1.3-.9 2.2h2.5c.2-.3.3-.6.5-.8z"/> + <path fill="url(#ao)" fill-rule="nonzero" d="M100 178.9l.1-.8 4.4-.2h15.6c0-.1.1-.2.2-.4H97.9c-.3.5-.7 1.3-.9 2.2h2.5c.2-.3.3-.6.5-.8z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M133 177.5c-.2.1-.3.2-.4.4l.4-.4z"/> + <path fill="url(#ap)" fill-rule="nonzero" d="M133 177.5c-.2.1-.3.2-.4.4l.4-.4z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M120.2 185.3l-4.7.1c-.3.4-.6.9-.8 1.4h4.1c.3-.6.8-1.1 1.4-1.5z"/> + <path fill="url(#aq)" fill-rule="nonzero" d="M120.2 185.3l-4.7.1c-.3.4-.6.9-.8 1.4h4.1c.3-.6.8-1.1 1.4-1.5z"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M120.1 177.9h4c.7-.7 1.6-1.1 2.6-1.1h.3l3.3.3c.1-.1.2-.2.3-.4h-10c-.1.3-.3.5-.4.8 0 .1 0 .2-.1.4z"/> + <path fill="url(#ar)" fill-rule="nonzero" d="M120.1 177.9h4c.7-.7 1.6-1.1 2.6-1.1h.3l3.3.3c.1-.1.2-.2.3-.4h-10c-.1.3-.3.5-.4.8 0 .1 0 .2-.1.4z"/> + <path fill="url(#as)" fill-rule="nonzero" d="M143.1 186.7c.2 0 .3-.1.3-.3v-1.1c0-.1-.1-.3-.3-.3h-7.9l-1.6 1.6h9.5v.1z"/> + <path fill="url(#at)" fill-rule="nonzero" d="M143.1 186.7c.2 0 .3-.1.3-.3v-1.1c0-.1-.1-.3-.3-.3h-7.9l-1.6 1.6h9.5v.1z"/> + <path fill="url(#au)" fill-rule="nonzero" d="M122 186.7h10.7c.3-.3.6-.6.8-.9l.7-.7h-4.4l-5.8.1c-.7.6-1.4 1.1-2 1.5z"/> + <path fill="url(#av)" fill-rule="nonzero" d="M122 186.7h10.7c.3-.3.6-.6.8-.9l.7-.7h-4.4l-5.8.1c-.7.6-1.4 1.1-2 1.5z"/> + <path fill="url(#aw)" fill-rule="nonzero" d="M140.9 177.9v.3h1c.4-.3.9-.7 1.3-1l-.1-.2c0-.1-.1-.2-.3-.2h-3.6c-.2.4-.5.8-.9 1.1h2.6z"/> + <path fill="url(#ax)" fill-rule="nonzero" d="M140.9 177.9v.3h1c.4-.3.9-.7 1.3-1l-.1-.2c0-.1-.1-.2-.3-.2h-3.6c-.2.4-.5.8-.9 1.1h2.6z"/> + <path fill="#FFF" fill-rule="nonzero" d="M133.9 177.5c.9 0 1.7-.3 2.4-.8h-2.5c-.2.3-.5.5-.8.8h.9z"/> + <path fill="url(#ay)" fill-rule="nonzero" d="M133.9 177.5c.9 0 1.7-.3 2.4-.8h-2.5c-.2.3-.5.5-.8.8h.9z"/> + <path fill="url(#az)" fill-rule="nonzero" d="M133.9 177.5c.9 0 1.7-.3 2.4-.8h-2.5c-.2.3-.5.5-.8.8h.9z"/> + <path fill="#D7D7DB" fill-rule="nonzero" d="M133 177.5l-.4.4h5.7c.3-.3.6-.7.9-1.1h-3c-.7.5-1.5.8-2.4.8h-.8v-.1z"/> + <path fill="url(#aA)" fill-rule="nonzero" d="M133 177.5l-.4.4h5.7c.3-.3.6-.7.9-1.1h-3c-.7.5-1.5.8-2.4.8h-.8v-.1z"/> + <path fill="url(#aB)" fill-rule="nonzero" d="M133 177.5l-.4.4h5.7c.3-.3.6-.7.9-1.1h-3c-.7.5-1.5.8-2.4.8h-.8v-.1z"/> + <path fill="url(#aC)" fill-rule="nonzero" d="M132.7 186.7h.9c.5-.6 1.1-1.1 1.6-1.6h-.9l-.7.7-.9.9z"/> + <path fill="url(#aC)" fill-rule="nonzero" d="M132.7 186.7h.9c.5-.6 1.1-1.1 1.6-1.6h-.9l-.7.7-.9.9z"/> + <path fill="url(#aD)" fill-rule="nonzero" d="M132.7 186.7h.9c.5-.6 1.1-1.1 1.6-1.6h-.9l-.7.7-.9.9z"/> + <path fill="url(#aE)" fill-rule="nonzero" d="M143.1 178.1c.1 0 .2 0 .2-.1.1-.1.1-.1.1-.2l-.2-.7c-.4.3-.9.7-1.3 1h1.2z"/> + <path fill="url(#aE)" fill-rule="nonzero" d="M143.1 178.1c.1 0 .2 0 .2-.1.1-.1.1-.1.1-.2l-.2-.7c-.4.3-.9.7-1.3 1h1.2z"/> + <path fill="url(#aF)" fill-rule="nonzero" d="M143.1 178.1c.1 0 .2 0 .2-.1.1-.1.1-.1.1-.2l-.2-.7c-.4.3-.9.7-1.3 1h1.2z"/> + <path fill="url(#aG)" fill-rule="nonzero" d="M127 176.8h-.3c-1 0-1.9.4-2.6 1.1h8.5l.4-.4c.2-.3.5-.5.8-.8h-3c-.1.1-.2.2-.3.4l-3.5-.3z"/> + <path fill="url(#aH)" fill-rule="nonzero" d="M127 176.8h-.3c-1 0-1.9.4-2.6 1.1h8.5l.4-.4c.2-.3.5-.5.8-.8h-3c-.1.1-.2.2-.3.4l-3.5-.3z"/> + <path fill="url(#aI)" fill-rule="nonzero" d="M120.2 185.3c-.6.5-1.1 1-1.5 1.5h3.4c.6-.5 1.3-1 2-1.5h-3.9z"/> + <path fill="url(#aJ)" fill-rule="nonzero" d="M120.2 185.3c-.6.5-1.1 1-1.5 1.5h3.4c.6-.5 1.3-1 2-1.5h-3.9z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M115.4 185.3h4.8l3.9-.1 5.8-.1h-29.6l.6.5"/> + <path fill="url(#aK)" fill-rule="nonzero" d="M115.4 185.3h4.8l3.9-.1 5.8-.1h-29.6l.6.5"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M100.1 184.9l-.1-.1c-.3-.5-.6-1-.7-1.6l-.1.9.9.8z"/> + <path fill="url(#aL)" fill-rule="nonzero" d="M100.1 184.9l-.1-.1c-.3-.5-.6-1-.7-1.6l-.1.9.9.8z"/> + <path fill="#FAFAFA" fill-rule="nonzero" d="M138.4 177.9h-33.8l-4.4.2-.1.8c.1-.2.3-.4.5-.5l.2-.2H141v-.3h-2.6z"/> + <path fill="url(#aM)" fill-rule="nonzero" d="M138.4 177.9h-33.8l-4.4.2-.1.8c.1-.2.3-.4.5-.5l.2-.2H141v-.3h-2.6z"/> + <path fill="url(#aN)" fill-rule="nonzero" d="M97.4 187.5c.5.3 1.5.6 3.1.7h.7c.3 0 .5-.1.6-.3l.3.2h41c.6 0 1.1-.2 1.4-.6.2-.2.4-.5.4-.7 0-.1.1-.3.1-.4v-1.1c0-1-.8-1.7-1.9-1.7h-1v-4h1c.6 0 1.1-.2 1.5-.7.4-.4.5-1 .3-1.5l-.1-.2-.2-.7c0-.1-.1-.3-.2-.4-.3-.6-.9-.9-1.7-.9h-40l-.5.4c-.3-.2-.7-.5-1.2-.5-1.2 0-2.9.2-3.8.8-.4.3-.8.8-1.1 1.5-.3.7-.6 1.5-.7 2.2-.2.8-.3 1.5-.3 2 0 2.1.6 4 1.6 5.2.2.3.4.5.7.7zm-.4-7.8c.2-.9.5-1.8.9-2.2l.3-.3c.4-.3 1.5-.5 2.8-.5.1.1.3.2.3.3.2.2.4.4.7.5l.4.1.9-.8h39. [...] + <path fill="#FFF" fill-rule="nonzero" d="M190.1 151.1c-8.6-5.4-23.3-5.4-23.5-5.4-.3 0-.6-.2-.6-.6 0-.3.2-.6.6-.6.1 0 7.6-.1 15.1 1.8 5.8 1.5 10 3.7 12.6 6.7h.2c-9.2-10.9-26.7-9.7-26.9-9.6-.3 0-.6-.2-.6-.5s.2-.6.5-.6c.2 0 15.7-1.1 25.6 7.7-2.1-2.3-5.5-5.2-10.1-7.3-6.7-2.9-14.7-1.9-17-1.5l-1.2 5.3 25.1 4.3c0 .3.1.3.2.3zM210.5 142.3c-5 2.4-8.1 5.4-10 7.7 8.8-8.7 22.6-8.9 22.7-8.9.3 0 .6.2.6.6 0 .3-.2.6-.6.6-.2 0-15.1.2-23.3 10 .2-.1.4-.2.6-.4 2.3-2.2 5.4-4 9.5-5.5 6.9-2.5 14.1-3.1 14.2- [...] + <path fill="url(#aO)" fill-rule="nonzero" d="M230.9 147v-.5c-.1-.3-.2-.6-.4-.8-.3-.4-.8-.5-1.3-.5h-.2c-.7-2.6-1.7-6.5-1.9-7.1-.1-.3-.4-.6-.7-.7-1.2-.5-10.5-.2-16.7 2.9-7.5 3.7-11.2 8.5-12.7 11-1.8-2.6-6-7.4-13.1-10.5-8.6-3.7-18.7-1.6-19.1-1.5-.4.1-.8.4-.8.8l-1.4 6.1h-.3c-.5 0-.9.2-1.3.5-.3.3-.5.8-.5 1.3l.3 4.3h.5l.4.1.5 4.9c1.6-.1 6-.5 6.5-.5.8 0 1.3.5 1.4 1.4.2 1.9-1.9 5-7.1 6.2-.3.1-.9.6-1.1.8-.1.1.2.6-.1.8l.2 2.7.1.8.1 1.2 1.2 15.4c.1.9.8 1.5 1.6 1.5l24.9 3.6c.9.9 2.9 2.3 6.2 2.3h [...] + <path fill="url(#aP)" fill-rule="nonzero" d="M223.7 156.1c-.2-.2-.6-.3-.9-.3l-11.5 1.8c-.5.1-.9.5-.9 1.1l-.1 5.5c0 .3.1.6.4.9.2.2.5.3.7.3h.2l10.6-1.6c-.4-.9-.5-1.8-.4-2.4.1-.8.6-1.4 1.4-1.4.1 0 .5 0 .9.1l.1-3.1c-.2-.4-.3-.7-.5-.9z"/> + <path fill="url(#aQ)" fill-rule="nonzero" d="M223.7 156.1c-.2-.2-.6-.3-.9-.3l-11.5 1.8c-.5.1-.9.5-.9 1.1l-.1 5.5c0 .3.1.6.4.9.2.2.5.3.7.3h.2l10.6-1.6c-.4-.9-.5-1.8-.4-2.4.1-.8.6-1.4 1.4-1.4.1 0 .5 0 .9.1l.1-3.1c-.2-.4-.3-.7-.5-.9z"/> + <path fill="#FFF" fill-rule="nonzero" d="M162.8 163.3c4.7-1 6.4-3.7 6.3-5 0-.4-.2-.4-.3-.4-.4 0-4.4.3-6.9.6h-.5l-.6-5.1c-.9 0-3.2.4-5.5 2.6-1.4 1.3-1.7 3.1-.9 4.6.9 2 3.7 3.8 8.4 2.7z"/> + <path fill="url(#aR)" fill-rule="nonzero" d="M161.7 166.1c.1 0 .2 0 .2-.1.3-.2 0-.7.1-.8.2-.2.8-.7 1.1-.8 5.2-1.1 7.3-4.3 7.1-6.2-.1-.8-.6-1.4-1.4-1.4-.5 0-4.9.4-6.5.5l-.5-4.9-.4-.1h-.5c-.8 0-2.3.2-4 1.2-.7.4-1.5 1-2.3 1.7-.8.7-1.3 1.6-1.5 2.5-.2.8-.2 1.6 0 2.4.1.3-.8-.1-1 .1v.1c-.1.9.5 2.4 2 3.7 1.4 1.5 3.9 2.5 7.6 2.1zm-6.5-9.9c2.3-2.3 4.6-2.6 5.5-2.6l.6 5.1h.5c2.6-.2 6.5-.6 6.9-.6.1 0 .2 0 .3.4.1 1.2-1.6 3.9-6.3 5-4.7 1-7.5-.7-8.5-2.5-.7-1.7-.3-3.4 1-4.8z"/> + <path fill="#FFF" fill-rule="nonzero" d="M231.1 156.6l-.6 5.1h-.5c-2.6-.2-6.5-.6-6.9-.6-.1 0-.2 0-.3.4-.1 1.2 1.6 3.9 6.3 5 4.7 1 7.5-.7 8.5-2.5.8-1.5.5-3.3-.9-4.6-2.5-2.4-4.7-2.7-5.6-2.8z"/> + <path fill="url(#aS)" fill-rule="nonzero" d="M236.7 157.9c-3.2-2.7-6.1-2.4-6.2-2.4h-.5l-.5 4.9c-1.2-.1-4-.3-5.5-.5-.5 0-.8-.1-.9-.1-.8 0-1.3.5-1.4 1.4-.1.6.1 1.5.4 2.4.8 1.9 2.7 4 6.2 4.7 1 .2-1.2 0-.4.3.4.1.7.2 1 .3.3.1.6.2 1 .2 2.8.5 5.1-.1 6.4-1.1.4-.3.8-.6 1-1 .4-.5.7-1.6.9-2.2.1-.2.2-.4.2-.5.8-1.6.7-3.3-.2-4.8-.2-.4-.5-.7-.9-1.1-.2-.1-.4-.3-.6-.5zm.8 6c-1 1.8-3.8 3.5-8.5 2.5s-6.4-3.7-6.3-5c0-.4.2-.4.3-.4.4 0 4.4.3 6.9.6h.5l.6-5.1c.9 0 3.2.4 5.5 2.6 1.5 1.5 1.8 3.2 1 4.8z"/> + <path fill="url(#aT)" fill-rule="nonzero" d="M189.7 154.7l.3 33.2s1.1 2.2 6.4 2.8c5.3.6 6.7-3.1 6.7-3.1l.8-33.7s-2.5 2.2-6.7 2.5c-4.2.3-7.5-1.7-7.5-1.7z"/> + <path fill="url(#aU)" fill-rule="nonzero" d="M189.7 154.7l.3 33.2s1.1 2.2 6.4 2.8c5.3.6 6.7-3.1 6.7-3.1l.8-33.7s-2.5 2.2-6.7 2.5c-4.2.3-7.5-1.7-7.5-1.7z"/> + <path d="M-31-22h352v303H-31z"/> + </g> +</svg> diff --git a/browser/extensions/onboarding/content/img/figure_performance.svg b/browser/extensions/onboarding/content/img/figure_performance.svg new file mode 100644 index 000000000000..f7c5c219aada --- /dev/null +++ b/browser/extensions/onboarding/content/img/figure_performance.svg @@ -0,0 +1 @@ +<svg width="297" height="245" viewBox="0 0 297 245" xmlns="http://www.w3.org/2000/svg"><title>performance</title><defs><linearGradient x1="-920.838%" y1="-294.992%" x2="891.374%" y2="366.984%" id="a"><stop stop-color="#FFFBCC" offset="0%"/><stop stop-color="#CEF7C6" offset="100%"/></linearGradient><linearGradient x1="-162.81%" y1="-242.422%" x2="179.364%" y2="239.183%" id="b"><stop stop-color="#FFFBCC" offset="0%"/><stop stop-color="#CEF7C6" offset="100%"/></linearGradient><linearGradien [...] \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/figure_private.svg b/browser/extensions/onboarding/content/img/figure_private.svg new file mode 100644 index 000000000000..f90163e4b4d7 --- /dev/null +++ b/browser/extensions/onboarding/content/img/figure_private.svg @@ -0,0 +1 @@ +<svg width="289" height="237" viewBox="0 0 289 237" xmlns="http://www.w3.org/2000/svg"><title>private-browsing</title><defs><linearGradient x1="12.376%" y1="17.359%" x2="82.943%" y2="91.352%" id="a"><stop stop-color="#E60024" offset="0%"/><stop stop-color="#ED00B5" offset="51.53%"/><stop stop-color="#8000D7" offset="100%"/></linearGradient><linearGradient x1="-3.914%" y1=".14%" x2="98.417%" y2="106.522%" id="b"><stop stop-color="#E60024" offset="0%"/><stop stop-color="#ED00B5" offset="51 [...] \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/figure_screenshots.svg b/browser/extensions/onboarding/content/img/figure_screenshots.svg new file mode 100644 index 000000000000..f4930d09f7af --- /dev/null +++ b/browser/extensions/onboarding/content/img/figure_screenshots.svg @@ -0,0 +1,191 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="281" height="233"> + <defs> + <linearGradient id="a" x1="-26.7072552%" x2="121.200691%" y1="-8.21456664%" y2="115.364749%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="b" x1="-171.534367%" x2="377.694136%" y1="-258.916232%" y2="507.082022%"> + <stop stop-color="#E6FCFF" offset="0%"/> + <stop stop-color="#B5F2FF" offset="100%"/> + </linearGradient> + <linearGradient id="c" x1="-275.615152%" x2="393.814483%" y1="-214.880097%" y2="329.931438%"> + <stop stop-color="#E6FCFF" offset="0%"/> + <stop stop-color="#B5F2FF" offset="100%"/> + </linearGradient> + <linearGradient id="d" x1="-71.2230562%" x2="141.268437%" y1="-46.5567621%" y2="122.213199%"> + <stop stop-color="#E6FCFF" offset="0%"/> + <stop stop-color="#B5F2FF" offset="100%"/> + </linearGradient> + <linearGradient id="e" x1="-912.187374%" x2="706.872366%" y1="-223.131903%" y2="247.7375%"> + <stop stop-color="#E6FCFF" offset="0%"/> + <stop stop-color="#B5F2FF" offset="100%"/> + </linearGradient> + <linearGradient id="f" x1="-636.509606%" x2="265.115932%" y1="-364.308744%" y2="178.753736%"> + <stop stop-color="#E6FCFF" offset="0%"/> + <stop stop-color="#B5F2FF" offset="100%"/> + </linearGradient> + <linearGradient id="g" x1="-96.7324958%" x2="214.858961%" y1="-489.128132%" y2="600.29142%"> + <stop stop-color="#E6FCFF" offset="0%"/> + <stop stop-color="#B5F2FF" offset="100%"/> + </linearGradient> + <linearGradient id="h" x1="-370.226425%" x2="176.655533%" y1="-420.236682%" y2="206.08556%"> + <stop stop-color="#E6FCFF" offset="0%"/> + <stop stop-color="#B5F2FF" offset="100%"/> + </linearGradient> + <linearGradient id="i" x1="-1573.85207%" x2="2621.18334%" y1="-918.807829%" y2="1582.542%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="j" x1="-1977.10979%" x2="2217.92561%" y1="-1158.35597%" y2="1342.99386%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="k" x1="-635.169191%" x2="1018.69953%" y1="-1184.44408%" y2="1785.60576%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="l" x1="-278.76866%" x2="377.256589%" y1="-697.981967%" y2="835.635246%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="m" x1="-553.131633%" x2="647.619338%" y1="-1374.34047%" y2="1418.49315%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="n" x1="-450.59361%" x2="546.286439%" y1="-895.950857%" y2="958.91224%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="o" x1="-511.211278%" x2="295.07392%" y1="-745.273546%" y2="396.265912%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="p" x1="-871.182847%" x2="303.781403%" y1="-595.928571%" y2="241.5435%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="q" x1="-450.336951%" x2="307.764971%" y1="-505.416691%" y2="315.448433%"> + <stop stop-color="#E6FCFF" offset="0%"/> + <stop stop-color="#B5F2FF" offset="100%"/> + </linearGradient> + <linearGradient id="r" x1="-2519.79056%" x2="1944.50093%" y1="-1090.70814%" y2="890.815528%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="s" x1="-134.127826%" x2="165.330874%" y1="-297.102666%" y2="260.202663%"> + <stop stop-color="#E6FCFF" offset="0%"/> + <stop stop-color="#B5F2FF" offset="100%"/> + </linearGradient> + <linearGradient id="t" x1="-1132.52358%" x2="304.180944%" y1="-1559.01765%" y2="393.843988%"> + <stop stop-color="#E6FCFF" offset="0%"/> + <stop stop-color="#B5F2FF" offset="100%"/> + </linearGradient> + <linearGradient id="u" x1="-1884.94918%" x2="1592.74001%" y1="-342.289711%" y2="381.222953%"> + <stop stop-color="#E6FCFF" offset="0%"/> + <stop stop-color="#B5F2FF" offset="100%"/> + </linearGradient> + <linearGradient id="v" x1="-109.932792%" x2="195.629347%" y1="-425.144051%" y2="431.622036%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="w" x1="-813.648281%" x2="368.736119%" y1="-1076.38789%" y2="459.249729%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="x" x1="-1092.12785%" x2="635.82518%" y1="-4587.46665%" y2="2425.66052%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="y" x1="-415.250984%" x2="1490.35841%" y1="-442.448072%" y2="1582.67684%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="z" x1="-167.167389%" x2="492.546376%" y1="-2085.55413%" y2="4392.09342%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="A" x1="-2989.85248%" x2="1926.86535%" y1="-1363.11821%" y2="921.90878%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + <linearGradient id="B" x1="-2586.45105%" x2="2652.41027%" y1="-792.93501%" y2="883.790987%"> + <stop stop-color="#00C8D7" offset="0%"/> + <stop stop-color="#008EA4" offset="100%"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd"> + <g fill="#D7D7DB" fill-rule="nonzero"> + <path d="M204.3 76.7h-77c-.6 0-1.1-.5-1.1-1.1 0-.6.5-1.1 1.1-1.1h77c.6 0 1.1.5 1.1 1.1 0 .6-.4 1.1-1.1 1.1zM193.9 71h-13.4c-.3 0-.6-.2-.6-.6 0-.3.2-.6.6-.6h13.4c.3 0 .6.2.6.6 0 .4-.2.6-.6.6zM176.4 81.7H163c-.3 0-.6-.2-.6-.6 0-.4.2-.6.6-.6h13.4c.3 0 .6.2.6.6 0 .4-.2.6-.6.6zm-22.2 0h-3.3c-.3 0-.6-.2-.6-.6 0-.4.2-.6.6-.6h3.3c.3 0 .6.2.6.6 0 .4-.3.6-.6.6zm-7.8 0h-1.1c-.3 0-.6-.2-.6-.6 0-.4.2-.6.6-.6h1.1c.3 0 .6.2.6.6 0 .4-.3.6-.6.6zm-11.2 0h-13.4c-.3 0-.6-.2-.6-.6 0-.4.2-.6.6-.6h13.4c. [...] + </g> + <g fill-rule="nonzero"> + <path fill="#F9F9FA" d="M152.3 47.8h23.8s-7.4-16.6 8.3-18.8c14.1-1.9 19.6 12.5 19.6 12.5s1.7-8.3 10-6.7c8.3 1.6 14.3 14.8 14.3 14.8H249"/> + <path fill="#D7D7DB" d="M249.5 45.8H245c-.3 0-.6-.2-.6-.6 0-.3.2-.6.6-.6h4.5c.3 0 .6.2.6.6-.1.4-.3.6-.6.6zm-14.5 0h-1.1c-.3 0-.6-.2-.6-.6 0-.4.2-.6.6-.6h1.1c.3 0 .6.2.6.6 0 .4-.3.6-.6.6zm-5.6 0h-.6c-.2 0-.4-.1-.5-.3-.1-.2-.6-1.1-1.3-2.3-.2-.3-.1-.6.2-.8.3-.2.6-.1.8.2.6.9 1 1.7 1.2 2.1h.3c.3 0 .6.2.6.6 0 .4-.4.5-.7.5zm-52.9-.7H175c-.3 0-.6-.2-.6-.6 0-.3.2-.6.6-.6h.6c-.1-.3-.2-.6-.4-1-.1-.3.1-.6.4-.7.3-.1.6.1.7.4.3 1 .6 1.7.6 1.7.1.2.1.4 0 .5-.1.1-.3.3-.4.3zm-10.4 0h-13.4c-.3 0-.6-.2 [...] + <path fill="#F9F9FA" d="M250.2 50.1h-97.9c-.6 0-1.1-.5-1.1-1.1 0-.6.5-1.1 1.1-1.1h97.9c.6 0 1.1.5 1.1 1.1 0 .6-.5 1.1-1.1 1.1z"/> + </g> + <g fill-rule="nonzero"> + <path fill="#F9F9FA" d="M49.3 29.4h13.2s-4.1-9.2 4.6-10.4c7.8-1.1 10.9 7 10.9 7s.9-4.6 5.6-3.8c4.6.9 8 8.3 8 8.3h11.5"/> + <path fill="#D7D7DB" d="M62.9 27.9H49.7c-.3 0-.6-.2-.6-.6 0-.3.2-.6.6-.6h12.8s.1-.1.2-.1c.3-.1.6 0 .7.3l.1.1c.1.2.1.4 0 .5-.2.3-.4.4-.6.4zm36.6-.1h-3.3c-.3 0-.6-.2-.6-.6 0-.3.2-.6.6-.6h3.3c.3 0 .6.2.6.6 0 .3-.3.6-.6.6zm-20.9-3.6h-.2c-.3-.1-.5-.4-.4-.7.3-.9 1.5-4 4.9-4 .4 0 .8 0 1.2.1 1.8.3 3.6 1.5 5.4 3.4.2.2.2.6 0 .8-.2.2-.6.2-.8 0-1.6-1.7-3.2-2.7-4.8-3-.4-.1-.7-.1-1-.1-2.6 0-3.5 2.2-3.8 3.2-.1.1-.3.3-.5.3zm-15.2-4.9c-.1 0-.3-.1-.4-.2-.2-.2-.2-.6 0-.8.8-.8 1.8-1.4 3.1-1.7.3-.1.6.1 [...] + <path fill="#F9F9FA" d="M104 31.6H49.6c-.6 0-1.1-.5-1.1-1.1 0-.6.5-1.1 1.1-1.1H104c.6 0 1.1.5 1.1 1.1 0 .6-.5 1.1-1.1 1.1z"/> + </g> + <g fill-rule="nonzero"> + <path fill="#FFF" d="M19.6 169.1c-2.8 0-5-2.2-5-4.8V46c0-3 2.4-5.4 5.4-5.4h127c3 0 5.4 2.4 5.4 5.4v118.3c0 2.6-2.3 4.8-5 4.8H19.6z"/> + <path fill="#D7D7DB" d="M146.9 41.8c2.3 0 4.2 1.9 4.2 4.2v118.3c0 2-1.8 3.7-3.9 3.7H19.6c-2.2 0-3.9-1.6-3.9-3.7V46c0-2.3 1.9-4.2 4.2-4.2h127zm0-2.2h-127c-3.6 0-6.5 2.9-6.5 6.5v118.3c0 3.3 2.8 5.9 6.2 5.9h127.6c3.4 0 6.2-2.7 6.2-5.9V46c0-3.5-2.9-6.4-6.5-6.4z"/> + </g> + <path fill="#D7D7DB" fill-rule="nonzero" d="M145.8 62.9V161c0 1-.1 1.2-.1 1.2s-.2.1-1.2.1h-122c-1 0-1.2-.1-1.2-.1s-.1-.2-.1-1.2V62.9h124.6zm1.1-1.2H20v99.2c0 2 .4 2.5 2.5 2.5h122c2 0 2.5-.4 2.5-2.5V61.7h-.1z"/> + <g fill="#D7D7DB" fill-rule="nonzero"> + <circle cx="3.8" cy="3.7" r="2.9" transform="translate(23 48)"/> + <circle cx="3" cy="3.7" r="2.9" transform="translate(33 48)"/> + <path d="M115.3 54.9H51.5c-1.7 0-3.1-1.4-3.1-3.1v-.3c0-1.7 1.4-3.1 3.1-3.1h63.8c1.7 0 3.1 1.4 3.1 3.1v.3c0 1.8-1.4 3.1-3.1 3.1z"/> + <g> + <circle cx="3.8" cy="3.7" r="2.9" transform="translate(127 48)"/> + <circle cx="3.1" cy="3.7" r="2.9" transform="translate(137 48)"/> + </g> + </g> + <g transform="translate(149 84)"> + <ellipse cx="42.7" cy="142" fill="#EDEDF0" fill-rule="nonzero" rx="42.5" ry="6.5"/> + <path fill="#F9F9FA" fill-rule="nonzero" d="M121.2 99.6c-1.3-3.1-4.3-5.2-7.7-5.2-.7 0-1.4.1-2.1.3-.8 0-3.1-.3-7.2-2-1.7-.7-4.8-3.9-8.4-10.5 5.2-19.9 5.5-36.8.7-50.3-.4-1-.9-2.1-1.5-3.2l-.3-1.4 2-1.7c1.6-1.4 2.3-3.5 1.7-5.6-.3-1.2-1-2.2-2-2.9 0-.3 0-.6-.1-.9-.4-2.3-2.2-4.1-4.5-4.4-.4-.1-10.6-1.7-17.1-1.7h-.4l-1.7-2.8C70 3.1 65.5.6 60.5.6c-2.6 0-5.2.7-7.5 2.1-2.6 1.6-4.5 3.9-5.7 6.7-6 .7-12.1 2.3-18.2 4.7l-3.4-1.4c-1.7-.7-3.5-1.1-5.4-1.1-5.8 0-10.9 3.5-13.1 8.8-2.7 6.6-.1 14 5.8 17.5 [...] + <path d="M115.2 101.4c-.4-.9-1.5-1.4-2.4-1-.2.1-.6.1-1.2.1-1.4 0-4.6-.3-9.8-2.4-5.5-2.2-10.3-10.6-12.7-15.5-.1-.2-.1-.5-.1-.8 5.4-19.5 5.9-35.8 1.4-48.4-.3-.8-.7-1.8-1.3-2.7-.1-.1-.1-.2-.1-.3L87.7 25c-.1-.4 0-.8.4-1.1l2.6-2.2-5.8-.9c-.5-.1-.9-.4-.9-.9s.2-.9.6-1.2l3-1.6c-3.5-.5-8.9-1.1-12.7-1.1-1.1 0-2 .1-2.6.2l-.4.1c-.4.1-.9-.1-1.1-.5l-3.4-5.5C66 8 63.5 6.7 60.9 6.7c-1.4 0-2.8.4-4.1 1.2-2.2 1.4-3.5 3.7-3.6 6.3 0 .6-.5 1-1.1 1.1-7.2.4-14.7 2.2-22.2 5.4-.3.1-.6.1-.9 0l-5.4-2.2c-.9-.4 [...] + <path fill="url(#a)" fill-rule="nonzero" d="M114.6 98c-.8-2.1-2.9-3.4-5.1-3.4-.6 0-1.1.1-1.7.3-.7 0-3.4 0-8.7-2.2-3-1.2-6.8-6-10.3-12.8 5.4-19.8 5.7-36.5 1-49.7-.3-1-.8-2-1.4-3l-.9-3.4 3.3-2.8c.8-.7 1.1-1.7.8-2.7-.3-1-1.1-1.7-2.1-1.9l-.7-.1c.5-.6.8-1.4.6-2.2-.2-1.1-1-2-2.1-2.1-.1 0-10.3-1.6-16.7-1.6-.7 0-1.3 0-1.9.1l-2.6-4.2C64 2.9 60.4.9 56.4.9c-2.1 0-4.2.6-6 1.7-2.5 1.6-4.3 4.1-5 6.9-6.6.6-13.5 2.3-20.3 5.1l-4.4-1.8c-1.4-.6-2.8-.8-4.3-.8-4.7 0-8.8 2.8-10.6 7.1-2.4 5.8.4 12.5 6.2 [...] + <path fill="url(#b)" fill-rule="nonzero" d="M36.6 40.6c-1.1 0-2.2-.2-3.3-.7l-16.2-6.6c-4.5-1.8-6.7-7-4.8-11.5 1.8-4.5 7-6.7 11.5-4.8L40 23.6c4.5 1.8 6.7 7 4.8 11.5-1.4 3.4-4.7 5.5-8.2 5.5z"/> + <path fill="url(#c)" fill-rule="nonzero" d="M70.8 39.3c-2.9 0-5.8-1.5-7.5-4.2L53.1 18.6c-2.6-4.1-1.3-9.6 2.8-12.1C60 3.9 65.5 5.2 68 9.3l10.2 16.5c2.6 4.1 1.3 9.6-2.8 12.1-1.4 1-3 1.4-4.6 1.4z"/> + <path fill="url(#d)" fill-rule="nonzero" d="M28.6 19.4c-2.2.9-12.8 10.5-11.1 37.1 1.7 26.2-21.6 21.8-3.8 53.4 3.9 6.9 50.2 17.7 58.6 12.7 2.5-1.5 31.6-54.6 19.1-89.8-4.1-11.5-28.5-28-62.8-13.4z"/> + <path fill="url(#e)" fill-rule="nonzero" d="M14.3 87.5s-2.6 17.8-1.7 26.6c1 8.8 3.3 13.7 5.1 12.8 1.7-.8 6.2-26.8 6.2-26.8l-9.6-12.6z"/> + <path fill="url(#f)" fill-rule="nonzero" d="M80.7 103s-5.5 17.1-10.3 24.6c-4.8 7.5-9.1 10.8-10.2 9.3-1.2-1.5 6.2-26.8 6.2-26.8l14.3-7.1z"/> + <path fill="url(#g)" fill-rule="nonzero" d="M33.5 19c7.8-4 28.9-2.7 38.4-4.1C77 14.1 91 16.3 91 16.3l-6 3.2 8.2 1.2-4.5 3.8 1.8 7.3-1.3-.7-46.3-12.8-9.4.7z"/> + <path fill="url(#h)" fill-rule="nonzero" d="M111.4 105.1c-2.3 0-6-.6-11.5-2.8-10-4-16.7-20.9-17.4-22.9-.6-1.5.2-3.2 1.7-3.8 1.5-.6 3.2.2 3.8 1.7 1.7 4.5 7.7 16.9 14.1 19.5 7.1 2.9 10.2 2.3 10.2 2.3 1.5-.6 3.2.1 3.8 1.6.6 1.5-.1 3.2-1.6 3.8-.4.3-1.4.6-3.1.6z"/> + <path fill="#FFF" fill-rule="nonzero" d="M35.4 29.8c-8.3 5.5-3.2 72.6 2.7 79.8 9.5 11.8 31.7 9.3 34.6 3 1.1-2.3 26-48.2 14.3-79.8-3-8-22.5-22.3-51.6-3z"/> + <path fill="url(#i)" fill-rule="nonzero" d="M50.3 43.8c.9.2 1.4 1.1 1.2 1.9l-.8 3.5c-.2.9-1.1 1.4-1.9 1.2-.9-.2-1.4-1.1-1.2-1.9l.8-3.5c.2-.9 1.1-1.4 1.9-1.2z"/> + <path fill="url(#j)" fill-rule="nonzero" d="M81.4 44.8c.9.2 1.4 1.1 1.2 1.9l-.8 3.5c-.2.9-1.1 1.4-1.9 1.2-.9-.2-1.4-1.1-1.2-1.9l.8-3.5c.2-.9 1-1.4 1.9-1.2z"/> + <path fill="url(#k)" fill-rule="nonzero" d="M48.9 57.6c-.5 0-1-.1-1.5-.2-3.5-.8-4.7-3.9-4.7-4.1-.3-.8.1-1.6.9-1.9.8-.3 1.6.1 1.9.9 0 .1.7 1.8 2.6 2.2 1.9.5 3.3-.8 3.3-.8.6-.6 1.5-.5 2.1 0 .6.6.5 1.5 0 2.1-.2.1-2 1.8-4.6 1.8z"/> + <path fill="url(#l)" fill-rule="nonzero" d="M56.6 69.2c-.8 0-1.4-.6-1.5-1.3-.1-.8.5-1.5 1.3-1.6 8.9-.7 17.1-2.5 18-3.8 1-1.7 1.2-4 1.2-4.1 0-.8.7-1.4 1.4-1.4.8 0 1.4.5 1.5 1.3.1 1.3.6 3.4 1.2 4.1 1.1 1.3 2.3 1.2 2.3 1.2.8 0 1.5.6 1.6 1.4.1.8-.6 1.5-1.4 1.6-1 .1-3.2-.3-4.8-2.3-.1-.2-.3-.4-.4-.6-.1.1-.1.2-.2.3-2 3.3-14.8 4.7-20.3 5.2h.1z"/> + <g fill-rule="nonzero"> + <path fill="url(#m)" d="M2.4 4.3C1.3 5 7.7 8.2 8.6 8.2c1.3 0 7.8-2.8 7.6-5C16 2.1 6.8 1.3 2.4 4.3z" transform="translate(70 52)"/> + <path fill="url(#n)" d="M8.6 9.7C7.5 9.7 1.5 7 .9 5c-.2-.8.1-1.5.7-2C5.8.2 13.9.3 16.3 1.4c1 .4 1.2 1.1 1.3 1.6.1.9-.2 1.7-1 2.6-1.8 2.1-6.4 4.1-8 4.1zm-3.9-5c1.3.8 3.5 1.9 4.1 2 .9-.1 4.3-1.7 5.5-2.8-2-.4-6.5-.5-9.6.8z" transform="translate(70 52)"/> + </g> + <g fill-rule="nonzero"> + <path fill="#C8C8CC" d="M115 92.8l-7.2.1-.5-40.7c0-3.3 2.5-6.1 5.7-6.3.3 0 .5.2.5.4l1.5 46.5z"/> + <path fill="#E1E1E6" d="M130.1 53.3c.2-.2.5-.1.7.1 1.9 2.7 1.4 6.4-1.1 8.5l-31.3 26-4.6-5.5 36.3-29.1z"/> + <path fill="url(#o)" d="M.7 10c-.4 2.6.2 5.2 1.9 7.1.8 1 1.8 1.7 2.9 2.3 3.5 1.6 7.8 1 11-1.7.2-.2.5-.4.7-.6l10.1-8.4c.4-.4.7-.9.8-1.4.1-.6-.1-1.1-.5-1.5l-2.9-3.4c-.2-.2-.4-.4-.7-.6-.2-.1-.5-.2-.7-.2-.6-.1-1.1.1-1.5.5l-2.9 2.4c-.1-.2-.2-.3-.4-.5-.8-1-1.8-1.7-2.9-2.3-3.5-1.6-7.8-1-11 1.7C2.5 5.1 1.2 7.5.7 10zm6.6-3.4c1.9-1.6 4.5-2.1 6.5-1.1.6.3 1.1.7 1.5 1.1 1.4 1.6 1.3 4.1.1 6.1-.5.7-1.1 1.4-2 2.1-1.9 1.3-4.2 1.5-5.9.7-.6-.3-1.1-.7-1.5-1.1-.8-1-1.2-2.4-.9-3.8 0-1.5.9-2.9 2.2-4z" [...] + <path fill="url(#p)" d="M0 2.5l.2 13.2v.9c.1 4.1 2.3 7.8 5.7 9.4 1.2.6 2.5.9 3.8.8 5.1-.1 9.3-4.7 9.2-10.4-.1-4.1-2.3-7.8-5.7-9.4-1.2-.6-2.5-.9-3.8-.8h-.6V2.4c0-.8-.5-1.5-1.2-1.9C7.3.4 7 .3 6.7.3L2.2.4C1.6.4 1.1.6.7 1 .2 1.4 0 2 0 2.5zm11.3 8.3c1.9.9 3.2 3.1 3.3 5.6 0 3.4-2.2 6.1-5 6.2-.7 0-1.3-.1-1.9-.4-1.8-.8-3-2.7-3.2-4.9v-.1c0-1.2.1-2.1.3-2.9.7-2.2 2.5-3.9 4.7-3.9.5 0 1.2.1 1.8.4z" transform="translate(107 83)"/> + <path fill="#C8C8CC" d="M111.3 70.6c1.3.1 2.2 1.3 2.1 2.5-.1 1.3-1.3 2.2-2.5 2.1-1.3-.1-2.2-1.3-2.1-2.5.1-1.2 1.2-2.2 2.5-2.1z"/> + </g> + <path fill="url(#q)" fill-rule="nonzero" d="M1.4 2.1L.3 5.7c-1 3.1.7 6.4 3.8 7.4 3.1 1 6.4-.7 7.4-3.8L14.4.1l-13 2z" transform="translate(57 67)"/> + <path fill="url(#r)" fill-rule="nonzero" d="M63.3 74.7h-.2c-.4-.1-.6-.5-.5-.9l2.2-6.8c.1-.4.5-.6.9-.5.4.1.6.5.5.9L64 74.2c-.1.3-.4.5-.7.5z"/> + <path fill="url(#s)" fill-rule="nonzero" d="M58.7 98.1c-17.5 0-33-27.8-33.6-29-.8-1.4-.3-3.2 1.2-4 1.4-.8 3.2-.3 4 1.2 4.2 7.6 17.5 27 29.4 25.9 15.2-1.4 22.4-6.9 22.4-7 1.3-1 3.1-.8 4.1.5 1 1.3.8 3.1-.4 4.1-.3.3-8.5 6.7-25.6 8.2-.5.1-1 .1-1.5.1z"/> + <path fill="url(#t)" fill-rule="nonzero" d="M112.5 97.8s-8 3.2-8.1 5.9c-.1 2.7 8.2 6 11.8.7 3.6-5.2-2.3-7.2-3.7-6.6z"/> + <path fill="url(#u)" fill-rule="nonzero" d="M30.5 65.3s.7 5.9 4.4 9.2c3.7 3.3-4.8 8.1-4.4 15.4.4 7.4 0-24.6 0-24.6z"/> + <path fill="url(#v)" fill-rule="nonzero" d="M58.8 98.9h-1.1C44 98.5 32 81 31.5 80.2c-.2-.3-.1-.8.2-1 .3-.2.8-.1 1 .2.1.2 12.1 17.7 25 18 12.8.3 25.3-6.2 27.1-7.7.5-.4.9-2.6.2-3.7-.7-1-2.4-.3-3.6.5-.3.2-.8.1-1-.2-.2-.3-.1-.8.2-1 3.4-2.1 4.9-.9 5.6-.1 1.2 1.6.8 4.7-.4 5.7-1.2 1-13.4 8-27 8z"/> + <path fill="url(#w)" fill-rule="nonzero" d="M110.8 108.3c-1.3 0-2.8-.3-4.4-1.3-1.9-1-2.8-2.2-2.7-3.6.2-2.7 4.7-4.5 5.2-4.7.4-.1.8 0 1 .4.1.4 0 .8-.4 1-1.6.6-4.2 2.1-4.3 3.5-.1.9 1 1.7 1.9 2.2 2.2 1.2 4.3 1.4 6.1.6 2.1-1 3.1-2.8 3.2-3.2.1-.6.5-2.4-.5-3.5-.7-.8-2.1-1.1-4.1-.9-.4 0-.8-.3-.8-.7 0-.4.3-.8.7-.8 2.5-.2 4.3.2 5.3 1.4 1.5 1.6 1 4 .8 4.7-.2.9-1.6 3.2-4 4.3-.8.3-1.8.6-3 .6z"/> + <path fill="url(#x)" fill-rule="nonzero" d="M61.1 125.5c-.4 0-.7-.3-.7-.7 0-.4.3-.7.7-.7 3.2 0 8.1-1 8.2-1 .4-.1.8.2.9.6.1.4-.2.8-.6.9-.2-.1-5.1.9-8.5.9z"/> + <path fill="url(#y)" fill-rule="nonzero" d="M23 25.4h-.2c-.4-.1-.6-.5-.5-.9.2-.7 2.4-5 7.8-7.4.4-.2.8 0 1 .4.2.4 0 .8-.4 1-4.7 2-6.7 5.8-6.9 6.4-.2.3-.5.5-.8.5z"/> + <path fill="url(#z)" fill-rule="nonzero" d="M68.5 14.8c-8.9 0-18.2-1.2-18.3-1.2-.4-.1-.7-.4-.6-.8.1-.4.4-.7.8-.6.1 0 14.1 1.8 24.1 1 .4 0 .8.3.8.7 0 .4-.3.8-.7.8-2 0-4 .1-6.1.1z"/> + <path fill="url(#A)" fill-rule="nonzero" d="M88.8 89h-.2c-.4-.1-.6-.5-.5-.9l2-6c.1-.4.5-.6.9-.5.4.1.6.5.5.9l-2 6c-.1.3-.4.5-.7.5z"/> + <path fill="url(#B)" fill-rule="nonzero" d="M21 119.1h-.1c-.4-.1-.7-.5-.6-.9l1.7-8.6c.1-.4.5-.7.9-.6.4.1.7.5.6.9l-1.7 8.6c-.2.4-.5.6-.8.6z"/> + </g> + <path fill="#D7D7DB" fill-rule="nonzero" d="M70.8 82.4c-3.7 0-6.6 3-6.6 6.6h6.6v-6.6zm20 0h-6.6V89h6.6v-6.6zm13.3 0V89h6.6c0-3.6-3-6.6-6.6-6.6zm-23.3 0h-6.6V89h6.6v-6.6zm19.9 0h-6.6V89h6.6v-6.6zm3.4 16.6h6.6v-6.6h-6.6V99zm0 20c3.7 0 6.6-3 6.6-6.6h-6.6v6.6zm0-10h6.6v-6.6h-6.6v6.6zm-1.5-7.2c-2.1-3-6.2-3.7-9.3-1.6l-12.7 9.4-6.5-4.6c0-.3.1-.6.1-1 0-2.7-1.3-5.1-3.3-6.6v-5h-6.6v3.5c-3.8.8-6.6 4.1-6.6 8.1 0 4.6 3.7 8.3 8.3 8.3 1.8 0 3.5-.6 4.8-1.6l4.1 2.9-4.6 3.3c-1.3-.8-2.7-1.2-4.3-1.2-4.6 [...] + <g fill="#D7D7DB" fill-rule="nonzero"> + <path d="M17.5 26.8l-.1-.1.1.1zM266.5 1.5v4.4c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V3h-2.9c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5h4.4c.8 0 1.5.7 1.5 1.5zM266.5 14.4v8.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5v-8.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5zm0 17V40c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5v-8.5c0-.8.7-1.5 1.5-1.5s1.5.6 1.5 1.4zm0 17.1V57c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5v-8.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5zm0 17V74c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5v-8.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1 [...] + </g> + <path d="M-18-32h352v303H-18z"/> + </g> +</svg> diff --git a/browser/extensions/onboarding/content/img/figure_singlesearch.svg b/browser/extensions/onboarding/content/img/figure_singlesearch.svg new file mode 100644 index 000000000000..9be029397ccf --- /dev/null +++ b/browser/extensions/onboarding/content/img/figure_singlesearch.svg @@ -0,0 +1 @@ +<svg width="303" height="253" viewBox="0 0 303 253" xmlns="http://www.w3.org/2000/svg"><title>search</title><defs><linearGradient x1="-18.632%" y1="-397.383%" x2="117.795%" y2="492.152%" id="a"><stop stop-color="#00C8D7" offset="0%"/><stop stop-color="#0A84FF" offset="100%"/></linearGradient><linearGradient x1="-312.046%" y1="-3945.649%" x2="293.266%" y2="2768.992%" id="b"><stop stop-color="#00C8D7" offset="0%"/><stop stop-color="#0A84FF" offset="100%"/></linearGradient><linearGradient x [...] \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/figure_sync.svg b/browser/extensions/onboarding/content/img/figure_sync.svg new file mode 100644 index 000000000000..74562d37236d --- /dev/null +++ b/browser/extensions/onboarding/content/img/figure_sync.svg @@ -0,0 +1 @@ +<svg width="279" height="212" viewBox="0 0 279 212" xmlns="http://www.w3.org/2000/svg"><title>sync</title><defs><linearGradient x1="-424.525%" y1="-219.797%" x2="201.215%" y2="136.157%" id="a"><stop stop-color="#CCFBFF" offset="0%"/><stop stop-color="#C9E4FF" offset="100%"/></linearGradient><linearGradient x1="-1416.558%" y1="-1417.275%" x2="631.855%" y2="631.14%" id="b"><stop stop-color="#CCFBFF" offset="0%"/><stop stop-color="#C9E4FF" offset="100%"/></linearGradient><linearGradient x1= [...] \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/icons_addons.svg b/browser/extensions/onboarding/content/img/icons_addons.svg new file mode 100644 index 000000000000..6b27dea39252 --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_addons.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="8 8 16 16"><title>Icons / Extension</title><g fill="none"><path d="M0 0h16v16H0z"/><path d="M22.5 16c-1 0-1 1-1.7 1-.5 0-.8-.3-.8-.7V13c0-.6-.4-1-1-1h-3.2c-.5 0-.8-.3-.8-.7 0-.8 1-.8 1-1.8 0-.9-.9-1.5-2-1.5s-2 .6-2 1.5c0 1 1 1 1 1.8 0 .4-.3.7-.7.7H9c-.6 0-1 .4-1 1v2.3c0 .4.3.7.8.7.7 0 .7-1 1.7-1 .9 0 1.5.9 1.5 2s-.6 2-1.5 2c-1 0-1-1-1.7-1-.5 0-.8.3-.8.8V23c0 .6.4 1 1 1h3.3c.4 0 .7-.3.7-.7 0-.8-1-.8-1-1.8 0-.9.9-1.5 2 [...] \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/icons_customize.svg b/browser/extensions/onboarding/content/img/icons_customize.svg new file mode 100644 index 000000000000..ae0a9409fa5c --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_customize.svg @@ -0,0 +1 @@ +<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><title>Glyph / Customize</title><g id="Symbols" fill="none" fill-rule="evenodd"><g id="Glyph-/-Customize" fill-rule="nonzero" fill="#3E3D40"><path d="M4 10c-.886.002-1.665.59-1.91 1.44 0 .01-.015.015-.018.025-.362 1.135-.705 2.11-1.76 2.573l-.022.012-.024.012c-.162.086-.265.254-.266.438 0 .276.224.5.5.5 1.74.12 3.46-.414 4.825-1.5.006-.006.007-.013.013-.02.62-.55.832-1.428.534-2.202C5.575 10.504 4.83 9.995 [...] \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/icons_default.svg b/browser/extensions/onboarding/content/img/icons_default.svg new file mode 100644 index 000000000000..235f7d65b685 --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_default.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><title>default-browser-16</title><path fill="context-fill" d="M8,6s0-4,3.5-4S15,5,15,6c0,4.5-7,9-7,9Z"/><path fill="context-fill" d="M8,6S8,2,4.5,2,1,5,1,6c0,4.5,7,9,7,9L9,9Z"/></svg> diff --git a/browser/extensions/onboarding/content/img/icons_library.svg b/browser/extensions/onboarding/content/img/icons_library.svg new file mode 100644 index 000000000000..064c2e619486 --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_library.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg width="92px" height="92px" viewBox="0 0 92 92" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>Tip / Icon / Library</title><desc>Created with Sketch.</desc><defs></defs><g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Tip-/-Icon-/-Library" fill-rule="nonzero" fill="#0C0C0D"><g id="Icon-/-Library-/-Web"><path d="M28.7405828,17.2350375 C25.5662458,17.2350375 22 [...] \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/icons_performance.svg b/browser/extensions/onboarding/content/img/icons_performance.svg new file mode 100644 index 000000000000..ad23ba27400c --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_performance.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M8 1a8.009 8.009 0 0 0-8 8 7.917 7.917 0 0 0 .78 3.43 1 1 0 1 0 1.8-.86A5.943 5.943 0 0 1 2 9a6 6 0 1 1 11.414 2.571 1 1 0 1 0 1.807.858A7.988 7.988 0 0 0 8 1z"/><path fill="context-fill" d="M11.769 7.078a.5.5 0 0 0-.69.153L8.616 11.1a2 2 0 1 0 .5 3.558 2.011 2.011 0 0 0 .54-.54 1.954 1.954 0 0 0-.2-2.479l2.463-3.871a.5.5 0 0 0-.15-.69z"/></svg> diff --git a/browser/extensions/onboarding/content/img/icons_private.svg b/browser/extensions/onboarding/content/img/icons_private.svg new file mode 100755 index 000000000000..7d4d2c416801 --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_private.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="8 8 16 16"><title>Icons / Private Browsing</title><g fill="none"><path d="M0 0h32v32H0z"/><path d="M20.4 20c-1.7 0-2.8-2-4.4-2-1.6 0-2.8 2-4.4 2-2 0-3.5-2-3.5-5.3-.1-2 .6-2.7 3.2-2.7s3.4 1.1 4.7 1.1c1.3 0 2.1-1.1 4.7-1.1s3.3.7 3.2 2.7c0 3.3-1.5 5.3-3.5 5.3zm-7.8-5.4c-1.6 0-2.3 1-2.3 1.2 0 .3 1.1.9 2.1.9 1.1 0 2.3-.4 2.3-.7-.2-1-1.1-1.6-2.1-1.4zm6.8 0c-1-.2-1.9.4-2.1 1.4 0 .3 1.2.7 2.3.7 1 0 2.1-.6 2.1-.9 0-.2-.7-1.2- [...] \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/icons_screenshots.svg b/browser/extensions/onboarding/content/img/icons_screenshots.svg new file mode 100644 index 000000000000..8d219dce78b5 --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_screenshots.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg width="92px" height="92px" viewBox="0 0 92 92" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>Tip / Icon / Screenshots</title><desc>Created with Sketch.</desc><defs></defs><g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Tip-/-Icon-/-Screenshots" fill-rule="nonzero" fill="#0C0C0D"><g id="Icon-/-Screenshot-/-Web"><path d="M23.0526905,5.75 C16.7062659,5.75 11. [...] \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/icons_singlesearch.svg b/browser/extensions/onboarding/content/img/icons_singlesearch.svg new file mode 100644 index 000000000000..3e06a3852288 --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_singlesearch.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="8 8 16 16 "><title>Icons / Search</title><g fill="none"><path d="M0 0h32v32H0z"/><path d="M23.7 22.3l-4.8-4.8c1.8-2.5 1.4-6.1-1-8.1s-5.9-1.9-8.1.4c-2.3 2.2-2.4 5.7-.4 8.1 2 2.4 5.6 2.8 8.1 1l4.8 4.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4zM14 18c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4c0 1.1-.4 2.1-1.1 2.9-.8.7-1.8 1.1-2.9 1.1z" fill="#3E3D40"/></g></svg> \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/icons_sync.svg b/browser/extensions/onboarding/content/img/icons_sync.svg new file mode 100644 index 000000000000..286422275aa7 --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_sync.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="8 8 16 16"><title> Icons / Sync</title><desc> Created with Sketch.</desc><g fill="none"><rect width="32" height="32"/><path d="M22 9C21.4 9 21 9.4 21 10L21 11.1C19.2 9.3 16.6 8.6 14.2 9.2 11.7 9.9 9.8 11.8 9.2 14.3 9.1 14.7 9.2 15 9.5 15.3 9.8 15.5 10.1 15.6 10.5 15.5 10.8 15.4 11.1 15.1 11.2 14.8 11.7 12.6 13.7 11 16 11 17.6 11 19 11.7 20 13L18 13C17.4 13 17 13.4 17 14 17 14.6 17.4 15 18 15L22 15C22.6 15 23 1 [...] diff --git a/browser/extensions/onboarding/content/img/icons_tour-complete.svg b/browser/extensions/onboarding/content/img/icons_tour-complete.svg new file mode 100644 index 000000000000..173e72c332df --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_tour-complete.svg @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 44.1 (41455) - http://www.bohemiancoding.com/sketch --> + <title>Tip / Check</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="Tips-/-Navigation" transform="translate(-30.000000, -117.000000)" stroke-width="2"> + <g id="Group"> + <g id="Tip-/-Check" transform="translate(30.000000, 117.000000)"> + <circle id="Oval-2" stroke="#FFFFFF" fill="#33F70C" fill-rule="evenodd" cx="10" cy="10" r="9"></circle> + <polyline id="Path-31" stroke="#165866" stroke-linecap="round" stroke-linejoin="round" points="5.5 10.5 8.5 13.5 14.5 6.5"></polyline> + </g> + </g> + </g> + </g> +</svg> \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/watermark.svg b/browser/extensions/onboarding/content/img/watermark.svg new file mode 100644 index 000000000000..c9345ed2ba1d --- /dev/null +++ b/browser/extensions/onboarding/content/img/watermark.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><title>newtab-firefox-gry</title><path d="M31.359,14.615h0c-.044-.289-.088-.459-.088-.459s-.113.131-.3.378A10.77,10.77,0,0,0,30.6,12.5a13.846,13.846,0,0,0-.937-2.411,10.048,10.048,0,0,0-.856-1.468q-.176-.263-.359-.51c-.57-.931-1.224-1.5-1.981-2.576a7.806,7.806,0,0,1-.991-2.685A10.844,10.844,0,0,0,25,4.607c-.777-.784-1.453-1.341-1.861-1.721C21.126,1.006,21.36.031,21.36.031h0S17.6,4.228,19.229,8.6a8.4,8.4,0, [...] diff --git a/browser/extensions/onboarding/content/onboarding-tour-agent.js b/browser/extensions/onboarding/content/onboarding-tour-agent.js new file mode 100644 index 000000000000..6a8729197f0f --- /dev/null +++ b/browser/extensions/onboarding/content/onboarding-tour-agent.js @@ -0,0 +1,114 @@ +/* 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/. */ + +/* globals Mozilla */ + +(function() { + "use strict"; + + let onCanSetDefaultBrowserInBackground = () => { + Mozilla.UITour.getConfiguration("appinfo", config => { + let canSetInBackGround = config.canSetDefaultBrowserInBackground; + let btn = document.getElementById( + "onboarding-tour-default-browser-button" + ); + btn.setAttribute("data-cansetbg", canSetInBackGround); + btn.textContent = canSetInBackGround + ? btn.getAttribute("data-bg") + : btn.getAttribute("data-panel"); + }); + }; + + let onClick = evt => { + switch (evt.target.id) { + case "onboarding-tour-addons-button": + Mozilla.UITour.showHighlight("addons"); + break; + case "onboarding-tour-customize-button": + Mozilla.UITour.showHighlight("customize"); + break; + case "onboarding-tour-default-browser-button": + Mozilla.UITour.getConfiguration("appinfo", config => { + let isDefaultBrowser = config.defaultBrowser; + let btn = document.getElementById( + "onboarding-tour-default-browser-button" + ); + let msg = document.getElementById( + "onboarding-tour-is-default-browser-msg" + ); + let canSetInBackGround = btn.getAttribute("data-cansetbg") === "true"; + if (isDefaultBrowser || canSetInBackGround) { + btn.classList.add("onboarding-hidden"); + msg.classList.remove("onboarding-hidden"); + if (canSetInBackGround) { + Mozilla.UITour.setConfiguration("defaultBrowser"); + } + } else { + btn.disabled = true; + Mozilla.UITour.setConfiguration("defaultBrowser"); + } + }); + break; + case "onboarding-tour-library-button": + Mozilla.UITour.showHighlight("library"); + break; + case "onboarding-tour-private-browsing-button": + Mozilla.UITour.showHighlight("privateWindow"); + break; + case "onboarding-tour-singlesearch-button": + Mozilla.UITour.showMenu("urlbar"); + break; + case "onboarding-tour-sync-button": + let emailInput = document.getElementById( + "onboarding-tour-sync-email-input" + ); + if (emailInput.checkValidity()) { + Mozilla.UITour.showFirefoxAccounts(null, emailInput.value); + } + break; + case "onboarding-tour-sync-connect-device-button": + Mozilla.UITour.showConnectAnotherDevice(); + break; + } + let classList = evt.target.classList; + // On keyboard navigation the target would be .onboarding-tour-item. + // On mouse clicking the target would be .onboarding-tour-item-container. + if ( + classList.contains("onboarding-tour-item") || + classList.contains("onboarding-tour-item-container") + ) { + Mozilla.UITour.hideHighlight(); // Clean up UITour if a user tries to change to other tours. + } + }; + + let overlay = document.getElementById("onboarding-overlay"); + overlay.addEventListener("submit", e => e.preventDefault()); + overlay.addEventListener("click", onClick); + overlay.addEventListener("keypress", e => { + let { target, key } = e; + let classList = target.classList; + if ( + (key == " " || key == "Enter") && + // On keyboard navigation the target would be .onboarding-tour-item. + // On mouse clicking the target would be .onboarding-tour-item-container. + (classList.contains("onboarding-tour-item") || + classList.contains("onboarding-tour-item-container")) + ) { + Mozilla.UITour.hideHighlight(); // Clean up UITour if a user tries to change to other tours. + } + }); + let overlayObserver = new MutationObserver(mutations => { + if (!overlay.classList.contains("onboarding-opened")) { + Mozilla.UITour.hideHighlight(); // Clean up UITour if a user tries to close the dialog. + } + }); + overlayObserver.observe(overlay, { attributes: true }); + document + .getElementById("onboarding-overlay-button") + .addEventListener("Agent:Destroy", () => Mozilla.UITour.hideHighlight()); + document.addEventListener( + "Agent:CanSetDefaultBrowserInBackground", + onCanSetDefaultBrowserInBackground + ); +})(); diff --git a/browser/extensions/onboarding/content/onboarding.css b/browser/extensions/onboarding/content/onboarding.css new file mode 100644 index 000000000000..8f2431477634 --- /dev/null +++ b/browser/extensions/onboarding/content/onboarding.css @@ -0,0 +1,589 @@ +/* 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/. */ +#onboarding-overlay * { + box-sizing: border-box; +} + +#onboarding-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + /* Ensuring we can put the overlay over elements using + z-index on original page */ + z-index: 20999; + color: #4d4d4d; + background: var(--newtab-overlay-color, rgb(245, 245, 247, 0.9)); /* #f7f7f5, 0.9 opacity */ + display: none; +} + +#onboarding-overlay.onboarding-opened { + display: block; +} + +#onboarding-overlay-button { + padding: 10px 0 0 0; + position: fixed; + cursor: pointer; + top: 4px; + inset-inline-start: 12px; + border: none; + /* Set to none so no grey contrast background in the high-contrast mode */ + background: none; + /* make sure the icon stay above the activity-stream searchbar */ + /* We want this always under #onboarding-overlay */ + z-index: 10; +} + +/* Keyboard focus styling */ +#onboarding-overlay-button:-moz-focusring { + outline: solid 2px rgba(0, 0, 0, 0.1); + -moz-outline-radius: 5px; + outline-offset: 5px; + transition: outline-offset 150ms; +} + +#onboarding-overlay-button > img { + width: 32px; + vertical-align: top; +} + +#onboarding-overlay-button::after { + content: " "; + border-radius: 50%; + margin-top: -1px; + margin-inline-start: -13px; + border: 2px solid #f2f2f2; + background: #0A84FF; + padding: 0; + width: 10px; + height: 10px; + min-width: unset; + max-width: unset; + display: block; + box-sizing: content-box; + float: inline-end; + position: relative; +} + +#onboarding-overlay-button:hover::after, +#onboarding-overlay-button.onboarding-speech-bubble::after { + background: #0060df; + font-size: 13px; + text-align: center; + color: #fff; + box-sizing: content-box; + font-weight: 400; + content: attr(aria-label); + border: 1px solid transparent; + border-radius: 2px; + padding: 10px 16px; + width: auto; + height: auto; + min-width: 100px; + max-width: 140px; + white-space: pre-line; + margin-inline-start: 4px; + margin-top: -10px; + box-shadow: -2px 0 5px 0 rgba(74, 74, 79, 0.25); +} + +#onboarding-overlay-button:dir(rtl)::after { + box-shadow: 2px 0 5px 0 rgba(74, 74, 79, 0.25); +} + +#onboarding-overlay-button-watermark-icon { + -moz-context-properties: fill; + fill: var(--newtab-icon-tertiary-color, #d7d7db); +} + +#onboarding-overlay-button-watermark-icon, +#onboarding-overlay-button.onboarding-watermark::after, +#onboarding-overlay-button.onboarding-watermark:not(:hover) > #onboarding-overlay-button-icon { + display: none; +} + +#onboarding-overlay-button.onboarding-watermark:not(:hover) > #onboarding-overlay-button-watermark-icon { + display: block; +} + +#onboarding-overlay-dialog, +.onboarding-hidden, +#onboarding-tour-sync-page[data-login-state=logged-in] .show-on-logged-out, +#onboarding-tour-sync-page[data-login-state=logged-out] .show-on-logged-in { + display: none; +} + +.onboarding-close-btn { + position: absolute; + top: 15px; + inset-inline-end: 15px; + cursor: pointer; + width: 16px; + height: 16px; + border: none; + background: none; + padding: 0; + } + +.onboarding-close-btn::before { + content: url("chrome://global/skin/icons/close.svg"); + -moz-context-properties: fill, fill-opacity; + fill-opacity: 0; + fill: var(--newtab-icon-primary-color, currentColor); +} + +.onboarding-close-btn:-moz-any(:hover, :active, :focus, :-moz-focusring)::before { + fill-opacity: 0.1; +} + +#onboarding-overlay.onboarding-opened > #onboarding-overlay-dialog { + width: 960px; + height: 510px; + background: #fff; + border: 1px solid rgba(9, 6, 13, 0.2); /* #09060D, 0.2 opacity */ + border-radius: 3px; + position: relative; + margin: 0 calc(50% - 480px); + top: calc(50% - 255px); + display: grid; + grid-template-rows: [dialog-start] 70px [page-start] 1fr [footer-start] 30px [dialog-end]; + grid-template-columns: [dialog-start] 230px [page-start] 1fr [dialog-end]; + box-shadow: 0 3px rgba(0, 0, 0, 0.04); +} + +#onboarding-overlay.onboarding-opened > #onboarding-overlay-dialog:-moz-focusring { + outline: none; +} + +@media (max-height: 510px) { + #onboarding-overlay.onboarding-opened > #onboarding-overlay-dialog { + top: 0; + } +} + +#onboarding-overlay-dialog > header { + grid-row: dialog-start / page-start; + grid-column: dialog-start / tour-end; + margin-top: 22px; + margin-bottom: 0; + margin-inline-end: 0; + margin-inline-start: 36px; + font-size: 22px; + font-weight: 200; +} + +#onboarding-overlay-dialog > nav { + grid-row: dialog-start / footer-start; + grid-column-start: dialog-start; + margin-top: 40px; + margin-bottom: 0; + margin-inline-end: 0; + margin-inline-start: 0; + padding: 0; +} + +#onboarding-overlay-dialog > footer { + grid-column: dialog-start / tour-end; + font-size: 13px; +} + +#onboarding-skip-tour-button { + margin-inline-start: 27px; + margin-bottom: 27px; +} + +/* Onboarding tour list */ +#onboarding-tour-list { + margin: 40px 0 0 0; + padding: 0; + margin-inline-start: 16px; +} + +#onboarding-tour-list .onboarding-tour-item-container { + list-style: none; + outline: none; + position: relative; +} + +#onboarding-tour-list .onboarding-tour-item { + pointer-events: none; + display: list-item; + padding-inline-start: 49px; + padding-top: 14px; + padding-bottom: 14px; + margin-bottom: 9px; + font-size: 16px; + cursor: pointer; + max-height: 54px; + --onboarding-tour-item-active-color: #0A84FF; +} + +#onboarding-tour-list .onboarding-tour-item:dir(rtl) { + background-position-x: right 17px; +} + +#onboarding-tour-list .onboarding-tour-item.onboarding-complete::before { + content: url("img/icons_tour-complete.svg"); + position: relative; + inset-inline-start: 3px; + top: -10px; + float: inline-start; +} + +#onboarding-tour-list .onboarding-tour-item.onboarding-complete { + padding-inline-start: 29px; +} + +#onboarding-tour-list .onboarding-tour-item::after { + content: ""; + display: block; + width: 48px; + height: 48px; + position: absolute; + inset-inline-start: 0px; + top: 0px; + background-color: #3E3D40; + mask-repeat: no-repeat; + mask-position: left 17px top 14px; + mask-size: 20px; +} + +#onboarding-tour-list .onboarding-tour-item:dir(rtl)::after { + mask-position: right 17px top 14px; +} + +#onboarding-tour-list .onboarding-tour-item.onboarding-active::after, +#onboarding-tour-list .onboarding-tour-item-container:hover .onboarding-tour-item::after { + background-color: var(--onboarding-tour-item-active-color); +} + +#onboarding-tour-list .onboarding-tour-item.onboarding-active, +#onboarding-tour-list .onboarding-tour-item-container:hover .onboarding-tour-item { + color: var(--onboarding-tour-item-active-color); + /* With 1px transparent outline, could see a border in the high-constrast mode */ + outline: 1px solid transparent; +} + +/* Default browser tour */ +#onboarding-tour-is-default-browser-msg { + font-size: 16px; + line-height: 21px; + float: inline-end; + margin-inline-end: 26px; + margin-top: -32px; + text-align: center; +} + +/* Sync tour */ +#onboarding-tour-sync-page form { + text-align: center; +} + +#onboarding-tour-sync-page form > h3 { + text-align: center; + margin: 0; + font-size: 22px; + font-weight: normal; +} + +#onboarding-tour-sync-page form > p { + text-align: center; + margin: 3px 0 0 0; + font-size: 15px; + font-weight: normal; +} + +#onboarding-tour-sync-page form > input { + margin-top: 10px; + height: 40px; + width: 80%; + padding: 7px; +} + +#onboarding-tour-sync-page form > #onboarding-tour-sync-button { + padding: 10px 20px; + min-width: 40%; + margin: 15px 0; + float: none; +} + +/* Onboarding tour pages */ +.onboarding-tour-page { + grid-row: page-start / footer-end; + grid-column: page-start; + display: grid; + grid-template-rows: [tour-page-start] 393px [tour-button-start] 1fr [tour-page-end]; + grid-template-columns: [tour-page-start] 368px [tour-content-start] 1fr [tour-page-end]; +} + +.onboarding-tour-description { + grid-row: tour-page-start / tour-page-end; + grid-column: tour-page-start / tour-content-start; + font-size: 15px; + line-height: 22px; + padding-inline-start: 40px; + padding-inline-end: 28px; + max-height: 360px; + overflow: auto; +} + +.onboarding-tour-description > h1 { + font-size: 36px; + margin-top: 16px; + font-weight: 300; + line-height: 44px; +} + +.onboarding-tour-content { + grid-row: tour-page-start / tour-button-start; + grid-column: tour-content-start / tour-page-end; + padding: 0; + text-align: end; +} + +.onboarding-tour-content > img { + width: 352px; + margin: 0; +} + +/* These illustrations need to be stuck on the right side to the border. Thus we + need to flip them horizontally on RTL . */ +.onboarding-tour-content > img:dir(rtl) { + transform: scaleX(-1); +} + +.onboarding-tour-content > iframe { + width: 100%; + height: 100%; + border: none; +} + +.onboarding-tour-button-container { + /* Get higher z-index in order to ensure buttons within container are selectable */ + z-index: 2; + grid-row: tour-button-start / tour-page-end; + grid-column: tour-content-start / tour-page-end; +} + +.onboarding-tour-action-button { + background: #0060df; + /* With 1px transparent border, could see a border in the high-constrast mode */ + border: 1px solid transparent; + border-radius: 2px; + padding: 10px 20px; + font-size: 14px; + font-weight: 600; + line-height: 16px; + color: #fff; + float: inline-end; + margin-inline-end: 26px; + margin-top: -32px; +} + +/* Remove default dotted outline around buttons' text */ +#onboarding-overlay button::-moz-focus-inner, +#onboarding-overlay-button::-moz-focus-inner { + border: 0; +} + +/* Keyboard focus specific outline */ +#onboarding-overlay button:-moz-focusring, +.onboarding-action-button:-moz-focusring, +#onboarding-tour-list .onboarding-tour-item:focus { + outline: 2px solid rgba(0,149,221,0.5); + outline-offset: 1px; + -moz-outline-radius: 2px; +} + +.onboarding-tour-action-button:hover:not([disabled]) { + background: #003eaa; + cursor: pointer; +} + +.onboarding-tour-action-button:active:not([disabled]) { + background: #002275; +} + +.onboarding-tour-action-button:disabled { + opacity: 0.5; +} + +/* Tour Icons */ +#onboarding-tour-singlesearch.onboarding-tour-item::after, +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-singlesearch] #onboarding-notification-tour-title::before { + mask-image: url("img/icons_singlesearch.svg"); +} + +#onboarding-tour-private-browsing.onboarding-tour-item::after, +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-private-browsing] #onboarding-notification-tour-title::before { + mask-image: url("img/icons_private.svg"); +} + +#onboarding-tour-addons.onboarding-tour-item::after, +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-addons] #onboarding-notification-tour-title::before { + mask-image: url("img/icons_addons.svg"); +} + +#onboarding-tour-customize.onboarding-tour-item::after, +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-customize] #onboarding-notification-tour-title::before { + mask-image: url("img/icons_customize.svg"); +} + +#onboarding-tour-default-browser.onboarding-tour-item::after, +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-default-browser] #onboarding-notification-tour-title::before { + mask-image: url("img/icons_default.svg"); +} + +#onboarding-tour-sync.onboarding-tour-item::after, +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-sync] #onboarding-notification-tour-title::before { + mask-image: url("img/icons_sync.svg"); +} + +#onboarding-tour-library.onboarding-tour-item::after, +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-library] #onboarding-notification-tour-title::before { + mask-image: url("img/icons_library.svg"); +} + +#onboarding-tour-performance.onboarding-tour-item::after, +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-performance] #onboarding-notification-tour-title::before { + mask-image: url("img/icons_performance.svg"); +} + +#onboarding-tour-screenshots.onboarding-tour-item::after, +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-screenshots] #onboarding-notification-tour-title::before { + mask-image: url("img/icons_screenshots.svg"); +} + +a#onboarding-tour-screenshots-button, +a#onboarding-tour-screenshots-button:hover, +a#onboarding-tour-screenshots-button:visited { + color: #fff; + text-decoration: none; +} + +/* Tour Notifications */ +#onboarding-notification-bar { + position: fixed; + z-index: 20998; /* We want this always under #onboarding-overlay */ + left: 0; + bottom: 0; + width: 100%; + height: 100px; + min-width: 640px; + background: var(--newtab-snippets-background-color, rgba(255, 255, 255, 0.97)); + border-top: 1px solid var(--newtab-snippets-hairline-color, #e9e9e9); + box-shadow: 0 -1px 4px 0 rgba(12, 12, 13, 0.1); + transition: transform 0.8s; + transform: translateY(122px); +} + +#onboarding-notification-bar.onboarding-opened { + transition: none; + transform: translateY(0px); +} + +#onboarding-notification-close-btn { + position: absolute; + inset-block-start: 50%; + inset-inline-end: 24px; + transform: translateY(-50%); +} + +#onboarding-notification-message-section { + height: 100%; + display: flex; + align-items: center; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +#onboarding-notification-body { + width: 500px; + margin: 0 18px; + color: var(--newtab-text-primary-color, #0c0c0d); + display: inline-block; + max-height: 120px; + overflow: auto; + padding: 15px 0; + box-sizing: border-box; +} + +#onboarding-notification-body * { + font-size: 12px; + font-weight: normal; + margin-top: 5px; +} + +#onboarding-notification-tour-title { + margin: 0; + font-weight: bold; + color: var(--newtab-text-primary-color, #0f1126); + font-size: 14px; +} + +#onboarding-notification-tour-title::before { + content: ""; + background-color: var(--newtab-text-primary-color, #0f1126); + mask-repeat: no-repeat; + mask-size: 14px; + height: 16px; + width: 16px; + float: inline-start; + margin-top: 2px; + margin-inline-end: 2px; +} + +#onboarding-notification-tour-icon { + min-width: 64px; + height: 64px; + background-size: 64px; + background-repeat: no-repeat; + background-image: url("chrome://branding/content/icon64.png"); +} + +.onboarding-action-button { + background: #fbfbfb; + /* With 1px border, could see a border in the high-constrast mode */ + border: 1px solid #c1c1c1; + border-radius: 2px; + padding: 10px 20px; + font-size: 14px; + font-weight: 600; + line-height: 16px; + color: #202340; + min-width: 130px; +} + +.onboarding-action-button:hover { + background-color: #ebebeb; + cursor: pointer; +} + +.onboarding-action-button:active { + background-color: #dadada; +} + +#onboarding-notification-bar .onboarding-action-button { + background-color: var(--newtab-button-secondary-color); + border-color: var(--newtab-border-primary-color); + border-radius: 4px; + color: var(--newtab-text-primary-color); +} + +#onboarding-notification-bar .onboarding-action-button:hover, +#onboarding-notification-bar .onboarding-action-button:active { + background-color: var(--newtab-button-secondary-color); + box-shadow: 0 0 0 5px var(--newtab-card-active-outline-color); + transition: box-shadow 150ms; +} + +@media (min-resolution: 2dppx) { + #onboarding-notification-tour-icon { + background-image: url("chrome://branding/content/icon128.png"); + } +} diff --git a/browser/extensions/onboarding/content/onboarding.js b/browser/extensions/onboarding/content/onboarding.js new file mode 100644 index 000000000000..7518a1ab6631 --- /dev/null +++ b/browser/extensions/onboarding/content/onboarding.js @@ -0,0 +1,49 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +ChromeUtils.defineModuleGetter( + this, + "Onboarding", + "resource://onboarding/Onboarding.jsm" +); + +const ABOUT_HOME_URL = "about:home"; +const ABOUT_NEWTAB_URL = "about:newtab"; +const ABOUT_WELCOME_URL = "about:welcome"; + +// Load onboarding module only when we enable it. +if (Services.prefs.getBoolPref("browser.onboarding.enabled", false)) { + addEventListener( + "load", + function onLoad(evt) { + if (!content || evt.target != content.document) { + return; + } + + let window = evt.target.defaultView; + let location = window.location.href; + if ( + location == ABOUT_NEWTAB_URL || + location == ABOUT_HOME_URL || + location == ABOUT_WELCOME_URL + ) { + // We just want to run tests as quickly as possible + // so in the automation test, we don't do `requestIdleCallback`. + if (Cu.isInAutomation) { + new Onboarding(this, window); + return; + } + window.requestIdleCallback(() => { + new Onboarding(this, window); + }); + } + }, + true + ); +} diff --git a/browser/extensions/onboarding/data_events.md b/browser/extensions/onboarding/data_events.md new file mode 100644 index 000000000000..3fc5ffa41b86 --- /dev/null +++ b/browser/extensions/onboarding/data_events.md @@ -0,0 +1,154 @@ +# Metrics we collect + +We adhere to [Activity Stream's data collection policy](https://github.com/mozilla/activity-stream/blob/master/docs/v2-system-addon/...). Data about your specific browsing behavior or the sites you visit is **never transmitted to any Mozilla server**. At any time, it is easy to **turn off** this data collection by [opting out of Firefox telemetry](https://support.mozilla.org/kb/share-telemetry-data-mozilla-help-improve-fir...). + +## User event pings + +The Onboarding system add-on sends 2 types of pings(HTTPS POST) to the backend [Onyx server](https://github.com/mozilla/onyx) : +- a `session` ping that describes the ending of an Onboarding session (a new tab is closed or refreshed, an overlay is closed, a notification bar is closed), and +- an `event` ping that records specific data about individual user interactions while interacting with Onboarding + +For reference, Onyx is a Mozilla owned service to serve tiles for the current newtab in Firefox. It also receives all the telemetry from the about:newtab and about:home page as well as Activity Stream. It's operated and monitored by the Cloud Services team. + +# Example Onboarding `session` Log + +```js +{ + // These fields are sent from the client + "addon_version": "1.0.0", + "category": ["onboarding-interactions"|"overlay-interactions"|"notification-interactions"], + "client_id": "374dc4d8-0cb2-4ac5-a3cf-c5a9bc3c602e", + "locale": "en-US", + "type": ["onboarding_session" | "overlay_session" | "notification_session"], + "page": ["about:newtab" | "about:home" | "about:welcome"], + "parent_session_id": "{45cddbeb-2bec-4f3a-bada-fb87d4b79a6c}", + "root_session_id": "{45cddbeb-2bec-4f3a-bada-fb87d4b79a6c}", + "session_begin": 1505440017018, + "session_end": 1505440021992, + "session_id": "{12dasd-213asda-213dkakj}", + "tour_type" ["new" | "update"], + + // These fields are generated on the server + "date": "2016-03-07", + "ip": "10.192.171.13", + "ua": "python-requests/2.9.1", + "receive_at": 1457396660000 +} +``` + +| KEY | DESCRIPTION | | +|-----|-------------|:-----:| +| `addon_version` | [Required] The version of the Onboarding addon. | :one: +| `category` | [Required] Either ["", "overlay-interactions", "notification-interactions"] to identify which kind of the interaction | :one: +| `client_id` | [Required] An identifier generated by [ClientID](https://github.com/mozilla/gecko-dev/blob/master/toolkit/modules/ClientID.js...) module to provide an identifier for this device. This data is automatically appended by `ping-centre` module | :one: +| `ip` | [Auto populated by Onyx] The IP address of the client. Onyx does use (with the permission) the IP address to infer user's geo-information so that it could prepare the corresponding tiles for the country she lives in. However, Ping-centre will NOT store IP address in the database, where only authorized Mozilla employees can access the telemetry data, and all the raw logs are being strictly managed by the Ops team and will expire according to the Mozilla's data retention policy.| :two: +| `locale` | The browser chrome's language (e.g. en-US). | :two: +| `page` | [Required] One of ["about:newtab", "about:home", "about:welcome"]| :one: +| `parent_session_id` | [Required] The unique identifier generated by `gUUIDGenerator` service to identify this event belongs to which parent session. Events happen upon overlay will have the `overlay session uuid` as its `parent_session_id`. Events happen upon notification will have the `notification session uuid` as its `parent_session_id`. | :one: +| `root_session_id` | [Required] The unique identifier generated by `gUUIDGenerator` service to identify this event belongs to which root session. Every event will have the same `onboarding session uuid` as its `root_session_id` when interact in the same tab. | :one: +| `session_begin` | [Required] Timestamp in (integer) milliseconds when onboarding/overlay/notification becoming visible. | :one: +| `session_end` | [Required] Timestamp in (integer) milliseconds when onboarding/overlay/notification losing focus. | :one: +| `session_id` | [Required] The unique identifier generated by `gUUIDGenerator` service to identify the specific user session. We will log different uuid when onboarding is inited/when the overlay is opened/when notification is shown. | :one: +| `tour_type` | [Required] One of ["new", "update"] indicates the user is a `new` user or the `update` user upgrade from the older version | :one: +| `type` | [Required] The type of event. Allowed event strings are defined in the below section | :one: +| `ua` | [Auto populated by Onyx] The user agent string. | :two: +| `ver` | [Auto populated by Onyx] The version of the Onyx API the ping was sent to. | :one: + +# Example Onboarding `event` Log + +```js +{ + "addon_version": "1.0.0", + "bubble_state": ["bubble" | "dot" | "hide"], + "category": ["logo-interactions"|"overlay-interactions"|"notification-interactions"], + "client_id": "374dc4d8-0cb2-4ac5-a3cf-c5a9bc3c602e", + "locale": "en-US", + "logo_state": ["logo" | "watermark"], + "notification_impression": [1-8], + "notification_state": ["show" | "hide" | "finished"], + "page": ["about:newtab" | "about:home" | "about:welcome"], + "parent_session_id": "{45cddbeb-2bec-4f3a-bada-fb87d4b79a6c}", + "root_session_id": "{45cddbeb-2bec-4f3a-bada-fb87d4b79a6c}", + "current_tour_id": ["onboarding-tour-private-browsing" | "onboarding-tour-addons"|...], // tour ids defined in 'onboardingTourset' + "target_tour_id": ["onboarding-tour-private-browsing" | "onboarding-tour-addons"|...], // tour ids defined in 'onboardingTourset', + "tour_id": ["onboarding-tour-private-browsing" | "onboarding-tour-addons"|...], // tour ids defined in 'onboardingTourset' + "timestamp": 1505440017019, + "tour_type" ["new" | "update"], + "type": ["notification-cta-click" | "overlay-cta-click" | "overlay-nav-click" | "overlay-skip-tour"...], + "width": 950, + + // These fields are generated on the server + "ip": "10.192.171.13", + "ua": "python-requests/2.9.1", + "receive_at": 1457396660000, + "date": "2016-03-07", +} +``` + + +| KEY | DESCRIPTION | | +|-----|-------------|:-----:| +| `addon_version` | [Required] The version of the Onboarding addon. | :one: +| `bubble_state` | [Optional] | One of ["bubble", "dot", "hide"] indicates the current visual state of the speach bubble (content dialog besides the onboarding logo). | :one: +| `category` | [Required] Either ("overlay-interactions", "notification-interactions") to identify which kind of the interaction | :one: +| `client_id` | [Required] An identifier generated by [ClientID](https://github.com/mozilla/gecko-dev/blob/master/toolkit/modules/ClientID.js...) module to provide an identifier for this device. This data is automatically appended by `ping-centre` module | :one: +| `current_tour_id` | [Optional] id of the current tour. We put "" when this field is not relevant to this event. | :one: +| `ip` | [Auto populated by Onyx] The IP address of the client. Onyx does use (with the permission) the IP address to infer user's geo-information so that it could prepare the corresponding tiles for the country she lives in. However, Ping-centre will NOT store IP address in the database, where only authorized Mozilla employees can access the telemetry data, and all the raw logs are being strictly managed by the Ops team and will expire according to the Mozilla's data retention policy.| :two: +| `locale` | The browser chrome's language (e.g. en-US). | :two: +| `logo_state` | [Optional] One of ["logo", "watermark"] indicates the overlay is opened while in the default or the watermark state. | :one: +| `notification_impression` | [Optional] An integer to record how many times the current notification tour is shown to the user. Each Notification tour can show not more than 8 times. We put `-1` when this field is not relevant to this event | :one: +| `notification_state` | [Optional] One of ["show", "hide", "finished"] indicates the current notification bar state. | :one: +| `page` | [Required] One of ["about:newtab", "about:home"]| :one: +| `parent_session_id` | [Required] The unique identifier generated by `gUUIDGenerator` service to identify this event belongs to which parent session. Events happen upon overlay will have the `overlay session uuid` as its `parent_session_id`. Events happen upon notification will have the `notification session uuid` as its `parent_session_id`. | :one: +| `root_session_id` | [Required] The unique identifier generated by `gUUIDGenerator` service to identify this event belongs to which root session. Every event will have the same `onboarding session uuid` as its `root_session_id` when interact in the same tab. | :one: +| `target_tour_id` | [Optional] id of the target switched tour. We put "" when this field is not relevant to this event. | :one: +| `timestamp` | [Required] Timestamp in (integer) milliseconds when the event triggered | :one: +| `tour_type` | [Required] One of ["new", "update"] indicates the user is a `new` user or the `update` user upgrade from the older version | :one: +| `type` | [Required] The type of event. Allowed event strings are defined in the below section | :one: +| `ua` | [Auto populated by Onyx] The user agent string. | :two: +| `ver` | [Auto populated by Onyx] The version of the Onyx API the ping was sent to. | :one: +| `width` | [Required] Current browser window width rounded by 50 pixels. Collecting rounded values reduces the risk to use these values to derive a unique user identifier. | :one: + +**Where:** + +:one: Firefox data +:two: HTTP protocol data + +## Event types + +Here are all allowed event `type` strings that defined in `OnboardingTelemetry::EVENT_WHITELIST`. + +### Onboarding events + +| EVENT | DESCRIPTION | +|-----------|---------------------| +| `onboarding-logo-click` | event is triggered when a user clicks the logo to open the overlay. | +| `onboarding-register-session` | internal event triggered when loading the onboarding module, will not send out any data. | +| `onboarding-session` | event is sent when the tab unload to track the start and end time of the onboarding session. | +| `onboarding-session-begin` | internal event triggered when the onboarding starts, will not send out any data. | +| `onboarding-session-end` | internal event triggered when the onboarding ends, `onboarding-session` event is the actual event that send to the server. | + +### Overlay events + +| EVENT | DESCRIPTION | +|-----------|---------------------| +| `overlay-close-button-click` | event is triggered when a user clicks close overlay button. | +| `overlay-close-outside-click` | event is triggered when a user clicks outside the overlay area to end the tour. | +| `overlay-cta-click` | event is triggered when a user clicks overlay's Call-To-Action button. | +| `overlay-current-tour` | event is sent when a tour is shown in the overlay. | +| `overlay-nav-click` | event is sent when a user clicks a navigation button in the overlay. | +| `overlay-session` | event is sent when an overlay is closed to track the start and end time of the overlay session. | +| `overlay-session-begin` | internal event triggered when open the overlay, will not send out any data. | +| `overlay-session-end` | internal event is triggered when an overlay session ends. `overlay-session` event is the actual event that send to the server. | +| `overlay-skip-tour` | event is sent when a user clicks `Skip Tour` button in the overlay. | + +### Notification events + +| EVENT | DESCRIPTION | +|-----------|---------------------| +| `notification-appear` | event is sent when a notification appears. | +| `notification-close-button-click` | event is sent when a user clicks close notification button. | +| `notification-cta-click` | event is sent when a user clicks the notification's Call-To-Action button. | +| `notification-session` | event is sent when user closes the notification to track the start and end time of the notification session. | +| `notification-session-begin` | internal event triggered when user open the notification, will not send out any data. | +| `notification-session-end` | internal event is triggered when a notification session ends. `notification-session` event is the actual event that send to the server. | diff --git a/browser/extensions/onboarding/jar.mn b/browser/extensions/onboarding/jar.mn new file mode 100644 index 000000000000..1d580be9861f --- /dev/null +++ b/browser/extensions/onboarding/jar.mn @@ -0,0 +1,14 @@ +# 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/. + +[features/onboarding@mozilla.org] chrome.jar: + # resource://onboarding/ is referenced in about:home about:newtab and about:welcome, + # so make it content-accessible. +% resource onboarding %content/ contentaccessible=yes + content/ (content/*) + # Package UITour-lib.js in here rather than under + # /browser/components/uitour to avoid "unreferenced files" error when + # Onboarding extension is not built. + content/lib/UITour-lib.js (/browser/components/uitour/UITour-lib.js) + content/modules/ (*.jsm) diff --git a/browser/extensions/onboarding/locales/en-US/onboarding.properties b/browser/extensions/onboarding/locales/en-US/onboarding.properties new file mode 100644 index 000000000000..cc545222a107 --- /dev/null +++ b/browser/extensions/onboarding/locales/en-US/onboarding.properties @@ -0,0 +1,126 @@ +# 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/. +# LOCALIZATION NOTE(onboarding.overlay-title2): This string will be used in the overlay title. +onboarding.overlay-title2=Let’s get started +onboarding.skip-tour-button-label=Skip Tour +#LOCALIZATION NOTE(onboarding.button.learnMore): this string is used as a button label, displayed near the message, and shared across all the onboarding notifications. +onboarding.button.learnMore=Learn More +# LOCALIZATION NOTE(onboarding.overlay-icon-tooltip2): This string will be used +# to show the tooltip alongside the notification icon in the overlay tour. %S is +# brandShortName. The tooltip is designed to show in two lines. Please use \n to +# do appropriate line breaking. +onboarding.overlay-icon-tooltip2=New to %S?\nLet’s get started. +# LOCALIZATION NOTE(onboarding.overlay-icon-tooltip-updated2): %S is +# brandShortName. The tooltip is designed to show in two lines. Please use \n to +# do appropriate line breaking. +onboarding.overlay-icon-tooltip-updated2=%S is all new.\nSee what you can do! +# LOCALIZATION NOTE(onboarding.overlay-close-button-tooltip): The overlay close button is an icon button. This tooltip would be shown when mousing hovering on the button. +onboarding.overlay-close-button-tooltip=Close +onboarding.notification-icon-tooltip-updated=See what’s new! +# LOCALIZATION NOTE(onboarding.notification-close-button-tooltip): The notification close button is an icon button. This tooltip would be shown when mousing hovering on the button. +onboarding.notification-close-button-tooltip=Dismiss + +# LOCALIZATION NOTE(onboarding.complete): This string is used to describe an +# onboarding tour item that is complete. +onboarding.complete=Complete + +onboarding.tour-private-browsing=Private Browsing +onboarding.tour-private-browsing.title2=Browse by yourself. +# LOCALIZATION NOTE(onboarding.tour-private-browsing.description3): This string will be used in the private-browsing tour description. %S is brandShortName. +onboarding.tour-private-browsing.description3=Want to keep something to yourself? Use Private Browsing with Tracking Protection. %S will block online trackers while you browse and won’t remember your history after you’ve ended your session. +onboarding.tour-private-browsing.button=Show Private Browsing in Menu +onboarding.notification.onboarding-tour-private-browsing.title=Browse by yourself. +onboarding.notification.onboarding-tour-private-browsing.message2=Want to keep something to yourself? Use Private Browsing with Tracking Protection. + +onboarding.tour-addons=Add-ons +onboarding.tour-addons.title2=Get more done. +# LOCALIZATION NOTE(onboarding.tour-addons.description2): This string will be used in the add-on tour description. %S is brandShortName +onboarding.tour-addons.description2=Add-ons let you add features to %S, so your browser works harder for you. Compare prices, check the weather or express your personality with a custom theme. +onboarding.tour-addons.button=Show Add-ons in Menu +onboarding.notification.onboarding-tour-addons.title=Get more done. +# LOCALIZATION NOTE(onboarding.notification.onboarding-tour-addons.message): This string will be used in the notification message for the add-ons tour. %S is brandShortName. +onboarding.notification.onboarding-tour-addons.message=Add-ons are small apps you can add to %S that do lots of things — from managing to-do lists, to downloading videos, to changing the look of your browser. + +onboarding.tour-customize=Customize +onboarding.tour-customize.title2=Rearrange your toolbar. +# LOCALIZATION NOTE(onboarding.tour-customize.description2): This string will be used in the customize tour description. %S is brandShortName +onboarding.tour-customize.description2=Put the tools you use most right at your fingertips. Drag, drop, and reorder %S’s toolbar and menu to fit your needs. Or choose a compact theme to make more room for tabbed browsing. +onboarding.tour-customize.button=Show Customize in Menu +onboarding.notification.onboarding-tour-customize.title=Rearrange your toolbar. +# LOCALIZATION NOTE(onboarding.notification.onboarding-tour-customize.message): This string will be used in the notification message for Customize tour. %S is brandShortName. +onboarding.notification.onboarding-tour-customize.message=Put the tools you use most right at your fingertips. Add more options to your toolbar. Or select a theme to make %S reflect your personality. + +onboarding.tour-default-browser=Default Browser +# LOCALIZATION NOTE(onboarding.tour-default-browser.title2): This string will be used in the default browser tour title. %S is brandShortName +onboarding.tour-default-browser.title2=Make %S your go-to browser. +# LOCALIZATION NOTE(onboarding.tour-default-browser.description2): This string will be used in the default browser tour description. %1$S is brandShortName +onboarding.tour-default-browser.description2=Love %1$S? Set it as your default browser. Open a link from another application, and %1$S will be there for you. +# LOCALIZATION NOTE(onboarding.tour-default-browser.button): Label for a button to open the OS default browser settings where it's not possible to set the default browser directly. (OSX, Linux, Windows 8 and higher) +onboarding.tour-default-browser.button=Open Default Browser Settings +# LOCALIZATION NOTE(onboarding.tour-default-browser.win7.button): Label for a button to directly set the default browser (Windows 7). %S is brandShortName +onboarding.tour-default-browser.win7.button=Make %S Your Default Browser +# LOCALIZATION NOTE(onboarding.tour-default-browser.is-default.message): Label displayed when Firefox is already set as default browser. followed on a new line by "tour-default-browser.is-default.2nd-message". +onboarding.tour-default-browser.is-default.message=You’ve got this! +# LOCALIZATION NOTE(onboarding.tour-default-browser.is-default.2nd-message): Label displayed when Firefox is already set as default browser. %S is brandShortName +onboarding.tour-default-browser.is-default.2nd-message=%S is already your default browser. +# LOCALIZATION NOTE(onboarding.notification.onboarding-tour-default-browser.title): This string will be used in the notification title for the default browser tour. %S is brandShortName. +onboarding.notification.onboarding-tour-default-browser.title=Make %S your go-to browser. +# LOCALIZATION NOTE(onboarding.notification.onboarding-tour-default-browser.message): This string will be used in the notification message for the default browser tour. %1$S is brandShortName +onboarding.notification.onboarding-tour-default-browser.message=It doesn’t take much to get the most from %1$S. Just set %1$S as your default browser and put control, customization, and protection on autopilot. + +onboarding.tour-sync2=Sync +onboarding.tour-sync.title2=Pick up where you left off. +onboarding.tour-sync.description2=Sync makes it easy to access bookmarks, passwords, and even open tabs on all your devices. Sync also gives you control of the types of information you want, and don’t want, to share. +onboarding.tour-sync.logged-in.title=You’re signed in to Sync! +# LOCALIZATION NOTE(onboarding.tour-sync.logged-in.description): %1$S is brandShortName. +onboarding.tour-sync.logged-in.description=Sync works when you’re signed in to %1$S on more than one device. Have a mobile device? Install the %1$S app and sign in to get your bookmarks, history, and passwords on the go. +# LOCALIZATION NOTE(onboarding.tour-sync.form.title): This string is displayed +# as a title and followed by onboarding.tour-sync.form.description. +onboarding.tour-sync.form.title=Create a Firefox Account +# LOCALIZATION NOTE(onboarding.tour-sync.form.description): The description +# continues after onboarding.tour-sync.form.title to create a complete sentence. +# If it's not possible for your locale, you can translate this string as +# "Continue to Firefox Sync" instead. +onboarding.tour-sync.form.description=to continue to Firefox Sync +onboarding.tour-sync.button=Next +onboarding.tour-sync.connect-device.button=Connect Another Device +onboarding.tour-sync.email-input.placeholder=Email +onboarding.notification.onboarding-tour-sync.title=Pick up where you left off. +onboarding.notification.onboarding-tour-sync.message=Still sending yourself links to save or read on your phone? Do it the easy way: get Sync and have the things you save here show up on all of your devices. + +onboarding.tour-library=Library +onboarding.tour-library.title=Keep it together. +# LOCALIZATION NOTE (onboarding.tour-library.description2): This string will be used in the library tour description. %1$S is brandShortName +onboarding.tour-library.description2=Check out the new %1$S library in the redesigned toolbar. The library puts the things you’ve seen and saved to %1$S — your browsing history, bookmarks, Pocket list, and synced tabs — in one convenient place. +onboarding.tour-library.button2=Show Library Menu +onboarding.notification.onboarding-tour-library.title=Keep it together. +# LOCALIZATION NOTE(onboarding.notification.onboarding-tour-library.message): This string will be used in the notification message for the library tour. %S is brandShortName +onboarding.notification.onboarding-tour-library.message=The new %S library puts the great things you’ve discovered on the web in one convenient place. + +onboarding.tour-singlesearch=Address Bar +onboarding.tour-singlesearch.title=Find it faster. +# LOCALIZATION NOTE(onboarding.tour-singlesearch.description): %S is brandShortName +onboarding.tour-singlesearch.description=The address bar might be the most powerful tool in the sleek new %S toolbar. Start typing, and see suggestions based on your browsing and search history. Go to a web address, search the whole web with your default search engine, or send your query directly to a single site with one-click search. +onboarding.tour-singlesearch.button=Show Address Bar +onboarding.notification.onboarding-tour-singlesearch.title=Find it faster. +onboarding.notification.onboarding-tour-singlesearch.message=The unified address bar is the only tool you need to find your way around the web. + +onboarding.tour-performance=Performance +onboarding.tour-performance.title=Browse with the best of ‘em. +# LOCALIZATION NOTE(onboarding.tour-performance.description): %1$S is brandShortName. +onboarding.tour-performance.description=It’s a whole new %1$S, built for faster page loading, smoother scrolling, and more responsive tab switching. These performance upgrades come paired with a modern, intuitive design. Start browsing and experience it for yourself: the best %1$S yet. +onboarding.notification.onboarding-tour-performance.title=Browse with the best of ‘em. +# LOCALIZATION NOTE(onboarding.notification.onboarding-tour-performance.message): %S is brandShortName. +onboarding.notification.onboarding-tour-performance.message=Prepare yourself for the fastest, smoothest, most reliable %S yet. + +# LOCALIZATION NOTE (onboarding.tour-screenshots): "Screenshots" is the name of the Firefox Screenshots feature and should not be localized. +onboarding.tour-screenshots=Screenshots +onboarding.tour-screenshots.title=Take better screenshots. +# LOCALIZATION NOTE(onboarding.tour-screenshots.description): %S is brandShortName. +onboarding.tour-screenshots.description=Take, save and share screenshots — without leaving %S. Capture a region or an entire page as you browse. Then save to the web for easy access and sharing. +# LOCALIZATION NOTE (onboarding.tour-screenshots.button): "Screenshots" is the name of the Firefox Screenshots feature and should not be localized. +onboarding.tour-screenshots.button=Open Screenshots Website +onboarding.notification.onboarding-tour-screenshots.title=Take better screenshots. +# LOCALIZATION NOTE(onboarding.notification.onboarding-tour-screenshots.message): %S is brandShortName. +onboarding.notification.onboarding-tour-screenshots.message=Take, save and share screenshots — without leaving %S. diff --git a/browser/extensions/moz.build b/browser/extensions/onboarding/locales/jar.mn similarity index 53% copy from browser/extensions/moz.build copy to browser/extensions/onboarding/locales/jar.mn index 2df11e89dd48..0801f8512775 100644 --- a/browser/extensions/moz.build +++ b/browser/extensions/onboarding/locales/jar.mn @@ -1,8 +1,8 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: +#filter substitution # 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/.
-DIRS += [ -] +[features/onboarding@mozilla.org] @AB_CD@.jar: +% locale onboarding @AB_CD@ %locale/@AB_CD@/ + locale/@AB_CD@/onboarding.properties (%onboarding.properties) diff --git a/browser/extensions/moz.build b/browser/extensions/onboarding/locales/moz.build similarity index 91% copy from browser/extensions/moz.build copy to browser/extensions/onboarding/locales/moz.build index 2df11e89dd48..d988c0ff9b16 100644 --- a/browser/extensions/moz.build +++ b/browser/extensions/onboarding/locales/moz.build @@ -4,5 +4,4 @@ # 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/.
-DIRS += [ -] +JAR_MANIFESTS += ["jar.mn"] diff --git a/browser/extensions/onboarding/manifest.json b/browser/extensions/onboarding/manifest.json new file mode 100644 index 000000000000..fcf46b444c9b --- /dev/null +++ b/browser/extensions/onboarding/manifest.json @@ -0,0 +1,26 @@ +{ + "manifest_version": 2, + "name": "Onboarding", + "version": "1.0", + + "applications": { + "gecko": { + "id": "onboarding@mozilla.org" + } + }, + + "background": { + "scripts": ["background.js"] + }, + + "experiment_apis": { + "onboarding": { + "schema": "schema.json", + "parent": { + "scopes": ["addon_parent"], + "script": "api.js", + "events": ["startup"] + } + } + } + } diff --git a/browser/extensions/onboarding/moz.build b/browser/extensions/onboarding/moz.build new file mode 100644 index 000000000000..4756afe507fb --- /dev/null +++ b/browser/extensions/onboarding/moz.build @@ -0,0 +1,26 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Tours") + +DEFINES["MOZ_APP_VERSION"] = CONFIG["MOZ_APP_VERSION"] +DEFINES["MOZ_APP_MAXVERSION"] = CONFIG["MOZ_APP_MAXVERSION"] + +DIRS += ["locales"] + +FINAL_TARGET_FILES.features["onboarding@mozilla.org"] += [ + "api.js", + "background.js", + "manifest.json", + "schema.json", +] + +BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"] + +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] + +JAR_MANIFESTS += ["jar.mn"] diff --git a/browser/extensions/onboarding/schema.json b/browser/extensions/onboarding/schema.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/browser/extensions/onboarding/schema.json @@ -0,0 +1 @@ +[] diff --git a/browser/extensions/onboarding/test/browser/.eslintrc.js b/browser/extensions/onboarding/test/browser/.eslintrc.js new file mode 100644 index 000000000000..1779fd7f1cf8 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/browser-test"], +}; diff --git a/browser/extensions/onboarding/test/browser/browser.ini b/browser/extensions/onboarding/test/browser/browser.ini new file mode 100644 index 000000000000..abc2e915d551 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser.ini @@ -0,0 +1,18 @@ +[DEFAULT] +support-files = + head.js + +[browser_onboarding_accessibility.js] +[browser_onboarding_keyboard.js] +skip-if = debug || os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences +[browser_onboarding_notification.js] +[browser_onboarding_notification_2.js] +[browser_onboarding_notification_3.js] +[browser_onboarding_notification_4.js] +[browser_onboarding_notification_5.js] +[browser_onboarding_notification_click_auto_complete_tour.js] +[browser_onboarding_select_default_tour.js] +[browser_onboarding_skip_tour.js] +[browser_onboarding_tours.js] +[browser_onboarding_tourset.js] +[browser_onboarding_uitour.js] diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_accessibility.js b/browser/extensions/onboarding/test/browser/browser_onboarding_accessibility.js new file mode 100644 index 000000000000..9f2f1c7c9ce9 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_accessibility.js @@ -0,0 +1,121 @@ +/* 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"; + +add_task(async function test_onboarding_overlay_button() { + resetOnboardingDefaultState(); + + info("Wait for onboarding overlay loaded"); + let tab = await openTab(ABOUT_HOME_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + + info("Test accessibility and semantics of the overlay button"); + await ContentTask.spawn(tab.linkedBrowser, {}, function() { + let doc = content.document; + let button = doc.body.firstElementChild; + is( + button.id, + "onboarding-overlay-button", + "First child is an overlay button" + ); + ok( + button.getAttribute("aria-label"), + "Onboarding button has an accessible label" + ); + is( + button.getAttribute("aria-haspopup"), + "true", + "Onboarding button should indicate that it triggers a popup" + ); + is( + button.getAttribute("aria-controls"), + "onboarding-overlay-dialog", + "Onboarding button semantically controls an overlay dialog" + ); + is( + button.firstElementChild.getAttribute("role"), + "presentation", + "Onboarding button icon should have presentation only semantics" + ); + }); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_onboarding_notification_bar() { + resetOnboardingDefaultState(); + skipMuteNotificationOnFirstSession(); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + + info("Test accessibility and semantics of the notification bar"); + await ContentTask.spawn(tab.linkedBrowser, {}, function() { + let doc = content.document; + let footer = doc.getElementById("onboarding-notification-bar"); + + is( + footer.getAttribute("aria-labelledby"), + doc.getElementById("onboarding-notification-tour-title").id, + "Notification bar should be labelled by the notification tour title text" + ); + + is( + footer.getAttribute("aria-live"), + "polite", + "Notification bar should be a live region" + ); + // Presentational elements + [ + "onboarding-notification-message-section", + "onboarding-notification-tour-icon", + "onboarding-notification-body", + ].forEach(id => + is( + doc.getElementById(id).getAttribute("role"), + "presentation", + "Element is only used for presentation" + ) + ); + }); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_onboarding_overlay_dialog() { + resetOnboardingDefaultState(); + + info("Wait for onboarding overlay loaded"); + let tab = await openTab(ABOUT_HOME_URL); + let browser = tab.linkedBrowser; + await promiseOnboardingOverlayLoaded(browser); + + info("Test accessibility and semantics of the dialog overlay"); + await assertModalDialog(browser, { visible: false }); + + info("Click on overlay button and check modal dialog state"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + browser + ); + await promiseOnboardingOverlayOpened(browser); + await assertModalDialog(browser, { + visible: true, + focusedId: "onboarding-overlay-dialog", + }); + + info("Close the dialog and check modal dialog state"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-close-btn", + {}, + browser + ); + await promiseOnboardingOverlayClosed(browser); + await assertModalDialog(browser, { visible: false }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_keyboard.js b/browser/extensions/onboarding/test/browser/browser_onboarding_keyboard.js new file mode 100644 index 000000000000..a67814bdae39 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_keyboard.js @@ -0,0 +1,205 @@ +/* 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"; + +function assertOverlayState(browser, args) { + return ContentTask.spawn(browser, args, ({ tourId, focusedId, visible }) => { + let { document: doc, window } = content; + if (tourId) { + let items = [...doc.querySelectorAll(".onboarding-tour-item")]; + items.forEach(item => + is( + item.getAttribute("aria-selected"), + item.id === tourId ? "true" : "false", + "Active item should have aria-selected set to true and inactive to false" + ) + ); + } + if (focusedId) { + let focused = doc.getElementById(focusedId); + is(focused, doc.activeElement, `Focus should be set on ${focusedId}`); + } + if (visible !== undefined) { + let overlay = doc.getElementById("onboarding-overlay"); + is( + window.getComputedStyle(overlay).getPropertyValue("display"), + visible ? "block" : "none", + `Onboarding overlay should be ${visible ? "visible" : "invisible"}` + ); + } + }); +} + +const TOUR_LIST_TEST_DATA = [ + { key: "VK_DOWN", expected: { tourId: TOUR_IDs[1], focusedId: TOUR_IDs[1] } }, + { key: "VK_DOWN", expected: { tourId: TOUR_IDs[2], focusedId: TOUR_IDs[2] } }, + { key: "VK_DOWN", expected: { tourId: TOUR_IDs[3], focusedId: TOUR_IDs[3] } }, + { key: "VK_DOWN", expected: { tourId: TOUR_IDs[4], focusedId: TOUR_IDs[4] } }, + { key: "VK_UP", expected: { tourId: TOUR_IDs[3], focusedId: TOUR_IDs[3] } }, + { key: "VK_UP", expected: { tourId: TOUR_IDs[2], focusedId: TOUR_IDs[2] } }, + { key: "VK_TAB", expected: { tourId: TOUR_IDs[2], focusedId: TOUR_IDs[3] } }, + { key: "VK_TAB", expected: { tourId: TOUR_IDs[2], focusedId: TOUR_IDs[4] } }, + { + key: "VK_RETURN", + expected: { tourId: TOUR_IDs[4], focusedId: TOUR_IDs[4] }, + }, + { + key: "VK_TAB", + options: { shiftKey: true }, + expected: { tourId: TOUR_IDs[4], focusedId: TOUR_IDs[3] }, + }, + { + key: "VK_TAB", + options: { shiftKey: true }, + expected: { tourId: TOUR_IDs[4], focusedId: TOUR_IDs[2] }, + }, + // VK_SPACE does not work well with EventUtils#synthesizeKey use " " instead + { key: " ", expected: { tourId: TOUR_IDs[2], focusedId: TOUR_IDs[2] } }, +]; + +const BUTTONS_TEST_DATA = [ + { key: " ", expected: { focusedId: TOUR_IDs[0], visible: true } }, + { + key: "VK_ESCAPE", + expected: { focusedId: "onboarding-overlay-button", visible: false }, + }, + { key: "VK_RETURN", expected: { focusedId: TOUR_IDs[1], visible: true } }, + { + key: "VK_TAB", + options: { shiftKey: true }, + expected: { focusedId: TOUR_IDs[0], visible: true }, + }, + { + key: "VK_TAB", + options: { shiftKey: true }, + expected: { focusedId: "onboarding-overlay-close-btn", visible: true }, + }, + { + key: " ", + expected: { focusedId: "onboarding-overlay-button", visible: false }, + }, + { key: "VK_RETURN", expected: { focusedId: TOUR_IDs[1], visible: true } }, + { + key: "VK_TAB", + options: { shiftKey: true }, + expected: { focusedId: TOUR_IDs[0], visible: true }, + }, + { + key: "VK_TAB", + options: { shiftKey: true }, + expected: { focusedId: "onboarding-overlay-close-btn", visible: true }, + }, + { key: "VK_TAB", expected: { focusedId: TOUR_IDs[0], visible: true } }, + { + key: "VK_TAB", + options: { shiftKey: true }, + expected: { focusedId: "onboarding-overlay-close-btn", visible: true }, + }, + { + key: "VK_RETURN", + expected: { focusedId: "onboarding-overlay-button", visible: false }, + }, +]; + +add_task(async function test_tour_list_keyboard_navigation() { + resetOnboardingDefaultState(); + + info("Display onboarding overlay on the home page"); + let tab = await openTab(ABOUT_HOME_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + tab.linkedBrowser + ); + await promiseOnboardingOverlayOpened(tab.linkedBrowser); + + info("Checking overall overlay tablist semantics"); + await assertOverlaySemantics(tab.linkedBrowser); + + info("Set initial focus on the currently active tab"); + await ContentTask.spawn(tab.linkedBrowser, {}, () => + content.document.querySelector(".onboarding-active").focus() + ); + await assertOverlayState(tab.linkedBrowser, { + tourId: TOUR_IDs[0], + focusedId: TOUR_IDs[0], + }); + + for (let { key, options = {}, expected } of TOUR_LIST_TEST_DATA) { + info( + `Pressing ${key} to select ${expected.tourId} and have focus on ${expected.focusedId}` + ); + await BrowserTestUtils.synthesizeKey(key, options, tab.linkedBrowser); + await assertOverlayState(tab.linkedBrowser, expected); + } + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_buttons_keyboard_navigation() { + resetOnboardingDefaultState(); + + info("Wait for onboarding overlay loaded"); + let tab = await openTab(ABOUT_HOME_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + + info("Set keyboard focus on the onboarding overlay button"); + await ContentTask.spawn(tab.linkedBrowser, {}, () => + content.document.getElementById("onboarding-overlay-button").focus() + ); + await assertOverlayState(tab.linkedBrowser, { + focusedId: "onboarding-overlay-button", + visible: false, + }); + + for (let { key, options = {}, expected } of BUTTONS_TEST_DATA) { + info( + `Pressing ${key} to have ${ + expected.visible ? "visible" : "invisible" + } overlay and have focus on ${expected.focusedId}` + ); + await BrowserTestUtils.synthesizeKey(key, options, tab.linkedBrowser); + await assertOverlayState(tab.linkedBrowser, expected); + } + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_overlay_dialog_keyboard_navigation() { + resetOnboardingDefaultState(); + + info("Wait for onboarding overlay loaded"); + let tab = await openTab(ABOUT_HOME_URL); + let browser = tab.linkedBrowser; + await promiseOnboardingOverlayLoaded(browser); + + info("Test accessibility and semantics of the dialog overlay"); + await assertModalDialog(browser, { visible: false }); + + info("Set keyboard focus on the onboarding overlay button"); + await ContentTask.spawn(browser, {}, () => + content.document.getElementById("onboarding-overlay-button").focus() + ); + info("Open dialog with keyboard and check the dialog state"); + await BrowserTestUtils.synthesizeKey(" ", {}, browser); + await promiseOnboardingOverlayOpened(browser); + await assertModalDialog(browser, { + visible: true, + keyboardFocus: true, + focusedId: TOUR_IDs[0], + }); + + info("Close the dialog and check modal dialog state"); + await BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, browser); + await promiseOnboardingOverlayClosed(browser); + await assertModalDialog(browser, { + visible: false, + keyboardFocus: true, + focusedId: "onboarding-overlay-button", + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_notification.js b/browser/extensions/onboarding/test/browser/browser_onboarding_notification.js new file mode 100644 index 000000000000..bb0d3d4f2479 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(3); + +add_task(async function test_show_tour_notifications_in_order() { + resetOnboardingDefaultState(); + Preferences.set( + "browser.onboarding.notification.max-prompt-count-per-tour", + 1 + ); + skipMuteNotificationOnFirstSession(); + + let tourIds = TOUR_IDs; + let tab = null; + let targetTourId = null; + let expectedPrefUpdates = null; + await loopTourNotificationQueueOnceInOrder(); + await loopTourNotificationQueueOnceInOrder(); + + expectedPrefUpdates = Promise.all([ + promisePrefUpdated("browser.onboarding.notification.finished", true), + promisePrefUpdated("browser.onboarding.state", ICON_STATE_WATERMARK), + ]); + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await expectedPrefUpdates; + await assertWatermarkIconDisplayed(tab.linkedBrowser); + let tourId = await getCurrentNotificationTargetTourId(tab.linkedBrowser); + ok(!tourId, "Should not prompt each tour for more than 2 chances."); + BrowserTestUtils.removeTab(tab); + + async function loopTourNotificationQueueOnceInOrder() { + for (let i = 0; i < tourIds.length; ++i) { + if (tab) { + await reloadTab(tab); + } else { + tab = await openTab(ABOUT_NEWTAB_URL); + } + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + targetTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + is(targetTourId, tourIds[i], "Should show tour notifications in order"); + } + } +}); + +add_task(async function test_open_target_tour_from_notification() { + resetOnboardingDefaultState(); + skipMuteNotificationOnFirstSession(); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + let targetTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-notification-action-btn", + {}, + tab.linkedBrowser + ); + await promiseOnboardingOverlayOpened(tab.linkedBrowser); + let { activeNavItemId, activePageId } = await getCurrentActiveTour( + tab.linkedBrowser + ); + + is(targetTourId, activeNavItemId, "Should navigate to the target tour item."); + is( + `${targetTourId}-page`, + activePageId, + "Should display the target tour page." + ); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_notification_2.js b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_2.js new file mode 100644 index 000000000000..0e517f6688de --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_2.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(3); + +add_task(async function test_not_show_notification_for_completed_tour() { + resetOnboardingDefaultState(); + skipMuteNotificationOnFirstSession(); + + let tourIds = TOUR_IDs; + // Make only the last tour uncompleted + let lastTourId = tourIds[tourIds.length - 1]; + for (let id of tourIds) { + if (id != lastTourId) { + setTourCompletedState(id, true); + } + } + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + let targetTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + is( + targetTourId, + lastTourId, + "Should not show notification for completed tour" + ); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_skip_notification_for_completed_tour() { + resetOnboardingDefaultState(); + Preferences.set( + "browser.onboarding.notification.max-prompt-count-per-tour", + 1 + ); + skipMuteNotificationOnFirstSession(); + + let tourIds = TOUR_IDs; + // Make only 2nd tour completed + await setTourCompletedState(tourIds[1], true); + + // Test show notification for the 1st tour + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + let targetTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + is(targetTourId, tourIds[0], "Should show notification for incompleted tour"); + + // Test skip the 2nd tour and show notification for the 3rd tour + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + targetTourId = await getCurrentNotificationTargetTourId(tab.linkedBrowser); + is( + targetTourId, + tourIds[2], + "Should skip notification for the completed 2nd tour" + ); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_mute_notification_on_1st_session() { + resetOnboardingDefaultState(); + + // Test no notifications during the mute duration on the 1st session + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + // The tour notification would be prompted on idle, so we wait idle twice here before proceeding + await waitUntilWindowIdle(tab.linkedBrowser); + await waitUntilWindowIdle(tab.linkedBrowser); + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await waitUntilWindowIdle(tab.linkedBrowser); + await waitUntilWindowIdle(tab.linkedBrowser); + let promptCount = Preferences.get( + "browser.onboarding.notification.prompt-count", + 0 + ); + is( + 0, + promptCount, + "Should not prompt tour notification during the mute duration on the 1st session" + ); + + // Test notification prompted after the mute duration on the 1st session + let muteTime = Preferences.get( + "browser.onboarding.notification.mute-duration-on-first-session-ms" + ); + let lastTime = Math.floor((Date.now() - muteTime - 1) / 1000); + Preferences.set( + "browser.onboarding.notification.last-time-of-changing-tour-sec", + lastTime + ); + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + promptCount = Preferences.get( + "browser.onboarding.notification.prompt-count", + 0 + ); + is( + 1, + promptCount, + "Should prompt tour notification after the mute duration on the 1st session" + ); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_notification_3.js b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_3.js new file mode 100644 index 000000000000..f9f435e5e554 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_3.js @@ -0,0 +1,135 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(3); + +add_task( + async function test_move_on_to_next_notification_when_reaching_max_prompt_count() { + resetOnboardingDefaultState(); + skipMuteNotificationOnFirstSession(); + let maxCount = Preferences.get( + "browser.onboarding.notification.max-prompt-count-per-tour" + ); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + let previousTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + + let currentTourId = null; + for (let i = maxCount - 1; i > 0; --i) { + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + currentTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + is( + previousTourId, + currentTourId, + "Should not move on to next tour notification until reaching the max prompt count per tour" + ); + } + + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + currentTourId = await getCurrentNotificationTargetTourId(tab.linkedBrowser); + isnot( + previousTourId, + currentTourId, + "Should move on to next tour notification when reaching the max prompt count per tour" + ); + + BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function test_move_on_to_next_notification_when_reaching_max_life_time() { + resetOnboardingDefaultState(); + skipMuteNotificationOnFirstSession(); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + let previousTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + + let maxTime = Preferences.get( + "browser.onboarding.notification.max-life-time-per-tour-ms" + ); + let lastTime = Math.floor((Date.now() - maxTime - 1) / 1000); + Preferences.set( + "browser.onboarding.notification.last-time-of-changing-tour-sec", + lastTime + ); + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + let currentTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + isnot( + previousTourId, + currentTourId, + "Should move on to next tour notification when reaching the max life time per tour" + ); + + BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function test_move_on_to_next_notification_after_interacting_with_notification() { + resetOnboardingDefaultState(); + skipMuteNotificationOnFirstSession(); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + let previousTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-notification-close-btn", + {}, + tab.linkedBrowser + ); + + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + let currentTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + isnot( + previousTourId, + currentTourId, + "Should move on to next tour notification after clicking #onboarding-notification-close-btn" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-notification-action-btn", + {}, + tab.linkedBrowser + ); + previousTourId = currentTourId; + + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + currentTourId = await getCurrentNotificationTargetTourId(tab.linkedBrowser); + isnot( + previousTourId, + currentTourId, + "Should move on to next tour notification after clicking #onboarding-notification-action-btn" + ); + + BrowserTestUtils.removeTab(tab); + } +); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_notification_4.js b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_4.js new file mode 100644 index 000000000000..41f42deec973 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_4.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(3); + +add_task( + async function test_remove_all_tour_notifications_through_close_button() { + resetOnboardingDefaultState(); + skipMuteNotificationOnFirstSession(); + + let tourIds = TOUR_IDs; + let tab = null; + let targetTourId = null; + await closeTourNotificationsOneByOne(); + + let expectedPrefUpdates = [ + promisePrefUpdated("browser.onboarding.notification.finished", true), + promisePrefUpdated("browser.onboarding.state", ICON_STATE_WATERMARK), + ]; + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await Promise.all(expectedPrefUpdates); + await assertWatermarkIconDisplayed(tab.linkedBrowser); + + let tourId = await getCurrentNotificationTargetTourId(tab.linkedBrowser); + ok( + !tourId, + "Should not prompt tour notifications any more after closing all notifcations." + ); + BrowserTestUtils.removeTab(tab); + + async function closeTourNotificationsOneByOne() { + for (let i = 0; i < tourIds.length; ++i) { + if (tab) { + await reloadTab(tab); + } else { + tab = await openTab(ABOUT_NEWTAB_URL); + } + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + targetTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + is( + targetTourId, + tourIds[i], + `Should show tour notifications of ${targetTourId}` + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-notification-close-btn", + {}, + tab.linkedBrowser + ); + await promiseTourNotificationClosed(tab.linkedBrowser); + } + } + } +); + +add_task( + async function test_remove_all_tour_notifications_through_action_button() { + resetOnboardingDefaultState(); + skipMuteNotificationOnFirstSession(); + + let tourIds = TOUR_IDs; + let tab = null; + let targetTourId = null; + await clickTourNotificationActionButtonsOneByOne(); + + let expectedPrefUpdates = [ + promisePrefUpdated("browser.onboarding.notification.finished", true), + promisePrefUpdated("browser.onboarding.state", ICON_STATE_WATERMARK), + ]; + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await Promise.all(expectedPrefUpdates); + await assertWatermarkIconDisplayed(tab.linkedBrowser); + + let tourId = await getCurrentNotificationTargetTourId(tab.linkedBrowser); + ok( + !tourId, + "Should not prompt tour notifcations any more after taking actions on all notifcations." + ); + BrowserTestUtils.removeTab(tab); + + async function clickTourNotificationActionButtonsOneByOne() { + for (let i = 0; i < tourIds.length; ++i) { + if (tab) { + await reloadTab(tab); + } else { + tab = await openTab(ABOUT_NEWTAB_URL); + } + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + targetTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + is( + targetTourId, + tourIds[i], + `Should show tour notifications of ${targetTourId}` + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-notification-action-btn", + {}, + tab.linkedBrowser + ); + await promiseTourNotificationClosed(tab.linkedBrowser); + } + } + } +); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_notification_5.js b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_5.js new file mode 100644 index 000000000000..b7e2aa477539 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_5.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + async function test_finish_tour_notifcations_after_total_max_life_time() { + resetOnboardingDefaultState(); + skipMuteNotificationOnFirstSession(); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + + let totalMaxTime = Preferences.get( + "browser.onboarding.notification.max-life-time-all-tours-ms" + ); + Preferences.set( + "browser.onboarding.notification.last-time-of-changing-tour-sec", + Math.floor((Date.now() - totalMaxTime) / 1000) + ); + let expectedPrefUpdates = Promise.all([ + promisePrefUpdated("browser.onboarding.notification.finished", true), + promisePrefUpdated("browser.onboarding.state", ICON_STATE_WATERMARK), + ]); + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await expectedPrefUpdates; + await assertWatermarkIconDisplayed(tab.linkedBrowser); + BrowserTestUtils.removeTab(tab); + } +); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_notification_click_auto_complete_tour.js b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_click_auto_complete_tour.js new file mode 100644 index 000000000000..cde56950b51f --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_click_auto_complete_tour.js @@ -0,0 +1,62 @@ +add_task(async function test_show_click_auto_complete_tour_in_notification() { + resetOnboardingDefaultState(); + skipMuteNotificationOnFirstSession(); + // the second tour is an click-auto-complete tour + await SpecialPowers.pushPrefEnv({ + set: [["browser.onboarding.newtour", "customize,library"]], + }); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + tab.linkedBrowser + ); + await promiseOnboardingOverlayOpened(tab.linkedBrowser); + + // Trigger CTA button to mark the tour as complete + let expectedPrefUpdates = [ + promisePrefUpdated( + `browser.onboarding.tour.onboarding-tour-customize.completed`, + true + ), + ]; + BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-tour-customize", + {}, + tab.linkedBrowser + ); + BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-tour-customize-button", + {}, + tab.linkedBrowser + ); + await Promise.all(expectedPrefUpdates); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-close-btn", + {}, + gBrowser.selectedBrowser + ); + let { activeNavItemId } = await getCurrentActiveTour(tab.linkedBrowser); + is( + "onboarding-tour-customize", + activeNavItemId, + "the active tour should be the previous shown tour" + ); + + await reloadTab(tab); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await promiseTourNotificationOpened(tab.linkedBrowser); + let targetTourId = await getCurrentNotificationTargetTourId( + tab.linkedBrowser + ); + is( + "onboarding-tour-library", + targetTourId, + "correctly show the click-autocomplete-tour in notification" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_select_default_tour.js b/browser/extensions/onboarding/test/browser/browser_onboarding_select_default_tour.js new file mode 100644 index 000000000000..8d7033b01d0f --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_select_default_tour.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const OVERLAY_ICON_ID = "#onboarding-overlay-button"; +const PRIVATE_BROWSING_TOUR_ID = "#onboarding-tour-private-browsing"; +const ADDONS_TOUR_ID = "#onboarding-tour-addons"; +const CUSTOMIZE_TOUR_ID = "#onboarding-tour-customize"; +const CLASS_ACTIVE = "onboarding-active"; + +add_task(async function test_default_tour_open_the_right_page() { + resetOnboardingDefaultState(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.onboarding.tour-type", "new"], + ["browser.onboarding.tourset-version", 1], + ["browser.onboarding.seen-tourset-version", 1], + ["browser.onboarding.newtour", "private,addons,customize"], + ], + }); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + OVERLAY_ICON_ID, + {}, + tab.linkedBrowser + ); + await promiseOnboardingOverlayOpened(tab.linkedBrowser); + + info("Make sure the default tour is active and open the right page"); + let { activeNavItemId, activePageId } = await getCurrentActiveTour( + tab.linkedBrowser + ); + is(`#${activeNavItemId}`, PRIVATE_BROWSING_TOUR_ID, "default tour is active"); + is( + activePageId, + "onboarding-tour-private-browsing-page", + "default tour page is shown" + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_select_first_uncomplete_tour() { + resetOnboardingDefaultState(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.onboarding.tour-type", "new"], + ["browser.onboarding.tourset-version", 1], + ["browser.onboarding.seen-tourset-version", 1], + ["browser.onboarding.newtour", "private,addons,customize"], + ], + }); + setTourCompletedState("onboarding-tour-private-browsing", true); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + OVERLAY_ICON_ID, + {}, + tab.linkedBrowser + ); + await promiseOnboardingOverlayOpened(tab.linkedBrowser); + + info("Make sure the first uncomplete tour is selected"); + let { activeNavItemId, activePageId } = await getCurrentActiveTour( + tab.linkedBrowser + ); + is(`#${activeNavItemId}`, ADDONS_TOUR_ID, "default tour is active"); + is(activePageId, "onboarding-tour-addons-page", "default tour page is shown"); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_select_first_tour_when_all_tours_are_complete() { + resetOnboardingDefaultState(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.onboarding.tour-type", "new"], + ["browser.onboarding.tourset-version", 1], + ["browser.onboarding.seen-tourset-version", 1], + ["browser.onboarding.newtour", "private,addons,customize"], + ], + }); + setTourCompletedState("onboarding-tour-private-browsing", true); + setTourCompletedState("onboarding-tour-addons", true); + setTourCompletedState("onboarding-tour-customize", true); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + OVERLAY_ICON_ID, + {}, + tab.linkedBrowser + ); + await promiseOnboardingOverlayOpened(tab.linkedBrowser); + + info("Make sure the first tour is selected when all tours are completed"); + let { activeNavItemId, activePageId } = await getCurrentActiveTour( + tab.linkedBrowser + ); + is(`#${activeNavItemId}`, PRIVATE_BROWSING_TOUR_ID, "default tour is active"); + is( + activePageId, + "onboarding-tour-private-browsing-page", + "default tour page is shown" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_skip_tour.js b/browser/extensions/onboarding/test/browser/browser_onboarding_skip_tour.js new file mode 100644 index 000000000000..7aec03c34c2b --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_skip_tour.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_skip_onboarding_tours() { + resetOnboardingDefaultState(); + + let tourIds = TOUR_IDs; + let expectedPrefUpdates = [ + promisePrefUpdated("browser.onboarding.notification.finished", true), + promisePrefUpdated("browser.onboarding.state", ICON_STATE_WATERMARK), + ]; + tourIds.forEach((id, idx) => + expectedPrefUpdates.push( + promisePrefUpdated(`browser.onboarding.tour.${id}.completed`, true) + ) + ); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + tab.linkedBrowser + ); + await promiseOnboardingOverlayOpened(tab.linkedBrowser); + + let overlayClosedPromise = promiseOnboardingOverlayClosed(tab.linkedBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-skip-tour-button", + {}, + tab.linkedBrowser + ); + await overlayClosedPromise; + await Promise.all(expectedPrefUpdates); + await assertWatermarkIconDisplayed(tab.linkedBrowser); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_hide_skip_button_via_perf() { + resetOnboardingDefaultState(); + Preferences.set("browser.onboarding.skip-tour-button.hide", true); + + let tab = await openTab(ABOUT_NEWTAB_URL); + let browser = tab.linkedBrowser; + await promiseOnboardingOverlayLoaded(browser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + browser + ); + await promiseOnboardingOverlayOpened(browser); + + let hasTourButton = await ContentTask.spawn(browser, null, () => { + return ( + content.document.querySelector("#onboarding-skip-tour-button") != null + ); + }); + + ok(!hasTourButton, "should not render the skip button"); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_tours.js b/browser/extensions/onboarding/test/browser/browser_onboarding_tours.js new file mode 100644 index 000000000000..db42e6c60637 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_tours.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +function assertTourCompleted(tourId, expectComplete, browser) { + return ContentTask.spawn(browser, { tourId, expectComplete }, function(args) { + let item = content.document.querySelector( + `#${args.tourId}.onboarding-tour-item` + ); + let completedTextId = `onboarding-complete-${args.tourId}-text`; + let completedText = item.querySelector(`#${completedTextId}`); + if (args.expectComplete) { + ok( + item.classList.contains("onboarding-complete"), + `Should set the complete #${args.tourId} tour with the complete style` + ); + ok(completedText, "Text label should be present for a completed item"); + is( + completedText.id, + completedTextId, + "Text label node should have a unique id" + ); + ok( + completedText.getAttribute("aria-label"), + "Text label node should have an aria-label attribute set" + ); + is( + item.getAttribute("aria-describedby"), + completedTextId, + "Completed item should have aria-describedby attribute set to text label node's id" + ); + } else { + ok( + !item.classList.contains("onboarding-complete"), + `Should not set the incomplete #${args.tourId} tour with the complete style` + ); + ok( + !completedText, + "Text label should not be present for an incomplete item" + ); + ok( + !item.hasAttribute("aria-describedby"), + "Incomplete item should not have aria-describedby attribute set" + ); + } + }); +} + +add_task(async function test_set_right_tour_completed_style_on_overlay() { + resetOnboardingDefaultState(); + + let tourIds = TOUR_IDs; + // Mark the tours of even number as completed + for (let i = 0; i < tourIds.length; ++i) { + setTourCompletedState(tourIds[i], i % 2 == 0); + } + + let tabs = []; + for (let url of URLs) { + let tab = await openTab(url); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + tab.linkedBrowser + ); + await promiseOnboardingOverlayOpened(tab.linkedBrowser); + tabs.push(tab); + } + + for (let i = tabs.length - 1; i >= 0; --i) { + let tab = tabs[i]; + await assertOverlaySemantics(tab.linkedBrowser); + for (let j = 0; j < tourIds.length; ++j) { + await assertTourCompleted(tourIds[j], j % 2 == 0, tab.linkedBrowser); + } + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function test_click_action_button_to_set_tour_completed() { + resetOnboardingDefaultState(); + const CUSTOM_TOUR_IDs = [ + "onboarding-tour-private-browsing", + "onboarding-tour-addons", + "onboarding-tour-customize", + ]; + await SpecialPowers.pushPrefEnv({ + set: [["browser.onboarding.newtour", "private,addons,customize"]], + }); + + let tourIds = CUSTOM_TOUR_IDs; + let tabs = []; + for (let url of URLs) { + let tab = await openTab(url); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + tab.linkedBrowser + ); + await promiseOnboardingOverlayOpened(tab.linkedBrowser); + tabs.push(tab); + } + + let completedTourId = tourIds[0]; + let expectedPrefUpdate = promisePrefUpdated( + `browser.onboarding.tour.${completedTourId}.completed`, + true + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${completedTourId}-page .onboarding-tour-action-button`, + {}, + gBrowser.selectedBrowser + ); + await expectedPrefUpdate; + + for (let i = tabs.length - 1; i >= 0; --i) { + let tab = tabs[i]; + await assertOverlaySemantics(tab.linkedBrowser); + for (let id of tourIds) { + await assertTourCompleted(id, id == completedTourId, tab.linkedBrowser); + } + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function test_set_watermark_after_all_tour_completed() { + resetOnboardingDefaultState(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.onboarding.tour-type", "new"]], + }); + + let tabs = []; + for (let url of URLs) { + let tab = await openTab(url); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + tab.linkedBrowser + ); + await promiseOnboardingOverlayOpened(tab.linkedBrowser); + tabs.push(tab); + } + let expectedPrefUpdate = promisePrefUpdated( + "browser.onboarding.state", + ICON_STATE_WATERMARK + ); + TOUR_IDs.forEach(id => + Preferences.set(`browser.onboarding.tour.${id}.completed`, true) + ); + await expectedPrefUpdate; + + for (let tab of tabs) { + await assertWatermarkIconDisplayed(tab.linkedBrowser); + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_tourset.js b/browser/extensions/onboarding/test/browser/browser_onboarding_tourset.js new file mode 100644 index 000000000000..f54727d624e4 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_tourset.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +async function testTourIDs(browser, tourIDs) { + await ContentTask.spawn(browser, tourIDs, async tourIDsContent => { + let doc = content.document; + let doms = doc.querySelectorAll(".onboarding-tour-item"); + Assert.equal(doms.length, tourIDsContent.length, "has exact tour numbers"); + doms.forEach((dom, idx) => { + Assert.equal( + tourIDsContent[idx], + dom.id, + "contain defined onboarding id" + ); + }); + }); +} + +add_task(async function test_onboarding_default_new_tourset() { + resetOnboardingDefaultState(); + + let tab = await openTab(ABOUT_NEWTAB_URL); + let browser = tab.linkedBrowser; + await promiseOnboardingOverlayLoaded(browser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + browser + ); + await promiseOnboardingOverlayOpened(browser); + + await testTourIDs(browser, TOUR_IDs); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_onboarding_custom_new_tourset() { + const CUSTOM_NEW_TOURs = [ + "onboarding-tour-private-browsing", + "onboarding-tour-addons", + "onboarding-tour-customize", + ]; + + resetOnboardingDefaultState(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.onboarding.tour-type", "new"], + ["browser.onboarding.tourset-version", 1], + ["browser.onboarding.seen-tourset-version", 1], + ["browser.onboarding.newtour", "private,addons,customize"], + ], + }); + + let tab = await openTab(ABOUT_NEWTAB_URL); + let browser = tab.linkedBrowser; + await promiseOnboardingOverlayLoaded(browser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + browser + ); + await promiseOnboardingOverlayOpened(browser); + + await testTourIDs(browser, CUSTOM_NEW_TOURs); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_onboarding_custom_update_tourset() { + const CUSTOM_UPDATE_TOURs = [ + "onboarding-tour-customize", + "onboarding-tour-private-browsing", + "onboarding-tour-addons", + ]; + resetOnboardingDefaultState(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.onboarding.tour-type", "update"], + ["browser.onboarding.tourset-version", 1], + ["browser.onboarding.seen-tourset-version", 1], + ["browser.onboarding.updatetour", "customize,private,addons"], + ], + }); + + let tab = await openTab(ABOUT_NEWTAB_URL); + let browser = tab.linkedBrowser; + await promiseOnboardingOverlayLoaded(browser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + browser + ); + await promiseOnboardingOverlayOpened(browser); + + await testTourIDs(browser, CUSTOM_UPDATE_TOURs); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_uitour.js b/browser/extensions/onboarding/test/browser/browser_onboarding_uitour.js new file mode 100644 index 000000000000..a4208e749390 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_uitour.js @@ -0,0 +1,247 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(3); + +function promisePopupChange(popup, expectedState) { + return new Promise(resolve => { + let event = expectedState == "open" ? "popupshown" : "popuphidden"; + popup.addEventListener(event, resolve, { once: true }); + }); +} + +async function promiseOpenOnboardingOverlay(tab) { + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-overlay-button", + {}, + tab.linkedBrowser + ); + return promiseOnboardingOverlayOpened(tab.linkedBrowser); +} + +async function triggerUITourHighlight(tourName, tab) { + await promiseOpenOnboardingOverlay(tab); + BrowserTestUtils.synthesizeMouseAtCenter( + `#onboarding-tour-${tourName}`, + {}, + tab.linkedBrowser + ); + BrowserTestUtils.synthesizeMouseAtCenter( + `#onboarding-tour-${tourName}-button`, + {}, + tab.linkedBrowser + ); +} + +add_task(async function test_clean_up_uitour_after_closing_overlay() { + resetOnboardingDefaultState(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.onboarding.newtour", "library"]], + }); + + // Trigger UITour showHighlight + let highlight = document.getElementById("UITourHighlightContainer"); + let highlightOpenPromise = promisePopupChange(highlight, "open"); + let tab = await openTab(ABOUT_NEWTAB_URL); + await triggerUITourHighlight("library", tab); + await highlightOpenPromise; + is(highlight.state, "open", "Should show UITour highlight"); + is( + highlight.getAttribute("targetName"), + "library", + "UITour should highlight library" + ); + + // Close the overlay by clicking the overlay + let highlightClosePromise = promisePopupChange(highlight, "closed"); + BrowserTestUtils.synthesizeMouseAtPoint(2, 2, {}, tab.linkedBrowser); + await promiseOnboardingOverlayClosed(tab.linkedBrowser); + await highlightClosePromise; + is( + highlight.state, + "closed", + "Should close UITour highlight after closing the overlay by clicking the overlay" + ); + + // Trigger UITour showHighlight again + highlightOpenPromise = promisePopupChange(highlight, "open"); + await triggerUITourHighlight("library", tab); + await highlightOpenPromise; + is(highlight.state, "open", "Should show UITour highlight"); + is( + highlight.getAttribute("targetName"), + "library", + "UITour should highlight library" + ); + + // Close the overlay by clicking the skip-tour button + highlightClosePromise = promisePopupChange(highlight, "closed"); + BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-skip-tour-btn", + {}, + tab.linkedBrowser + ); + await promiseOnboardingOverlayClosed(tab.linkedBrowser); + await highlightClosePromise; + is( + highlight.state, + "closed", + "Should close UITour highlight after closing the overlay by clicking the skip-tour button" + ); + BrowserTestUtils.removeTab(tab); +}); + +add_task( + async function test_clean_up_uitour_after_navigating_to_other_tour_by_keyboard() { + resetOnboardingDefaultState(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.onboarding.newtour", "singlesearch,customize"]], + }); + + let tab = await openTab(ABOUT_NEWTAB_URL); + await promiseOpenOnboardingOverlay(tab); + + // Navigate to the Customize tour to trigger UITour showHighlight + let highlight = document.getElementById("UITourHighlightContainer"); + let highlightOpenPromise = promisePopupChange(highlight, "open"); + tab.linkedBrowser.focus(); // Make sure the key event will be fired on the focused page + await BrowserTestUtils.synthesizeKey("VK_TAB", {}, tab.linkedBrowser); + await BrowserTestUtils.synthesizeKey("VK_TAB", {}, tab.linkedBrowser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser); + await BrowserTestUtils.synthesizeKey("VK_TAB", {}, tab.linkedBrowser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser); + await highlightOpenPromise; + is(highlight.state, "open", "Should show UITour highlight"); + is( + highlight.getAttribute("targetName"), + "customize", + "UITour should highlight customize" + ); + + // Navigate to the Single-Search tour + let highlightClosePromise = promisePopupChange(highlight, "closed"); + tab.linkedBrowser.focus(); // Make sure the key event will be fired on the focused page + await BrowserTestUtils.synthesizeKey( + "VK_TAB", + { shiftKey: true }, + tab.linkedBrowser + ); + await BrowserTestUtils.synthesizeKey( + "VK_TAB", + { shiftKey: true }, + tab.linkedBrowser + ); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser); + await highlightClosePromise; + is( + highlight.state, + "closed", + "Should close UITour highlight after navigating to another tour by keyboard" + ); + BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function test_clean_up_uitour_after_navigating_to_other_tour_by_mouse() { + resetOnboardingDefaultState(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.onboarding.newtour", "singlesearch,customize"]], + }); + + // Navigate to the Customize tour to trigger UITour showHighlight + let highlight = document.getElementById("UITourHighlightContainer"); + let highlightOpenPromise = promisePopupChange(highlight, "open"); + let tab = await openTab(ABOUT_NEWTAB_URL); + await triggerUITourHighlight("customize", tab); + await highlightOpenPromise; + is(highlight.state, "open", "Should show UITour highlight"); + is( + highlight.getAttribute("targetName"), + "customize", + "UITour should highlight customize" + ); + + // Navigate to the Single-Search tour + let highlightClosePromise = promisePopupChange(highlight, "closed"); + BrowserTestUtils.synthesizeMouseAtCenter( + "#onboarding-tour-singlesearch", + {}, + tab.linkedBrowser + ); + await highlightClosePromise; + is( + highlight.state, + "closed", + "Should close UITour highlight after navigating to another tour by mouse" + ); + BrowserTestUtils.removeTab(tab); + } +); + +add_task(async function test_clean_up_uitour_on_page_unload() { + resetOnboardingDefaultState(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.onboarding.newtour", "singlesearch,customize"]], + }); + + // Trigger UITour showHighlight + let highlight = document.getElementById("UITourHighlightContainer"); + let highlightOpenPromise = promisePopupChange(highlight, "open"); + let tab = await openTab(ABOUT_NEWTAB_URL); + await triggerUITourHighlight("customize", tab); + await highlightOpenPromise; + is(highlight.state, "open", "Should show UITour highlight"); + is( + highlight.getAttribute("targetName"), + "customize", + "UITour should highlight customize" + ); + + // Load another page to unload the current page + let highlightClosePromise = promisePopupChange(highlight, "closed"); + await BrowserTestUtils.loadURI(tab.linkedBrowser, "http://example.com"); + await highlightClosePromise; + is( + highlight.state, + "closed", + "Should close UITour highlight after page unloaded" + ); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_clean_up_uitour_on_window_resize() { + resetOnboardingDefaultState(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.onboarding.newtour", "singlesearch,customize"]], + }); + + // Trigger UITour showHighlight + let highlight = document.getElementById("UITourHighlightContainer"); + let highlightOpenPromise = promisePopupChange(highlight, "open"); + let tab = await openTab(ABOUT_NEWTAB_URL); + await triggerUITourHighlight("customize", tab); + await highlightOpenPromise; + is(highlight.state, "open", "Should show UITour highlight"); + is( + highlight.getAttribute("targetName"), + "customize", + "UITour should highlight customize" + ); + + // Resize window to destroy the onboarding tour + const originalWidth = window.innerWidth; + let highlightClosePromise = promisePopupChange(highlight, "closed"); + window.innerWidth = 300; + await highlightClosePromise; + is( + highlight.state, + "closed", + "Should close UITour highlight after window resized" + ); + window.innerWidth = originalWidth; + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/onboarding/test/browser/head.js b/browser/extensions/onboarding/test/browser/head.js new file mode 100644 index 000000000000..4a151cc97192 --- /dev/null +++ b/browser/extensions/onboarding/test/browser/head.js @@ -0,0 +1,387 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let { Preferences } = ChromeUtils.import( + "resource://gre/modules/Preferences.jsm", + {} +); + +const ABOUT_HOME_URL = "about:home"; +const ABOUT_NEWTAB_URL = "about:newtab"; +const URLs = [ABOUT_HOME_URL, ABOUT_NEWTAB_URL]; +const TOUR_IDs = [ + "onboarding-tour-performance", + "onboarding-tour-private-browsing", + "onboarding-tour-screenshots", + "onboarding-tour-addons", + "onboarding-tour-customize", + "onboarding-tour-default-browser", +]; +const UPDATE_TOUR_IDs = [ + "onboarding-tour-performance", + "onboarding-tour-library", + "onboarding-tour-screenshots", + "onboarding-tour-singlesearch", + "onboarding-tour-customize", + "onboarding-tour-sync", +]; +const ICON_STATE_WATERMARK = "watermark"; +const ICON_STATE_DEFAULT = "default"; + +registerCleanupFunction(resetOnboardingDefaultState); + +function resetOnboardingDefaultState() { + // All the prefs should be reset to the default states + // and no need to revert back so we don't use `SpecialPowers.pushPrefEnv` here. + Preferences.set("browser.onboarding.enabled", true); + Preferences.set("browser.onboarding.state", ICON_STATE_DEFAULT); + Preferences.set("browser.onboarding.notification.finished", false); + Preferences.set( + "browser.onboarding.notification.mute-duration-on-first-session-ms", + 300000 + ); + Preferences.set( + "browser.onboarding.notification.max-life-time-per-tour-ms", + 432000000 + ); + Preferences.set( + "browser.onboarding.notification.max-life-time-all-tours-ms", + 1209600000 + ); + Preferences.set( + "browser.onboarding.notification.max-prompt-count-per-tour", + 8 + ); + Preferences.reset( + "browser.onboarding.notification.last-time-of-changing-tour-sec" + ); + Preferences.reset("browser.onboarding.notification.prompt-count"); + Preferences.reset("browser.onboarding.notification.tour-ids-queue"); + Preferences.reset("browser.onboarding.skip-tour-button.hide"); + TOUR_IDs.forEach(id => + Preferences.reset(`browser.onboarding.tour.${id}.completed`) + ); + UPDATE_TOUR_IDs.forEach(id => + Preferences.reset(`browser.onboarding.tour.${id}.completed`) + ); +} + +function setTourCompletedState(tourId, state) { + Preferences.set(`browser.onboarding.tour.${tourId}.completed`, state); +} + +async function openTab(url) { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let loadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await BrowserTestUtils.loadURI(tab.linkedBrowser, url); + await loadedPromise; + return tab; +} + +function reloadTab(tab) { + let reloadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.linkedBrowser.reload(); + return reloadPromise; +} + +function promiseOnboardingOverlayLoaded(browser) { + function isLoaded() { + let doc = content && content.document; + if (doc.querySelector("#onboarding-overlay")) { + ok(true, "Should load onboarding overlay"); + return Promise.resolve(); + } + return new Promise(resolve => { + let observer = new content.MutationObserver(mutations => { + mutations.forEach(mutation => { + let overlay = Array.from(mutation.addedNodes).find( + node => node.id == "onboarding-overlay" + ); + if (overlay) { + observer.disconnect(); + ok(true, "Should load onboarding overlay"); + resolve(); + } + }); + }); + observer.observe(doc.body, { childList: true }); + }); + } + return ContentTask.spawn(browser, {}, isLoaded); +} + +function promiseOnboardingOverlayOpened(browser) { + return BrowserTestUtils.waitForCondition( + () => + ContentTask.spawn(browser, {}, () => + content.document + .querySelector("#onboarding-overlay") + .classList.contains("onboarding-opened") + ), + "Should open onboarding overlay", + 100, + 30 + ); +} + +function promiseOnboardingOverlayClosed(browser) { + return BrowserTestUtils.waitForCondition( + () => + ContentTask.spawn( + browser, + {}, + () => + !content.document + .querySelector("#onboarding-overlay") + .classList.contains("onboarding-opened") + ), + "Should close onboarding overlay", + 100, + 30 + ); +} + +function promisePrefUpdated(name, expectedValue) { + return new Promise(resolve => { + let onUpdate = actualValue => { + Preferences.ignore(name, onUpdate); + is(expectedValue, actualValue, `Should update the pref of ${name}`); + resolve(); + }; + Preferences.observe(name, onUpdate); + }); +} + +function promiseTourNotificationOpened(browser) { + function isOpened() { + let doc = content && content.document; + let notification = doc.querySelector("#onboarding-notification-bar"); + if (notification && notification.classList.contains("onboarding-opened")) { + ok(true, "Should open tour notification"); + return Promise.resolve(); + } + return new Promise(resolve => { + let observer = new content.MutationObserver(mutations => { + mutations.forEach(mutation => { + let bar = Array.from(mutation.addedNodes).find( + node => node.id == "onboarding-notification-bar" + ); + if (bar && bar.classList.contains("onboarding-opened")) { + observer.disconnect(); + ok(true, "Should open tour notification"); + resolve(); + } + }); + }); + observer.observe(doc.body, { childList: true }); + }); + } + return ContentTask.spawn(browser, {}, isOpened); +} + +function promiseTourNotificationClosed(browser) { + let condition = () => { + return ContentTask.spawn(browser, {}, function() { + return new Promise(resolve => { + let bar = content.document.querySelector( + "#onboarding-notification-bar" + ); + if (bar && !bar.classList.contains("onboarding-opened")) { + resolve(true); + return; + } + resolve(false); + }); + }); + }; + return BrowserTestUtils.waitForCondition( + condition, + "Should close tour notification", + 100, + 30 + ); +} + +function getCurrentNotificationTargetTourId(browser) { + return ContentTask.spawn(browser, {}, function() { + let bar = content.document.querySelector("#onboarding-notification-bar"); + return bar ? bar.dataset.targetTourId : null; + }); +} + +function getCurrentActiveTour(browser) { + return ContentTask.spawn(browser, {}, function() { + let list = content.document.querySelector("#onboarding-tour-list"); + let items = list.querySelectorAll(".onboarding-tour-item"); + let activeNavItemId = null; + for (let item of items) { + if (item.classList.contains("onboarding-active")) { + if (!activeNavItemId) { + activeNavItemId = item.id; + } else { + ok(false, "There are more than one item marked as active."); + } + } + } + let activePageId = null; + let pages = content.document.querySelectorAll(".onboarding-tour-page"); + for (let page of pages) { + if (page.style.display != "none") { + if (!activePageId) { + activePageId = page.id; + } else { + ok(false, "Thre are more than one tour page visible."); + } + } + } + return { activeNavItemId, activePageId }; + }); +} + +function waitUntilWindowIdle(browser) { + return ContentTask.spawn(browser, {}, function() { + return new Promise(resolve => content.requestIdleCallback(resolve)); + }); +} + +function skipMuteNotificationOnFirstSession() { + Preferences.set( + "browser.onboarding.notification.mute-duration-on-first-session-ms", + 0 + ); +} + +function assertOverlaySemantics(browser) { + return ContentTask.spawn(browser, {}, function() { + let doc = content.document; + + info("Checking dialog"); + let dialog = doc.getElementById("onboarding-overlay-dialog"); + is( + dialog.getAttribute("role"), + "dialog", + "Dialog should have a dialog role attribute set" + ); + is( + dialog.tabIndex, + "-1", + "Dialog should be focusable but not in tab order" + ); + is( + dialog.getAttribute("aria-labelledby"), + "onboarding-header", + "Dialog should be labaled by its header" + ); + + info("Checking the tablist container"); + is( + doc.getElementById("onboarding-tour-list").getAttribute("role"), + "tablist", + "Tour list should have a tablist role attribute set" + ); + + info("Checking each tour item that represents the tab"); + let items = [...doc.querySelectorAll(".onboarding-tour-item")]; + items.forEach(item => { + is( + item.parentNode.getAttribute("role"), + "presentation", + "Parent should have no semantic value" + ); + is( + item.getAttribute("aria-selected"), + item.classList.contains("onboarding-active") ? "true" : "false", + "Active item should have aria-selected set to true and inactive to false" + ); + is( + item.tabIndex, + "0", + "Item tab index must be set for keyboard accessibility" + ); + is( + item.getAttribute("role"), + "tab", + "Item should have a tab role attribute set" + ); + let tourPanelId = `${item.id}-page`; + is( + item.getAttribute("aria-controls"), + tourPanelId, + "Item should have aria-controls attribute point to its tabpanel" + ); + let panel = doc.getElementById(tourPanelId); + is( + panel.getAttribute("role"), + "tabpanel", + "Tour panel should have a tabpanel role attribute set" + ); + is( + panel.getAttribute("aria-labelledby"), + item.id, + "Tour panel should have aria-labelledby attribute point to its tab" + ); + }); + }); +} + +function assertModalDialog(browser, args) { + return ContentTask.spawn( + browser, + args, + ({ keyboardFocus, visible, focusedId }) => { + let doc = content.document; + let overlayButton = doc.getElementById("onboarding-overlay-button"); + if (visible) { + [...doc.body.children].forEach( + child => + child.id !== "onboarding-overlay" && + is( + child.getAttribute("aria-hidden"), + "true", + "Content should not be visible to screen reader" + ) + ); + is( + focusedId ? doc.getElementById(focusedId) : doc.body, + doc.activeElement, + `Focus should be on ${focusedId || "body"}` + ); + is( + keyboardFocus ? "true" : undefined, + overlayButton.dataset.keyboardFocus, + "Overlay button focus state is saved correctly" + ); + } else { + [...doc.body.children].forEach(child => + ok( + !child.hasAttribute("aria-hidden"), + "Content should be visible to screen reader" + ) + ); + if (keyboardFocus) { + is( + overlayButton, + doc.activeElement, + "Focus should be set on overlay button" + ); + } + ok( + !overlayButton.dataset.keyboardFocus, + "Overlay button focus state should be cleared" + ); + } + } + ); +} + +function assertWatermarkIconDisplayed(browser) { + return ContentTask.spawn(browser, {}, function() { + let overlayButton = content.document.getElementById( + "onboarding-overlay-button" + ); + ok( + overlayButton.classList.contains("onboarding-watermark"), + "Should display the watermark onboarding icon" + ); + }); +} diff --git a/browser/extensions/onboarding/test/unit/.eslintrc.js b/browser/extensions/onboarding/test/unit/.eslintrc.js new file mode 100644 index 000000000000..69e89d0054ba --- /dev/null +++ b/browser/extensions/onboarding/test/unit/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/xpcshell-test"], +}; diff --git a/browser/extensions/onboarding/test/unit/head.js b/browser/extensions/onboarding/test/unit/head.js new file mode 100644 index 000000000000..45c603e34dd1 --- /dev/null +++ b/browser/extensions/onboarding/test/unit/head.js @@ -0,0 +1,58 @@ +/** + * Provides infrastructure for automated onboarding components tests. + */ + +"use strict"; + +/* global Cc, Ci, Cu */ +ChromeUtils.import("resource://gre/modules/Preferences.jsm"); +ChromeUtils.import("resource://gre/modules/Services.jsm"); +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter( + this, + "resProto", + "@mozilla.org/network/protocol;1?name=resource", + "nsISubstitutingProtocolHandler" +); + +// Load our bootstrap extension manifest so we can access our chrome/resource URIs. +// Cargo culted from formautofill system add-on +const EXTENSION_ID = "onboarding@mozilla.org"; +let extensionDir = Services.dirsvc.get("GreD", Ci.nsIFile); +extensionDir.append("browser"); +extensionDir.append("features"); +extensionDir.append(EXTENSION_ID); +let resourceURI; +// If the unpacked extension doesn't exist, use the packed version. +if (!extensionDir.exists()) { + extensionDir.leafName += ".xpi"; + + resourceURI = + "jar:" + Services.io.newFileURI(extensionDir).spec + "!/chrome/content/"; +} else { + resourceURI = Services.io.newFileURI(extensionDir).spec + "/chrome/content/"; +} +Components.manager.addBootstrappedManifestLocation(extensionDir); + +resProto.setSubstitution("onboarding", Services.io.newURI(resourceURI)); + +const TOURSET_VERSION = 1; +const NEXT_TOURSET_VERSION = 2; +const PREF_TOUR_TYPE = "browser.onboarding.tour-type"; +const PREF_TOURSET_VERSION = "browser.onboarding.tourset-version"; +const PREF_SEEN_TOURSET_VERSION = "browser.onboarding.seen-tourset-version"; + +function resetOnboardingDefaultState() { + // All the prefs should be reset to what prefs should looks like in a new user profile + Services.prefs.setIntPref(PREF_TOURSET_VERSION, TOURSET_VERSION); + Services.prefs.clearUserPref(PREF_SEEN_TOURSET_VERSION); + Services.prefs.clearUserPref(PREF_TOUR_TYPE); +} + +function resetOldProfileDefaultState() { + // All the prefs should be reset to what prefs should looks like in a older new user profile + Services.prefs.setIntPref(PREF_TOURSET_VERSION, TOURSET_VERSION); + Services.prefs.setIntPref(PREF_SEEN_TOURSET_VERSION, 0); + Services.prefs.clearUserPref(PREF_TOUR_TYPE); +} diff --git a/browser/extensions/onboarding/test/unit/test-onboarding-tour-type.js b/browser/extensions/onboarding/test/unit/test-onboarding-tour-type.js new file mode 100644 index 000000000000..83804c7ee719 --- /dev/null +++ b/browser/extensions/onboarding/test/unit/test-onboarding-tour-type.js @@ -0,0 +1,155 @@ +/* + * Test for onboarding tour type check. + */ + +"use strict"; + +ChromeUtils.import("resource://onboarding/modules/OnboardingTourType.jsm"); + +add_task(async function() { + info("Starting testcase: When New user open the browser first time"); + resetOnboardingDefaultState(); + OnboardingTourType.check(); + + Assert.equal( + Preferences.get(PREF_TOUR_TYPE), + "new", + "should show the new user tour" + ); + Assert.equal( + Preferences.get(PREF_TOURSET_VERSION), + TOURSET_VERSION, + "tourset version should not change" + ); + Assert.equal( + Preferences.get(PREF_SEEN_TOURSET_VERSION), + TOURSET_VERSION, + "seen tourset version should be set as the tourset version" + ); +}); + +add_task(async function() { + info("Starting testcase: When New user restart the browser"); + resetOnboardingDefaultState(); + Preferences.set(PREF_TOUR_TYPE, "new"); + Preferences.set(PREF_SEEN_TOURSET_VERSION, TOURSET_VERSION); + OnboardingTourType.check(); + + Assert.equal( + Preferences.get(PREF_TOUR_TYPE), + "new", + "should show the new user tour" + ); + Assert.equal( + Preferences.get(PREF_TOURSET_VERSION), + TOURSET_VERSION, + "tourset version should not change" + ); + Assert.equal( + Preferences.get(PREF_SEEN_TOURSET_VERSION), + TOURSET_VERSION, + "seen tourset version should be set as the tourset version" + ); +}); + +add_task(async function() { + info( + "Starting testcase: When New User choosed to hide the overlay and restart the browser" + ); + resetOnboardingDefaultState(); + Preferences.set(PREF_TOUR_TYPE, "new"); + Preferences.set(PREF_SEEN_TOURSET_VERSION, TOURSET_VERSION); + OnboardingTourType.check(); + + Assert.equal( + Preferences.get(PREF_TOUR_TYPE), + "new", + "should show the new user tour" + ); + Assert.equal( + Preferences.get(PREF_TOURSET_VERSION), + TOURSET_VERSION, + "tourset version should not change" + ); + Assert.equal( + Preferences.get(PREF_SEEN_TOURSET_VERSION), + TOURSET_VERSION, + "seen tourset version should be set as the tourset version" + ); +}); + +add_task(async function() { + info( + "Starting testcase: When New User updated to the next major version and restart the browser" + ); + resetOnboardingDefaultState(); + Preferences.set(PREF_TOURSET_VERSION, NEXT_TOURSET_VERSION); + Preferences.set(PREF_TOUR_TYPE, "new"); + Preferences.set(PREF_SEEN_TOURSET_VERSION, TOURSET_VERSION); + OnboardingTourType.check(); + + Assert.equal( + Preferences.get(PREF_TOUR_TYPE), + "update", + "should show the update user tour" + ); + Assert.equal( + Preferences.get(PREF_TOURSET_VERSION), + NEXT_TOURSET_VERSION, + "tourset version should not change" + ); + Assert.equal( + Preferences.get(PREF_SEEN_TOURSET_VERSION), + NEXT_TOURSET_VERSION, + "seen tourset version should be set as the tourset version" + ); +}); + +add_task(async function() { + info( + "Starting testcase: When New User prefer hide the tour, then updated to the next major version and restart the browser" + ); + resetOnboardingDefaultState(); + Preferences.set(PREF_TOURSET_VERSION, NEXT_TOURSET_VERSION); + Preferences.set(PREF_TOUR_TYPE, "new"); + Preferences.set(PREF_SEEN_TOURSET_VERSION, TOURSET_VERSION); + OnboardingTourType.check(); + + Assert.equal( + Preferences.get(PREF_TOUR_TYPE), + "update", + "should show the update user tour" + ); + Assert.equal( + Preferences.get(PREF_TOURSET_VERSION), + NEXT_TOURSET_VERSION, + "tourset version should not change" + ); + Assert.equal( + Preferences.get(PREF_SEEN_TOURSET_VERSION), + NEXT_TOURSET_VERSION, + "seen tourset version should be set as the tourset version" + ); +}); + +add_task(async function() { + info("Starting testcase: When User update from browser version < 56"); + resetOldProfileDefaultState(); + OnboardingTourType.check(); + + Assert.equal( + Preferences.get(PREF_TOUR_TYPE), + "update", + "should show the update user tour" + ); + Assert.equal( + Preferences.get(PREF_TOURSET_VERSION), + TOURSET_VERSION, + "tourset version should not change" + ); + Assert.equal( + Preferences.get(PREF_SEEN_TOURSET_VERSION), + TOURSET_VERSION, + "seen tourset version should be set as the tourset version" + ); +}); diff --git a/browser/extensions/onboarding/test/unit/xpcshell.ini b/browser/extensions/onboarding/test/unit/xpcshell.ini new file mode 100644 index 000000000000..ed484d0f200f --- /dev/null +++ b/browser/extensions/onboarding/test/unit/xpcshell.ini @@ -0,0 +1,5 @@ +[DEFAULT] +firefox-appdir = browser +head = head.js + +[test-onboarding-tour-type.js] diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 9b5a82add399..ee46e9b7d30d 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -243,6 +243,7 @@ @RESPATH@/browser/chrome/icons/default/default64.png @RESPATH@/browser/chrome/icons/default/default128.png #endif +@RESPATH@/browser/features/*
; Base Browser @RESPATH@/browser/chrome/newidentity.manifest diff --git a/browser/locales/Makefile.in b/browser/locales/Makefile.in index 07e699675878..2cb5e78a1a2f 100644 --- a/browser/locales/Makefile.in +++ b/browser/locales/Makefile.in @@ -52,6 +52,7 @@ l10n-%: @$(MAKE) -C ../../toolkit/locales l10n-$* XPI_ROOT_APPID='$(XPI_ROOT_APPID)' @$(MAKE) -C ../../services/sync/locales AB_CD=$* XPI_NAME=locale-$* @$(MAKE) -C ../../extensions/spellcheck/locales AB_CD=$* XPI_NAME=locale-$* + @$(MAKE) -C ../extensions/onboarding/locales AB_CD=$* XPI_NAME=locale-$* @$(MAKE) -C ../../devtools/client/locales AB_CD=$* XPI_NAME=locale-$* XPI_ROOT_APPID='$(XPI_ROOT_APPID)' @$(MAKE) -C ../../devtools/startup/locales AB_CD=$* XPI_NAME=locale-$* XPI_ROOT_APPID='$(XPI_ROOT_APPID)' @$(MAKE) l10n AB_CD=$* XPI_NAME=locale-$* PREF_DIR=$(PREF_DIR) @@ -65,6 +66,7 @@ chrome-%: @$(MAKE) -C ../../toolkit/locales chrome-$* @$(MAKE) -C ../../services/sync/locales chrome AB_CD=$* @$(MAKE) -C ../../extensions/spellcheck/locales chrome AB_CD=$* + @$(MAKE) -C ../extensions/onboarding/locales chrome AB_CD=$* @$(MAKE) -C ../../devtools/client/locales chrome AB_CD=$* @$(MAKE) -C ../../devtools/startup/locales chrome AB_CD=$* @$(MAKE) chrome AB_CD=$* diff --git a/browser/locales/filter.py b/browser/locales/filter.py index 22eb5cbdb177..504c498debf5 100644 --- a/browser/locales/filter.py +++ b/browser/locales/filter.py @@ -17,6 +17,7 @@ def test(mod, path, entity=None): "devtools/startup", "browser", "browser/extensions/formautofill", + "browser/extensions/onboarding", "browser/extensions/report-site-issue", "extensions/spellcheck", "other-licenses/branding/firefox", diff --git a/browser/locales/l10n.ini b/browser/locales/l10n.ini index 7a6599740b20..c70485a63d53 100644 --- a/browser/locales/l10n.ini +++ b/browser/locales/l10n.ini @@ -13,6 +13,7 @@ dirs = browser devtools/client devtools/startup browser/extensions/formautofill + browser/extensions/onboarding browser/extensions/report-site-issue
[includes] diff --git a/browser/locales/l10n.toml b/browser/locales/l10n.toml index e9d50107cb10..0fdbfa131898 100644 --- a/browser/locales/l10n.toml +++ b/browser/locales/l10n.toml @@ -133,6 +133,10 @@ locales = [ reference = "browser/extensions/formautofill/locales/en-US/**" l10n = "{l}browser/extensions/formautofill/**"
+[[paths]] + reference = "browser/extensions/onboarding/locales/en-US/**" + l10n = "{l}browser/extensions/onboarding/**" + [[paths]] reference = "browser/extensions/report-site-issue/locales/en-US/**" l10n = "{l}browser/extensions/report-site-issue/**" diff --git a/extensions/permissions/PermissionManager.cpp b/extensions/permissions/PermissionManager.cpp index 4b291e99ccc2..4c959490b44b 100644 --- a/extensions/permissions/PermissionManager.cpp +++ b/extensions/permissions/PermissionManager.cpp @@ -127,7 +127,11 @@ static const nsLiteralCString kPreloadPermissions[] = { // interception when a user has disabled storage for a specific site. Once // service worker interception moves to the parent process this should be // removed. See bug 1428130. - "cookie"_ns}; + "cookie"_ns, + + // Bug 28822: Make sure uitour permissions are preloaded in content + // processes. + "uitour"_ns};
// NOTE: nullptr can be passed as aType - if it is this function will return // "false" unconditionally. diff --git a/tools/lint/codespell.yml b/tools/lint/codespell.yml index fa9c940c7052..fab4ebf22899 100644 --- a/tools/lint/codespell.yml +++ b/tools/lint/codespell.yml @@ -9,6 +9,7 @@ codespell: - browser/components/touchbar/docs/ - browser/components/urlbar/docs/ - browser/extensions/formautofill/locales/en-US/ + - browser/extensions/onboarding/locales/en-US/ - browser/extensions/report-site-issue/locales/en-US/ - browser/installer/windows/docs/ - browser/locales/en-US/