diff --git a/.eslintrc.js b/.eslintrc.js index 9f0366e628..5df8184c87 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,6 +35,7 @@ module.exports = { files: [ '.ember-cli.js', '.eslintrc.js', + '.prettierrc.js', '.stylelintrc.js', '.template-lintrc.js', 'ember-cli-build.js', diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000000..e1eb1e906c --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + singleQuote: true, + overrides: [ + { + files: '**/*.hbs', + options: { + parser: 'glimmer', + singleQuote: false + } + } + ] +}; diff --git a/app/controllers/component-tree.js b/app/controllers/component-tree.js index c02a862148..d550f33cea 100644 --- a/app/controllers/component-tree.js +++ b/app/controllers/component-tree.js @@ -218,6 +218,14 @@ class RenderItem { return this.renderNode.name; } + get args() { + return this.renderNode.args; + } + + get isCurlyInvocation() { + return this.renderNode.args && this.renderNode.args.positional; + } + get hasInstance() { let { instance } = this.renderNode; return typeof instance === 'object' && instance !== null; diff --git a/app/helpers/component-argument.js b/app/helpers/component-argument.js new file mode 100644 index 0000000000..883da07f70 --- /dev/null +++ b/app/helpers/component-argument.js @@ -0,0 +1,22 @@ +import { helper } from '@ember/component/helper'; +import truncate from 'ember-inspector/utils/truncate'; + +/** + * Determine the type of the component argument for display + * + * @method componentArgumentDisplay + * @param {*} argument + * @return {*} The argument with the correct type for display + */ +export function componentArgumentDisplay([argument]) { + if (typeof argument === 'string') { + // Escape any interior quotes – we will add the surrounding quotes in the template + return truncate(argument.replace(/"/g, '\\"')); + } else if (typeof argument === 'object' && argument !== null) { + return '...'; + } + + return String(argument); +} + +export default helper(componentArgumentDisplay); diff --git a/app/helpers/is-string.js b/app/helpers/is-string.js new file mode 100644 index 0000000000..865e5732ef --- /dev/null +++ b/app/helpers/is-string.js @@ -0,0 +1,7 @@ +import { helper } from '@ember/component/helper'; + +export function isString([str]) { + return typeof str === 'string'; +} + +export default helper(isString); diff --git a/app/styles/colors.scss b/app/styles/colors.scss index 6e10be981d..8ddb830af1 100644 --- a/app/styles/colors.scss +++ b/app/styles/colors.scss @@ -32,8 +32,10 @@ --transparent: transparent; --focus: #3F81EE; --focus-text: var(--white); - --component-text: #7c027c; + --component-name: #1171E8; --component-brackets-selected: rgba(255, 255, 255, 0.4); + --component-attr: #DB00A9; + --component-highlighted-bg: #F2F2F3; --pill-bg: rgba(0, 0, 0, 0.1); --pill-bg-active: rgba(255, 255, 255, 0.3); --warning-text: #735c0f; @@ -74,8 +76,10 @@ --transparent: transparent; --focus: #2270EC; --focus-text: var(--white); - --component-text: #FF8BC9; + --component-name: #77BEFF; --component-brackets-selected: rgba(255, 255, 255, 0.4); + --component-attr: #FE7AE9; + --component-highlighted-bg: #333333; --pill-bg: rgba(255, 255, 255, 0.2); --pill-bg-active: rgba(255, 255, 255, 0.3); --warning-text: #8ca3f0; diff --git a/app/styles/component_tree.scss b/app/styles/component_tree.scss index 786913a9da..9d4fc652db 100644 --- a/app/styles/component_tree.scss +++ b/app/styles/component_tree.scss @@ -1,63 +1,139 @@ .component-tree-item { - align-items: center; - border-radius: 4px; color: var(--base12); - display: flex; font-size: 12px; - margin: 0 3px; min-height: 22px; - position: relative; -} -.component-tree-item__expand { - align-self: stretch; - cursor: pointer; - padding-left: 3px; - padding-right: 3px; -} + &__actions { + min-height: 22px; + opacity: 0; + right: var(--unit1); + + /** + * __actions is position:sticky so it is always visible and on top. + * Due to margins and padding on various elements, it was + * difficult to keep the background of __actions on top + * of .tree-item text and align with the .tree-item background. + * This pseudo element is 100% of the width of __actions plus + * a little extra to guarantee alignment. + */ + &:after { + border-bottom-right-radius: var(--unit1); + border-top-right-radius: var(--unit1); + bottom: 0; + content: ''; + left: calc(var(--unit1) * -1); + position: absolute; + right: calc(var(--unit1) * -1); + top: 0; + } -.component-tree-item__action { - align-items: center; - background: transparent; - border: 0; - cursor: pointer; - display: inline-flex; - height: 100%; - margin-left: 10px; - opacity: 0; - padding: 0; - - &:focus { - outline: none; + /** + * __actions is position:sticky so it is always visible and on top. + * :before is a gradient to soften the edge when overlaying text. + */ + &:before { + bottom: 0; + content: ''; + left: calc(var(--unit3) * -1); + position: absolute; + top: 0; + width: var(--unit3); + } } - &.disabled { - cursor: not-allowed; + &__action { + &:focus { + outline: none; + } + + polygon, + rect, + path { + fill: var(--base15); + } } - polygon, - rect, - path { - fill: var(--base15); + /** + * This element helps with a visual bug. + * When an item overflows the bounds of the list, + * the item background color clips at the point of the overflow. + * This child of the item has the same background color + * and will extend past the overflow point. + */ + .component-tree-item-background { + min-height: 22px; + } + + &:hover { + background-color: var(--component-highlighted-bg); + + .component-tree-item-background { + background-color: var(--component-highlighted-bg); + } + + .component-tree-item__actions { + opacity: 1; + + &:after { + background-color: var(--component-highlighted-bg); + } + + &:before { + background: linear-gradient(to right, var(--transparent), var(--component-highlighted-bg) 75%); + } + } } } .component-tree-item__tag { - cursor: default; - display: flex; + .component-name { + color: var(--component-name); + } + + .bracket-token { + color: var(--base09); + } + + .key-token { + color: var(--component-attr); + } + + .value-token { + color: var(--spec03); + } } -.component-tree-item--has-instance .component-tree-item__tag { - cursor: pointer; +/** + * Modifier + * highlighted - the component currently being previewed + */ + +.component-tree-item.component-tree-item--highlighted { + background-color: var(--component-highlighted-bg); + border-radius: 0; +} + +.component-tree-item--selected .component-tree-item__tag { + .component-name, + .bracket-token, + .key-token, + .value-token { + color: var(--focus-text); + } +} + +.component-tree-item__bracket:before, +.component-tree-item__bracket:after, +.component-tree-item-classic__bracket:before, +.component-tree-item-classic__bracket:after { + color: var(--base09); } .component-tree-item__bracket:before { - color: var(--base07); content: '<'; } .component-tree-item__bracket:after { - color: var(--base07); content: '>'; } @@ -65,31 +141,17 @@ content: '/>'; } -.component-tree-item:hover { - background-color: var(--spec06); - - .component-tree-item__action { - opacity: 1; - } - - .component-tree-item__action.disabled { - opacity: 0.65; - } +.component-tree-item-classic__bracket:before { + content: '{{'; } -/** - * Modifier - * highlighted - children of selected component - */ - -.component-tree-item.component-tree-item--highlighted { - background-color: var(--spec06); - border-radius: 0; +.component-tree-item-classic__bracket:after { + content: '}}'; } /** * Modifier - * selected + * selected - user clicked on component */ .component-tree-item.component-tree-item--selected { @@ -100,6 +162,22 @@ background: var(--focus); } + .component-tree-item-background { + background: var(--focus); + } + + .component-tree-item__actions { + opacity: 1; + + &:after { + background: var(--focus); + } + + &:before { + background: linear-gradient(to right, var(--transparent), var(--focus) 75%); + } + } + .component-tree-item__bracket:before, .component-tree-item__bracket:after { color: var(--component-backets-selected); @@ -115,12 +193,8 @@ fill: var(--focus-text); } } - - .component-tree-item__action.disabled { - opacity: 0.65; - } } .component-tree-item--component { - color: var(--component-text); + color: var(--base09); } diff --git a/app/styles/utils.scss b/app/styles/utils.scss index cbc20cfb33..19979733f9 100644 --- a/app/styles/utils.scss +++ b/app/styles/utils.scss @@ -1,6 +1,18 @@ $units: 0, 1, 2, 3, 4; +.relative { position: relative; } + +.sticky { + position: -webkit-sticky; + position: sticky; +} + +.right-0 { + right: 0; +} + .flex { display: flex; } +.inline-flex { display: inline-flex; } .flex-grow { flex-grow: 1; } .flex-grow-0 { flex-grow: 0; } @@ -8,6 +20,7 @@ $units: 0, 1, 2, 3, 4; .flex-shrink-0 { flex-shrink: 0; } .items-center { align-items: center; } +.self-stretch { align-self: stretch; } .w-5 { width: var(--unit5); } .w-6 { width: var(--unit6); } @@ -18,10 +31,13 @@ $units: 0, 1, 2, 3, 4; .w-11 { width: var(--unit11); } .w-12 { width: var(--unit12); } +.h-full { height: 100%; } + @each $key in $units { .w-#{$key} { width: var(--unit#{$key}); } .h-#{$key} { height: var(--unit#{$key}); } + .p-#{$key} { padding: var(--unit#{$key}); } .pt-#{$key} { padding-top: var(--unit#{$key}); } .pr-#{$key} { padding-right: var(--unit#{$key}); } .pb-#{$key} { padding-bottom: var(--unit#{$key}); } @@ -37,6 +53,7 @@ $units: 0, 1, 2, 3, 4; padding-top: var(--unit#{$key}); } + .m-#{$key} { margin: var(--unit#{$key}); } .mt-#{$key} { margin-top: var(--unit#{$key}); } .mr-#{$key} { margin-right: var(--unit#{$key}); } .mb-#{$key} { margin-bottom: var(--unit#{$key}); } @@ -53,10 +70,12 @@ $units: 0, 1, 2, 3, 4; } } +.border-0 { border-width: 0; } + .rounded-none { border-radius: 0; } -.rounded-sm { border-radius: 0.125rem; } -.rounded { border-radius: 0.25rem; } -.rounded-lg { border-radius: 0.5rem; } +.rounded-sm { border-radius: calc(var(--unit1) / 2); } +.rounded { border-radius: var(--unit1); } +.rounded-lg { border-radius: var(--unit2); } .rounded-full { border-radius: 9999px; } .font-bold { font-weight: bold; } @@ -65,7 +84,14 @@ $units: 0, 1, 2, 3, 4; .text-left { text-align: left; } .text-center { text-align: center; } .text-right { text-align: right; } +.whitespace-no-wrap { white-space: nowrap; } +.bg-transparent { background-color: var(--transparent); } .bg-base00 { background-color: var(--base00); } .bg-base01 { background-color: var(--base01); } .bg-base02 { background-color: var(--base02); } + +.cursor-default { cursor: default; } +.cursor-pointer { cursor: pointer; } + +.z-10 { z-index: 10; } diff --git a/app/templates/components/component-tree-item.hbs b/app/templates/components/component-tree-item.hbs index ffbf4cb031..4d71fb5ff1 100644 --- a/app/templates/components/component-tree-item.hbs +++ b/app/templates/components/component-tree-item.hbs @@ -1,54 +1,115 @@
- {{#if @item.hasChildren}} - - {{/if}} - - - {{#if @item.isComponent}} - {{classify @item.name}} - {{else if @item.isOutlet}} - \{{outlet "{{@item.name}}"}} - {{else if @item.isEngine}} - \{{mount "{{@item.name}}"}} - {{else if @item.isRouteTemplate}} - {{@item.name}} route +
+ {{#if @item.hasChildren}} + {{/if}} - - {{#if @item.hasBounds}} - + {{#if @item.isComponent}} + {{#if @item.isCurlyInvocation}} + + {{@item.name}} + - + {{#each @item.args.positional as |value|}} +
+ {{if (is-string value) "\""}} + + {{component-argument value}} + + {{if (is-string value) "\""}} +
+ {{/each}} + + {{#each-in @item.args.named as |name value|}} +
+ + {{name}} + + ={{if (is-string value) "\""}} + + {{component-argument value}} + + {{if (is-string value) "\""}} +
+ {{/each-in}} + {{else}} + + {{classify @item.name}} + + + {{#each-in @item.args.named as |name value|}} +
+ + @{{name}} + + ={{#if (is-string value)}}"{{else}}\{{{{/if}} + + {{component-argument value}} + + {{#if (is-string value)}}"{{else}}}}{{/if}} +
+ {{/each-in}} + {{/if}} + {{else if @item.isOutlet}} + \{{outlet "{{@item.name}}"}} + {{else if @item.isEngine}} + \{{mount "{{@item.name}}"}} + {{else if @item.isRouteTemplate}} + {{@item.name}} route + {{/if}} + +
+ + {{#if @item.hasBounds}} +
+ + + +
{{/if}}
diff --git a/app/utils/truncate.js b/app/utils/truncate.js new file mode 100644 index 0000000000..d9153b0a99 --- /dev/null +++ b/app/utils/truncate.js @@ -0,0 +1,35 @@ +export default function truncate(str, limit = 20, rest = '…') { + if (str.length <= limit) { + return str; + } + + if (rest.length >= limit) { + throw new Error('rest cannot be longer than limit'); + } + + let parts = str.split(/(\s+|\b)/); // split by whitespace or word boundaries + let selected = []; // the parts to keep + let targetLength = limit - rest.length; // leave room for the "..." + let currentLength = 0; + + // eslint-disable-next-line + while (true) { + let candidate = parts.shift(); + + if (currentLength + candidate.length <= targetLength) { + selected.push(candidate); + currentLength += candidate.length; + } else { + if (currentLength === 0) { + // We have no choice but to break in the middle of a long word + selected.push(candidate.slice(0, targetLength)); + } + + break; + } + } + + selected.push(rest); + + return selected.join(''); +} diff --git a/lib/ui/addon/styles/_list.scss b/lib/ui/addon/styles/_list.scss index ee5b906c5d..b99dab892d 100644 --- a/lib/ui/addon/styles/_list.scss +++ b/lib/ui/addon/styles/_list.scss @@ -8,8 +8,7 @@ } .list__content { - overflow: hidden; - overflow-y: scroll; + overflow: scroll; transform: translateZ(0); } diff --git a/lib/ui/addon/styles/_split.scss b/lib/ui/addon/styles/_split.scss index 92a8030945..593921f944 100644 --- a/lib/ui/addon/styles/_split.scss +++ b/lib/ui/addon/styles/_split.scss @@ -24,6 +24,7 @@ flex: auto; flex-direction: column; position: relative; + overflow: hidden; } .split__panel__hd, @@ -51,7 +52,9 @@ .split__panel__bd { background: var(--base00); flex: auto; - overflow: auto; + overflow: scroll; + max-width: 100%; + width: auto; } .split__panel__ft { @@ -83,7 +86,7 @@ /* Fix main list-view scrolling */ .split--main > .split__panel > .split__panel__bd { - overflow-y: hidden; + overflow: hidden; } .split--main > .split__panel--sidebar-1 > .split__panel__bd { diff --git a/tests/acceptance/component-tree-test.js b/tests/acceptance/component-tree-test.js index 1185ecada5..7ecfea96bb 100644 --- a/tests/acceptance/component-tree-test.js +++ b/tests/acceptance/component-tree-test.js @@ -246,7 +246,7 @@ module('Component Tab', function (hooks) { return false; }); - await click('.js-scroll-into-view'); + await click('[data-test="scroll-into-view"]'); }); test('View DOM element in Elements panel', async function (assert) { @@ -257,7 +257,7 @@ module('Component Tab', function (hooks) { return false; }); - await click('.js-view-dom-element'); + await click('[data-test="view-dom-element"]'); }); test('Inspects the component in the object inspector on click and shows tooltip', async function (assert) {