diff --git a/index.html b/index.html index e2037fedbf..deaf381e3f 100644 --- a/index.html +++ b/index.html @@ -78,6 +78,7 @@ openmct.install(openmct.plugins.Notebook()); openmct.install(openmct.plugins.FolderView()); openmct.install(openmct.plugins.Tabs()); + openmct.install(openmct.plugins.FlexibleLayout()); openmct.time.clock('local', {start: -THIRTY_MINUTES, end: 0}); openmct.time.timeSystem('utc'); openmct.start(); diff --git a/src/plugins/flexibleLayout/components/container.vue b/src/plugins/flexibleLayout/components/container.vue new file mode 100644 index 0000000000..ce82b542e6 --- /dev/null +++ b/src/plugins/flexibleLayout/components/container.vue @@ -0,0 +1,214 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + + + + diff --git a/src/plugins/flexibleLayout/components/dropHint.vue b/src/plugins/flexibleLayout/components/dropHint.vue new file mode 100644 index 0000000000..57ca840781 --- /dev/null +++ b/src/plugins/flexibleLayout/components/dropHint.vue @@ -0,0 +1,58 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + + + + + + diff --git a/src/plugins/flexibleLayout/components/flexibleLayout.vue b/src/plugins/flexibleLayout/components/flexibleLayout.vue new file mode 100644 index 0000000000..453519cf8c --- /dev/null +++ b/src/plugins/flexibleLayout/components/flexibleLayout.vue @@ -0,0 +1,684 @@ + + + + + diff --git a/src/plugins/flexibleLayout/components/frame.vue b/src/plugins/flexibleLayout/components/frame.vue new file mode 100644 index 0000000000..bde8ed57c6 --- /dev/null +++ b/src/plugins/flexibleLayout/components/frame.vue @@ -0,0 +1,129 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + + + + diff --git a/src/plugins/flexibleLayout/components/resizeHandle.vue b/src/plugins/flexibleLayout/components/resizeHandle.vue new file mode 100644 index 0000000000..6f141785f7 --- /dev/null +++ b/src/plugins/flexibleLayout/components/resizeHandle.vue @@ -0,0 +1,75 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + + + + diff --git a/src/plugins/flexibleLayout/flexibleLayoutViewProvider.js b/src/plugins/flexibleLayout/flexibleLayoutViewProvider.js new file mode 100644 index 0000000000..76a4620e68 --- /dev/null +++ b/src/plugins/flexibleLayout/flexibleLayoutViewProvider.js @@ -0,0 +1,67 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + './components/flexibleLayout.vue', + 'vue' +], function ( + FlexibleLayoutComponent, + Vue +) { + function FlexibleLayoutViewProvider(openmct) { + return { + key: 'flexible-layout', + name: 'FlexibleLayout', + cssClass: 'icon-layout-view', + canView: function (domainObject) { + return domainObject.type === 'flexible-layout'; + }, + view: function (domainObject) { + let component; + + return { + show: function (element) { + component = new Vue({ + components: { + FlexibleLayoutComponent: FlexibleLayoutComponent.default + }, + provide: { + openmct, + domainObject + }, + el: element, + template: '' + }); + }, + destroy: function (element) { + component.$destroy(); + component = undefined; + } + }; + }, + priority: function () { + return 1; + } + }; + } + return FlexibleLayoutViewProvider; +}); diff --git a/src/plugins/flexibleLayout/plugin.js b/src/plugins/flexibleLayout/plugin.js new file mode 100644 index 0000000000..8b5500a0fd --- /dev/null +++ b/src/plugins/flexibleLayout/plugin.js @@ -0,0 +1,163 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + './flexibleLayoutViewProvider', + './utils/container' +], function ( + FlexibleLayoutViewProvider, + Container +) { + return function plugin() { + + return function install(openmct) { + openmct.objectViews.addProvider(new FlexibleLayoutViewProvider(openmct)); + + openmct.types.addType('flexible-layout', { + name: "Flexible Layout", + creatable: true, + description: "A fluid, flexible layout canvas that can display multiple objects in rows or columns.", + cssClass: 'icon-flexible-layout', + initialize: function (domainObject) { + domainObject.configuration = { + containers: [new Container.default(50), new Container.default(50)], + rowsLayout: false + }; + } + }); + + openmct.toolbars.addProvider({ + name: "Flexible Layout Toolbar", + key: "flex-layout", + description: "A toolbar for objects inside a Flexible Layout.", + forSelection: function (selection) { + let context = selection[0].context; + + return (openmct.editor.isEditing() && context && context.type && + (context.type === 'flexible-layout' || context.type === 'container' || context.type === 'frame')); + }, + toolbar: function (selection) { + + let primary = selection[0], + parent = selection[1], + deleteFrame, + toggleContainer, + deleteContainer, + addContainer, + toggleFrame, + separator; + + addContainer = { + control: "button", + domainObject: parent ? parent.context.item : primary.context.item, + method: parent ? parent.context.addContainer : primary.context.addContainer, + key: "add", + icon: "icon-plus-in-rect", + title: 'Add Container' + }; + + separator = { + control: "separator", + domainObject: selection[0].context.item, + key: "separator" + }; + + toggleContainer = { + control: 'toggle-button', + key: 'toggle-layout', + domainObject: parent ? parent.context.item : primary.context.item, + property: 'configuration.rowsLayout', + options: [ + { + value: false, + icon: 'icon-columns', + title: 'Columns' + }, + { + value: true, + icon: 'icon-rows', + title: 'Rows' + } + ] + }; + + if (primary.context.type === 'frame') { + + deleteFrame = { + control: "button", + domainObject: primary.context.item, + method: primary.context.method, + key: "remove", + icon: "icon-trash", + title: "Remove Frame" + }; + toggleFrame = { + control: "toggle-button", + domainObject: parent.context.item, + property: `configuration.containers[${parent.context.index}].frames[${primary.context.index}].noFrame`, + options: [ + { + value: false, + icon: 'icon-frame-hide', + title: "Hide frame" + }, + { + value: true, + icon: 'icon-frame-show', + title: "Show frame" + } + ] + }; + + + } else if (primary.context.type === 'container') { + + deleteContainer = { + control: "button", + domainObject: primary.context.item, + method: primary.context.method, + key: "remove", + icon: "icon-trash", + title: "Remove Container" + }; + + } else if (primary.context.type === 'flexible-layout') { + + addContainer = { + control: "button", + domainObject: primary.context.item, + method: primary.context.addContainer, + key: "add", + icon: "icon-plus-in-rect", + title: 'Add Container' + }; + + } + + let toolbar = [toggleContainer, addContainer, toggleFrame, separator, deleteFrame, deleteContainer]; + + return toolbar.filter(button => button !== undefined); + } + }); + }; + }; +}); diff --git a/src/plugins/flexibleLayout/utils/container.js b/src/plugins/flexibleLayout/utils/container.js new file mode 100644 index 0000000000..e21e920b8a --- /dev/null +++ b/src/plugins/flexibleLayout/utils/container.js @@ -0,0 +1,14 @@ +import Frame from './frame'; + +class Container { + constructor (width) { + this.frames = [new Frame({}, '', 'c-fl-frame--first-in-container')]; + this.width = width; + } + + addFrame(frameObject) { + this.frames.push(frameObject); + } +} + +export default Container; diff --git a/src/plugins/flexibleLayout/utils/frame.js b/src/plugins/flexibleLayout/utils/frame.js new file mode 100644 index 0000000000..e7853d5332 --- /dev/null +++ b/src/plugins/flexibleLayout/utils/frame.js @@ -0,0 +1,10 @@ +class Frame { + constructor(domainObject, height, cssClass) { + this.domainObject = domainObject; + this.height = height; + this.cssClass = cssClass ? cssClass : ''; + this.noFrame = false; + } +} + +export default Frame; diff --git a/src/plugins/folderView/components/GridView.vue b/src/plugins/folderView/components/GridView.vue index a9e8d35674..8fd3a271f7 100644 --- a/src/plugins/folderView/components/GridView.vue +++ b/src/plugins/folderView/components/GridView.vue @@ -33,6 +33,7 @@ .l-grid-view { display: flex; flex-flow: column nowrap; + overflow: auto; &__item { flex: 0 0 auto; diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index a9e0c7ade3..d25d12e32f 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -37,6 +37,7 @@ define([ './notebook/plugin', './displayLayout/plugin', './folderView/plugin', + './flexibleLayout/plugin', './tabs/plugin', '../../platform/features/fixed/plugin' ], function ( @@ -56,6 +57,7 @@ define([ Notebook, DisplayLayoutPlugin, FolderView, + FlexibleLayout, Tabs, FixedView ) { @@ -171,6 +173,7 @@ define([ plugins.FolderView = FolderView; plugins.Tabs = Tabs; plugins.FixedView = FixedView; + plugins.FlexibleLayout = FlexibleLayout; return plugins; }); diff --git a/src/styles-new/_constants-espresso.scss b/src/styles-new/_constants-espresso.scss index 2194ebc823..a7d52dc712 100644 --- a/src/styles-new/_constants-espresso.scss +++ b/src/styles-new/_constants-espresso.scss @@ -88,17 +88,35 @@ $colorTimeSubtle: darken($colorTime, 20%); $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov -// Edit Colors +/************************************************** EDITING */ +// Base Colors +$dlSpread: 20%; $editColor: #00c7c3; -$editColorFg: $colorBodyFg; +$editColorBg: darken($editColor, $dlSpread); +$editColorFg: lighten($editColor, $dlSpread); +$editColorHov: lighten($editColor, 20%); +// Canvas +$editCanvasColorBg: #002524; +$editCanvasColorGrid: darken($editCanvasColorBg, 2%); +// Selectable +$editSelectableColor: #006563; +$editSelectableColorFg: lighten($editSelectableColor, 20%); +$editSelectableColorHov: lighten($editSelectableColor, 10%); +// Selectable selected +$editSelectableColorSelected: $editSelectableColorHov; +$editSelectableColorSelectedFg: lighten($editSelectableColorSelected, 30%); +$editSelectableColorFg: darken($editSelectableColor, 40%); +$editSelectableBorder: 1px dotted $editSelectableColor; +$editSelectableBorderHov: 1px dotted $editColor; +$editSelectableBorderSelected: 1px solid $editColor; +$editMoveableSelectedShdw: rgba($editColor, 0.5) 0 0 10px; +$editBorderDrilledIn: 1px dashed #9971ff; +$colorGridLines: rgba($editColor, 0.2); + +/************************************************** BROWSING */ $browseBorderSelectableHov: 1px dotted rgba($colorBodyFg, 0.2); $browseShdwSelectableHov: rgba($colorBodyFg, 0.2) 0 0 3px; $browseBorderSelected: 1px solid rgba($colorBodyFg, 0.6); -$editBorderSelectable: 1px dotted rgba($editColor, 1); -$editBorderSelectableHov: 1px dashed rgba($editColor, 1); -$editBorderSelected: 1px solid rgba($editColor, 0.7); -$editBorderDrilledIn: 1px dashed #ff4d9a; -$colorGridLines: rgba($editColor, 0.2); // Icons $colorIconAlias: #4af6f3; diff --git a/src/styles-new/_constants-snow.scss b/src/styles-new/_constants-snow.scss index 5ff8c98849..ed19ac356b 100644 --- a/src/styles-new/_constants-snow.scss +++ b/src/styles-new/_constants-snow.scss @@ -88,17 +88,34 @@ $colorTimeSubtle: lighten($colorTime, 20%); $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov -// Edit Colors +/************************************************** EDITING */ +// Base Colors +$dlSpread: 20%; $editColor: #00c7c3; -$editColorFg: $colorBodyFg; +$editColorBg: darken($editColor, $dlSpread); +$editColorFg: lighten($editColor, $dlSpread); +$editColorHov: lighten($editColor, 20%); +// Canvas +$editCanvasColorBg: #e6ffff; +$editCanvasColorGrid: darken($editCanvasColorBg, 10%); +// Selectable +$editSelectableColor: darken($colorBodyBg, 10%); +$editSelectableColorFg: darken($editSelectableColor, 20%); +$editSelectableColorHov: darken($editSelectableColor, 10%); //darken($colorBodyBg, 20%); +// Selectable selected +$editSelectableColorSelected: $editColor; //$editSelectableColorHov; +$editSelectableColorSelectedFg: lighten($editSelectableColorSelected, 50%); +$editSelectableColorFg: darken($editSelectableColor, 40%); +$editSelectableBorder: 1px dotted $editSelectableColor; +$editSelectableBorderHov: 1px dotted $editColor; +$editSelectableBorderSelected: 1px solid $editColor; +$editMoveableSelectedShdw: rgba($editColor, 0.5) 0 0 10px; +$editBorderDrilledIn: 1px dashed #9971ff; + +/************************************************** BROWSING */ $browseBorderSelectableHov: 1px dotted rgba($colorBodyFg, 0.2); $browseShdwSelectableHov: rgba($colorBodyFg, 0.2) 0 0 3px; $browseBorderSelected: 1px solid rgba($colorBodyFg, 0.6); -$editBorderSelectable: 1px dotted rgba($editColor, 1); -$editBorderSelectableHov: 1px dashed rgba($editColor, 1); -$editBorderSelected: 1px solid $editColor; -$editBorderDrilledIn: 1px dashed #ff4d9a; -$colorGridLines: rgba($editColor, 0.2); // Icons $colorIconAlias: #4af6f3; @@ -125,9 +142,9 @@ $colorClickIcon: $colorKey; $colorClickIconBgHov: rgba($colorKey, 0.2); $colorClickIconFgHov: $colorKeyHov; $colorDropHint: $colorKey; -$colorDropHintBg: darken($colorDropHint, 10%); -$colorDropHintBgHov: $colorDropHint; -$colorDropHintFg: lighten($colorDropHint, 40%); +$colorDropHintBg: lighten($colorDropHint, 30%); +$colorDropHintBgHov: lighten($colorDropHint, 40%); +$colorDropHintFg: lighten($colorDropHint, 0); // Menus $colorMenuBg: lighten($colorBodyBg, 10%); diff --git a/src/styles-new/_controls.scss b/src/styles-new/_controls.scss index de5b87223b..7b0ef505b7 100644 --- a/src/styles-new/_controls.scss +++ b/src/styles-new/_controls.scss @@ -512,13 +512,15 @@ input[type=number]::-webkit-outer-spin-button { transition: $transOut; z-index: 50; - opacity: 0; // Must use this (rather than display: none) to enable transition effects - pointer-events: none; + &:not(.c-drop-hint--always-show) { + opacity: 0; // Must use this (rather than display: none) to enable transition effects + pointer-events: none; + } &:before { $h: 80%; - $mh: 50px; - background: $bg-icon-activity; // TODO: change to $bg-icon-plus + $mh: 25px; + background: $bg-icon-plus; background-size: contain; background-position: center center; background-repeat: no-repeat; @@ -529,13 +531,15 @@ input[type=number]::-webkit-outer-spin-button { max-height: $mh; max-width: $mh; } - .is-dragging & { + .is-dragging &, + &.is-dragging { pointer-events: inherit; transition: $transIn; opacity: 0.8; } - .is-mouse-over & { + .is-mouse-over &, + &.is-mouse-over { transition: $transIn; background-color: $colorDropHintBgHov; opacity: 0.9; diff --git a/src/styles-new/_global.scss b/src/styles-new/_global.scss index 613939aa0d..adc3cf2b63 100644 --- a/src/styles-new/_global.scss +++ b/src/styles-new/_global.scss @@ -235,8 +235,8 @@ body.desktop .has-local-controls { .c-grid { pointer-events: none; - &__x { @include bgTicks($colorGridLines, 'x'); } - &__y { @include bgTicks($colorGridLines, 'y'); } + &__x { @include bgTicks($editCanvasColorGrid, 'x'); } + &__y { @include bgTicks($editCanvasColorGrid, 'y'); } } /*************************** SELECTION */ @@ -249,15 +249,15 @@ body.desktop .has-local-controls { /**************************** EDITING */ .is-editing { *:not(.is-drilled-in).is-selectable { - border: $editBorderSelectable; + border: $editSelectableBorder; &:hover { - border: $editBorderSelectableHov; + border: $editSelectableBorderHov; } &[s-selected], &.is-selected { - border: $editBorderSelected; + border: $editSelectableBorderSelected; > .c-frame-edit { display: block; // Show the editing rect and handles @@ -269,6 +269,10 @@ body.desktop .has-local-controls { border: $editBorderDrilledIn; } + *[s-selected] .is-moveable { + cursor: move; + } + .u-links { // Applied in markup to objects that provide links. Disable while editing. pointer-events: none; @@ -284,7 +288,7 @@ body.desktop .has-local-controls { &__move { @include abs(); - box-shadow: rgba($editColor, 0.5) 0 0 10px; + box-shadow: $editMoveableSelectedShdw; cursor: move; z-index: $z; } @@ -339,10 +343,11 @@ body.desktop .has-local-controls { } } + // TODO: move this into DisplayLayout and Fixed Position vue files respectively .l-shell__main-container > .l-layout, .l-shell__main-container > .c-object-view .l-fixed-position { // Target the top-most layout container and color its background - background: rgba($editColor, 0.1); + background: $editCanvasColorBg; } // Layouts diff --git a/src/styles-new/_mixins.scss b/src/styles-new/_mixins.scss index a3ab063023..f462cbf729 100644 --- a/src/styles-new/_mixins.scss +++ b/src/styles-new/_mixins.scss @@ -128,10 +128,10 @@ } @mixin bgVertStripes($c: yellow, $a: 0.1, $d: 40px) { - @include background-image(linear-gradient(-90deg, + background-image: linear-gradient(-90deg, rgba($c, $a) 0%, rgba($c, $a) 50%, transparent 50%, transparent 100% - )); + ); background-repeat: repeat; background-size: $d $d; } diff --git a/src/ui/components/layout/ObjectView.vue b/src/ui/components/layout/ObjectView.vue index 46e973be78..d2b8628d21 100644 --- a/src/ui/components/layout/ObjectView.vue +++ b/src/ui/components/layout/ObjectView.vue @@ -76,14 +76,18 @@ export default { }, onDrop(event) { let parentObject = this.currentObject; - let childObject = JSON.parse(event.dataTransfer.getData("domainObject")); + let d = event.dataTransfer.getData("domainObject"); - if (this.openmct.composition.checkPolicy(parentObject, childObject)){ - if (!this.openmct.editor.isEditing() && parentObject.type !== 'folder'){ - this.openmct.editor.edit(); + if (d) { + let childObject = JSON.parse(d); + + if (this.openmct.composition.checkPolicy(parentObject, childObject)){ + if (!this.openmct.editor.isEditing() && parentObject.type !== 'folder'){ + this.openmct.editor.edit(); + } + parentObject.composition.push(childObject.identifier); + this.openmct.objects.mutate(parentObject, 'composition', parentObject.composition); } - parentObject.composition.push(childObject.identifier); - this.openmct.objects.mutate(parentObject, 'composition', parentObject.composition); } event.preventDefault(); diff --git a/src/ui/components/utils/frameHeader.vue b/src/ui/components/utils/frameHeader.vue new file mode 100644 index 0000000000..264337685c --- /dev/null +++ b/src/ui/components/utils/frameHeader.vue @@ -0,0 +1,25 @@ + + +