diff options
Diffstat (limited to 'devtools/client/application/src/components')
44 files changed, 2499 insertions, 0 deletions
diff --git a/devtools/client/application/src/components/App.css b/devtools/client/application/src/components/App.css new file mode 100644 index 0000000000..e0bcef7d2d --- /dev/null +++ b/devtools/client/application/src/components/App.css @@ -0,0 +1,34 @@ +/* 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/. */ + +/* + * General styles + */ + +a.disabled-link, +a.disabled-link:hover, +a.disabled-link:visited { + opacity: 0.5; + cursor: not-allowed; +} + +.app { + display: grid; + min-width: calc(var(--base-unit) * 90); +} + +/* wide layout -> two columns, 1 row */ +@media(min-width: 701px) { + .app { + grid-template-columns: calc(var(--base-unit) * 50) auto; + height: 100vh; + } +} + +/* vertical layout -> 1 column, 2 rows */ +@media(max-width: 700px) { + .app { + grid-template-rows: auto 1fr; + } +} diff --git a/devtools/client/application/src/components/App.js b/devtools/client/application/src/components/App.js new file mode 100644 index 0000000000..eac53e0cf6 --- /dev/null +++ b/devtools/client/application/src/components/App.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + main, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const LocalizationProvider = createFactory(FluentReact.LocalizationProvider); + +const PageSwitcher = createFactory( + require("resource://devtools/client/application/src/components/routing/PageSwitcher.js") +); +const Sidebar = createFactory( + require("resource://devtools/client/application/src/components/routing/Sidebar.js") +); + +/** + * This is the main component for the application panel. + */ +class App extends PureComponent { + static get propTypes() { + return { + fluentBundles: PropTypes.array.isRequired, + }; + } + + render() { + const { fluentBundles } = this.props; + + return LocalizationProvider( + { bundles: fluentBundles }, + main({ className: `app` }, Sidebar({}), PageSwitcher({})) + ); + } +} + +module.exports = App; diff --git a/devtools/client/application/src/components/manifest/Manifest.js b/devtools/client/application/src/components/manifest/Manifest.js new file mode 100644 index 0000000000..f6b5f19b37 --- /dev/null +++ b/devtools/client/application/src/components/manifest/Manifest.js @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + article, + h1, + table, + tbody, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); +const { + l10n, +} = require("resource://devtools/client/application/src/modules/l10n.js"); + +const ManifestColorItem = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestColorItem.js") +); +const ManifestIconItem = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestIconItem.js") +); +const ManifestUrlItem = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestUrlItem.js") +); +const ManifestItem = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestItem.js") +); +const ManifestIssueList = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestIssueList.js") +); +const ManifestSection = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestSection.js") +); +const ManifestJsonLink = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestJsonLink.js") +); + +const { + MANIFEST_MEMBER_VALUE_TYPES, +} = require("resource://devtools/client/application/src/constants.js"); +const Types = require("resource://devtools/client/application/src/types/index.js"); + +/** + * A canonical manifest, splitted in different sections + */ +class Manifest extends PureComponent { + static get propTypes() { + return { + ...Types.manifest, // { identity, presentation, icons, validation, url } + }; + } + + renderIssueSection() { + const { validation } = this.props; + const shouldRender = validation && !!validation.length; + + return shouldRender + ? ManifestSection( + { + key: `manifest-section-0`, + title: l10n.getString("manifest-item-warnings"), + }, + ManifestIssueList({ issues: validation }) + ) + : null; + } + + renderMember({ key, value, type }, index) { + let domKey = key; + switch (type) { + case MANIFEST_MEMBER_VALUE_TYPES.COLOR: + return ManifestColorItem({ label: key, key: domKey, value }); + case MANIFEST_MEMBER_VALUE_TYPES.ICON: + // since the manifest may have keys with empty size/contentType, + // we cannot use them as unique IDs + domKey = index; + return ManifestIconItem({ label: key, key: domKey, value }); + case MANIFEST_MEMBER_VALUE_TYPES.URL: + return ManifestUrlItem({ label: key, key: domKey, value }); + case MANIFEST_MEMBER_VALUE_TYPES.STRING: + default: + return ManifestItem({ label: key, key: domKey }, value); + } + } + + renderItemSections() { + const { identity, icons, presentation } = this.props; + + const manifestMembers = [ + { localizationId: "manifest-item-identity", items: identity }, + { localizationId: "manifest-item-presentation", items: presentation }, + { localizationId: "manifest-item-icons", items: icons }, + ]; + + return manifestMembers.map(({ localizationId, items }, index) => { + return ManifestSection( + { + key: `manifest-section-${index + 1}`, + title: l10n.getString(localizationId), + }, + // NOTE: this table should probably be its own component, to keep + // the same level of abstraction as with the validation issues + // Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1577138 + table({}, tbody({}, items.map(this.renderMember))) + ); + }); + } + + render() { + const { url } = this.props; + + return article( + { className: "js-manifest" }, + Localized( + { + id: "manifest-view-header", + }, + h1({ className: "app-page__title" }) + ), + ManifestJsonLink({ url }), + this.renderIssueSection(), + this.renderItemSections() + ); + } +} + +// Exports +module.exports = Manifest; diff --git a/devtools/client/application/src/components/manifest/ManifestColorItem.css b/devtools/client/application/src/components/manifest/ManifestColorItem.css new file mode 100644 index 0000000000..92dadec271 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestColorItem.css @@ -0,0 +1,29 @@ +/* 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/. */ + +.manifest-item__color { + /* NOTE: platform converts any color format that is in the manifest to + hexadecimal, so we can uppercase */ + text-transform: uppercase; + direction: ltr; /* force LTR so the # stays at the beginning of the hex number */ + display: inline-block; +} + +.manifest-item__color::before { + display: inline-block; + content: ''; + background-color: #fff; + background-image: linear-gradient(var(--color-value), var(--color-value)), /* injected via React */ + linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc), + linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc); + background-size: 6px 6px; + background-position: 0 0, 3px 3px; + border-radius: 50%; + width: calc(var(--base-unit) * 3); + height: calc(var(--base-unit) * 3); + margin-block-start: calc(var(--base-unit) * -0.5); + margin-inline-end: var(--base-unit); + box-shadow: 0 0 0 1px var(--theme-splitter-color); + vertical-align: middle; +} diff --git a/devtools/client/application/src/components/manifest/ManifestColorItem.js b/devtools/client/application/src/components/manifest/ManifestColorItem.js new file mode 100644 index 0000000000..ac4b54e82f --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestColorItem.js @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + div, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const Types = require("resource://devtools/client/application/src/types/index.js"); +const ManifestItem = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestItem.js") +); + +/** + * This component displays a Manifest member which holds a color value + */ +class ManifestColorItem extends PureComponent { + static get propTypes() { + return { + ...Types.manifestItemColor, // { label, value } + }; + } + + renderColor() { + let { value } = this.props; + if (!value) { + return null; + } + + // Truncate colors in #rrggbbaa format to #rrggbb + if (value.length === 9 && value.toLowerCase().endsWith("ff")) { + value = value.slice(0, 7); + } + + /* div instead of span because CSS `direction` works with block elements */ + return div( + { + className: "manifest-item__color", + style: { "--color-value": value }, + }, + value + ); + } + + render() { + const { label } = this.props; + return ManifestItem({ label }, this.renderColor()); + } +} + +module.exports = ManifestColorItem; diff --git a/devtools/client/application/src/components/manifest/ManifestEmpty.js b/devtools/client/application/src/components/manifest/ManifestEmpty.js new file mode 100644 index 0000000000..3e0eb2de48 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestEmpty.js @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { openDocLink } = require("resource://devtools/client/shared/link.js"); + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + a, + article, + aside, + div, + h1, + img, + p, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const DOC_URL = + "https://developer.mozilla.org/en-US/docs/Web/Manifest" + + "?utm_source=devtools&utm_medium=sw-panel-blank"; + +/** + * This component displays help information when no manifest is found for the + * current target. + */ +class ManifestEmpty extends PureComponent { + openDocumentation() { + openDocLink(DOC_URL); + } + + render() { + return article( + { className: "app-page__icon-container js-manifest-empty" }, + aside( + {}, + Localized( + { + id: "sidebar-item-manifest", + attrs: { + alt: true, + }, + }, + img({ + className: "app-page__icon", + src: "chrome://devtools/skin/images/application-manifest.svg", + }) + ) + ), + div( + {}, + Localized( + { + id: "manifest-empty-intro2", + }, + h1({ className: "app-page__title" }) + ), + p( + {}, + Localized( + { id: "manifest-empty-intro-link" }, + a({ + onClick: () => this.openDocumentation(), + }) + ) + ), + Localized({ id: "manifest-non-existing" }, p({})) + ) + ); + } +} + +// Exports +module.exports = ManifestEmpty; diff --git a/devtools/client/application/src/components/manifest/ManifestIconItem.css b/devtools/client/application/src/components/manifest/ManifestIconItem.css new file mode 100644 index 0000000000..a2bbfd9d34 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestIconItem.css @@ -0,0 +1,7 @@ +/* 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/. */ + +.manifest-item__icon { + max-width: 100%; +} diff --git a/devtools/client/application/src/components/manifest/ManifestIconItem.js b/devtools/client/application/src/components/manifest/ManifestIconItem.js new file mode 100644 index 0000000000..a525fbcdb9 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestIconItem.js @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + br, + code, + img, + span, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); +const { + l10n, +} = require("resource://devtools/client/application/src/modules/l10n.js"); + +const Types = require("resource://devtools/client/application/src/types/index.js"); +const ManifestItem = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestItem.js") +); + +/** + * This component displays a Manifest member which holds a color value + */ +class ManifestIconItem extends PureComponent { + static get propTypes() { + return { + // { + // label: { contentType, sizes }, + // value: { src, purpose } + // } + ...Types.manifestItemIcon, + }; + } + + getLocalizedImgTitle() { + const { sizes } = this.props.label; + + return sizes && sizes.length + ? l10n.getString("manifest-icon-img-title", { sizes }) + : l10n.getString("manifest-icon-img-title-no-sizes"); + } + + renderLabel() { + const { contentType, sizes } = this.props.label; + + // sinze both `contentType` and `sizes` may be undefined, we don't need to + // render the <br> if one –or both– are not to be displayed + const shallRenderBr = contentType && sizes; + + return [ + sizes ? sizes : null, + shallRenderBr ? br({ key: "label-br" }) : null, + contentType ? contentType : null, + ]; + } + + render() { + const { src, purpose } = this.props.value; + + return ManifestItem( + { + label: this.renderLabel(), + }, + Localized( + { + id: "manifest-icon-img", + attrs: { + alt: true, + }, + }, + img({ + className: "manifest-item__icon", + src, + title: this.getLocalizedImgTitle(), + }) + ), + br({}), + Localized( + { + id: "manifest-icon-purpose", + code: code({}), + $purpose: purpose, + }, + span({}) + ) + ); + } +} + +module.exports = ManifestIconItem; diff --git a/devtools/client/application/src/components/manifest/ManifestIssue.css b/devtools/client/application/src/components/manifest/ManifestIssue.css new file mode 100644 index 0000000000..96bcdae5dd --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestIssue.css @@ -0,0 +1,17 @@ +/* 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/. */ + +.manifest-issue__icon { + -moz-context-properties: fill; + width: calc(var(--base-unit) * 3); + height: calc(var(--base-unit) * 3); +} + +.manifest-issue__icon--warning { + fill: var(--theme-icon-warning-color); +} + +.manifest-issue__icon--error { + fill: var(--theme-icon-error-color); +} diff --git a/devtools/client/application/src/components/manifest/ManifestIssue.js b/devtools/client/application/src/components/manifest/ManifestIssue.js new file mode 100644 index 0000000000..6a9680d604 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestIssue.js @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + img, + li, + span, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const { + MANIFEST_ISSUE_LEVELS, +} = require("resource://devtools/client/application/src/constants.js"); +const Types = require("resource://devtools/client/application/src/types/index.js"); + +/** + * A Manifest validation issue (warning, error) + */ +class ManifestIssue extends PureComponent { + static get propTypes() { + return { + className: PropTypes.string, + ...Types.manifestIssue, // { level, message } + }; + } + + getIconData(level) { + switch (level) { + case MANIFEST_ISSUE_LEVELS.WARNING: + return { + src: "chrome://devtools/skin/images/alert-small.svg", + localizationId: "icon-warning", + }; + case MANIFEST_ISSUE_LEVELS.ERROR: + default: + return { + src: "resource://devtools-shared-images/error-small.svg", + localizationId: "icon-error", + }; + } + } + + render() { + const { level, message, className } = this.props; + const icon = this.getIconData(level); + + return li( + { className: `js-manifest-issue ${className ? className : ""}` }, + Localized( + { id: icon.localizationId, attrs: { alt: true, title: true } }, + img({ + className: `manifest-issue__icon manifest-issue__icon--${level}`, + src: icon.src, + }) + ), + span({}, message) + ); + } +} + +// Exports +module.exports = ManifestIssue; diff --git a/devtools/client/application/src/components/manifest/ManifestIssueList.css b/devtools/client/application/src/components/manifest/ManifestIssueList.css new file mode 100644 index 0000000000..ccb3f08df5 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestIssueList.css @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.manifest-issues { + list-style-type: none; + padding-inline-start: 0; + display: grid; + grid-template-columns: auto 1fr; + grid-gap: var(--base-unit); +} + +.manifest-issues__item { + display: contents; +} diff --git a/devtools/client/application/src/components/manifest/ManifestIssueList.js b/devtools/client/application/src/components/manifest/ManifestIssueList.js new file mode 100644 index 0000000000..4b3d1f46c8 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestIssueList.js @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + ul, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + MANIFEST_ISSUE_LEVELS, +} = require("resource://devtools/client/application/src/constants.js"); +const Types = require("resource://devtools/client/application/src/types/index.js"); + +const ManifestIssue = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestIssue.js") +); + +/** + * A collection of manifest issues (errors, warnings) + */ +class ManifestIssueList extends PureComponent { + static get propTypes() { + return { + issues: Types.manifestIssueArray.isRequired, + }; + } + + // group the errors by level, and order by severity + groupIssuesByLevel() { + const { issues } = this.props; + + const errors = issues.filter(x => x.level === MANIFEST_ISSUE_LEVELS.ERROR); + const warnings = issues.filter( + x => x.level === MANIFEST_ISSUE_LEVELS.WARNING + ); + + return [errors, warnings]; + } + + render() { + const groups = this.groupIssuesByLevel().filter(list => !!list.length); + + return groups.map((list, listIndex) => { + return ul( + { + className: "manifest-issues js-manifest-issues", + key: `issuelist-${listIndex}`, + }, + list.map((issue, issueIndex) => + ManifestIssue({ + className: "manifest-issues__item", + key: `issue-${issueIndex}`, + ...issue, + }) + ) + ); + }); + } +} + +// Exports +module.exports = ManifestIssueList; diff --git a/devtools/client/application/src/components/manifest/ManifestItem.css b/devtools/client/application/src/components/manifest/ManifestItem.css new file mode 100644 index 0000000000..94da03e9b9 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestItem.css @@ -0,0 +1,28 @@ +/* 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/. */ + +.manifest-item { + vertical-align: baseline; +} + +.manifest-item__label { + box-sizing: border-box; + min-width: calc(var(--base-unit) * 32); + padding-inline-end: calc(var(--base-unit) * 4); + padding-inline-start: 0; + vertical-align: top; + color: var(--theme-text-color-alt); + font-weight: inherit; + text-align: start; +} + +.manifest-item__value { + word-break: break-all; + vertical-align: top; +} + +.manifest-item__label, +.manifest-item__value { + padding-block: calc(var(--base-unit) * 1); +} diff --git a/devtools/client/application/src/components/manifest/ManifestItem.js b/devtools/client/application/src/components/manifest/ManifestItem.js new file mode 100644 index 0000000000..96c47a1a7f --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestItem.js @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + tr, + td, + th, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +/** + * This component displays a key-value data pair from a manifest + */ +class ManifestItem extends PureComponent { + static get propTypes() { + return { + label: PropTypes.node.isRequired, + children: PropTypes.node, + }; + } + + render() { + const { children, label } = this.props; + return tr( + { + className: "manifest-item js-manifest-item", + }, + th( + { + className: "manifest-item__label js-manifest-item-label", + scope: "row", + }, + label + ), + td( + { className: "manifest-item__value js-manifest-item-content" }, + children + ) + ); + } +} + +// Exports +module.exports = ManifestItem; diff --git a/devtools/client/application/src/components/manifest/ManifestJsonLink.css b/devtools/client/application/src/components/manifest/ManifestJsonLink.css new file mode 100644 index 0000000000..52343226f9 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestJsonLink.css @@ -0,0 +1,9 @@ +/* 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/. */ + +.manifest-json-link { + /* this is so it has an implicit width and the link inside gets to truncate + with an ellipsis */ + display: grid; +} diff --git a/devtools/client/application/src/components/manifest/ManifestJsonLink.js b/devtools/client/application/src/components/manifest/ManifestJsonLink.js new file mode 100644 index 0000000000..d35924b5c3 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestJsonLink.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { openDocLink } = require("resource://devtools/client/shared/link.js"); + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); + +const { + a, + p, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +/** + * This component displays an "Open JSON" link for the Manifest + */ +class ManifestJsonLink extends PureComponent { + static get propTypes() { + return { + url: PropTypes.string.isRequired, + }; + } + + get shouldRenderLink() { + const { url } = this.props; + // Firefox blocks the loading of Data URLs with mimetypes manifest+json unless + // explicitly typed by the user in the address bar. + // So we are not showing the link in this case. + // See more details in this post: + // https://blog.mozilla.org/security/2017/11/27/blocking-top-level-navigations-data-urls-firefox-59/ + return !url.startsWith("data:"); + } + + renderLink() { + const { url } = this.props; + + return a( + { + className: "js-manifest-json-link devtools-ellipsis-text", + href: "#", + title: url, + onClick: () => openDocLink(url), + }, + url + ); + } + + render() { + return p( + { className: "manifest-json-link" }, + this.shouldRenderLink + ? this.renderLink() + : Localized({ id: "manifest-json-link-data-url" }) + ); + } +} + +module.exports = ManifestJsonLink; diff --git a/devtools/client/application/src/components/manifest/ManifestLoader.css b/devtools/client/application/src/components/manifest/ManifestLoader.css new file mode 100644 index 0000000000..4f728ff6e3 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestLoader.css @@ -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/. */ + +.manifest-loader { + font-size: var(--body-20-font-size); + font-weight: var(--body-20-font-weight); +} + +.manifest-loader__load { + /* TODO: implement a spinner when tackling the UX review bug + https://bugzilla.mozilla.org/show_bug.cgi?id=1566023 */ + text-align: center; +} diff --git a/devtools/client/application/src/components/manifest/ManifestLoader.js b/devtools/client/application/src/components/manifest/ManifestLoader.js new file mode 100644 index 0000000000..9183b50ff3 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestLoader.js @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const { + aside, + h1, + p, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const { + fetchManifest, +} = require("resource://devtools/client/application/src/actions/manifest.js"); + +class ManifestLoader extends PureComponent { + static get propTypes() { + return { + // these props get automatically injected via `connect` + dispatch: PropTypes.func.isRequired, + error: PropTypes.string, + hasFetchedManifest: PropTypes.bool.isRequired, + isLoading: PropTypes.bool.isRequired, + }; + } + + componentDidMount() { + this.loadManifestIfNeeded(); + } + + componentDidUpdate() { + this.loadManifestIfNeeded(); + } + + loadManifestIfNeeded() { + const { isLoading, hasFetchedManifest } = this.props; + const shallLoad = !isLoading && !hasFetchedManifest; + if (shallLoad) { + this.props.dispatch(fetchManifest()); + } + } + + renderResult() { + return Localized( + { id: "manifest-loaded-ok" }, + p({ className: "js-manifest-loaded-ok" }) + ); + } + + renderError() { + const { error } = this.props; + + return [ + Localized( + { + id: "manifest-loaded-error", + key: "manifest-error-label", + }, + h1({ className: "js-manifest-loaded-error app-page__title" }) + ), + p({ className: "technical-text", key: "manifest-error-message" }, error), + ]; + } + + render() { + const { error, isLoading } = this.props; + + const loadingDOM = isLoading + ? Localized( + { id: "manifest-loading" }, + p({ className: "manifest-loader__load js-manifest-loading" }) + ) + : null; + + const errorDOM = error ? this.renderError() : null; + const resultDOM = !isLoading && !error ? this.renderResult() : null; + + return aside( + { className: "manifest-loader" }, + loadingDOM, + errorDOM, + resultDOM + ); + } +} + +const mapDispatchToProps = dispatch => ({ dispatch }); +const mapStateToProps = state => ({ + error: state.manifest.errorMessage, + hasFetchedManifest: typeof state.manifest.manifest !== "undefined", + isLoading: state.manifest.isLoading, +}); + +module.exports = connect(mapStateToProps, mapDispatchToProps)(ManifestLoader); diff --git a/devtools/client/application/src/components/manifest/ManifestPage.js b/devtools/client/application/src/components/manifest/ManifestPage.js new file mode 100644 index 0000000000..60caea591e --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestPage.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + section, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const Types = require("resource://devtools/client/application/src/types/index.js"); + +const ManifestLoader = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestLoader.js") +); +const Manifest = createFactory( + require("resource://devtools/client/application/src/components/manifest/Manifest.js") +); +const ManifestEmpty = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestEmpty.js") +); + +class ManifestPage extends PureComponent { + static get propTypes() { + return { + // these props are automatically injected via connect + hasLoadingFailed: PropTypes.bool.isRequired, + isManifestLoading: PropTypes.bool.isRequired, + manifest: PropTypes.shape(Types.manifest), + }; + } + + get shouldShowLoader() { + const { isManifestLoading, hasLoadingFailed } = this.props; + const mustLoadManifest = typeof this.props.manifest === "undefined"; + return isManifestLoading || mustLoadManifest || hasLoadingFailed; + } + + renderManifest() { + const { manifest } = this.props; + return manifest ? Manifest({ ...manifest }) : ManifestEmpty({}); + } + + render() { + const { manifest } = this.props; + + return section( + { + className: `app-page js-manifest-page ${ + !manifest ? "app-page--empty" : "" + }`, + }, + this.shouldShowLoader ? ManifestLoader({}) : this.renderManifest() + ); + } +} + +function mapStateToProps(state) { + return { + hasLoadingFailed: !!state.manifest.errorMessage, + isManifestLoading: state.manifest.isLoading, + manifest: state.manifest.manifest, + }; +} + +// Exports +module.exports = connect(mapStateToProps)(ManifestPage); diff --git a/devtools/client/application/src/components/manifest/ManifestSection.css b/devtools/client/application/src/components/manifest/ManifestSection.css new file mode 100644 index 0000000000..479a6d1f79 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestSection.css @@ -0,0 +1,25 @@ +/* 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/. */ + +.manifest-section { + padding-block: calc(var(--base-unit) * 2); + width: 100%; + border-spacing: calc(var(--base-unit) * 2) 0; + font-size: var(--body-10-font-size); + font-weight: var(--body-10-font-weight); +} + +.manifest-section--empty { + padding-block-end: 0; +} + +.manifest-section:not(:last-child) { + border-bottom: 1px solid var(--separator-color); +} + +.manifest-section__title { + font-size: var(--title-10-font-size); + font-weight: var(--title-10-font-weight); + margin: 0; +} diff --git a/devtools/client/application/src/components/manifest/ManifestSection.js b/devtools/client/application/src/components/manifest/ManifestSection.js new file mode 100644 index 0000000000..4aa92c6a15 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestSection.js @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + h2, + section, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +/** + * A section of a manifest in the form of a captioned table. + */ +class ManifestSection extends PureComponent { + static get propTypes() { + return { + children: PropTypes.node, + title: PropTypes.string.isRequired, + }; + } + + render() { + const { children, title } = this.props; + const isEmpty = !children || children.length === 0; + + return section( + { + className: `manifest-section ${ + isEmpty ? "manifest-section--empty" : "" + }`, + }, + h2({ className: "manifest-section__title" }, title), + children + ); + } +} + +// Exports +module.exports = ManifestSection; diff --git a/devtools/client/application/src/components/manifest/ManifestUrlItem.css b/devtools/client/application/src/components/manifest/ManifestUrlItem.css new file mode 100644 index 0000000000..9702e7e261 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestUrlItem.css @@ -0,0 +1,8 @@ +/* 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/. */ + +.manifest-item__url { + direction: ltr; /* force LTR so the # stays at the beginning of the hex number */ + display: inline-block; +} diff --git a/devtools/client/application/src/components/manifest/ManifestUrlItem.js b/devtools/client/application/src/components/manifest/ManifestUrlItem.js new file mode 100644 index 0000000000..03705d441a --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestUrlItem.js @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + div, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const Types = require("resource://devtools/client/application/src/types/index.js"); +const ManifestItem = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestItem.js") +); + +/** + * This component displays a Manifest member which holds a URL + */ +class ManifestUrlItem extends PureComponent { + static get propTypes() { + return { + ...Types.manifestItemUrl, // { label, value } + }; + } + + render() { + const { label, value } = this.props; + return ManifestItem( + { label }, + div({ className: "manifest-item__url" }, value) + ); + } +} + +module.exports = ManifestUrlItem; diff --git a/devtools/client/application/src/components/manifest/moz.build b/devtools/client/application/src/components/manifest/moz.build new file mode 100644 index 0000000000..bb799cbfc4 --- /dev/null +++ b/devtools/client/application/src/components/manifest/moz.build @@ -0,0 +1,18 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "Manifest.js", + "ManifestColorItem.js", + "ManifestEmpty.js", + "ManifestIconItem.js", + "ManifestIssue.js", + "ManifestIssueList.js", + "ManifestItem.js", + "ManifestJsonLink.js", + "ManifestLoader.js", + "ManifestPage.js", + "ManifestSection.js", + "ManifestUrlItem.js", +) diff --git a/devtools/client/application/src/components/moz.build b/devtools/client/application/src/components/moz.build new file mode 100644 index 0000000000..361ec01204 --- /dev/null +++ b/devtools/client/application/src/components/moz.build @@ -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/. + +DIRS += [ + "routing", + "manifest", + "service-workers", + "ui", +] + +DevToolsModules( + "App.js", +) diff --git a/devtools/client/application/src/components/routing/PageSwitcher.css b/devtools/client/application/src/components/routing/PageSwitcher.css new file mode 100644 index 0000000000..e713adb1bf --- /dev/null +++ b/devtools/client/application/src/components/routing/PageSwitcher.css @@ -0,0 +1,45 @@ +/* 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/. */ + + +/* + * Page container for worker + manifest views + */ + +.app-page { + padding: calc(var(--base-unit) * 3) calc(var(--base-unit) * 8); + user-select: none; + overflow-y: auto; +} + +.app-page--empty { + display: grid; + align-items: center; + justify-content: center; + font-size: var(--body-10-font-size); + color: var(--theme-toolbar-color); +} + +.app-page__title { + font-size: var(--title-20-font-size); + font-weight: var(--title-20-font-weight); + margin: 0; +} + +.app-page__icon-container { + display: grid; + grid-template-columns: auto 1fr; + grid-column-gap: calc(var(--base-unit) * 4); +} + +.app-page__icon { + width: calc(var(--base-unit) * 10); + height: calc(var(--base-unit) * 10); + + fill: var(--dimmed-icon-color); + -moz-context-properties: fill; + + /* alignment fix for text to compensate for low baseline */ + margin-block-start: var(--base-unit); +} diff --git a/devtools/client/application/src/components/routing/PageSwitcher.js b/devtools/client/application/src/components/routing/PageSwitcher.js new file mode 100644 index 0000000000..9305744da9 --- /dev/null +++ b/devtools/client/application/src/components/routing/PageSwitcher.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const { + PAGE_TYPES, +} = require("resource://devtools/client/application/src/constants.js"); +const Types = require("resource://devtools/client/application/src/types/index.js"); + +const ManifestPage = createFactory( + require("resource://devtools/client/application/src/components/manifest/ManifestPage.js") +); +const WorkersPage = createFactory( + require("resource://devtools/client/application/src/components/service-workers/WorkersPage.js") +); + +class PageSwitcher extends PureComponent { + static get propTypes() { + return { + page: Types.page.isRequired, + }; + } + + render() { + let component = null; + + switch (this.props.page) { + case PAGE_TYPES.MANIFEST: + component = ManifestPage({}); + break; + case PAGE_TYPES.SERVICE_WORKERS: + component = WorkersPage({}); + break; + default: + console.error("Unknown path. Can not direct to a page."); + return null; + } + + return component; + } +} + +function mapStateToProps(state) { + return { + page: state.ui.selectedPage, + }; +} + +module.exports = connect(mapStateToProps)(PageSwitcher); diff --git a/devtools/client/application/src/components/routing/Sidebar.css b/devtools/client/application/src/components/routing/Sidebar.css new file mode 100644 index 0000000000..872f5cca86 --- /dev/null +++ b/devtools/client/application/src/components/routing/Sidebar.css @@ -0,0 +1,33 @@ +/* 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/. */ + + +/* + * Sidebar list container + */ +.sidebar { + background-color: var(--bg-color); +} + +/* vertical layout -> the sidebar is the first row */ +@media(max-width: 700px) { + .sidebar { + border-block-end: 1px solid var(--separator-color); + } +} + +/* wide layout -> the sidebar occupies a whole column on the side */ +@media(min-width: 701px) { + .sidebar { + min-height: 100vh; + border-inline-end: 1px solid var(--separator-color); + } +} + +.sidebar__list { + list-style: none; + padding: 0; + font-size: var(--body-10-font-size); + font-weight: var(--body-10-font-weight); +} diff --git a/devtools/client/application/src/components/routing/Sidebar.js b/devtools/client/application/src/components/routing/Sidebar.js new file mode 100644 index 0000000000..99d260571e --- /dev/null +++ b/devtools/client/application/src/components/routing/Sidebar.js @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + aside, + ul, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const SidebarItem = createFactory( + require("resource://devtools/client/application/src/components/routing/SidebarItem.js") +); + +const Types = require("resource://devtools/client/application/src/types/index.js"); +const { + PAGE_TYPES, +} = require("resource://devtools/client/application/src/constants.js"); + +class Sidebar extends PureComponent { + static get propTypes() { + return { + // this prop is automatically injected via connect + selectedPage: Types.page.isRequired, + }; + } + + render() { + const navItems = [PAGE_TYPES.SERVICE_WORKERS, PAGE_TYPES.MANIFEST]; + + const isSelected = page => { + return page === this.props.selectedPage; + }; + + return aside( + { + className: "sidebar js-sidebar", + }, + ul( + { + className: "sidebar__list", + }, + navItems.map(page => { + return SidebarItem({ + page, + key: `sidebar-item-${page}`, + isSelected: isSelected(page), + }); + }) + ) + ); + } +} + +function mapStateToProps(state) { + return { + selectedPage: state.ui.selectedPage, + }; +} + +module.exports = connect(mapStateToProps)(Sidebar); diff --git a/devtools/client/application/src/components/routing/SidebarItem.css b/devtools/client/application/src/components/routing/SidebarItem.css new file mode 100644 index 0000000000..f1852748ab --- /dev/null +++ b/devtools/client/application/src/components/routing/SidebarItem.css @@ -0,0 +1,33 @@ +/* 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/. */ + + +/* + * Sidebar list items + */ + +.sidebar-item { + display: grid; + grid-template-columns: auto 1fr; + grid-gap: var(--base-unit); + padding: calc(var(--base-unit)) calc(var(--base-unit) * 6); + user-select: none; + cursor: pointer; +} + +.sidebar-item--selected { + background-color: var(--theme-selection-background); + color: var(--theme-selection-color); +} + +.sidebar-item:not(.sidebar-item--selected):hover { + background-color: var(--highlight-color); +} + +.sidebar-item__icon { + height: calc(var(--base-unit) * 4); + width: calc(var(--base-unit) * 4); + -moz-context-properties: fill; + fill: currentColor; +} diff --git a/devtools/client/application/src/components/routing/SidebarItem.js b/devtools/client/application/src/components/routing/SidebarItem.js new file mode 100644 index 0000000000..5683856aaf --- /dev/null +++ b/devtools/client/application/src/components/routing/SidebarItem.js @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + img, + li, + span, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const Actions = require("resource://devtools/client/application/src/actions/index.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const { + PAGE_TYPES, +} = require("resource://devtools/client/application/src/constants.js"); +const Types = require("resource://devtools/client/application/src/types/index.js"); + +const ICONS = { + [PAGE_TYPES.MANIFEST]: + "chrome://devtools/skin/images/application-manifest.svg", + [PAGE_TYPES.SERVICE_WORKERS]: + "chrome://devtools/skin/images/debugging-workers.svg", +}; + +const LOCALIZATION_IDS = { + [PAGE_TYPES.MANIFEST]: "sidebar-item-manifest", + [PAGE_TYPES.SERVICE_WORKERS]: "sidebar-item-service-workers", +}; + +class SidebarItem extends PureComponent { + static get propTypes() { + return { + page: Types.page.isRequired, + isSelected: PropTypes.bool.isRequired, + // this prop is automatically injected via connect + dispatch: PropTypes.func.isRequired, + }; + } + + render() { + const { isSelected, page } = this.props; + + return li( + { + className: `sidebar-item js-sidebar-${page} ${ + isSelected ? "sidebar-item--selected" : "" + }`, + onClick: () => { + const { dispatch } = this.props; + dispatch(Actions.updateSelectedPage(page)); + }, + role: "link", + }, + Localized( + { + id: LOCALIZATION_IDS[page], + attrs: { + alt: true, + title: true, + }, + }, + img({ + src: ICONS[page], + className: "sidebar-item__icon", + }) + ), + Localized( + { + id: LOCALIZATION_IDS[page], + attrs: { + title: true, + }, + }, + span({ className: "devtools-ellipsis-text" }) + ) + ); + } +} + +const mapDispatchToProps = dispatch => ({ dispatch }); +module.exports = connect(mapDispatchToProps)(SidebarItem); diff --git a/devtools/client/application/src/components/routing/moz.build b/devtools/client/application/src/components/routing/moz.build new file mode 100644 index 0000000000..7e22985614 --- /dev/null +++ b/devtools/client/application/src/components/routing/moz.build @@ -0,0 +1,5 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules("PageSwitcher.js", "Sidebar.js", "SidebarItem.js") diff --git a/devtools/client/application/src/components/service-workers/Registration.css b/devtools/client/application/src/components/service-workers/Registration.css new file mode 100644 index 0000000000..84b6de58e1 --- /dev/null +++ b/devtools/client/application/src/components/service-workers/Registration.css @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * The current layout of a registration is + * + * +------+----------------------+----------------+ + * | Header - scope + timestamp | Unregister_btn | + * +------+----------------------+----------------| + * | worker 1 | + | worker 2 | + | ... | + +----------------------------------------------+ + | Unregister btn | + +----------------------------------------------+ + */ + +.registration { + line-height: 1.5; + font-size: var(--body-10-font-size); + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: minmax(calc(var(--base-unit) * 6), auto) 1fr auto; + grid-column-gap: calc(4 * var(--base-unit)); + grid-row-gap: calc(2 * var(--base-unit)); + grid-template-areas: "header header-controls" + "workers workers" + "footer-controls footer-controls"; +} + +/* vertical layout */ +@media(max-width: 700px) { + .registration__controls { + grid-area: footer-controls; + justify-self: end; + } +} + +/* wide layout */ +@media(min-width: 701px) { + .registration__controls { + grid-area: header-controls; + } +} + +.registration__header { + grid-area: header; +} + +.registration__scope { + font-size: var(--title-10-font-size); + font-weight: var(--title-10-font-weight); + user-select: text; + margin: 0; + + grid-area: scope; +} + +.registration__updated-time { + color: var(--theme-text-color-alt); + grid-area: timestamp; +} + +.registration__workers { + grid-area: workers; + list-style-type: none; + padding: 0; +} + +.registration__workers-item:not(:first-child) { + margin-block-start: calc(var(--base-unit) * 2); +} diff --git a/devtools/client/application/src/components/service-workers/Registration.js b/devtools/client/application/src/components/service-workers/Registration.js new file mode 100644 index 0000000000..97569f57e2 --- /dev/null +++ b/devtools/client/application/src/components/service-workers/Registration.js @@ -0,0 +1,153 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); + +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const { + article, + aside, + h2, + header, + li, + p, + time, + ul, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + getUnicodeUrl, +} = require("resource://devtools/client/shared/unicode-url.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const Types = require("resource://devtools/client/application/src/types/index.js"); + +const { + unregisterWorker, +} = require("resource://devtools/client/application/src/actions/workers.js"); + +const UIButton = createFactory( + require("resource://devtools/client/application/src/components/ui/UIButton.js") +); + +const Worker = createFactory( + require("resource://devtools/client/application/src/components/service-workers/Worker.js") +); + +/** + * This component is dedicated to display a service worker registration, along + * the list of attached workers to it. + * It displays information about the registration as well as an Unregister + * button. + */ +class Registration extends PureComponent { + static get propTypes() { + return { + className: PropTypes.string, + isDebugEnabled: PropTypes.bool.isRequired, + registration: PropTypes.shape(Types.registration).isRequired, + // this prop get automatically injected via `connect` + dispatch: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.unregister = this.unregister.bind(this); + } + + unregister() { + this.props.dispatch(unregisterWorker(this.props.registration)); + } + + isActive() { + const { workers } = this.props.registration; + return workers.some( + x => x.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED + ); + } + + formatScope(scope) { + const [, remainder] = getUnicodeUrl(scope).split("://"); + // remove the last slash from the url, if present + // or return the full scope if there's no remainder + return remainder ? remainder.replace(/\/$/, "") : scope; + } + + render() { + const { registration, isDebugEnabled, className } = this.props; + + const unregisterButton = this.isActive() + ? Localized( + { id: "serviceworker-worker-unregister" }, + UIButton({ + onClick: this.unregister, + className: "js-unregister-button", + }) + ) + : null; + + const lastUpdated = registration.lastUpdateTime + ? Localized( + { + id: "serviceworker-worker-updated", + // XXX: $date should normally be a Date object, but we pass the timestamp as a + // workaround. See Bug 1465718. registration.lastUpdateTime is in microseconds, + // convert to a valid timestamp in milliseconds by dividing by 1000. + $date: registration.lastUpdateTime / 1000, + time: time({ className: "js-sw-updated" }), + }, + p({ className: "registration__updated-time" }) + ) + : null; + + const scope = h2( + { + title: registration.scope, + className: "registration__scope js-sw-scope devtools-ellipsis-text", + }, + this.formatScope(registration.scope) + ); + + return li( + { className: className ? className : "" }, + article( + { className: "registration js-sw-container" }, + header({ className: "registration__header" }, scope, lastUpdated), + aside({ className: "registration__controls" }, unregisterButton), + // render list of workers + ul( + { className: "registration__workers" }, + registration.workers.map(worker => { + return li( + { + key: worker.id, + className: "registration__workers-item", + }, + Worker({ + worker, + isDebugEnabled, + }) + ); + }) + ) + ) + ); + } +} + +const mapDispatchToProps = dispatch => ({ dispatch }); +module.exports = connect(mapDispatchToProps)(Registration); diff --git a/devtools/client/application/src/components/service-workers/RegistrationList.css b/devtools/client/application/src/components/service-workers/RegistrationList.css new file mode 100644 index 0000000000..021ed7e351 --- /dev/null +++ b/devtools/client/application/src/components/service-workers/RegistrationList.css @@ -0,0 +1,54 @@ +/* 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/. */ + +.aboutdebugging-plug { + padding-block: calc(var(--base-unit) * 3); + border-block-start: 1px solid var(--separator-color); + + /* display flex to handle showing the icon with ::before */ + display: flex; + flex-direction: row; + column-gap: calc(var(--base-unit) * 2); + align-items: baseline; + font-size: var(--body-10-font-size); + font-weight: var(--body-10-font-weight); +} + +.aboutdebugging-plug::before { + flex: 0 0 auto; + width: calc(var(--base-unit) * 4); + height: calc(var(--base-unit) * 4); + content: ""; + -moz-context-properties: fill; + fill: currentColor; + background-image: url(chrome://global/skin/icons/developer.svg); + /* the icon size is taller than the line-height of the text. Since the + text can occupy multiple lines, and we want to keep the icon aligned + with respect to the first line, instead of align-items: center in + .aboutdebugging-plug, we use baseline, and fine tune the position here. */ + position: relative; + top: 3px; +} + +.registrations-container { + flex-grow: 1; +} + +.registrations-container__list { + padding-inline-start: 0; +} + +.registrations-container__item { + list-style-type: none; + margin: 0; + padding: calc(var(--base-unit) * 5) 0; +} + +.registrations-container__item:first-child { + padding-top: 0; +} + +.registrations-container__item:not(:last-child) { + border-bottom: 1px solid var(--separator-color); +} diff --git a/devtools/client/application/src/components/service-workers/RegistrationList.js b/devtools/client/application/src/components/service-workers/RegistrationList.js new file mode 100644 index 0000000000..ed89c9cff3 --- /dev/null +++ b/devtools/client/application/src/components/service-workers/RegistrationList.js @@ -0,0 +1,92 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + openTrustedLink, +} = require("resource://devtools/client/shared/link.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + a, + article, + footer, + h1, + p, + ul, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const Types = require("resource://devtools/client/application/src/types/index.js"); +const Registration = createFactory( + require("resource://devtools/client/application/src/components/service-workers/Registration.js") +); + +/** + * This component handles the list of service workers displayed in the application panel + * and also displays a suggestion to use about debugging for debugging other service + * workers. + */ +class RegistrationList extends PureComponent { + static get propTypes() { + return { + canDebugWorkers: PropTypes.bool.isRequired, + registrations: Types.registrationArray.isRequired, + }; + } + + render() { + const { canDebugWorkers, registrations } = this.props; + + return [ + article( + { + className: "registrations-container", + key: "registrations-container", + }, + Localized( + { id: "serviceworker-list-header" }, + h1({ + className: "app-page__title", + }) + ), + ul( + { className: "registrations-container__list" }, + registrations.map(registration => + Registration({ + key: registration.id, + isDebugEnabled: canDebugWorkers, + registration, + className: "registrations-container__item", + }) + ) + ) + ), + + footer( + { className: "aboutdebugging-plug" }, + Localized( + { + id: "serviceworker-list-aboutdebugging", + key: "serviceworkerlist-footer", + a: a({ + className: "aboutdebugging-plug__link", + onClick: () => openTrustedLink("about:debugging#workers"), + }), + }, + p({}) + ) + ), + ]; + } +} + +// Exports +module.exports = RegistrationList; diff --git a/devtools/client/application/src/components/service-workers/RegistrationListEmpty.js b/devtools/client/application/src/components/service-workers/RegistrationListEmpty.js new file mode 100644 index 0000000000..2454f121e8 --- /dev/null +++ b/devtools/client/application/src/components/service-workers/RegistrationListEmpty.js @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + openDocLink, + openTrustedLink, +} = require("resource://devtools/client/shared/link.js"); +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + a, + article, + aside, + div, + h1, + img, + p, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const { + services, +} = require("resource://devtools/client/application/src/modules/application-services.js"); + +const DOC_URL = + "https://developer.mozilla.org/docs/Web/API/Service_Worker_API/Using_Service_Workers" + + "?utm_source=devtools&utm_medium=sw-panel-blank"; + +/** + * This component displays help information when no service workers are found for the + * current target. + */ +class RegistrationListEmpty extends PureComponent { + switchToConsole() { + services.selectTool("webconsole"); + } + + switchToDebugger() { + services.selectTool("jsdebugger"); + } + + openAboutDebugging() { + openTrustedLink("about:debugging#workers"); + } + + openDocumentation() { + openDocLink(DOC_URL); + } + + render() { + return article( + { className: "app-page__icon-container js-registration-list-empty" }, + aside( + {}, + Localized( + { + id: "sidebar-item-service-workers", + attrs: { + alt: true, + }, + }, + img({ + className: "app-page__icon", + src: "chrome://devtools/skin/images/debugging-workers.svg", + }) + ) + ), + div( + {}, + Localized( + { + id: "serviceworker-empty-intro2", + }, + h1({ className: "app-page__title" }) + ), + Localized( + { + id: "serviceworker-empty-suggestions2", + a: a({ + onClick: () => this.switchToConsole(), + }), + // NOTE: for <Localized> to parse the markup in the string, the + // markup needs to be actual HTML elements + span: a({ + onClick: () => this.switchToDebugger(), + }), + }, + p({}) + ), + p( + {}, + Localized( + { id: "serviceworker-empty-intro-link" }, + a({ + onClick: () => this.openDocumentation(), + }) + ) + ), + p( + {}, + Localized( + { id: "serviceworker-empty-suggestions-aboutdebugging2" }, + a({ + className: "js-trusted-link", + onClick: () => this.openAboutDebugging(), + }) + ) + ) + ) + ); + } +} + +// Exports +module.exports = RegistrationListEmpty; diff --git a/devtools/client/application/src/components/service-workers/Worker.css b/devtools/client/application/src/components/service-workers/Worker.css new file mode 100644 index 0000000000..e44b49ef6b --- /dev/null +++ b/devtools/client/application/src/components/service-workers/Worker.css @@ -0,0 +1,75 @@ +/* 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/. */ + + /* + * The current layout of a service worker item is + * + * +------------+------------------------------+ + * | Worker | script_name | + * | Icon |------------------------------| + * | | status start_button | + * +------------+------------------------------+ + */ + +.worker { + display: grid; + grid-template-columns: auto 1fr; + grid-template-areas: "icon source" + "icon misc"; + column-gap: calc(var(--base-unit) * 2); + row-gap: var(--base-unit); + + line-height: calc(var(--base-unit) * 4); + font-size: var(--body-10-font-size); +} + +.worker__icon { + grid-area: icon; +} + +.worker__icon-image { + width: calc(var(--base-unit) * 4); + height: calc(var(--base-unit) * 4); +} + +.worker__source { + grid-area: source; + user-select: text; +} + +.worker__misc { + grid-area: misc; +} + +.worker__status { + text-transform: capitalize; + --status-bg-color: transparent; + --status-border-color: transparent; +} + +.worker__status::before { + content: ""; + margin-inline-end: var(--base-unit); + width: calc(var(--base-unit) * 2); + height: calc(var(--base-unit) * 2); + display: inline-block; + background: var(--status-bg-color); + border: 1px solid var(--status-border-color); + border-radius: 100%; +} + +.worker__status--active { + --status-bg-color: var(--green-60); + --status-border-color: var(--green-60); +} + +.worker__status--waiting { + --status-bg-color: var(--theme-text-color-alt); + --status-border-color: var(--theme-text-color-alt); +} + +.worker__status--installing, .worker__status--default { + --status-bg-color: transparent; + --status-border-color: var(--theme-text-color-alt); +} diff --git a/devtools/client/application/src/components/service-workers/Worker.js b/devtools/client/application/src/components/service-workers/Worker.js new file mode 100644 index 0000000000..bc95e084a9 --- /dev/null +++ b/devtools/client/application/src/components/service-workers/Worker.js @@ -0,0 +1,225 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const { + a, + img, + p, + section, + span, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + getUnicodeUrlPath, +} = require("resource://devtools/client/shared/unicode-url.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); +const { + l10n, +} = require("resource://devtools/client/application/src/modules/l10n.js"); + +const { + services, +} = require("resource://devtools/client/application/src/modules/application-services.js"); +const Types = require("resource://devtools/client/application/src/types/index.js"); + +const { + startWorker, +} = require("resource://devtools/client/application/src/actions/workers.js"); + +const UIButton = createFactory( + require("resource://devtools/client/application/src/components/ui/UIButton.js") +); + +/** + * This component is dedicated to display a worker, more accurately a service worker, in + * the list of workers displayed in the application panel. It displays information about + * the worker as well as action links and buttons to interact with the worker (e.g. debug, + * unregister, update etc...). + */ +class Worker extends PureComponent { + static get propTypes() { + return { + isDebugEnabled: PropTypes.bool.isRequired, + worker: PropTypes.shape(Types.worker).isRequired, + // this prop get automatically injected via `connect` + dispatch: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.debug = this.debug.bind(this); + this.viewSource = this.viewSource.bind(this); + this.start = this.start.bind(this); + } + + debug() { + if (!this.isRunning()) { + console.log("Service workers cannot be debugged if they are not running"); + return; + } + + services.openWorkerInDebugger(this.props.worker.workerDescriptorFront); + } + + viewSource() { + if (!this.isRunning()) { + console.log( + "Service workers cannot be inspected if they are not running" + ); + return; + } + + services.viewWorkerSource(this.props.worker.workerDescriptorFront); + } + + start() { + if (!this.isActive() || this.isRunning()) { + console.log("Running or inactive service workers cannot be started"); + return; + } + + this.props.dispatch(startWorker(this.props.worker)); + } + + isRunning() { + // We know the worker is running if it has a worker actor. + return !!this.props.worker.workerDescriptorFront; + } + + isActive() { + return this.props.worker.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED; + } + + getLocalizedStatus() { + if (this.isActive() && this.isRunning()) { + return l10n.getString("serviceworker-worker-status-running"); + } else if (this.isActive()) { + return l10n.getString("serviceworker-worker-status-stopped"); + } + // NOTE: this is already localized by the service worker front + // (strings are in debugger.properties) + return this.props.worker.stateText; + } + + getClassNameForStatus(baseClass) { + const { state } = this.props.worker; + + switch (state) { + case Ci.nsIServiceWorkerInfo.STATE_PARSED: + case Ci.nsIServiceWorkerInfo.STATE_INSTALLING: + return "worker__status--installing"; + case Ci.nsIServiceWorkerInfo.STATE_INSTALLED: + case Ci.nsIServiceWorkerInfo.STATE_ACTIVATING: + return "worker__status--waiting"; + case Ci.nsIServiceWorkerInfo.STATE_ACTIVATED: + return "worker__status--active"; + } + + return "worker__status--default"; + } + + formatSource(source) { + const parts = source.split("/"); + return getUnicodeUrlPath(parts[parts.length - 1]); + } + + renderInspectLink(url) { + // avoid rendering the inspect link if sw is not running + const isDisabled = !this.isRunning(); + // view source instead of debugging when debugging sw is not available + const callbackFn = this.props.isDebugEnabled ? this.debug : this.viewSource; + + const sourceUrl = span( + { className: "js-source-url" }, + this.formatSource(url) + ); + + return isDisabled + ? sourceUrl + : a( + { + onClick: callbackFn, + title: url, + href: "#", + className: "js-inspect-link", + }, + sourceUrl, + "\u00A0", // + Localized( + { + id: "serviceworker-worker-inspect-icon", + attrs: { + alt: true, + }, + }, + img({ + src: "chrome://devtools/skin/images/application-debug.svg", + }) + ) + ); + } + + renderStartButton() { + // avoid rendering the button at all for workers that are either running, + // or in a state that prevents them from starting (like waiting) + if (this.isRunning() || !this.isActive()) { + return null; + } + + return Localized( + { id: "serviceworker-worker-start3" }, + UIButton({ + onClick: this.start, + className: `js-start-button`, + size: "micro", + }) + ); + } + + render() { + const { worker } = this.props; + const statusText = this.getLocalizedStatus(); + const statusClassName = this.getClassNameForStatus(); + + return section( + { className: "worker js-sw-worker" }, + p( + { className: "worker__icon" }, + img({ + className: "worker__icon-image", + src: "chrome://devtools/skin/images/debugging-workers.svg", + }) + ), + p({ className: "worker__source" }, this.renderInspectLink(worker.url)), + p( + { className: "worker__misc" }, + span( + { className: `js-worker-status worker__status ${statusClassName}` }, + statusText + ), + " ", + this.renderStartButton() + ) + ); + } +} + +const mapDispatchToProps = dispatch => ({ dispatch }); +module.exports = connect(mapDispatchToProps)(Worker); diff --git a/devtools/client/application/src/components/service-workers/WorkersPage.js b/devtools/client/application/src/components/service-workers/WorkersPage.js new file mode 100644 index 0000000000..a44dd292f8 --- /dev/null +++ b/devtools/client/application/src/components/service-workers/WorkersPage.js @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + section, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const Types = require("resource://devtools/client/application/src/types/index.js"); +const RegistrationList = createFactory( + require("resource://devtools/client/application/src/components/service-workers/RegistrationList.js") +); +const RegistrationListEmpty = createFactory( + require("resource://devtools/client/application/src/components/service-workers/RegistrationListEmpty.js") +); + +class WorkersPage extends PureComponent { + static get propTypes() { + return { + // mapped from state + canDebugWorkers: PropTypes.bool.isRequired, + domain: PropTypes.string.isRequired, + registrations: Types.registrationArray.isRequired, + }; + } + + render() { + const { canDebugWorkers, domain, registrations } = this.props; + + // Filter out workers from other domains + const domainWorkers = registrations.filter( + x => !!x.workers.length && new URL(x.workers[0].url).hostname === domain + ); + const isListEmpty = domainWorkers.length === 0; + + return section( + { + className: `app-page js-service-workers-page ${ + isListEmpty ? "app-page--empty" : "" + }`, + }, + isListEmpty + ? RegistrationListEmpty({}) + : RegistrationList({ + canDebugWorkers, + registrations: domainWorkers, + }) + ); + } +} + +function mapStateToProps(state) { + return { + canDebugWorkers: state.workers.canDebugWorkers, + domain: state.page.domain, + registrations: state.workers.list, + }; +} + +// Exports +module.exports = connect(mapStateToProps)(WorkersPage); diff --git a/devtools/client/application/src/components/service-workers/moz.build b/devtools/client/application/src/components/service-workers/moz.build new file mode 100644 index 0000000000..f9704b9df8 --- /dev/null +++ b/devtools/client/application/src/components/service-workers/moz.build @@ -0,0 +1,11 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "Registration.js", + "RegistrationList.js", + "RegistrationListEmpty.js", + "Worker.js", + "WorkersPage.js", +) diff --git a/devtools/client/application/src/components/ui/UIButton.css b/devtools/client/application/src/components/ui/UIButton.css new file mode 100644 index 0000000000..2d614e09b0 --- /dev/null +++ b/devtools/client/application/src/components/ui/UIButton.css @@ -0,0 +1,75 @@ +/* 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/. */ + +/* these styles com from Photon. Keep in mind that the "default" style is not used + in panels, and we should use the "micro" instead for default, stand-alone buttons. */ + +:root.theme-light { + --button-text-color: var(--grey-90); + --button-text-hover-color: var(--grey-90); + --button-text-pressed-color: var(--grey-90); + --button-background-color: var(--grey-90-a10); + --button-background-hover-color: var(--grey-90-a20); + --button-background-pressed-color: var(--grey-90-a30); +} + +:root.theme-dark { + --button-text-color: var(--grey-40); + --button-text-hover-color: var(--grey-30); + --button-text-pressed-color: var(--grey-30); + --button-background-color: var(--grey-10-a20); + --button-background-hover-color: var(--grey-10-a25); + --button-background-pressed-color: var(--grey-10-a30); +} + +.ui-button { + appearance: none; + transition: background-color 0.05s ease-in-out; + + margin: 0; + height: calc(var(--base-unit) * 6); + padding-inline-start: calc(2 * var(--base-unit)); + padding-inline-end: calc(2 * var(--base-unit)); + border: none; + border-radius: calc(var(--base-unit) / 2); + + color: var(--button-text-color); + background: var(--button-background-color); + font-size: var(--caption-10-font-size); +} + +.ui-button:-moz-focusring { + outline: none; +} +.ui-button::-moz-focus-inner { + border: 0; + padding: 0; +} + +.ui-button:enabled:hover { + background: var(--button-background-hover-color); + color: var(--button-text-hover-color); +} + +.ui-button:enabled:active { + background: var(--button-background-pressed-color); + color: var(--button-text-pressed-color); +} + +.ui-button:focus { + box-shadow: 0 0 0 1px var(--blue-50) inset, + 0 0 0 1px var(--blue-50), + 0 0 0 4px var(--blue-50-a30); +} + +.ui-button:disabled { + opacity: 0.4; +} + +/* Note: this "micro" variant here is not the same as the "micro" variant + in Photon docs (since we are using that one for our default size) */ +.ui-button--micro { + height: auto; + padding: calc(var(--base-unit) * 0.5) var(--base-unit); +} diff --git a/devtools/client/application/src/components/ui/UIButton.js b/devtools/client/application/src/components/ui/UIButton.js new file mode 100644 index 0000000000..bc3f297d85 --- /dev/null +++ b/devtools/client/application/src/components/ui/UIButton.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + button, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +class UIButton extends PureComponent { + static get propTypes() { + return { + children: PropTypes.node, + className: PropTypes.string, + disabled: PropTypes.bool, + onClick: PropTypes.func, + size: PropTypes.oneOf(["micro"]), + }; + } + + render() { + const { className, disabled, onClick, size } = this.props; + const sizeClass = size ? `ui-button--${size}` : ""; + + return button( + { + className: `ui-button ${className || ""} ${sizeClass}`, + onClick, + disabled, + }, + this.props.children + ); + } +} + +module.exports = UIButton; diff --git a/devtools/client/application/src/components/ui/moz.build b/devtools/client/application/src/components/ui/moz.build new file mode 100644 index 0000000000..f62f66d310 --- /dev/null +++ b/devtools/client/application/src/components/ui/moz.build @@ -0,0 +1,7 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "UIButton.js", +) |