diff --git a/styleguide/assets/additional.css b/styleguide/assets/additional.css
index 256f5fc54f..2372ff988e 100644
--- a/styleguide/assets/additional.css
+++ b/styleguide/assets/additional.css
@@ -3,8 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@import 'default.css';
-@import 'server.css';
@import 'dark.css';
+@import 'server.css';
+@import 'apps.css';
div[data-preview] * {
box-sizing: border-box;
@@ -14,11 +15,6 @@ body {
position: relative;
}
-h1,
-h2 {
- line-height: 1.5 !important;
-}
-
div[data-preview="AppNavigationItem"],
div[data-preview="AppNavigationCaption"] {
width: auto;
diff --git a/styleguide/assets/apps.css b/styleguide/assets/apps.css
new file mode 100644
index 0000000000..83c554ec85
--- /dev/null
+++ b/styleguide/assets/apps.css
@@ -0,0 +1 @@
+:root{--border-radius-rounded: calc(var(--default-clickable-area) / 2 + var(--default-grid-baseline) * 2 - 2px);--body-container-radius: var(--border-radius-rounded);--body-container-margin: calc(var(--default-grid-baseline) * 2);--body-height: calc(100% - env(safe-area-inset-bottom) - 50px - var(--body-container-margin))}@media screen and (max-width: 1024px){:root{--body-container-margin: 0px;--body-container-radius: 0px}}html{width:100%;height:100%;position:absolute;background-color:var(--color-background-plain, var(--color-main-background))}body{background-color:var(--color-background-plain, var(--color-main-background));background-image:var(--image-background, var(--image-background-plain, var(--image-background-default)));background-size:cover;background-position:center;position:fixed;width:100%;height:calc(100vh - env(safe-area-inset-bottom))}h2,h3,h4,h5,h6{font-weight:600;line-height:1.5;margin-top:24px;margin-bottom:12px;color:var(--color-main-text)}h2{font-size:30px}h3{font-size:24px}h4{font-size:21px}h5{font-size:17px}h6{font-size:var(--default-font-size)}em{font-style:normal;color:var(--color-text-maxcontrast)}dl{padding:12px 0}dt,dd{display:inline-block;padding:12px;padding-left:0}dt{width:130px;white-space:nowrap;text-align:right}kbd{padding:4px 10px;border:1px solid #ccc;box-shadow:0 1px 0 rgba(0,0,0,.2);border-radius:var(--border-radius);display:inline-block;white-space:nowrap}#content[class*=app-] *{box-sizing:border-box}#app-navigation:not(.vue){--border-radius-pill: calc(var(--default-clickable-area) / 2);--color-text-maxcontrast: var(--color-text-maxcontrast-background-blur, var(--color-main-text));width:300px;z-index:500;overflow-y:auto;overflow-x:hidden;background-color:var(--color-main-background-blur);backdrop-filter:var(--filter-background-blur);-webkit-backdrop-filter:var(--filter-background-blur);-webkit-user-select:none;position:sticky;height:100%;-moz-user-select:none;-ms-user-select:none;user-select:none;display:flex;flex-direction:column;flex-grow:0;flex-shrink:0}#app-navigation:not(.vue) .app-navigation-caption{font-weight:bold;line-height:44px;padding:10px 44px 0 44px;white-space:nowrap;text-overflow:ellipsis;box-shadow:none !important;user-select:none;pointer-events:none;margin-left:10px}.app-navigation-personal .app-navigation-new,.app-navigation-administration .app-navigation-new{display:block;padding:calc(var(--default-grid-baseline)*2)}.app-navigation-personal .app-navigation-new button,.app-navigation-administration .app-navigation-new button{display:inline-block;width:100%;padding:10px;padding-left:34px;background-position:10px center;text-align:left;margin:0}.app-navigation-personal li,.app-navigation-administration li{position:relative}.app-navigation-personal>ul,.app-navigation-administration>ul{position:relative;height:100%;width:100%;overflow-x:hidden;overflow-y:auto;box-sizing:border-box;display:flex;flex-direction:column;padding:calc(var(--default-grid-baseline)*2);padding-bottom:0}.app-navigation-personal>ul:last-child,.app-navigation-administration>ul:last-child{padding-bottom:calc(var(--default-grid-baseline)*2)}.app-navigation-personal>ul>li,.app-navigation-administration>ul>li{display:inline-flex;flex-wrap:wrap;order:1;flex-shrink:0;margin:0;margin-bottom:3px;width:100%;border-radius:var(--border-radius-pill)}.app-navigation-personal>ul>li.pinned,.app-navigation-administration>ul>li.pinned{order:2}.app-navigation-personal>ul>li.pinned.first-pinned,.app-navigation-administration>ul>li.pinned.first-pinned{margin-top:auto !important}.app-navigation-personal>ul>li>.app-navigation-entry-deleted,.app-navigation-administration>ul>li>.app-navigation-entry-deleted{padding-left:44px !important}.app-navigation-personal>ul>li>.app-navigation-entry-edit,.app-navigation-administration>ul>li>.app-navigation-entry-edit{padding-left:38px !important}.app-navigation-personal>ul>li a:hover,.app-navigation-personal>ul>li a:hover>a,.app-navigation-personal>ul>li a:focus,.app-navigation-personal>ul>li a:focus>a,.app-navigation-administration>ul>li a:hover,.app-navigation-administration>ul>li a:hover>a,.app-navigation-administration>ul>li a:focus,.app-navigation-administration>ul>li a:focus>a{background-color:var(--color-background-hover)}.app-navigation-personal>ul>li a:focus-visible,.app-navigation-administration>ul>li a:focus-visible{box-shadow:0 0 0 4px var(--color-main-background);outline:2px solid var(--color-main-text)}.app-navigation-personal>ul>li.active,.app-navigation-personal>ul>li.active>a,.app-navigation-personal>ul>li a:active,.app-navigation-personal>ul>li a:active>a,.app-navigation-personal>ul>li a.selected,.app-navigation-personal>ul>li a.selected>a,.app-navigation-personal>ul>li a.active,.app-navigation-personal>ul>li a.active>a,.app-navigation-administration>ul>li.active,.app-navigation-administration>ul>li.active>a,.app-navigation-administration>ul>li a:active,.app-navigation-administration>ul>li a:active>a,.app-navigation-administration>ul>li a.selected,.app-navigation-administration>ul>li a.selected>a,.app-navigation-administration>ul>li a.active,.app-navigation-administration>ul>li a.active>a{background-color:var(--color-primary-element);color:var(--color-primary-element-text)}.app-navigation-personal>ul>li.active:first-child>img,.app-navigation-personal>ul>li.active>a:first-child>img,.app-navigation-personal>ul>li a:active:first-child>img,.app-navigation-personal>ul>li a:active>a:first-child>img,.app-navigation-personal>ul>li a.selected:first-child>img,.app-navigation-personal>ul>li a.selected>a:first-child>img,.app-navigation-personal>ul>li a.active:first-child>img,.app-navigation-personal>ul>li a.active>a:first-child>img,.app-navigation-administration>ul>li.active:first-child>img,.app-navigation-administration>ul>li.active>a:first-child>img,.app-navigation-administration>ul>li a:active:first-child>img,.app-navigation-administration>ul>li a:active>a:first-child>img,.app-navigation-administration>ul>li a.selected:first-child>img,.app-navigation-administration>ul>li a.selected>a:first-child>img,.app-navigation-administration>ul>li a.active:first-child>img,.app-navigation-administration>ul>li a.active>a:first-child>img{filter:var(--primary-invert-if-dark)}.app-navigation-personal>ul>li.icon-loading-small:after,.app-navigation-administration>ul>li.icon-loading-small:after{left:22px;top:22px}.app-navigation-personal>ul>li.deleted>ul,.app-navigation-personal>ul>li.collapsible:not(.open)>ul,.app-navigation-administration>ul>li.deleted>ul,.app-navigation-administration>ul>li.collapsible:not(.open)>ul{display:none}.app-navigation-personal>ul>li>ul,.app-navigation-administration>ul>li>ul{flex:0 1 auto;width:100%;position:relative}.app-navigation-personal>ul>li>ul>li,.app-navigation-administration>ul>li>ul>li{display:inline-flex;flex-wrap:wrap;padding-left:44px;width:100%;margin-bottom:3px}.app-navigation-personal>ul>li>ul>li:hover,.app-navigation-personal>ul>li>ul>li:hover>a,.app-navigation-personal>ul>li>ul>li:focus,.app-navigation-personal>ul>li>ul>li:focus>a,.app-navigation-administration>ul>li>ul>li:hover,.app-navigation-administration>ul>li>ul>li:hover>a,.app-navigation-administration>ul>li>ul>li:focus,.app-navigation-administration>ul>li>ul>li:focus>a{border-radius:var(--border-radius-pill);background-color:var(--color-background-hover)}.app-navigation-personal>ul>li>ul>li.active,.app-navigation-personal>ul>li>ul>li.active>a,.app-navigation-personal>ul>li>ul>li a.selected,.app-navigation-personal>ul>li>ul>li a.selected>a,.app-navigation-administration>ul>li>ul>li.active,.app-navigation-administration>ul>li>ul>li.active>a,.app-navigation-administration>ul>li>ul>li a.selected,.app-navigation-administration>ul>li>ul>li a.selected>a{border-radius:var(--border-radius-pill);background-color:var(--color-primary-element-light)}.app-navigation-personal>ul>li>ul>li.active:first-child>img,.app-navigation-personal>ul>li>ul>li.active>a:first-child>img,.app-navigation-personal>ul>li>ul>li a.selected:first-child>img,.app-navigation-personal>ul>li>ul>li a.selected>a:first-child>img,.app-navigation-administration>ul>li>ul>li.active:first-child>img,.app-navigation-administration>ul>li>ul>li.active>a:first-child>img,.app-navigation-administration>ul>li>ul>li a.selected:first-child>img,.app-navigation-administration>ul>li>ul>li a.selected>a:first-child>img{filter:var(--primary-invert-if-dark)}.app-navigation-personal>ul>li>ul>li.icon-loading-small:after,.app-navigation-administration>ul>li>ul>li.icon-loading-small:after{left:22px}.app-navigation-personal>ul>li>ul>li>.app-navigation-entry-deleted,.app-navigation-administration>ul>li>ul>li>.app-navigation-entry-deleted{margin-left:4px;padding-left:84px}.app-navigation-personal>ul>li>ul>li>.app-navigation-entry-edit,.app-navigation-administration>ul>li>ul>li>.app-navigation-entry-edit{margin-left:4px;padding-left:78px !important}.app-navigation-personal>ul>li,.app-navigation-personal>ul>li>ul>li,.app-navigation-administration>ul>li,.app-navigation-administration>ul>li>ul>li{position:relative;box-sizing:border-box}.app-navigation-personal>ul>li.icon-loading-small>a,.app-navigation-personal>ul>li.icon-loading-small>.app-navigation-entry-bullet,.app-navigation-personal>ul>li>ul>li.icon-loading-small>a,.app-navigation-personal>ul>li>ul>li.icon-loading-small>.app-navigation-entry-bullet,.app-navigation-administration>ul>li.icon-loading-small>a,.app-navigation-administration>ul>li.icon-loading-small>.app-navigation-entry-bullet,.app-navigation-administration>ul>li>ul>li.icon-loading-small>a,.app-navigation-administration>ul>li>ul>li.icon-loading-small>.app-navigation-entry-bullet{background:rgba(0,0,0,0) !important}.app-navigation-personal>ul>li>a,.app-navigation-personal>ul>li>ul>li>a,.app-navigation-administration>ul>li>a,.app-navigation-administration>ul>li>ul>li>a{background-size:16px 16px;background-position:14px center;background-repeat:no-repeat;display:block;justify-content:space-between;line-height:44px;min-height:44px;padding:0 12px 0 14px;overflow:hidden;box-sizing:border-box;white-space:nowrap;text-overflow:ellipsis;border-radius:var(--border-radius-pill);color:var(--color-main-text);flex:1 1 0px;z-index:100}.app-navigation-personal>ul>li>a.svg,.app-navigation-personal>ul>li>ul>li>a.svg,.app-navigation-administration>ul>li>a.svg,.app-navigation-administration>ul>li>ul>li>a.svg{padding:0 12px 0 44px}.app-navigation-personal>ul>li>a.svg :focus-visible,.app-navigation-personal>ul>li>ul>li>a.svg :focus-visible,.app-navigation-administration>ul>li>a.svg :focus-visible,.app-navigation-administration>ul>li>ul>li>a.svg :focus-visible{padding:0 8px 0 42px}.app-navigation-personal>ul>li>a:first-child img,.app-navigation-personal>ul>li>ul>li>a:first-child img,.app-navigation-administration>ul>li>a:first-child img,.app-navigation-administration>ul>li>ul>li>a:first-child img{margin-right:11px !important;width:16px;height:16px;filter:var(--background-invert-if-dark)}.app-navigation-personal>ul>li>a>.app-navigation-entry-utils,.app-navigation-personal>ul>li>ul>li>a>.app-navigation-entry-utils,.app-navigation-administration>ul>li>a>.app-navigation-entry-utils,.app-navigation-administration>ul>li>ul>li>a>.app-navigation-entry-utils{display:inline-block;float:right}.app-navigation-personal>ul>li>a>.app-navigation-entry-utils .app-navigation-entry-utils-counter,.app-navigation-personal>ul>li>ul>li>a>.app-navigation-entry-utils .app-navigation-entry-utils-counter,.app-navigation-administration>ul>li>a>.app-navigation-entry-utils .app-navigation-entry-utils-counter,.app-navigation-administration>ul>li>ul>li>a>.app-navigation-entry-utils .app-navigation-entry-utils-counter{padding-right:0 !important}.app-navigation-personal>ul>li>.app-navigation-entry-bullet,.app-navigation-personal>ul>li>ul>li>.app-navigation-entry-bullet,.app-navigation-administration>ul>li>.app-navigation-entry-bullet,.app-navigation-administration>ul>li>ul>li>.app-navigation-entry-bullet{position:absolute;display:block;margin:16px;width:12px;height:12px;border:none;border-radius:50%;cursor:pointer;transition:background 100ms ease-in-out}.app-navigation-personal>ul>li>.app-navigation-entry-bullet+a,.app-navigation-personal>ul>li>ul>li>.app-navigation-entry-bullet+a,.app-navigation-administration>ul>li>.app-navigation-entry-bullet+a,.app-navigation-administration>ul>li>ul>li>.app-navigation-entry-bullet+a{background:rgba(0,0,0,0) !important}.app-navigation-personal>ul>li>.app-navigation-entry-menu,.app-navigation-personal>ul>li>ul>li>.app-navigation-entry-menu,.app-navigation-administration>ul>li>.app-navigation-entry-menu,.app-navigation-administration>ul>li>ul>li>.app-navigation-entry-menu{top:44px}.app-navigation-personal>ul>li.editing .app-navigation-entry-edit,.app-navigation-personal>ul>li>ul>li.editing .app-navigation-entry-edit,.app-navigation-administration>ul>li.editing .app-navigation-entry-edit,.app-navigation-administration>ul>li>ul>li.editing .app-navigation-entry-edit{opacity:1;z-index:250}.app-navigation-personal>ul>li.deleted .app-navigation-entry-deleted,.app-navigation-personal>ul>li>ul>li.deleted .app-navigation-entry-deleted,.app-navigation-administration>ul>li.deleted .app-navigation-entry-deleted,.app-navigation-administration>ul>li>ul>li.deleted .app-navigation-entry-deleted{transform:translateX(0);z-index:250}.app-navigation-personal.hidden,.app-navigation-administration.hidden{display:none}.app-navigation-personal .app-navigation-entry-utils .app-navigation-entry-utils-menu-button>button,.app-navigation-personal .app-navigation-entry-deleted .app-navigation-entry-deleted-button,.app-navigation-administration .app-navigation-entry-utils .app-navigation-entry-utils-menu-button>button,.app-navigation-administration .app-navigation-entry-deleted .app-navigation-entry-deleted-button{border:0;opacity:.5;background-color:rgba(0,0,0,0);background-repeat:no-repeat;background-position:center}.app-navigation-personal .app-navigation-entry-utils .app-navigation-entry-utils-menu-button>button:hover,.app-navigation-personal .app-navigation-entry-utils .app-navigation-entry-utils-menu-button>button:focus,.app-navigation-personal .app-navigation-entry-deleted .app-navigation-entry-deleted-button:hover,.app-navigation-personal .app-navigation-entry-deleted .app-navigation-entry-deleted-button:focus,.app-navigation-administration .app-navigation-entry-utils .app-navigation-entry-utils-menu-button>button:hover,.app-navigation-administration .app-navigation-entry-utils .app-navigation-entry-utils-menu-button>button:focus,.app-navigation-administration .app-navigation-entry-deleted .app-navigation-entry-deleted-button:hover,.app-navigation-administration .app-navigation-entry-deleted .app-navigation-entry-deleted-button:focus{background-color:rgba(0,0,0,0);opacity:1}.app-navigation-personal .collapsible .collapse,.app-navigation-administration .collapsible .collapse{opacity:0;position:absolute;width:44px;height:44px;margin:0;z-index:110;left:0}.app-navigation-personal .collapsible .collapse:focus-visible,.app-navigation-administration .collapsible .collapse:focus-visible{opacity:1;border-width:0;box-shadow:inset 0 0 0 2px var(--color-primary-element);background:none}.app-navigation-personal .collapsible:before,.app-navigation-administration .collapsible:before{position:absolute;height:44px;width:44px;margin:0;padding:0;background:none;background-image:var(--icon-triangle-s-dark);background-size:16px;background-repeat:no-repeat;background-position:center;border:none;border-radius:0;outline:none !important;box-shadow:none;content:" ";opacity:0;-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);transform:rotate(-90deg);z-index:105;border-radius:50%;transition:opacity 100ms ease-in-out}.app-navigation-personal .collapsible>a:first-child,.app-navigation-administration .collapsible>a:first-child{padding-left:44px}.app-navigation-personal .collapsible:hover:before,.app-navigation-personal .collapsible:focus:before,.app-navigation-administration .collapsible:hover:before,.app-navigation-administration .collapsible:focus:before{opacity:1}.app-navigation-personal .collapsible:hover>a,.app-navigation-personal .collapsible:focus>a,.app-navigation-administration .collapsible:hover>a,.app-navigation-administration .collapsible:focus>a{background-image:none}.app-navigation-personal .collapsible:hover>.app-navigation-entry-bullet,.app-navigation-personal .collapsible:focus>.app-navigation-entry-bullet,.app-navigation-administration .collapsible:hover>.app-navigation-entry-bullet,.app-navigation-administration .collapsible:focus>.app-navigation-entry-bullet{background:rgba(0,0,0,0) !important}.app-navigation-personal .collapsible.open:before,.app-navigation-administration .collapsible.open:before{-webkit-transform:rotate(0);-ms-transform:rotate(0);transform:rotate(0)}.app-navigation-personal .app-navigation-entry-utils,.app-navigation-administration .app-navigation-entry-utils{flex:0 1 auto}.app-navigation-personal .app-navigation-entry-utils ul,.app-navigation-administration .app-navigation-entry-utils ul{display:flex !important;align-items:center;justify-content:flex-end}.app-navigation-personal .app-navigation-entry-utils li,.app-navigation-administration .app-navigation-entry-utils li{width:44px !important;height:44px}.app-navigation-personal .app-navigation-entry-utils button,.app-navigation-administration .app-navigation-entry-utils button{height:100%;width:100%;margin:0;box-shadow:none}.app-navigation-personal .app-navigation-entry-utils .app-navigation-entry-utils-menu-button button:not([class^=icon-]):not([class*=" icon-"]),.app-navigation-administration .app-navigation-entry-utils .app-navigation-entry-utils-menu-button button:not([class^=icon-]):not([class*=" icon-"]){background-image:var(--icon-more-dark)}.app-navigation-personal .app-navigation-entry-utils .app-navigation-entry-utils-menu-button:hover button,.app-navigation-personal .app-navigation-entry-utils .app-navigation-entry-utils-menu-button:focus button,.app-navigation-administration .app-navigation-entry-utils .app-navigation-entry-utils-menu-button:hover button,.app-navigation-administration .app-navigation-entry-utils .app-navigation-entry-utils-menu-button:focus button{background-color:rgba(0,0,0,0);opacity:1}.app-navigation-personal .app-navigation-entry-utils .app-navigation-entry-utils-counter,.app-navigation-administration .app-navigation-entry-utils .app-navigation-entry-utils-counter{overflow:hidden;text-align:right;font-size:9pt;line-height:44px;padding:0 12px}.app-navigation-personal .app-navigation-entry-utils .app-navigation-entry-utils-counter.highlighted,.app-navigation-administration .app-navigation-entry-utils .app-navigation-entry-utils-counter.highlighted{padding:0;text-align:center}.app-navigation-personal .app-navigation-entry-utils .app-navigation-entry-utils-counter.highlighted span,.app-navigation-administration .app-navigation-entry-utils .app-navigation-entry-utils-counter.highlighted span{padding:2px 5px;border-radius:10px;background-color:var(--color-primary-element);color:var(--color-primary-element-text)}.app-navigation-personal .app-navigation-entry-edit,.app-navigation-administration .app-navigation-entry-edit{padding-left:5px;padding-right:5px;display:block;width:calc(100% - 1px);transition:opacity 250ms ease-in-out;opacity:0;position:absolute;background-color:var(--color-main-background);z-index:-1}.app-navigation-personal .app-navigation-entry-edit form,.app-navigation-personal .app-navigation-entry-edit div,.app-navigation-administration .app-navigation-entry-edit form,.app-navigation-administration .app-navigation-entry-edit div{display:inline-flex;width:100%}.app-navigation-personal .app-navigation-entry-edit input,.app-navigation-administration .app-navigation-entry-edit input{padding:5px;margin-right:0;height:38px}.app-navigation-personal .app-navigation-entry-edit input:hover,.app-navigation-personal .app-navigation-entry-edit input:focus,.app-navigation-administration .app-navigation-entry-edit input:hover,.app-navigation-administration .app-navigation-entry-edit input:focus{z-index:1}.app-navigation-personal .app-navigation-entry-edit input[type=text],.app-navigation-administration .app-navigation-entry-edit input[type=text]{width:100%;min-width:0;border-bottom-right-radius:0;border-top-right-radius:0}.app-navigation-personal .app-navigation-entry-edit button,.app-navigation-personal .app-navigation-entry-edit input:not([type=text]),.app-navigation-administration .app-navigation-entry-edit button,.app-navigation-administration .app-navigation-entry-edit input:not([type=text]){width:36px;height:38px;flex:0 0 36px}.app-navigation-personal .app-navigation-entry-edit button:not(:last-child),.app-navigation-personal .app-navigation-entry-edit input:not([type=text]):not(:last-child),.app-navigation-administration .app-navigation-entry-edit button:not(:last-child),.app-navigation-administration .app-navigation-entry-edit input:not([type=text]):not(:last-child){border-radius:0 !important}.app-navigation-personal .app-navigation-entry-edit button:not(:first-child),.app-navigation-personal .app-navigation-entry-edit input:not([type=text]):not(:first-child),.app-navigation-administration .app-navigation-entry-edit button:not(:first-child),.app-navigation-administration .app-navigation-entry-edit input:not([type=text]):not(:first-child){margin-left:-1px}.app-navigation-personal .app-navigation-entry-edit button:last-child,.app-navigation-personal .app-navigation-entry-edit input:not([type=text]):last-child,.app-navigation-administration .app-navigation-entry-edit button:last-child,.app-navigation-administration .app-navigation-entry-edit input:not([type=text]):last-child{border-bottom-right-radius:var(--border-radius);border-top-right-radius:var(--border-radius);border-bottom-left-radius:0;border-top-left-radius:0}.app-navigation-personal .app-navigation-entry-deleted,.app-navigation-administration .app-navigation-entry-deleted{display:inline-flex;padding-left:44px;transform:translateX(300px)}.app-navigation-personal .app-navigation-entry-deleted .app-navigation-entry-deleted-description,.app-navigation-administration .app-navigation-entry-deleted .app-navigation-entry-deleted-description{position:relative;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;flex:1 1 0px;line-height:44px}.app-navigation-personal .app-navigation-entry-deleted .app-navigation-entry-deleted-button,.app-navigation-administration .app-navigation-entry-deleted .app-navigation-entry-deleted-button{margin:0;height:44px;width:44px;line-height:44px}.app-navigation-personal .app-navigation-entry-deleted .app-navigation-entry-deleted-button:hover,.app-navigation-personal .app-navigation-entry-deleted .app-navigation-entry-deleted-button:focus,.app-navigation-administration .app-navigation-entry-deleted .app-navigation-entry-deleted-button:hover,.app-navigation-administration .app-navigation-entry-deleted .app-navigation-entry-deleted-button:focus{opacity:1}.app-navigation-personal .app-navigation-entry-edit,.app-navigation-personal .app-navigation-entry-deleted,.app-navigation-administration .app-navigation-entry-edit,.app-navigation-administration .app-navigation-entry-deleted{width:calc(100% - 1px);transition:transform 250ms ease-in-out,opacity 250ms ease-in-out,z-index 250ms ease-in-out;position:absolute;left:0;background-color:var(--color-main-background);box-sizing:border-box}.app-navigation-personal .drag-and-drop,.app-navigation-administration .drag-and-drop{-webkit-transition:padding-bottom 500ms ease 0s;transition:padding-bottom 500ms ease 0s;padding-bottom:40px}.app-navigation-personal .error,.app-navigation-administration .error{color:var(--color-error)}.app-navigation-personal .app-navigation-entry-utils ul,.app-navigation-personal .app-navigation-entry-menu ul,.app-navigation-administration .app-navigation-entry-utils ul,.app-navigation-administration .app-navigation-entry-menu ul{list-style-type:none}#content{box-sizing:border-box;position:static;margin:var(--body-container-margin);margin-top:50px;padding:0;display:flex;width:calc(100% - var(--body-container-margin)*2);height:var(--body-height);border-radius:var(--body-container-radius);overflow:clip}#content:not(.with-sidebar--full){position:fixed}@media only screen and (max-width: 1024px){#content{border-top-left-radius:var(--border-radius-large);border-top-right-radius:var(--border-radius-large)}#app-navigation{border-top-left-radius:var(--border-radius-large)}#app-sidebar{border-top-right-radius:var(--border-radius-large)}}#app-content{z-index:1000;background-color:var(--color-main-background);flex-basis:100vw;overflow:auto;position:initial;height:100%}#app-content>.section:first-child{border-top:none}#app-content #app-content-wrapper{display:flex;position:relative;align-items:stretch;min-height:100%}#app-content #app-content-wrapper .app-content-details{flex:1 1 524px}#app-content #app-content-wrapper .app-content-details #app-navigation-toggle-back{display:none}#app-content::-webkit-scrollbar-button{height:var(--body-container-radius)}#app-sidebar{width:27vw;min-width:300px;max-width:500px;display:block;position:-webkit-sticky;position:sticky;top:50px;right:0;overflow-y:auto;overflow-x:hidden;z-index:1500;opacity:.7px;height:calc(100vh - 50px);background:var(--color-main-background);border-left:1px solid var(--color-border);flex-shrink:0}#app-sidebar.disappear{display:none}#app-settings{margin-top:auto}#app-settings.open #app-settings-content,#app-settings.opened #app-settings-content{display:block}#app-settings-content{display:none;padding:calc(var(--default-grid-baseline)*2);padding-top:0;padding-left:calc(var(--default-grid-baseline)*4);max-height:300px;overflow-y:auto;box-sizing:border-box}#app-settings-content input[type=text]{width:93%}#app-settings-content .info-text{padding:5px 0 7px 22px;color:var(--color-text-lighter)}#app-settings-content input[type=checkbox].radio+label,#app-settings-content input[type=checkbox].checkbox+label,#app-settings-content input[type=radio].radio+label,#app-settings-content input[type=radio].checkbox+label{display:inline-block;width:100%;padding:5px 0}#app-settings-header{box-sizing:border-box;background-color:rgba(0,0,0,0);overflow:hidden;border-radius:calc(var(--default-clickable-area)/2);padding:calc(var(--default-grid-baseline)*2);padding-top:0}#app-settings-header .settings-button{display:flex;align-items:center;height:44px;width:100%;padding:0;margin:0;background-color:rgba(0,0,0,0);box-shadow:none;border:0;border-radius:calc(var(--default-clickable-area)/2);text-align:left;font-weight:normal;font-size:100%;opacity:.8;color:var(--color-main-text)}#app-settings-header .settings-button.opened{border-top:solid 1px var(--color-border);background-color:var(--color-main-background);margin-top:8px}#app-settings-header .settings-button:hover,#app-settings-header .settings-button:focus{background-color:var(--color-background-hover)}#app-settings-header .settings-button::before{background-image:var(--icon-settings-dark);background-position:14px center;background-repeat:no-repeat;content:"";width:44px;height:44px;top:0;left:0;display:block}#app-settings-header .settings-button:focus-visible{box-shadow:0 0 0 2px inset var(--color-primary-element) !important;background-position:12px center}.section{display:block;padding:30px;margin-bottom:24px}.section.hidden{display:none !important}.section input[type=checkbox],.section input[type=radio]{vertical-align:-2px;margin-right:4px}.sub-section{position:relative;margin-top:10px;margin-left:27px;margin-bottom:10px}.appear{opacity:1;-webkit-transition:opacity 500ms ease 0s;-moz-transition:opacity 500ms ease 0s;-ms-transition:opacity 500ms ease 0s;-o-transition:opacity 500ms ease 0s;transition:opacity 500ms ease 0s}.appear.transparent{opacity:0}.tabHeaders{display:flex;margin-bottom:16px}.tabHeaders .tabHeader{display:flex;flex-direction:column;flex-grow:1;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;color:var(--color-text-lighter);margin-bottom:1px;padding:5px}.tabHeaders .tabHeader.hidden{display:none}.tabHeaders .tabHeader:first-child{padding-left:15px}.tabHeaders .tabHeader:last-child{padding-right:15px}.tabHeaders .tabHeader .icon{display:inline-block;width:100%;height:16px;background-size:16px;vertical-align:middle;margin-top:-2px;margin-right:3px;opacity:.7;cursor:pointer}.tabHeaders .tabHeader a{color:var(--color-text-lighter);margin-bottom:1px;overflow:hidden;text-overflow:ellipsis}.tabHeaders .tabHeader.selected{font-weight:bold}.tabHeaders .tabHeader.selected,.tabHeaders .tabHeader:hover,.tabHeaders .tabHeader:focus{margin-bottom:0px;color:var(--color-main-text);border-bottom:1px solid var(--color-text-lighter)}.tabsContainer{clear:left}.tabsContainer .tab{padding:0 15px 15px}.v-popper__inner div.open>ul>li>a>span.action-link__icon,.v-popper__inner div.open>ul>li>a>img{filter:var(--background-invert-if-dark)}.v-popper__inner div.open>ul>li>a>span.action-link__icon[src^=data],.v-popper__inner div.open>ul>li>a>img[src^=data]{filter:none}.bubble,.app-navigation-entry-menu,.popovermenu{position:absolute;background-color:var(--color-main-background);color:var(--color-main-text);border-radius:var(--border-radius-large);padding:3px;z-index:110;margin:5px;margin-top:-5px;right:0;filter:drop-shadow(0 1px 3px var(--color-box-shadow));display:none;will-change:filter}.bubble:after,.app-navigation-entry-menu:after,.popovermenu:after{bottom:100%;right:7px;border:solid rgba(0,0,0,0);content:" ";height:0;width:0;position:absolute;pointer-events:none;border-bottom-color:var(--color-main-background);border-width:9px}.bubble.menu-center,.app-navigation-entry-menu.menu-center,.popovermenu.menu-center{transform:translateX(50%);right:50%;margin-right:0}.bubble.menu-center:after,.app-navigation-entry-menu.menu-center:after,.popovermenu.menu-center:after{right:50%;transform:translateX(50%)}.bubble.menu-left,.app-navigation-entry-menu.menu-left,.popovermenu.menu-left{right:auto;left:0;margin-right:0}.bubble.menu-left:after,.app-navigation-entry-menu.menu-left:after,.popovermenu.menu-left:after{left:6px;right:auto}.bubble.open,.app-navigation-entry-menu.open,.popovermenu.open{display:block}.bubble.contactsmenu-popover,.app-navigation-entry-menu.contactsmenu-popover,.popovermenu.contactsmenu-popover{margin:0}.bubble ul,.app-navigation-entry-menu ul,.popovermenu ul{display:flex !important;flex-direction:column}.bubble li,.app-navigation-entry-menu li,.popovermenu li{display:flex;flex:0 0 auto}.bubble li.hidden,.app-navigation-entry-menu li.hidden,.popovermenu li.hidden{display:none}.bubble li>button,.bubble li>a,.bubble li>.menuitem,.app-navigation-entry-menu li>button,.app-navigation-entry-menu li>a,.app-navigation-entry-menu li>.menuitem,.popovermenu li>button,.popovermenu li>a,.popovermenu li>.menuitem{cursor:pointer;line-height:44px;border:0;border-radius:var(--border-radius-large);background-color:rgba(0,0,0,0);display:flex;align-items:flex-start;height:auto;margin:0;font-weight:normal;box-shadow:none;width:100%;color:var(--color-main-text);white-space:nowrap}.bubble li>button span[class^=icon-],.bubble li>button span[class*=" icon-"],.bubble li>button[class^=icon-],.bubble li>button[class*=" icon-"],.bubble li>a span[class^=icon-],.bubble li>a span[class*=" icon-"],.bubble li>a[class^=icon-],.bubble li>a[class*=" icon-"],.bubble li>.menuitem span[class^=icon-],.bubble li>.menuitem span[class*=" icon-"],.bubble li>.menuitem[class^=icon-],.bubble li>.menuitem[class*=" icon-"],.app-navigation-entry-menu li>button span[class^=icon-],.app-navigation-entry-menu li>button span[class*=" icon-"],.app-navigation-entry-menu li>button[class^=icon-],.app-navigation-entry-menu li>button[class*=" icon-"],.app-navigation-entry-menu li>a span[class^=icon-],.app-navigation-entry-menu li>a span[class*=" icon-"],.app-navigation-entry-menu li>a[class^=icon-],.app-navigation-entry-menu li>a[class*=" icon-"],.app-navigation-entry-menu li>.menuitem span[class^=icon-],.app-navigation-entry-menu li>.menuitem span[class*=" icon-"],.app-navigation-entry-menu li>.menuitem[class^=icon-],.app-navigation-entry-menu li>.menuitem[class*=" icon-"],.popovermenu li>button span[class^=icon-],.popovermenu li>button span[class*=" icon-"],.popovermenu li>button[class^=icon-],.popovermenu li>button[class*=" icon-"],.popovermenu li>a span[class^=icon-],.popovermenu li>a span[class*=" icon-"],.popovermenu li>a[class^=icon-],.popovermenu li>a[class*=" icon-"],.popovermenu li>.menuitem span[class^=icon-],.popovermenu li>.menuitem span[class*=" icon-"],.popovermenu li>.menuitem[class^=icon-],.popovermenu li>.menuitem[class*=" icon-"]{min-width:0;min-height:0;background-position:14px center;background-size:16px}.bubble li>button span[class^=icon-],.bubble li>button span[class*=" icon-"],.bubble li>a span[class^=icon-],.bubble li>a span[class*=" icon-"],.bubble li>.menuitem span[class^=icon-],.bubble li>.menuitem span[class*=" icon-"],.app-navigation-entry-menu li>button span[class^=icon-],.app-navigation-entry-menu li>button span[class*=" icon-"],.app-navigation-entry-menu li>a span[class^=icon-],.app-navigation-entry-menu li>a span[class*=" icon-"],.app-navigation-entry-menu li>.menuitem span[class^=icon-],.app-navigation-entry-menu li>.menuitem span[class*=" icon-"],.popovermenu li>button span[class^=icon-],.popovermenu li>button span[class*=" icon-"],.popovermenu li>a span[class^=icon-],.popovermenu li>a span[class*=" icon-"],.popovermenu li>.menuitem span[class^=icon-],.popovermenu li>.menuitem span[class*=" icon-"]{padding:22px 0 22px 44px}.bubble li>button:not([class^=icon-]):not([class*=icon-])>span:not([class^=icon-]):not([class*=icon-]):first-child,.bubble li>button:not([class^=icon-]):not([class*=icon-])>input:not([class^=icon-]):not([class*=icon-]):first-child,.bubble li>button:not([class^=icon-]):not([class*=icon-])>form:not([class^=icon-]):not([class*=icon-]):first-child,.bubble li>a:not([class^=icon-]):not([class*=icon-])>span:not([class^=icon-]):not([class*=icon-]):first-child,.bubble li>a:not([class^=icon-]):not([class*=icon-])>input:not([class^=icon-]):not([class*=icon-]):first-child,.bubble li>a:not([class^=icon-]):not([class*=icon-])>form:not([class^=icon-]):not([class*=icon-]):first-child,.bubble li>.menuitem:not([class^=icon-]):not([class*=icon-])>span:not([class^=icon-]):not([class*=icon-]):first-child,.bubble li>.menuitem:not([class^=icon-]):not([class*=icon-])>input:not([class^=icon-]):not([class*=icon-]):first-child,.bubble li>.menuitem:not([class^=icon-]):not([class*=icon-])>form:not([class^=icon-]):not([class*=icon-]):first-child,.app-navigation-entry-menu li>button:not([class^=icon-]):not([class*=icon-])>span:not([class^=icon-]):not([class*=icon-]):first-child,.app-navigation-entry-menu li>button:not([class^=icon-]):not([class*=icon-])>input:not([class^=icon-]):not([class*=icon-]):first-child,.app-navigation-entry-menu li>button:not([class^=icon-]):not([class*=icon-])>form:not([class^=icon-]):not([class*=icon-]):first-child,.app-navigation-entry-menu li>a:not([class^=icon-]):not([class*=icon-])>span:not([class^=icon-]):not([class*=icon-]):first-child,.app-navigation-entry-menu li>a:not([class^=icon-]):not([class*=icon-])>input:not([class^=icon-]):not([class*=icon-]):first-child,.app-navigation-entry-menu li>a:not([class^=icon-]):not([class*=icon-])>form:not([class^=icon-]):not([class*=icon-]):first-child,.app-navigation-entry-menu li>.menuitem:not([class^=icon-]):not([class*=icon-])>span:not([class^=icon-]):not([class*=icon-]):first-child,.app-navigation-entry-menu li>.menuitem:not([class^=icon-]):not([class*=icon-])>input:not([class^=icon-]):not([class*=icon-]):first-child,.app-navigation-entry-menu li>.menuitem:not([class^=icon-]):not([class*=icon-])>form:not([class^=icon-]):not([class*=icon-]):first-child,.popovermenu li>button:not([class^=icon-]):not([class*=icon-])>span:not([class^=icon-]):not([class*=icon-]):first-child,.popovermenu li>button:not([class^=icon-]):not([class*=icon-])>input:not([class^=icon-]):not([class*=icon-]):first-child,.popovermenu li>button:not([class^=icon-]):not([class*=icon-])>form:not([class^=icon-]):not([class*=icon-]):first-child,.popovermenu li>a:not([class^=icon-]):not([class*=icon-])>span:not([class^=icon-]):not([class*=icon-]):first-child,.popovermenu li>a:not([class^=icon-]):not([class*=icon-])>input:not([class^=icon-]):not([class*=icon-]):first-child,.popovermenu li>a:not([class^=icon-]):not([class*=icon-])>form:not([class^=icon-]):not([class*=icon-]):first-child,.popovermenu li>.menuitem:not([class^=icon-]):not([class*=icon-])>span:not([class^=icon-]):not([class*=icon-]):first-child,.popovermenu li>.menuitem:not([class^=icon-]):not([class*=icon-])>input:not([class^=icon-]):not([class*=icon-]):first-child,.popovermenu li>.menuitem:not([class^=icon-]):not([class*=icon-])>form:not([class^=icon-]):not([class*=icon-]):first-child{margin-left:44px}.bubble li>button[class^=icon-],.bubble li>button[class*=" icon-"],.bubble li>a[class^=icon-],.bubble li>a[class*=" icon-"],.bubble li>.menuitem[class^=icon-],.bubble li>.menuitem[class*=" icon-"],.app-navigation-entry-menu li>button[class^=icon-],.app-navigation-entry-menu li>button[class*=" icon-"],.app-navigation-entry-menu li>a[class^=icon-],.app-navigation-entry-menu li>a[class*=" icon-"],.app-navigation-entry-menu li>.menuitem[class^=icon-],.app-navigation-entry-menu li>.menuitem[class*=" icon-"],.popovermenu li>button[class^=icon-],.popovermenu li>button[class*=" icon-"],.popovermenu li>a[class^=icon-],.popovermenu li>a[class*=" icon-"],.popovermenu li>.menuitem[class^=icon-],.popovermenu li>.menuitem[class*=" icon-"]{padding:0 14px 0 44px !important}.bubble li>button:hover,.bubble li>button:focus,.bubble li>a:hover,.bubble li>a:focus,.bubble li>.menuitem:hover,.bubble li>.menuitem:focus,.app-navigation-entry-menu li>button:hover,.app-navigation-entry-menu li>button:focus,.app-navigation-entry-menu li>a:hover,.app-navigation-entry-menu li>a:focus,.app-navigation-entry-menu li>.menuitem:hover,.app-navigation-entry-menu li>.menuitem:focus,.popovermenu li>button:hover,.popovermenu li>button:focus,.popovermenu li>a:hover,.popovermenu li>a:focus,.popovermenu li>.menuitem:hover,.popovermenu li>.menuitem:focus{background-color:var(--color-background-hover)}.bubble li>button:focus,.bubble li>button:focus-visible,.bubble li>a:focus,.bubble li>a:focus-visible,.bubble li>.menuitem:focus,.bubble li>.menuitem:focus-visible,.app-navigation-entry-menu li>button:focus,.app-navigation-entry-menu li>button:focus-visible,.app-navigation-entry-menu li>a:focus,.app-navigation-entry-menu li>a:focus-visible,.app-navigation-entry-menu li>.menuitem:focus,.app-navigation-entry-menu li>.menuitem:focus-visible,.popovermenu li>button:focus,.popovermenu li>button:focus-visible,.popovermenu li>a:focus,.popovermenu li>a:focus-visible,.popovermenu li>.menuitem:focus,.popovermenu li>.menuitem:focus-visible{box-shadow:0 0 0 2px var(--color-primary-element)}.bubble li>button.active,.bubble li>a.active,.bubble li>.menuitem.active,.app-navigation-entry-menu li>button.active,.app-navigation-entry-menu li>a.active,.app-navigation-entry-menu li>.menuitem.active,.popovermenu li>button.active,.popovermenu li>a.active,.popovermenu li>.menuitem.active{border-radius:var(--border-radius-pill);background-color:var(--color-primary-element-light)}.bubble li>button.action,.bubble li>a.action,.bubble li>.menuitem.action,.app-navigation-entry-menu li>button.action,.app-navigation-entry-menu li>a.action,.app-navigation-entry-menu li>.menuitem.action,.popovermenu li>button.action,.popovermenu li>a.action,.popovermenu li>.menuitem.action{padding:inherit !important}.bubble li>button>span,.bubble li>a>span,.bubble li>.menuitem>span,.app-navigation-entry-menu li>button>span,.app-navigation-entry-menu li>a>span,.app-navigation-entry-menu li>.menuitem>span,.popovermenu li>button>span,.popovermenu li>a>span,.popovermenu li>.menuitem>span{cursor:pointer;white-space:nowrap}.bubble li>button>p,.bubble li>a>p,.bubble li>.menuitem>p,.app-navigation-entry-menu li>button>p,.app-navigation-entry-menu li>a>p,.app-navigation-entry-menu li>.menuitem>p,.popovermenu li>button>p,.popovermenu li>a>p,.popovermenu li>.menuitem>p{width:150px;line-height:1.6em;padding:8px 0;white-space:normal}.bubble li>button>select,.bubble li>a>select,.bubble li>.menuitem>select,.app-navigation-entry-menu li>button>select,.app-navigation-entry-menu li>a>select,.app-navigation-entry-menu li>.menuitem>select,.popovermenu li>button>select,.popovermenu li>a>select,.popovermenu li>.menuitem>select{margin:0;margin-left:6px}.bubble li>button:not(:empty),.bubble li>a:not(:empty),.bubble li>.menuitem:not(:empty),.app-navigation-entry-menu li>button:not(:empty),.app-navigation-entry-menu li>a:not(:empty),.app-navigation-entry-menu li>.menuitem:not(:empty),.popovermenu li>button:not(:empty),.popovermenu li>a:not(:empty),.popovermenu li>.menuitem:not(:empty){padding-right:14px !important}.bubble li>button>img,.bubble li>a>img,.bubble li>.menuitem>img,.app-navigation-entry-menu li>button>img,.app-navigation-entry-menu li>a>img,.app-navigation-entry-menu li>.menuitem>img,.popovermenu li>button>img,.popovermenu li>a>img,.popovermenu li>.menuitem>img{width:16px;padding:14px}.bubble li>button>input.radio+label,.bubble li>button>input.checkbox+label,.bubble li>a>input.radio+label,.bubble li>a>input.checkbox+label,.bubble li>.menuitem>input.radio+label,.bubble li>.menuitem>input.checkbox+label,.app-navigation-entry-menu li>button>input.radio+label,.app-navigation-entry-menu li>button>input.checkbox+label,.app-navigation-entry-menu li>a>input.radio+label,.app-navigation-entry-menu li>a>input.checkbox+label,.app-navigation-entry-menu li>.menuitem>input.radio+label,.app-navigation-entry-menu li>.menuitem>input.checkbox+label,.popovermenu li>button>input.radio+label,.popovermenu li>button>input.checkbox+label,.popovermenu li>a>input.radio+label,.popovermenu li>a>input.checkbox+label,.popovermenu li>.menuitem>input.radio+label,.popovermenu li>.menuitem>input.checkbox+label{padding:0 !important;width:100%}.bubble li>button>input.checkbox+label::before,.bubble li>a>input.checkbox+label::before,.bubble li>.menuitem>input.checkbox+label::before,.app-navigation-entry-menu li>button>input.checkbox+label::before,.app-navigation-entry-menu li>a>input.checkbox+label::before,.app-navigation-entry-menu li>.menuitem>input.checkbox+label::before,.popovermenu li>button>input.checkbox+label::before,.popovermenu li>a>input.checkbox+label::before,.popovermenu li>.menuitem>input.checkbox+label::before{margin:-2px 13px 0}.bubble li>button>input.radio+label::before,.bubble li>a>input.radio+label::before,.bubble li>.menuitem>input.radio+label::before,.app-navigation-entry-menu li>button>input.radio+label::before,.app-navigation-entry-menu li>a>input.radio+label::before,.app-navigation-entry-menu li>.menuitem>input.radio+label::before,.popovermenu li>button>input.radio+label::before,.popovermenu li>a>input.radio+label::before,.popovermenu li>.menuitem>input.radio+label::before{margin:-2px 12px 0}.bubble li>button>input:not([type=radio]):not([type=checkbox]):not([type=image]),.bubble li>a>input:not([type=radio]):not([type=checkbox]):not([type=image]),.bubble li>.menuitem>input:not([type=radio]):not([type=checkbox]):not([type=image]),.app-navigation-entry-menu li>button>input:not([type=radio]):not([type=checkbox]):not([type=image]),.app-navigation-entry-menu li>a>input:not([type=radio]):not([type=checkbox]):not([type=image]),.app-navigation-entry-menu li>.menuitem>input:not([type=radio]):not([type=checkbox]):not([type=image]),.popovermenu li>button>input:not([type=radio]):not([type=checkbox]):not([type=image]),.popovermenu li>a>input:not([type=radio]):not([type=checkbox]):not([type=image]),.popovermenu li>.menuitem>input:not([type=radio]):not([type=checkbox]):not([type=image]){width:150px}.bubble li>button form,.bubble li>a form,.bubble li>.menuitem form,.app-navigation-entry-menu li>button form,.app-navigation-entry-menu li>a form,.app-navigation-entry-menu li>.menuitem form,.popovermenu li>button form,.popovermenu li>a form,.popovermenu li>.menuitem form{display:flex;flex:1 1 auto;align-items:center}.bubble li>button form:not(:first-child),.bubble li>a form:not(:first-child),.bubble li>.menuitem form:not(:first-child),.app-navigation-entry-menu li>button form:not(:first-child),.app-navigation-entry-menu li>a form:not(:first-child),.app-navigation-entry-menu li>.menuitem form:not(:first-child),.popovermenu li>button form:not(:first-child),.popovermenu li>a form:not(:first-child),.popovermenu li>.menuitem form:not(:first-child){margin-left:5px}.bubble li>button>span.hidden+form,.bubble li>button>span[style*="display:none"]+form,.bubble li>a>span.hidden+form,.bubble li>a>span[style*="display:none"]+form,.bubble li>.menuitem>span.hidden+form,.bubble li>.menuitem>span[style*="display:none"]+form,.app-navigation-entry-menu li>button>span.hidden+form,.app-navigation-entry-menu li>button>span[style*="display:none"]+form,.app-navigation-entry-menu li>a>span.hidden+form,.app-navigation-entry-menu li>a>span[style*="display:none"]+form,.app-navigation-entry-menu li>.menuitem>span.hidden+form,.app-navigation-entry-menu li>.menuitem>span[style*="display:none"]+form,.popovermenu li>button>span.hidden+form,.popovermenu li>button>span[style*="display:none"]+form,.popovermenu li>a>span.hidden+form,.popovermenu li>a>span[style*="display:none"]+form,.popovermenu li>.menuitem>span.hidden+form,.popovermenu li>.menuitem>span[style*="display:none"]+form{margin-left:0}.bubble li>button input,.bubble li>a input,.bubble li>.menuitem input,.app-navigation-entry-menu li>button input,.app-navigation-entry-menu li>a input,.app-navigation-entry-menu li>.menuitem input,.popovermenu li>button input,.popovermenu li>a input,.popovermenu li>.menuitem input{min-width:44px;max-height:40px;margin:2px 0;flex:1 1 auto}.bubble li>button input:not(:first-child),.bubble li>a input:not(:first-child),.bubble li>.menuitem input:not(:first-child),.app-navigation-entry-menu li>button input:not(:first-child),.app-navigation-entry-menu li>a input:not(:first-child),.app-navigation-entry-menu li>.menuitem input:not(:first-child),.popovermenu li>button input:not(:first-child),.popovermenu li>a input:not(:first-child),.popovermenu li>.menuitem input:not(:first-child){margin-left:5px}.bubble li:not(.hidden):not([style*="display:none"]):first-of-type>button>form,.bubble li:not(.hidden):not([style*="display:none"]):first-of-type>button>input,.bubble li:not(.hidden):not([style*="display:none"]):first-of-type>a>form,.bubble li:not(.hidden):not([style*="display:none"]):first-of-type>a>input,.bubble li:not(.hidden):not([style*="display:none"]):first-of-type>.menuitem>form,.bubble li:not(.hidden):not([style*="display:none"]):first-of-type>.menuitem>input,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):first-of-type>button>form,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):first-of-type>button>input,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):first-of-type>a>form,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):first-of-type>a>input,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):first-of-type>.menuitem>form,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):first-of-type>.menuitem>input,.popovermenu li:not(.hidden):not([style*="display:none"]):first-of-type>button>form,.popovermenu li:not(.hidden):not([style*="display:none"]):first-of-type>button>input,.popovermenu li:not(.hidden):not([style*="display:none"]):first-of-type>a>form,.popovermenu li:not(.hidden):not([style*="display:none"]):first-of-type>a>input,.popovermenu li:not(.hidden):not([style*="display:none"]):first-of-type>.menuitem>form,.popovermenu li:not(.hidden):not([style*="display:none"]):first-of-type>.menuitem>input{margin-top:12px}.bubble li:not(.hidden):not([style*="display:none"]):last-of-type>button>form,.bubble li:not(.hidden):not([style*="display:none"]):last-of-type>button>input,.bubble li:not(.hidden):not([style*="display:none"]):last-of-type>a>form,.bubble li:not(.hidden):not([style*="display:none"]):last-of-type>a>input,.bubble li:not(.hidden):not([style*="display:none"]):last-of-type>.menuitem>form,.bubble li:not(.hidden):not([style*="display:none"]):last-of-type>.menuitem>input,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):last-of-type>button>form,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):last-of-type>button>input,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):last-of-type>a>form,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):last-of-type>a>input,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):last-of-type>.menuitem>form,.app-navigation-entry-menu li:not(.hidden):not([style*="display:none"]):last-of-type>.menuitem>input,.popovermenu li:not(.hidden):not([style*="display:none"]):last-of-type>button>form,.popovermenu li:not(.hidden):not([style*="display:none"]):last-of-type>button>input,.popovermenu li:not(.hidden):not([style*="display:none"]):last-of-type>a>form,.popovermenu li:not(.hidden):not([style*="display:none"]):last-of-type>a>input,.popovermenu li:not(.hidden):not([style*="display:none"]):last-of-type>.menuitem>form,.popovermenu li:not(.hidden):not([style*="display:none"]):last-of-type>.menuitem>input{margin-bottom:0px}.bubble li>button,.app-navigation-entry-menu li>button,.popovermenu li>button{padding:0}.bubble li>button span,.app-navigation-entry-menu li>button span,.popovermenu li>button span{opacity:1}.popovermenu li>button>img,.popovermenu li>a>img,.popovermenu li>.menuitem>img{width:44px;height:44px}#contactsmenu .contact .popovermenu li>a>img{width:16px;height:16px}.app-content-list{position:-webkit-sticky;position:relative;top:0;border-right:1px solid var(--color-border);display:flex;flex-direction:column;transition:transform 250ms ease-in-out;min-height:100%;max-height:100%;overflow-y:auto;overflow-x:hidden;flex:1 1 200px;min-width:200px;max-width:300px}.app-content-list .app-content-list-item{position:relative;height:68px;cursor:pointer;padding:10px 7px;display:flex;flex-wrap:wrap;align-items:center;flex:0 0 auto}.app-content-list .app-content-list-item>[class^=icon-],.app-content-list .app-content-list-item>[class*=" icon-"],.app-content-list .app-content-list-item>.app-content-list-item-menu>[class^=icon-],.app-content-list .app-content-list-item>.app-content-list-item-menu>[class*=" icon-"]{order:4;width:24px;height:24px;margin:-7px;padding:22px;opacity:.3;cursor:pointer}.app-content-list .app-content-list-item>[class^=icon-]:hover,.app-content-list .app-content-list-item>[class^=icon-]:focus,.app-content-list .app-content-list-item>[class*=" icon-"]:hover,.app-content-list .app-content-list-item>[class*=" icon-"]:focus,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class^=icon-]:hover,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class^=icon-]:focus,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class*=" icon-"]:hover,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class*=" icon-"]:focus{opacity:.7}.app-content-list .app-content-list-item>[class^=icon-][class^=icon-star],.app-content-list .app-content-list-item>[class^=icon-][class*=" icon-star"],.app-content-list .app-content-list-item>[class*=" icon-"][class^=icon-star],.app-content-list .app-content-list-item>[class*=" icon-"][class*=" icon-star"],.app-content-list .app-content-list-item>.app-content-list-item-menu>[class^=icon-][class^=icon-star],.app-content-list .app-content-list-item>.app-content-list-item-menu>[class^=icon-][class*=" icon-star"],.app-content-list .app-content-list-item>.app-content-list-item-menu>[class*=" icon-"][class^=icon-star],.app-content-list .app-content-list-item>.app-content-list-item-menu>[class*=" icon-"][class*=" icon-star"]{opacity:.7}.app-content-list .app-content-list-item>[class^=icon-][class^=icon-star]:hover,.app-content-list .app-content-list-item>[class^=icon-][class^=icon-star]:focus,.app-content-list .app-content-list-item>[class^=icon-][class*=" icon-star"]:hover,.app-content-list .app-content-list-item>[class^=icon-][class*=" icon-star"]:focus,.app-content-list .app-content-list-item>[class*=" icon-"][class^=icon-star]:hover,.app-content-list .app-content-list-item>[class*=" icon-"][class^=icon-star]:focus,.app-content-list .app-content-list-item>[class*=" icon-"][class*=" icon-star"]:hover,.app-content-list .app-content-list-item>[class*=" icon-"][class*=" icon-star"]:focus,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class^=icon-][class^=icon-star]:hover,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class^=icon-][class^=icon-star]:focus,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class^=icon-][class*=" icon-star"]:hover,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class^=icon-][class*=" icon-star"]:focus,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class*=" icon-"][class^=icon-star]:hover,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class*=" icon-"][class^=icon-star]:focus,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class*=" icon-"][class*=" icon-star"]:hover,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class*=" icon-"][class*=" icon-star"]:focus{opacity:1}.app-content-list .app-content-list-item>[class^=icon-].icon-starred,.app-content-list .app-content-list-item>[class*=" icon-"].icon-starred,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class^=icon-].icon-starred,.app-content-list .app-content-list-item>.app-content-list-item-menu>[class*=" icon-"].icon-starred{opacity:1}.app-content-list .app-content-list-item:hover,.app-content-list .app-content-list-item:focus,.app-content-list .app-content-list-item.active{background-color:var(--color-background-dark)}.app-content-list .app-content-list-item:hover .app-content-list-item-checkbox.checkbox+label,.app-content-list .app-content-list-item:focus .app-content-list-item-checkbox.checkbox+label,.app-content-list .app-content-list-item.active .app-content-list-item-checkbox.checkbox+label{display:flex}.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox+label,.app-content-list .app-content-list-item .app-content-list-item-star{position:absolute;height:40px;width:40px;z-index:50}.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox:checked+label,.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox:hover+label,.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox:focus+label,.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox.active+label{display:flex}.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox:checked+label+.app-content-list-item-icon,.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox:hover+label+.app-content-list-item-icon,.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox:focus+label+.app-content-list-item-icon,.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox.active+label+.app-content-list-item-icon{opacity:.7}.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox+label{top:14px;left:7px;display:none}.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox+label::before{margin:0}.app-content-list .app-content-list-item .app-content-list-item-checkbox.checkbox+label~.app-content-list-item-star{display:none}.app-content-list .app-content-list-item .app-content-list-item-star{display:flex;top:10px;left:32px;background-size:16px;height:20px;width:20px;margin:0;padding:0}.app-content-list .app-content-list-item .app-content-list-item-icon{position:absolute;display:inline-block;height:40px;width:40px;line-height:40px;border-radius:50%;vertical-align:middle;margin-right:10px;color:#fff;text-align:center;font-size:1.5em;text-transform:capitalize;object-fit:cover;user-select:none;cursor:pointer;top:50%;margin-top:-20px}.app-content-list .app-content-list-item .app-content-list-item-line-one,.app-content-list .app-content-list-item .app-content-list-item-line-two{display:block;padding-left:50px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;order:1;flex:1 1 0px;padding-right:10px;cursor:pointer}.app-content-list .app-content-list-item .app-content-list-item-line-two{opacity:.5;order:3;flex:1 0;flex-basis:calc(100% - 44px)}.app-content-list .app-content-list-item .app-content-list-item-details{order:2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100px;opacity:.5;font-size:80%;user-select:none}.app-content-list .app-content-list-item .app-content-list-item-menu{order:4;position:relative}.app-content-list .app-content-list-item .app-content-list-item-menu .popovermenu{margin:0;right:-2px}.app-content-list.selection .app-content-list-item-checkbox.checkbox+label{display:flex}.button.primary.skip-navigation:focus-visible{box-shadow:0 0 0 4px var(--color-main-background) !important;outline:2px solid var(--color-main-text) !important}/*# sourceMappingURL=apps.css.map */
diff --git a/tests/component/components/NcAppNavigationItem/AppNavigation.story.vue b/tests/component/components/NcAppNavigationItem/AppNavigation.story.vue
new file mode 100644
index 0000000000..1d3d0b201e
--- /dev/null
+++ b/tests/component/components/NcAppNavigationItem/AppNavigation.story.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/component/components/NcAppNavigationItem/visual.spec.ts b/tests/component/components/NcAppNavigationItem/visual.spec.ts
new file mode 100644
index 0000000000..7b170e8dda
--- /dev/null
+++ b/tests/component/components/NcAppNavigationItem/visual.spec.ts
@@ -0,0 +1,83 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { HooksConfig } from '../../setup/index'
+import { expect, test } from '@playwright/experimental-ct-vue'
+
+import AppNavigation from './AppNavigation.story.vue'
+
+test.skip(({ browserName }) => browserName !== 'chromium')
+
+// A little bit hacky but we test a wrapper element so we need to use the real NcContent and NcAppNavigation
+test.beforeEach(async ({ mount, page }) => {
+ const handle = await page.locator('#app-content').elementHandle()
+ expect(handle).not.toBeNull()
+ await handle!.evaluate((node) => { node.innerHTML = ''; node.id = 'root' })
+
+ await mount
(AppNavigation, {
+ hooksConfig: {
+ routes: [
+ { path: '/', component: AppNavigation },
+ { path: '/foo', component: AppNavigation },
+ ],
+ },
+ })
+})
+
+test('has primary styling on active route', { tag: '@visual' }, async ({ page }) => {
+ const navigation = page.getByRole('navigation', { name: 'In-app navigation' })
+
+ await expect(navigation).toBeVisible()
+ await expect(navigation.getByRole('listitem').filter({ hasText: 'Home' })).toHaveScreenshot()
+})
+
+test('has primary button styling on active route with editing=true', { tag: '@visual' }, async ({ page }) => {
+ const navigation = page.getByRole('navigation', { name: 'In-app navigation' })
+ const item = navigation.getByRole('listitem').filter({ hasText: 'Home' })
+
+ await expect(navigation).toBeVisible()
+ await expect(item).toBeVisible()
+
+ await item.getByRole('button', { name: 'Edit item' }).click()
+ await expect(item.getByRole('textbox')).toBeVisible()
+
+ await expect(item).toHaveScreenshot()
+})
+
+test('has tertiary styling on non active route', { tag: '@visual' }, async ({ page }) => {
+ const navigation = page.getByRole('navigation', { name: 'In-app navigation' })
+ const item = navigation.getByRole('listitem').filter({ hasText: 'Foo' })
+
+ await expect(navigation).toBeVisible()
+ await expect(item).toHaveScreenshot()
+})
+
+test('has primary styling on active entry', { tag: '@visual' }, async ({ page }) => {
+ const navigation = page.getByRole('navigation', { name: 'In-app navigation' })
+ const item = navigation.getByRole('listitem').filter({ hasText: 'Back' })
+
+ await expect(navigation).toBeVisible()
+ await expect(item).toHaveScreenshot()
+})
+
+test('has primary button styling on active entry with editing=true', { tag: '@visual' }, async ({ page }) => {
+ const navigation = page.getByRole('navigation', { name: 'In-app navigation' })
+ const item = navigation.getByRole('listitem').filter({ hasText: 'Back' })
+
+ await expect(navigation).toBeVisible()
+ await expect(item).toBeVisible()
+
+ await item.getByRole('button', { name: 'Edit item' }).click()
+ await expect(item.getByRole('textbox')).toBeVisible()
+
+ await expect(item).toHaveScreenshot()
+})
+
+test('has tertiary styling on non active entry', { tag: '@visual' }, async ({ page }) => {
+ const navigation = page.getByRole('navigation', { name: 'In-app navigation' })
+ const item = navigation.getByRole('listitem').filter({ hasText: 'Bar' })
+
+ await expect(navigation).toBeVisible()
+ await expect(item).toHaveScreenshot()
+})
diff --git a/tests/component/components/NcAppNavigationSpacer/NcAppNavigationSpacer.spec.ts b/tests/component/components/NcAppNavigationSpacer/NcAppNavigationSpacer.spec.ts
new file mode 100644
index 0000000000..628d54559a
--- /dev/null
+++ b/tests/component/components/NcAppNavigationSpacer/NcAppNavigationSpacer.spec.ts
@@ -0,0 +1,23 @@
+/**
+ * SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { expect, test } from '@playwright/experimental-ct-vue'
+import SpacedAppNaviation from './SpacedAppNavigation.story.vue'
+
+test('it correctly spaces two elements', async ({ mount, page }) => {
+ await mount(SpacedAppNaviation)
+
+ const first = page.getByRole('link', { name: 'First' })
+ const second = page.getByRole('link', { name: 'Second' })
+
+ await expect(first).toBeVisible()
+ await expect(second).toBeVisible()
+
+ const firstRect = await first.boundingBox()
+ const secondRect = await second.boundingBox()
+
+ // Check that the second element is at least 17px below the first one (thats our spacer)
+ expect(secondRect!.y - 17).toBeGreaterThanOrEqual(firstRect!.y + firstRect!.height)
+})
diff --git a/tests/component/components/NcAppNavigationSpacer/SpacedAppNavigation.story.vue b/tests/component/components/NcAppNavigationSpacer/SpacedAppNavigation.story.vue
new file mode 100644
index 0000000000..2faedb5d76
--- /dev/null
+++ b/tests/component/components/NcAppNavigationSpacer/SpacedAppNavigation.story.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/component/components/NcAppSettingsDialog/AppSettings.story.vue b/tests/component/components/NcAppSettingsDialog/AppSettings.story.vue
new file mode 100644
index 0000000000..18039d23dd
--- /dev/null
+++ b/tests/component/components/NcAppSettingsDialog/AppSettings.story.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+ First content
+
+
+ Second content
+
+
+
+
+
diff --git a/tests/component/components/NcAppSettingsDialog/NcAppSettingsDialog.spec.ts b/tests/component/components/NcAppSettingsDialog/NcAppSettingsDialog.spec.ts
new file mode 100644
index 0000000000..c1bc25da6c
--- /dev/null
+++ b/tests/component/components/NcAppSettingsDialog/NcAppSettingsDialog.spec.ts
@@ -0,0 +1,54 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { expect, test } from '@playwright/experimental-ct-vue'
+
+import AppSettings from './AppSettings.story.vue'
+
+test('Dialog has visible headline', async ({ mount, page }) => {
+ await mount(AppSettings)
+
+ const dialog = page.getByRole('dialog')
+ await expect(dialog).toBeVisible()
+ await expect(dialog.getByRole('heading', { level: 2 })).toBeVisible()
+ await expect(dialog.getByRole('heading', { level: 2 })).toHaveText('Settings dialog')
+})
+
+test('Dialog is correctly labelled', async ({ mount, page }) => {
+ await mount(AppSettings)
+
+ const dialog = page.getByRole('dialog')
+ await expect(dialog).toBeVisible()
+ await expect(dialog).toHaveAccessibleName('Settings dialog')
+})
+
+test('Dialog sections have navigation entries', async ({ mount, page }) => {
+ await mount(AppSettings)
+
+ const dialog = page.getByRole('dialog')
+ await expect(dialog).toBeVisible()
+
+ const navigation = dialog.getByRole('navigation')
+ await expect(navigation).toBeVisible()
+ await expect(navigation.getByRole('link', { name: 'First section' })).toBeVisible()
+ await expect(navigation.getByRole('link', { name: 'Second section' })).toBeVisible()
+})
+
+test('Dialog sections are correctly labelled', async ({ mount, page }) => {
+ await mount(AppSettings)
+
+ const dialog = page.getByRole('dialog')
+ await expect(dialog).toBeVisible()
+
+ const firstSection = dialog.getByRole('region', { name: 'First section' })
+ await expect(firstSection).toHaveCount(1)
+ await expect(firstSection).toContainText('First content')
+ await expect(firstSection.getByRole('heading')).toHaveText('First section')
+
+ const secondSection = dialog.getByRole('region', { name: 'Second section' })
+ await expect(secondSection).toHaveCount(1)
+ await expect(secondSection).toContainText('Second content')
+ await expect(secondSection.getByRole('heading')).toHaveText('Second section')
+})
diff --git a/tests/component/components/NcAppSidebar/AppSidebar.story.vue b/tests/component/components/NcAppSidebar/AppSidebar.story.vue
new file mode 100644
index 0000000000..d1c606d4da
--- /dev/null
+++ b/tests/component/components/NcAppSidebar/AppSidebar.story.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+ App content
+
+
+
+
+
+
+
+
+
+
+ Action1
+
+
+ Action2
+
+
+ Sidebar content
+
+
+
+
+
diff --git a/tests/component/components/NcAppSidebar/visual.spec.ts b/tests/component/components/NcAppSidebar/visual.spec.ts
new file mode 100644
index 0000000000..189ecbb674
--- /dev/null
+++ b/tests/component/components/NcAppSidebar/visual.spec.ts
@@ -0,0 +1,39 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { expect, test } from '@playwright/experimental-ct-vue'
+
+import AppSidebar from './AppSidebar.story.vue'
+
+test.skip(({ browserName }) => browserName !== 'chromium')
+
+// A little bit hacky but we test a wrapper element so we need to use the real NcContent and NcAppNavigation
+test.beforeEach(async ({ page }) => {
+ const handle = await page.locator('#content').elementHandle()
+ expect(handle).not.toBeNull()
+ await handle!.evaluate((node) => { node.innerHTML = ''; node.id = 'root' })
+})
+
+new Array(2 ** 5)
+ .fill('')
+ .map((_, index) => ({
+ compact: Boolean(index & 16),
+ subname: Boolean(index & 8),
+ nameEditable: Boolean(index & 4),
+ header: Boolean(index & 2),
+ secondaryActions: Boolean(index & 1),
+ }))
+ .forEach((props, index) => {
+ test(
+ `Sidebar header${props.compact ? ': compact ' : (index > 0 ? ': ' : '')}${props.subname ? 'subname ' : ''}${props.nameEditable ? 'editable ' : ''}${props.header ? 'header ' : ''}${props.secondaryActions ? 'actions' : ''}`,
+ { tag: '@visual' },
+ async ({ mount },
+ ) => {
+ const component = await mount(AppSidebar, {
+ props,
+ })
+ await expect(component).toBeVisible()
+ await expect(component.locator('header')).toHaveScreenshot({ caret: 'hide' })
+ })
+ })
diff --git a/tests/component/components/NcButton.spec.ts b/tests/component/components/NcButton.spec.ts
new file mode 100644
index 0000000000..5be303afe0
--- /dev/null
+++ b/tests/component/components/NcButton.spec.ts
@@ -0,0 +1,30 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { expect, test } from '@playwright/experimental-ct-vue'
+import NcButton from '../../../src/components/NcButton/NcButton.vue'
+
+test('Text is rendered when provided', async ({ mount, page }) => {
+ await mount(NcButton, {
+ slots: {
+ default: 'Nextcloud',
+ },
+ })
+ await expect(page.getByRole('button')).toHaveText('Nextcloud')
+ await expect(page.getByRole('button')).toHaveAccessibleName('Nextcloud')
+})
+
+test('Can overwrite accessible name', async ({ mount, page }) => {
+ await mount(NcButton, {
+ props: {
+ ariaLabel: 'Nextcloud'
+ },
+ slots: {
+ default: 'Your favorite cloud',
+ },
+ })
+ await expect(page.getByRole('button')).toHaveText('Your favorite cloud')
+ await expect(page.getByRole('button')).toHaveAccessibleName('Nextcloud')
+})
diff --git a/tests/component/components/NcDialog.spec.ts b/tests/component/components/NcDialog.spec.ts
new file mode 100644
index 0000000000..4ff5ed461f
--- /dev/null
+++ b/tests/component/components/NcDialog.spec.ts
@@ -0,0 +1,21 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { expect, test } from '@playwright/experimental-ct-vue'
+import NcDialog from '../../../src/components/NcDialog/NcDialog.vue'
+
+test('Dialog is correctly labelled', async ({ mount, page }) => {
+ const component = await mount(NcDialog, {
+ props: {
+ open: true,
+ name: 'My dialog',
+ },
+ slots: {
+ default: 'Text',
+ },
+ })
+
+ await expect(page.getByRole('dialog', { name: 'My dialog' })).toBeVisible()
+})
diff --git a/tests/component/components/NcModal.spec.ts b/tests/component/components/NcModal.spec.ts
new file mode 100644
index 0000000000..37622d5fb5
--- /dev/null
+++ b/tests/component/components/NcModal.spec.ts
@@ -0,0 +1,82 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { Component } from 'vue'
+
+import { expect, test } from '@playwright/experimental-ct-vue'
+import NcModal from '../../../src/components/NcModal/NcModal.vue'
+
+test('Modal is labelled correctly if name is set', async ({ mount, page }) => {
+ await mount(NcModal, {
+ props: {
+ show: true,
+ name: 'My modal',
+ size: 'small',
+ },
+ slots: {
+ default: 'Text',
+ },
+ })
+
+ await expect(page.getByRole('dialog', { name: 'My modal' })).toBeVisible()
+})
+
+test('Modal is labelled correctly if `labelId` is set', async ({ mount, page }) => {
+ await mount(NcModal, {
+ props: {
+ show: true,
+ size: 'small',
+ labelId: 'my-id',
+ },
+ slots: {
+ default: 'Labelled modal
',
+ },
+ })
+ // There should be the dialog spawned
+ const dialog = page.getByRole('dialog')
+ await expect(dialog).toBeVisible()
+ // With the heading inside
+ await expect(dialog.getByRole('heading')).toHaveText('Labelled modal')
+ // And the heading is used to label the dialog
+ await expect(dialog).toHaveAccessibleName('Labelled modal')
+})
+
+test('Modal is labelled correctly if `labelId` and `name` are set', async ({ mount, page }) => {
+ await mount(NcModal, {
+ props: {
+ show: true,
+ size: 'small',
+ name: 'My modal',
+ labelId: 'my-id',
+ },
+ slots: {
+ default: 'Real name
',
+ },
+ })
+ await expect(page.getByRole('dialog', { name: 'Real name' })).toBeVisible()
+})
+
+test('Close button is visible when content is scrolled', async ({ mount, page }) => {
+ await mount(NcModal, {
+ props: {
+ show: true,
+ size: 'small',
+ name: 'My modal',
+ labelId: 'my-id',
+ },
+ slots: {
+ default: '',
+ },
+ })
+
+ const dialog = page.getByRole('dialog')
+ await expect(dialog).toBeVisible()
+
+ await dialog.getByTestId('bottom').scrollIntoViewIfNeeded()
+ await expect(dialog.getByTestId('bottom')).toBeVisible()
+
+ await expect(dialog.getByRole('button', { name: 'Close' })).toBeVisible()
+ await expect(dialog.getByRole('button', { name: 'Close' })).toBeInViewport()
+})
diff --git a/tests/component/components/NcRichText/NcRichText.spec.ts b/tests/component/components/NcRichText/NcRichText.spec.ts
new file mode 100644
index 0000000000..fef26d1b7b
--- /dev/null
+++ b/tests/component/components/NcRichText/NcRichText.spec.ts
@@ -0,0 +1,18 @@
+/**
+ * SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { expect, test } from '@playwright/experimental-ct-vue';
+import NcRichText from '../../../../src/components/NcRichText/NcRichText.vue'
+
+test('XML-like text (escaped and unescaped)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'text</span>',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component).toHaveText('text')
+})
diff --git a/tests/component/components/NcRichText/markown-rendering.spec.ts b/tests/component/components/NcRichText/markown-rendering.spec.ts
new file mode 100644
index 0000000000..8116f6e58f
--- /dev/null
+++ b/tests/component/components/NcRichText/markown-rendering.spec.ts
@@ -0,0 +1,634 @@
+/**
+ * SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+// Markdown guide: https://www.markdownguide.org/basic-syntax/
+// Reference tests: https://github.com/nextcloud-deps/CDMarkdownKit/tree/master/CDMarkdownKitTests
+
+import { expect, test } from '@playwright/experimental-ct-vue';
+import NcRichText from '../../../../src/components/NcRichText/NcRichText.vue'
+
+test.describe('dividers', () => {
+ test('dividers with asterisks', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'First\n\n***\n\nsecond',
+ useMarkdown: true,
+ },
+ })
+
+ expect(await component.locator('hr').all()).toHaveLength(1)
+ })
+
+ test('dividers with dashes', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'First\n\n---\n\nsecond',
+ useMarkdown: true,
+ },
+ })
+
+ expect(await component.locator('hr').all()).toHaveLength(1)
+ })
+
+ test('dividers with underlines', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'First\n\n___\n\nsecond',
+ useMarkdown: true,
+ },
+ })
+
+ expect(await component.locator('hr').all()).toHaveLength(1)
+ })
+})
+
+test.describe('headings', () => {
+ test('heading (with hash (#) syntax divided with space from text)', async ({ mount }) => {
+ const testCases = [
+ { tag: 'h1', input: '# heading 1', output: 'heading 1' },
+ { tag: 'h2', input: '## heading 2', output: 'heading 2' },
+ { tag: 'h3', input: '### heading 3', output: 'heading 3' },
+ { tag: 'h4', input: '#### heading 4', output: 'heading 4' },
+ { tag: 'h5', input: '##### heading 5', output: 'heading 5' },
+ { tag: 'h6', input: '###### heading 6', output: 'heading 6' },
+ ]
+
+ const component = await mount(NcRichText, {
+ props: {
+ text: testCases.map(i => i.input).join('\n'),
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('heading', { level: 1 })).toHaveText('heading 1')
+ await expect(component.getByRole('heading', { level: 2 })).toHaveText('heading 2')
+ await expect(component.getByRole('heading', { level: 3 })).toHaveText('heading 3')
+ await expect(component.getByRole('heading', { level: 4 })).toHaveText('heading 4')
+ await expect(component.getByRole('heading', { level: 5 })).toHaveText('heading 5')
+ await expect(component.getByRole('heading', { level: 6 })).toHaveText('heading 6')
+ })
+
+ test('ignore heading (with hash (#) syntax padded to the text)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '#heading',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('heading', { level: 1 })).toHaveCount(0)
+ await expect(component.getByText('#heading')).toBeVisible()
+ })
+
+ test('render heading 1 (with equal (=) syntax on the next line)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'heading 1\n==',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('heading', { level: 1 })).toHaveText('heading 1')
+ })
+
+ test('render heading 2 (with dash (-) syntax on the next line)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'heading 2\n--',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('heading', { level: 2 })).toHaveText('heading 2')
+ })
+})
+
+test.describe('bold text', () => {
+ test('bold text (single with asterisk syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '**bold asterisk**',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('strong')).toHaveText('bold asterisk')
+ })
+
+ test('bold text (single with underscore syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '__bold underscore__',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('strong')).toHaveText('bold underscore')
+ })
+
+ test('bold text (several in line with different syntax)', async ({ mount }) => {
+ const outputs = ['bold underscore', 'bold asterisk']
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'normal text __bold underscore__ normal text **bold asterisk** normal text',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('strong')).toHaveCount(2)
+ expect(await component.getByRole('strong').allInnerTexts()).toEqual(outputs)
+ })
+
+ test('bold text (between normal texts with asterisk syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'text**bold**text',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component).toHaveText('textboldtext')
+ await expect(component.getByRole('strong')).toHaveText('bold')
+ })
+
+ test('ignored bold text (between normal texts with underscore syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'text__bold__text',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component).toHaveText('text__bold__text')
+ await expect(component.getByRole('strong')).toHaveCount(0)
+ })
+
+ test('normal text (between bold texts with asterisk syntax)', async ({ mount }) => {
+ const outputs = ['bold asterisk', 'bold asterisk']
+ const component = await mount(NcRichText, {
+ props: {
+ text: '**bold asterisk**normal text**bold asterisk**',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('strong')).toHaveCount(2)
+ expect(await component.getByRole('strong').allInnerTexts()).toEqual(outputs)
+ })
+})
+
+
+test.describe('italic text', () => {
+ test('italic text (single with asterisk syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '*italic asterisk*',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('em')).toHaveText('italic asterisk')
+ })
+
+ test('italic text (single with underscore syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '_italic underscore_',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('em')).toHaveText('italic underscore')
+ })
+
+ test('italic text (several in line with different syntax)', async ({ mount }) => {
+ const outputs = ['italic underscore', 'italic asterisk']
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'normal text _italic underscore_ normal text *italic asterisk* normal text',
+ useMarkdown: true,
+ },
+ })
+
+ expect(await component.locator('em').count()).toBe(2)
+ expect(await component.locator('em').allInnerTexts()).toEqual(outputs)
+ })
+
+ test('italic text (between normal texts with asterisk syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'text*italic*text',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('em')).toHaveText('italic')
+ })
+
+ test('ignored italic text (between normal texts with underscore syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'text_italic_text',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByText('text_italic_text')).toBeVisible()
+ await expect(component.locator('em')).toHaveCount(0)
+ })
+
+ test('normal text (between italic texts with asterisk syntax)', async ({ mount }) => {
+ const outputs = ['italic asterisk', 'italic asterisk']
+ const component = await mount(NcRichText, {
+ props: {
+ text: '*italic asterisk*normal text*italic asterisk*',
+ useMarkdown: true,
+ },
+ })
+
+ expect(await component.locator('em').count()).toBe(2)
+ expect(await component.locator('em').allInnerTexts()).toEqual(outputs)
+ })
+})
+
+
+test.describe('strikethrough text', () => {
+ test('strikethrough text (with single tilda syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '~strikethrough single~',
+ useExtendedMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('del')).toHaveText('strikethrough single')
+ })
+
+ test('strikethrough text (with double tilda syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '~~strikethrough double~~',
+ useExtendedMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('del')).toHaveText('strikethrough double')
+ })
+
+ test('strikethrough text (several in line with different syntax)', async ({ mount }) => {
+ const outputs = ['strikethrough single', 'strikethrough double']
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'normal text ~strikethrough single~ normal text ~~strikethrough double~~ normal text',
+ useExtendedMarkdown: true,
+ },
+ })
+
+ expect(await component.locator('del').count()).toBe(2)
+ expect(await component.locator('del').allInnerTexts()).toEqual(outputs)
+ })
+
+ test('strikethrough text (between normal texts with different syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'text~strikethrough~text~~strikethrough~~text',
+ useExtendedMarkdown: true,
+ },
+ })
+
+ expect(await component.locator('del').count()).toBe(2)
+ expect(await component.locator('del').allInnerTexts()).toEqual(['strikethrough', 'strikethrough'])
+ })
+})
+
+
+test.describe('inline code', () => {
+ test('inline code (single with backticks syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'normal text `inline code` normal text',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('code')).toHaveText('inline code')
+ })
+
+ test('inline code (single with double backticks syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'normal text ``inline code`` normal text',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('code')).toHaveText('inline code')
+ })
+
+ test('inline code (single with triple backticks syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'normal text ```inline code``` normal text',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('code')).toHaveText('inline code')
+ })
+
+ test('inline code (several in line )', async ({ mount }) => {
+ const outputs = ['inline code 1', 'inline code 2']
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'normal text `inline code 1`normal text ``inline code 2`` normal text',
+ useMarkdown: true,
+ },
+ })
+
+ expect(await component.locator('code').count()).toBe(2)
+ expect(await component.locator('code').allInnerTexts()).toEqual(outputs)
+ })
+
+ test('inline code (between normal texts)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'text`inline code`text',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('code')).toHaveText('inline code')
+ })
+
+ test('inline code (with ignored bold, italic, XML-like syntax))', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '`inline code **bold text** _italic text_ text</span>`',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('code')).toHaveText('inline code **bold text** _italic text_ text')
+ })
+})
+
+
+test.describe('multiline code', () => {
+ test('multiline code (with triple backticks syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '```\nmultiline code\n```',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('pre')).toHaveText('multiline code\n')
+ })
+
+ test('multiline code (ignored info)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '```vue\nmultiline code\n```',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('pre')).toHaveText('multiline code\n')
+ })
+
+ test('empty multiline code', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '``````',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('pre')).toBeEmpty()
+ })
+
+ test('empty multiline code (with new line)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '```\n```',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('pre')).toBeEmpty()
+ })
+
+ test('multiline code (with several lines)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '```\nline 1\nline 2\nline 3\n```',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('pre')).toHaveText('line 1\nline 2\nline 3\n')
+ await expect(component.locator('code')).toHaveText('line 1\nline 2\nline 3\n')
+ })
+
+ test('multiline code (with ignored bold, italic, inline code, XML-like syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '```\n**bold text**\n_italic text_\n`inline code`\ntext</span>\n```',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('pre')).toHaveText('**bold text**\n_italic text_\n`inline code`\ntext\n')
+ })
+})
+
+
+test.describe('blockquote', () => {
+ test('blockquote (with greater then (>) syntax - normal)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '> blockquote',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('blockquote')).toHaveText('\nblockquote\n')
+ })
+
+ test('blockquote (with greater then (>) syntax - escaped)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '> blockquote',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('blockquote')).toHaveText('\nblockquote\n')
+ })
+
+ test('blockquote (with bold, italic text, inline code)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '> blockquote **bold text** _italic text_ `inline code`',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('blockquote')).toHaveText('\nblockquote bold text italic text inline code\n')
+ await expect(component.locator('strong')).toHaveText('bold text')
+ await expect(component.locator('em')).toHaveText('italic text')
+ await expect(component.locator('code')).toHaveText('inline code')
+ })
+
+ test('blockquote (with several lines)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '> line 1\nline 2\n line 3',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('blockquote')).toHaveText('\nline 1\nline 2\nline 3\n')
+ })
+
+ test('blockquote (divided from normal text)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'normal text\n> line 1\nline 2\n\nnormal text',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('blockquote')).toHaveText('\nline 1\nline 2\n')
+ })
+
+ test('blockquote (with several paragraphs)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '> line 1\n>\n> line 3',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('blockquote')).toHaveText('\nline 1\nline 3\n')
+ })
+
+ test('blockquote (with nested blockquote)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: '> blockquote\n>\n>> nested blockquote',
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.locator('blockquote blockquote')).toHaveText('nested blockquote')
+ })
+})
+
+
+test.describe('lists', () => {
+ test('ordered list (with number + `.` syntax divided with space from text)', async ({ mount }) => {
+ const testCases = [
+ { input: '1. item 1', output: 'item 1' },
+ { input: '2. item 2', output: 'item 2' },
+ { input: '3. item 3', output: 'item 3' },
+ ]
+
+ const component = await mount(NcRichText, {
+ props: {
+ text: testCases.map(i => i.input).join('\n'),
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('list')).toBeVisible()
+ await expect(component.locator('ol')).toHaveCount(1)
+ await expect(component.getByRole('listitem')).toHaveText(testCases.map(({ output }) => output))
+ })
+
+ test('unordered list (with unite syntax divided with space from text)', async ({ mount }) => {
+ const testCases = [
+ { input: '* item 1', output: 'item 1' },
+ { input: '* item 2', output: 'item 2' },
+ { input: '* item 3', output: 'item 3' },
+ ]
+
+ const component = await mount(NcRichText, {
+ props: {
+ text: testCases.map(i => i.input).join('\n'),
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('list')).toBeVisible()
+ await expect(component.locator('ul')).toHaveCount(1)
+ await expect(component.getByRole('listitem')).toHaveText(testCases.map(({ output }) => output))
+ })
+
+ test('unordered lists (with different syntax divided with space from text)', async ({ mount }) => {
+ const testCases = [
+ { input: '* item 1', output: 'item 1' },
+ { input: '+ item 2', output: 'item 2' },
+ { input: '- item 3', output: 'item 3' },
+ ]
+
+ const component = await mount(NcRichText, {
+ props: {
+ text: testCases.map(i => i.input).join('\n'),
+ useMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('list')).toHaveCount(testCases.length)
+ await expect(component.getByRole('listitem')).toHaveText(testCases.map(({ output }) => output))
+ })
+})
+
+
+test.describe('task lists', () => {
+ test('task list (with `- [ ]` and `- [x]` syntax divided with space from text)', async ({ mount }) => {
+ const testCases = [
+ { input: '- [ ] item 1', output: 'item 1', checked: false },
+ { input: '- [x] item 2', output: 'item 2', checked: true },
+ { input: '- [ ] item 3', output: 'item 3', checked: false },
+ ]
+
+ const component = await mount(NcRichText, {
+ props: {
+ text: testCases.map(i => i.input).join('\n'),
+ useExtendedMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('list')).toBeVisible()
+ await expect(component.locator('ul')).toHaveCount(1)
+ await expect(component.getByRole('listitem')).toHaveCount(testCases.length)
+
+ for(const [index, testcase] of testCases.entries()) {
+ await expect(component.getByRole('listitem').nth(index)).toHaveText(testcase.output)
+ await expect(component.getByRole('listitem').nth(index).getByRole('checkbox')).toBeChecked({ checked: testcase.checked})
+ }
+ })
+})
+
+
+test.describe('tables', () => {
+ test('table (with `-- | --` syntax)', async ({ mount }) => {
+ const component = await mount(NcRichText, {
+ props: {
+ text: 'Table | Column A | Column B\n-- | -- | --\nRow 1 | Value A1 | Value B1\nRow 2 | Value A2 | Value B2',
+ useExtendedMarkdown: true,
+ },
+ })
+
+ await expect(component.getByRole('table')).toBeVisible()
+ await expect(component.locator('thead')).toHaveCount(1)
+ await expect(component.locator('tbody')).toHaveCount(1)
+
+ await expect(component.locator('th')).toHaveCount(3)
+ await expect(component.locator('tr')).toHaveCount(3)
+ await expect(component.locator('td')).toHaveCount(6)
+ })
+})
diff --git a/tests/component/components/NcSelect/UserSelect.spec.ts b/tests/component/components/NcSelect/UserSelect.spec.ts
new file mode 100644
index 0000000000..43f6f4c276
--- /dev/null
+++ b/tests/component/components/NcSelect/UserSelect.spec.ts
@@ -0,0 +1,68 @@
+/**
+ * SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { expect, test } from '@playwright/experimental-ct-vue'
+import UserSelect from './UserSelect.story.vue'
+
+test('has options', async ({ mount, page }) => {
+ const component = await mount(UserSelect)
+
+ await expect(component.getByRole('searchbox')).toBeVisible()
+ await component.getByRole('searchbox').click()
+
+ expect(await page.getByRole('option').all()).toHaveLength(3)
+ await expect(page.getByRole('option', { name: 'Olivia' })).toBeVisible()
+ await expect(page.getByRole('option', { name: 'John' })).toBeVisible()
+ await expect(page.getByRole('option', { name: 'Emma' })).toBeVisible()
+})
+
+test('can filter by name', async ({ mount, page }) => {
+ const component = await mount(UserSelect)
+
+ await expect(component.getByRole('searchbox')).toBeVisible()
+ await component.getByRole('searchbox').fill('Em')
+
+ await expect(page.getByRole('option', { name: 'Emma' })).toBeVisible()
+ expect(await page.getByRole('option').all()).toHaveLength(1)
+})
+
+test('can filter by mail', async ({ mount, page }) => {
+ const component = await mount(UserSelect)
+
+ await expect(component.getByRole('searchbox')).toBeVisible()
+ await component.getByRole('searchbox').fill('olivia@example')
+
+ await expect(page.getByRole('option', { name: 'Olivia' })).toBeVisible()
+ expect(await page.getByRole('option').all()).toHaveLength(1)
+})
+
+test(
+ 'can filter by mail in brackets',
+ {
+ annotation: {
+ type: 'issue',
+ description: 'https://github.com/nextcloud-libraries/nextcloud-vue/issues/4491',
+ },
+ },
+ async ({ mount, page }) => {
+ const component = await mount(UserSelect)
+
+ await expect(component.getByRole('searchbox')).toBeVisible()
+ await component.getByRole('searchbox').fill('O. <')
+
+ // should not exist right now as neither Name no email provided
+ await expect(page.getByText('No results')).toBeVisible()
+ expect(await page.getByRole('option', { name: 'Olivia' }).all()).toHaveLength(0)
+
+ await component.getByRole('searchbox').fill('O. ')
+ // now it should match the email
+ await expect(page.getByRole('option', { name: 'Olivia' })).toBeVisible()
+ expect(await page.getByRole('option').all()).toHaveLength(1)
+ },
+)
diff --git a/tests/component/components/NcSelect/UserSelect.story.vue b/tests/component/components/NcSelect/UserSelect.story.vue
new file mode 100644
index 0000000000..07b09696ac
--- /dev/null
+++ b/tests/component/components/NcSelect/UserSelect.story.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
diff --git a/tests/component/setup/index.css b/tests/component/setup/index.css
new file mode 100644
index 0000000000..da16dec761
--- /dev/null
+++ b/tests/component/setup/index.css
@@ -0,0 +1,9 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/* Enforce Roboto to ensure CI and local snapshots look the same */
+:root {
+ --font-face: 'Roboto' !important;
+}
diff --git a/tests/component/setup/index.html b/tests/component/setup/index.html
new file mode 100644
index 0000000000..a929aa4238
--- /dev/null
+++ b/tests/component/setup/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Testing Page
+
+
+
+
+
+
+
+
+
diff --git a/tests/component/setup/index.ts b/tests/component/setup/index.ts
new file mode 100644
index 0000000000..fb498e61a5
--- /dev/null
+++ b/tests/component/setup/index.ts
@@ -0,0 +1,25 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+// Visual setup
+import '../../../styleguide/assets/additional.css'
+import '@fontsource/roboto/index.css'
+import './index.css'
+
+// Mount hooks
+import { beforeMount } from '@playwright/experimental-ct-vue/hooks'
+import { createMemoryHistory, createRouter, type RouteRecordRaw } from 'vue-router'
+
+export type HooksConfig = {
+ routes?: RouteRecordRaw[];
+}
+
+beforeMount(async ({ hooksConfig, app }) => {
+ const router = createRouter({
+ routes: hooksConfig?.routes ?? [],
+ history: createMemoryHistory(),
+ })
+ app.use(router)
+})
diff --git a/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-button-styling-on-active-entry-with-editing-true-1-chromium-linux.png b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-button-styling-on-active-entry-with-editing-true-1-chromium-linux.png
new file mode 100644
index 0000000000..2ebe6dff40
Binary files /dev/null and b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-button-styling-on-active-entry-with-editing-true-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-button-styling-on-active-route-with-editing-true-1-chromium-linux.png b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-button-styling-on-active-route-with-editing-true-1-chromium-linux.png
new file mode 100644
index 0000000000..e0c675e7d6
Binary files /dev/null and b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-button-styling-on-active-route-with-editing-true-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-styling-on-active-entry-1-chromium-linux.png b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-styling-on-active-entry-1-chromium-linux.png
new file mode 100644
index 0000000000..6dd0e1febd
Binary files /dev/null and b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-styling-on-active-entry-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-styling-on-active-route-1-chromium-linux.png b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-styling-on-active-route-1-chromium-linux.png
new file mode 100644
index 0000000000..ec8b873510
Binary files /dev/null and b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-primary-styling-on-active-route-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-tertiary-styling-on-non-active-entry-1-chromium-linux.png b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-tertiary-styling-on-non-active-entry-1-chromium-linux.png
new file mode 100644
index 0000000000..ba63c0ec3a
Binary files /dev/null and b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-tertiary-styling-on-non-active-entry-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-tertiary-styling-on-non-active-route-1-chromium-linux.png b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-tertiary-styling-on-non-active-route-1-chromium-linux.png
new file mode 100644
index 0000000000..acd4c585a3
Binary files /dev/null and b/tests/component/snapshots/components/NcAppNavigationItem/visual.spec.ts-snapshots/has-tertiary-styling-on-non-active-route-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-1-chromium-linux.png
new file mode 100644
index 0000000000..3f2b17b808
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..4bc08f2295
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-1-chromium-linux.png
new file mode 100644
index 0000000000..3f2b17b808
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..4bc08f2295
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-1-chromium-linux.png
new file mode 100644
index 0000000000..46645a4326
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..0042f97e1e
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-header-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-header-1-chromium-linux.png
new file mode 100644
index 0000000000..8cbcc91a7b
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-header-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-header-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-header-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..83f1eb149d
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-editable-header-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-header-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-header-1-chromium-linux.png
new file mode 100644
index 0000000000..c11ade8530
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-header-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-header-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-header-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..ed84719657
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-header-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-1-chromium-linux.png
new file mode 100644
index 0000000000..025397c2e0
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..642305727c
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-1-chromium-linux.png
new file mode 100644
index 0000000000..94ef933ed8
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..d3fcdf78f7
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-header-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-header-1-chromium-linux.png
new file mode 100644
index 0000000000..da1fdbf605
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-header-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-header-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-header-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..c6b16d49e4
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-editable-header-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-header-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-header-1-chromium-linux.png
new file mode 100644
index 0000000000..b8b9171a8e
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-header-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-header-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-header-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..b2eb2a49f5
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-compact-subname-header-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-1-chromium-linux.png
new file mode 100644
index 0000000000..46645a4326
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..0042f97e1e
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-header-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-header-1-chromium-linux.png
new file mode 100644
index 0000000000..f79be6372c
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-header-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-header-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-header-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..b6722f3055
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-editable-header-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-header-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-header-1-chromium-linux.png
new file mode 100644
index 0000000000..abb75ec5dd
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-header-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-header-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-header-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..735b72e9a4
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-header-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-1-chromium-linux.png
new file mode 100644
index 0000000000..025397c2e0
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..642305727c
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-1-chromium-linux.png
new file mode 100644
index 0000000000..94ef933ed8
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..d3fcdf78f7
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-header-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-header-1-chromium-linux.png
new file mode 100644
index 0000000000..79bbcffdb5
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-header-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-header-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-header-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..334c643834
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-editable-header-actions-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-header-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-header-1-chromium-linux.png
new file mode 100644
index 0000000000..d4f51fec97
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-header-1-chromium-linux.png differ
diff --git a/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-header-actions-1-chromium-linux.png b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-header-actions-1-chromium-linux.png
new file mode 100644
index 0000000000..74b0bc2a83
Binary files /dev/null and b/tests/component/snapshots/components/NcAppSidebar/visual.spec.ts-snapshots/Sidebar-header-subname-header-actions-1-chromium-linux.png differ
diff --git a/tests/tsconfig.json b/tests/tsconfig.json
index d86eb9fab3..c08b2b70b2 100644
--- a/tests/tsconfig.json
+++ b/tests/tsconfig.json
@@ -1,7 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
- "rootDir": ".."
+ "rootDir": "..",
+ "types": [
+ "vitest"
+ ]
},
"exclude": [],
}
diff --git a/tsconfig.json b/tsconfig.json
index 981473fb41..75cff7068b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["./src/**/*.js","./src/**/*.ts", "./src/**/*.vue", "**/*.ts"],
- "exclude": ["./src/**/*.cy.ts", "cypress", "tests"],
+ "exclude": ["tests", "vitest.config.ts"],
+
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
@@ -18,12 +19,4 @@
"vueCompilerOptions": {
"target": 3.3,
},
-
- "ts-node": {
- "compilerOptions": {
- "module": "ES2015",
- "target": "ES2015",
- "moduleResolution": "node"
- }
- }
}
diff --git a/vite.config.ts b/vite.config.ts
index 157281702d..5a66d6278d 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -23,7 +23,7 @@ const entryPoints = {
const name = item
.replace(/\/index\.(j|t)s/, '')
.replace('src/directives/', 'Directives/')
- acc[name] = join(__dirname, item)
+ acc[name] = join(import.meta.dirname, item)
return acc
}, {}),
@@ -31,7 +31,7 @@ const entryPoints = {
const name = item
.replace(/\/index\.(j|t)s/, '')
.replace('src/components/', 'Components/')
- acc[name] = join(__dirname, item)
+ acc[name] = join(import.meta.dirname, item)
return acc
}, {}),
@@ -39,7 +39,7 @@ const entryPoints = {
const name = item
.replace(/\/index\.(j|t)s/, '')
.replace('src/functions/', 'Functions/')
- acc[name] = join(__dirname, item)
+ acc[name] = join(import.meta.dirname, item)
return acc
}, {}),
@@ -47,7 +47,7 @@ const entryPoints = {
const name = item
.replace(/\/index\.(j|t)s/, '')
.replace('src/mixins/', 'Mixins/')
- acc[name] = join(__dirname, item)
+ acc[name] = join(import.meta.dirname, item)
return acc
}, {}),
@@ -55,15 +55,15 @@ const entryPoints = {
const name = item
.replace(/\/index\.(j|t)s/, '')
.replace('src/composables/', 'Composables/')
- acc[name] = join(__dirname, item)
+ acc[name] = join(import.meta.dirname, item)
return acc
}, {}),
- index: resolve(__dirname, 'src/index.ts'),
+ index: resolve(import.meta.dirname, 'src/index.ts'),
}
// Plugin for stripping out sections from vue files
-const vueDocsPlugin: Plugin = {
+export const vueDocsPlugin: Plugin = {
name: 'vue-docs-plugin',
transform(code, id) {
if (!/vue&type=doc/.test(id)) {
@@ -77,7 +77,7 @@ const vueDocsPlugin: Plugin = {
const overrides = defineConfig({
plugins: [
vueDocsPlugin,
- l10nPlugin(resolve(__dirname, 'l10n')),
+ l10nPlugin(resolve(import.meta.dirname, 'l10n')),
],
css: {
devSourcemap: true,
@@ -86,7 +86,7 @@ const overrides = defineConfig({
additionalData: `@use 'sass:math'; $scope_version:${SCOPE_VERSION}; @import 'variables'; @import 'material-icons';`,
sourceMapContents: false,
includePaths: [
- resolve(__dirname, 'src/assets'),
+ resolve(import.meta.dirname, 'src/assets'),
],
},
},
diff --git a/vitest.config.ts b/vitest.config.ts
index dde8d4fbfb..b7c33154e9 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { UserConfig } from 'vite'
import { resolve } from 'node:path'
+import { defineConfig } from 'vite'
import viteConfig from './vite.config'
export default async (env) => {
@@ -12,11 +12,16 @@ export default async (env) => {
// node-externals conflicts with vitest
config.plugins = config.plugins!.filter((plugin) => plugin && (!('name' in plugin) || plugin?.name !== 'node-externals'))
- return {
+ return defineConfig({
...config,
test: {
environment: 'jsdom',
setupFiles: resolve(__dirname, './tests/setup.js'),
+ exclude: [
+ 'tests/component/**',
+ 'node_modules/**',
+ 'docs/**',
+ ],
},
- } as UserConfig
+ })
}