new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/actions/changes.js
@@ -0,0 +1,27 @@
+/* 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 {
+ RESET_CHANGES,
+ TRACK_CHANGE,
+} = require("./index");
+
+module.exports = {
+
+ resetChanges() {
+ return {
+ type: RESET_CHANGES,
+ };
+ },
+
+ trackChange(data) {
+ return {
+ type: TRACK_CHANGE,
+ data,
+ };
+ },
+
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/actions/index.js
@@ -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/. */
+
+"use strict";
+
+const { createEnum } = require("devtools/client/shared/enum");
+
+createEnum([
+
+ // Remove all changes
+ "RESET_CHANGES",
+
+ // Track a style change
+ "TRACK_CHANGE",
+
+], module.exports);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/actions/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'changes.js',
+ 'index.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/change-manager.js
@@ -0,0 +1,72 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 {
+ trackChange,
+} = require("./actions/changes");
+
+class ChangeManager {
+ constructor(inspector) {
+ this.inspector = inspector;
+ this.pageStyle = this.inspector.pageStyle;
+ this.store = this.inspector.store;
+ }
+
+ async track(change) {
+ const defaults = { rule: null, add: null, remove: null };
+ change = { ...defaults, ...change };
+ const { rule, add, remove } = change;
+
+ if (!rule) {
+ console.warn(`Missing rule for change: ${change}`);
+ return;
+ }
+
+ // Do not track partial property additions.
+ if (add && add.property && !add.value) {
+ return;
+ }
+
+ // Do not track partial property removals.
+ // TODO: figure out whole property removal; Property removal happens in two actions.
+ if (remove && remove.property && !remove.value) {
+ return;
+ }
+
+ // Source is the stylesheet URL if defined. Otherwise, assume it's an inline <style>
+ let href = (rule.sheet && rule.sheet.href) ? rule.sheet.href : "inline stylesheet";
+ let selector = rule.selectorText;
+ const tag = this.inspector.selection.nodeFront.tagName;
+
+ // If the rule is actually and inline style, correct previous assumptions.
+ if (rule.isInlineStyle) {
+ href = "inline";
+ selector = await this.inspector.selection.nodeFront.getUniqueSelector();
+ }
+
+ const data = {
+ href,
+ selector,
+ add,
+ remove,
+ tag,
+ };
+
+ this.store.dispatch(trackChange(data));
+ }
+
+ undo() {
+
+ }
+
+ redo() {
+
+ }
+}
+
+module.exports = ChangeManager;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/changes.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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, createElement } = require("devtools/client/shared/vendor/react");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+
+const ChangesApp = createFactory(require("./components/ChangesApp"));
+
+const {
+ resetChanges,
+} = require("./actions/changes");
+
+class ChangesView {
+ constructor(inspector) {
+ this.inspector = inspector;
+ this.store = this.inspector.store;
+
+ this.update = this.update.bind(this);
+ this.destroy = this.destroy.bind(this);
+
+ this.init();
+ }
+
+ init() {
+ const changeApp = ChangesApp({});
+
+ // Expose the provider to let inspector.js use it in setupSidebar.
+ this.provider = createElement(Provider, {
+ id: "changesview",
+ key: "changesview",
+ store: this.store,
+ title: "Changes",
+ }, changeApp);
+
+ this.inspector.sidebar.on("changesview-selected", this.update);
+ // TODO: save store and restore/replay on refresh.
+ this.inspector.target.once("will-navigate", this.destroy);
+ }
+
+ /**
+ * Destruction function called when the inspector is destroyed. Removes event listeners
+ * and cleans up references.
+ */
+ destroy() {
+ this.store.dispatch(resetChanges());
+ this.inspector.sidebar.off("changesview-selected", this.update);
+ this.inspector = null;
+ this.store = null;
+ }
+ /**
+ * @return {Boolean}
+ */
+ isPanelVisible() {
+ return this.inspector &&
+ this.inspector.sidebar &&
+ this.inspector.sidebar.getCurrentTabID() === "changesview";
+ }
+
+ update() {
+
+ }
+}
+
+module.exports = ChangesView;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/components/ChangesApp.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 { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+class ChangesApp extends PureComponent {
+ static get propTypes() {
+ return {
+ // TODO define object shape in types.js
+ changes: PropTypes.object.isRequired
+ };
+ }
+
+ renderMutations(remove = {}, add = {}) {
+ const removals = Object.keys(remove).map(key => {
+ return dom.div(
+ { className: "line diff-remove"},
+ `${key}: ${remove[key]};`
+ );
+ });
+
+ const additions = Object.keys(add).map(key => {
+ return dom.div(
+ { className: "line diff-add"},
+ `${key}: ${add[key]};`
+ );
+ });
+
+ return [removals, additions];
+ }
+
+ renderSelectors(selectors = {}) {
+ return Object.keys(selectors).map(sel => {
+ return dom.details(
+ { className: "selector", open: true },
+ dom.summary({}, sel),
+ this.renderMutations(selectors[sel].remove, selectors[sel].add)
+ );
+ });
+ }
+
+ renderDiff(diff = {}) {
+ // Render groups of style sources: stylesheets, embedded styles and inline styles
+ return Object.keys(diff).map(href => {
+ return dom.details(
+ { className: "source", open: true },
+ dom.summary({}, href),
+ // Render groups of selectors
+ this.renderSelectors(diff[href])
+ );
+ });
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "theme-sidebar inspector-tabpanel",
+ id: "sidebar-panel-changes"
+ },
+ dom.div(
+ {
+ id: "diff-container"
+ },
+ this.renderDiff(this.props.changes.diff)
+ )
+ );
+ }
+}
+
+module.exports = connect(state => state)(ChangesApp);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/components/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'ChangesApp.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/moz.build
@@ -0,0 +1,16 @@
+# -*- 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/.
+
+DIRS += [
+ 'actions',
+ 'components',
+ 'reducers',
+]
+
+DevToolsModules(
+ 'change-manager.js',
+ 'changes.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/reducers/changes.js
@@ -0,0 +1,102 @@
+/* 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 {
+ RESET_CHANGES,
+ TRACK_CHANGE,
+} = require("../actions/index");
+
+const INITIAL_STATE = {
+ /**
+ * Diff of changes groupped by stylesheet href, then by selector, then into add/remove
+ * objects with CSS property names and values for corresponding changes.
+ *
+ * Structure:
+ *
+ * diff = {
+ * "href": {
+ * "selector": {
+ * add: {
+ * "property": value
+ * ... // more properties
+ * },
+ * remove: {
+ * "property": value
+ * ...
+ * }
+ * },
+ * ... // more selectors
+ * }
+ * ... // more stylesheet hrefs
+ * }
+ */
+ diff: {},
+ // Operations for undo/redo stack
+ operations: [],
+ // Current index in the undo/redo stack
+ operationIndex: 0
+};
+
+/**
+ * Mutate the given diff object with data about a new change.
+ *
+ * @param {Object} d
+ * Diff object from the store.
+ * @param {Object} c
+ * Data about the change: which property was added or removed, on which selector,
+ * in which stylesheet or whether the source is an element's inline style.
+ * @return {Object}
+ * Mutated diff object.
+ */
+function updateDiff(d = {}, c = {}) {
+ // Ensure expected diff structure exists
+ d[c.href] = d[c.href] || {};
+ d[c.href][c.selector] = d[c.href][c.selector] || {};
+ d[c.href][c.selector].add = d[c.href][c.selector].add || {};
+ d[c.href][c.selector].remove = d[c.href][c.selector].remove || {};
+
+ // If the property to remove was previously added by a change, skip removing it.
+ // Doing so makes repeated changes of the same property coalesce into a single change
+ // when shwon in the UI.
+ if (c.remove && c.remove.property && !d[c.href][c.selector].add[c.remove.property]) {
+ d[c.href][c.selector].remove[c.remove.property] = c.remove.value;
+ }
+
+ // Set or overwrite any existing property value
+ if (c.add && c.add.property) {
+ d[c.href][c.selector].add[c.add.property] = c.add.value;
+ }
+
+ d[c.href][c.selector].tag = c.tag;
+
+ return d;
+}
+
+const reducers = {
+
+ [TRACK_CHANGE](state, { data }) {
+ // Shallow clone of state.
+ const newState = { ...state };
+ // Deep clone of previous diff object.
+ const newDiff = JSON.parse(JSON.stringify(state.diff));
+ // Update the diff with information about this change
+ newState.diff = updateDiff(newDiff, data);
+ return newState;
+ },
+
+ [RESET_CHANGES](state) {
+ return { ...INITIAL_STATE };
+ },
+
+};
+
+module.exports = function(state = INITIAL_STATE, action) {
+ const reducer = reducers[action.type];
+ if (!reducer) {
+ return state;
+ }
+ return reducer(state, action);
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/reducers/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'changes.js',
+)
--- a/devtools/client/inspector/index.xhtml
+++ b/devtools/client/inspector/index.xhtml
@@ -7,16 +7,17 @@
<html xmlns="http://www.w3.org/1999/xhtml" dir="">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" href="chrome://devtools/skin/breadcrumbs.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/inspector.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/rules.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/computed.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/changes.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/fonts.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/boxmodel.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/layout.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/animation.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/Tabs.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/TabBar.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/SidebarToggle.css"/>
<link rel="stylesheet" href="resource://devtools/client/inspector/components/InspectorTabPanel.css"/>
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -11,16 +11,17 @@
const Services = require("Services");
const promise = require("promise");
const EventEmitter = require("devtools/shared/event-emitter");
const {executeSoon} = require("devtools/shared/DevToolsUtils");
const {Toolbox} = require("devtools/client/framework/toolbox");
const ReflowTracker = require("devtools/client/inspector/shared/reflow-tracker");
const Store = require("devtools/client/inspector/store");
const InspectorStyleChangeTracker = require("devtools/client/inspector/shared/style-change-tracker");
+const ChangeManager = require("devtools/client/inspector/changes/change-manager");
// Use privileged promise in panel documents to prevent having them to freeze
// during toolbox destruction. See bug 1402779.
const Promise = require("Promise");
loader.lazyRequireGetter(this, "initCssProperties", "devtools/shared/fronts/css-properties", true);
loader.lazyRequireGetter(this, "HTMLBreadcrumbs", "devtools/client/inspector/breadcrumbs", true);
loader.lazyRequireGetter(this, "ThreePaneOnboardingTooltip", "devtools/client/inspector/shared/three-pane-onboarding-tooltip");
@@ -116,16 +117,17 @@ function Inspector(toolbox) {
this.store = Store();
// Map [panel id => panel instance]
// Stores all the instances of sidebar panels like rule view, computed view, ...
this._panels = new Map();
this.reflowTracker = new ReflowTracker(this._target);
this.styleChangeTracker = new InspectorStyleChangeTracker(this);
+ this.changeManager = new ChangeManager(this);
// Store the URL of the target page prior to navigation in order to ensure
// telemetry counts in the Grid Inspector are not double counted on reload.
this.previousURL = this.target.url;
this.is3PaneModeFirstRun = Services.prefs.getBoolPref(THREE_PANE_FIRST_RUN_PREF);
this.show3PaneTooltip = Services.prefs.getBoolPref(SHOW_THREE_PANE_ONBOARDING_PREF);
@@ -976,16 +978,39 @@ Inspector.prototype = {
this.fontinspector = new FontInspector(this, this.panelWin);
}
return this.fontinspector.provider;
}
},
defaultTab == fontId);
+ // Inject a lazy loaded react tab by exposing a fake React object
+ // with a lazy defined Tab thanks to `panel` being a function
+ const changesId = "changesview";
+ this.sidebar.queueTab(
+ changesId,
+ "Changes (Prototype)",
+ {
+ props: {
+ id: changesId,
+ title: "Changes (Prototype)"
+ },
+ panel: () => {
+ if (!this.changesview) {
+ const ChangesView =
+ this.browserRequire("devtools/client/inspector/changes/changes");
+ this.changesview = new ChangesView(this, this.panelWin);
+ }
+
+ return this.changesview.provider;
+ }
+ },
+ defaultTab == changesId);
+
this.sidebar.addAllQueuedTabs();
// Persist splitter state in preferences.
this.sidebar.on("show", this.onSidebarShown);
this.sidebar.on("hide", this.onSidebarHidden);
this.sidebar.on("destroy", this.onSidebarHidden);
this.sidebar.show(defaultTab);
@@ -1414,16 +1439,20 @@ Inspector.prototype = {
panel.destroy();
}
this._panels.clear();
if (this.layoutview) {
this.layoutview.destroy();
}
+ if (this.changesview) {
+ this.changesview.destroy();
+ }
+
if (this.fontinspector) {
this.fontinspector.destroy();
}
if (this.animationinspector) {
this.animationinspector.destroy();
}
@@ -1448,16 +1477,18 @@ Inspector.prototype = {
const markupDestroyer = this._destroyMarkup();
this.teardownSplitter();
this.teardownToolbar();
this.breadcrumbs.destroy();
this.reflowTracker.destroy();
this.styleChangeTracker.destroy();
+ // TODO: Implement and call changeManager.destroy()
+ this.changeManager = null;
this._is3PaneModeChromeEnabled = null;
this._is3PaneModeEnabled = null;
this._notificationBox = null;
this._target = null;
this._toolbox = null;
this.breadcrumbs = null;
this.is3PaneModeFirstRun = null;
--- a/devtools/client/inspector/moz.build
+++ b/devtools/client/inspector/moz.build
@@ -1,16 +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/.
DIRS += [
'animation',
'animation-old',
'boxmodel',
+ 'changes',
'components',
'computed',
'extensions',
'flexbox',
'fonts',
'grids',
'layout',
'markup',
--- a/devtools/client/inspector/reducers.js
+++ b/devtools/client/inspector/reducers.js
@@ -4,15 +4,16 @@
"use strict";
// This file exposes the Redux reducers of the box model, grid and grid highlighter
// settings.
exports.animations = require("devtools/client/inspector/animation/reducers/animations");
exports.boxModel = require("devtools/client/inspector/boxmodel/reducers/box-model");
+exports.changes = require("devtools/client/inspector/changes/reducers/changes");
exports.extensionsSidebar = require("devtools/client/inspector/extensions/reducers/sidebar");
exports.flexbox = require("devtools/client/inspector/flexbox/reducers/flexbox");
exports.fontOptions = require("devtools/client/inspector/fonts/reducers/font-options");
exports.fontData = require("devtools/client/inspector/fonts/reducers/fonts");
exports.fontEditor = require("devtools/client/inspector/fonts/reducers/font-editor");
exports.grids = require("devtools/client/inspector/grids/reducers/grids");
exports.highlighterSettings = require("devtools/client/inspector/grids/reducers/highlighter-settings");
--- a/devtools/client/inspector/rules/views/rule-editor.js
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -65,27 +65,30 @@ function RuleEditor(ruleView, rule) {
this._onNewProperty = this._onNewProperty.bind(this);
this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
this._onSelectorDone = this._onSelectorDone.bind(this);
this._locationChanged = this._locationChanged.bind(this);
this.updateSourceLink = this.updateSourceLink.bind(this);
this._onToolChanged = this._onToolChanged.bind(this);
this._updateLocation = this._updateLocation.bind(this);
this._onSourceClick = this._onSourceClick.bind(this);
+ this.onTrackChange = this.onTrackChange.bind(this);
this.rule.domRule.on("location-changed", this._locationChanged);
+ this.rule.on("track-change", this.onTrackChange);
this.toolbox.on("tool-registered", this._onToolChanged);
this.toolbox.on("tool-unregistered", this._onToolChanged);
this._create();
}
RuleEditor.prototype = {
destroy: function() {
this.rule.domRule.off("location-changed");
+ this.rule.off("track-change", this.onTrackChange);
this.toolbox.off("tool-registered", this._onToolChanged);
this.toolbox.off("tool-unregistered", this._onToolChanged);
let url = null;
if (this.rule.sheet) {
url = this.rule.sheet.href || this.rule.sheet.nodeHref;
}
if (url && !this.rule.isSystem && this.rule.domRule.type !== ELEMENT_STYLE) {
@@ -96,16 +99,20 @@ RuleEditor.prototype = {
if (this._sourceMapURLService) {
this._sourceMapURLService.unsubscribe(url, sourceLine, sourceColumn,
this._updateLocation);
}
}
},
+ onTrackChange(change) {
+ this.ruleView.inspector.changeManager.track(change);
+ },
+
get sourceMapURLService() {
if (!this._sourceMapURLService) {
// sourceMapURLService is a lazy getter in the toolbox.
this._sourceMapURLService = this.toolbox.sourceMapURLService;
}
return this._sourceMapURLService;
},
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -226,16 +226,17 @@ devtools.jar:
skin/images/vview-lock@2x.png (themes/images/vview-lock@2x.png)
skin/images/vview-open-inspector.png (themes/images/vview-open-inspector.png)
skin/images/vview-open-inspector@2x.png (themes/images/vview-open-inspector@2x.png)
skin/images/sort-ascending-arrow.svg (themes/images/sort-ascending-arrow.svg)
skin/images/sort-descending-arrow.svg (themes/images/sort-descending-arrow.svg)
skin/images/cubic-bezier-swatch.png (themes/images/cubic-bezier-swatch.png)
skin/images/cubic-bezier-swatch@2x.png (themes/images/cubic-bezier-swatch@2x.png)
skin/fonts.css (themes/fonts.css)
+ skin/changes.css (themes/changes.css)
skin/computed.css (themes/computed.css)
skin/layout.css (themes/layout.css)
skin/images/arrow-e.png (themes/images/arrow-e.png)
skin/images/arrow-e@2x.png (themes/images/arrow-e@2x.png)
skin/images/search-clear-failed.svg (themes/images/search-clear-failed.svg)
skin/images/search-clear-light.svg (themes/images/search-clear-light.svg)
skin/images/search-clear-dark.svg (themes/images/search-clear-dark.svg)
skin/tooltip/arrow-horizontal-dark.png (themes/tooltip/arrow-horizontal-dark.png)
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/changes.css
@@ -0,0 +1,93 @@
+/* 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-panel-changes {
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+}
+
+#diff-container {
+}
+
+details {
+ font-family: monospace;
+ font-size: 12px;
+}
+
+summary {
+ user-select: none;
+ outline: none;
+ padding: 5px 0;
+ -moz-user-select: none;
+ cursor: pointer;
+}
+
+details.source[open] {
+ padding-bottom: 10px;
+}
+
+details.source > summary {
+ background: #f9f9fa;
+ border-top: 1px solid #dee1e4;
+ border-bottom: 1px solid #dee1e4;
+ padding-left: 5px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+details.selector > summary {
+ padding-left: 10px;
+}
+
+details.selector summary::after{
+ content: "{...}";
+ display: inline-block;
+ padding-left: 5px;
+}
+
+details.selector[open]{
+ margin-bottom: 5px;
+}
+
+details.selector[open] summary::after{
+ content: "{";
+}
+
+details.selector[open]::after{
+ content: "}";
+ display: block;
+ padding-left: 10px;
+}
+
+.line {
+ padding: 3px 5px 3px 15px;
+ position: relative;
+}
+
+.diff-add::before,
+.diff-remove::before{
+ position: absolute;
+ left: 5px;
+}
+
+.diff-add {
+ background-color: #f1feec;
+}
+
+.diff-add::before {
+ content: "+";
+ color: #54983f;
+}
+
+.diff-remove {
+ background-color: #fbf2f5;
+}
+
+.diff-remove::before{
+ content: "-";
+ color: #bf7173;
+}