diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d61bb40ad0..895a1a9f30 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -33,6 +33,8 @@ elements/ → All Polymer web components nuxeo-results/ → Result list/grid display addons/ → Optional addon bundles (Drive, LiveConnect, CSV, Spreadsheet, etc.) i18n/ → Localization JSON files (16 languages), merged at build time +themes/ → Themeable CSS (default, dark) +test/ → Unit tests (Karma + Mocha + Chai + Sinon) themes/ → Themeable CSS (default, dark, light, kawaii) test/ → Unit tests (@web/test-runner + Mocha + Chai + Sinon) ftest/ → Functional tests (Cucumber/Gherkin .feature files) diff --git a/.github/instructions/themes.instructions.md b/.github/instructions/themes.instructions.md index 801e667a88..9a7db8f27d 100644 --- a/.github/instructions/themes.instructions.md +++ b/.github/instructions/themes.instructions.md @@ -16,8 +16,6 @@ themes/ preview.jpg → Theme preview thumbnail README.md → Theme description dark/ → Dark theme (same structure) - light/ → Light theme (same structure) - kawaii/ → Kawaii theme (same structure, includes background.png) ``` ## Theme File (`theme.html`) @@ -66,14 +64,14 @@ This provides layout utilities, button styles, and common CSS rules used across ## Theme Loading (`loader.js`) - Reads `localStorage.getItem('theme')` to determine active theme -- Falls back to `default` if the stored theme is not found (404 check) +- Falls back to `default` if the stored theme is deprecated or not found (404 check) - Injects a `` for the selected `themes//theme.html` - Users switch themes from their profile settings at runtime ## Rules - Always use CSS custom properties (e.g., `var(--nuxeo-primary-color)`) — never hardcode colors -- Test changes against all 4 themes (default, dark, light, kawaii) +- Test changes against the supported built-in themes (default, dark) - Keep the same custom property interface across themes — all themes must define the same set of variables - New custom properties should be added to all theme files simultaneously - `base.js` changes affect every component — modify with care diff --git a/.github/workflows/a11y.yaml b/.github/workflows/a11y.yaml index 3198abe6ed..879edafc9e 100644 --- a/.github/workflows/a11y.yaml +++ b/.github/workflows/a11y.yaml @@ -5,6 +5,7 @@ on: branches: - maintenance-3.1.x - lts-2025 + - WEBUI-1935-POC-REBRANDING workflow_call: inputs: diff --git a/.github/workflows/clean.yaml b/.github/workflows/clean.yaml index 0067a61c25..3fc4681e53 100644 --- a/.github/workflows/clean.yaml +++ b/.github/workflows/clean.yaml @@ -5,6 +5,7 @@ on: types: [closed, unlabeled] branches: - maintenance-3.1.x + - WEBUI-1935-POC-REBRANDING workflow_dispatch: inputs: diff --git a/.github/workflows/crowdin.yaml b/.github/workflows/crowdin.yaml index 81e5ded5d3..0414048615 100644 --- a/.github/workflows/crowdin.yaml +++ b/.github/workflows/crowdin.yaml @@ -8,7 +8,7 @@ on: # Sync when a commit is done on maintenance-3.1.x push: - branches: ["maintenance-3.1.x"] + branches: ["maintenance-3.1.x", "WEBUI-1935-POC-REBRANDING"] paths: - 'i18n/messages.json' diff --git a/.github/workflows/ftest.yaml b/.github/workflows/ftest.yaml index 023ceeb5b4..b5a715e606 100644 --- a/.github/workflows/ftest.yaml +++ b/.github/workflows/ftest.yaml @@ -5,6 +5,7 @@ on: branches: - maintenance-3.1.x - lts-2025 + - WEBUI-1935-POC-REBRANDING workflow_call: inputs: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 77d30b57df..2cb5af0c06 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -4,6 +4,7 @@ on: pull_request: branches: - maintenance-3.1.x + - WEBUI-1935-POC-REBRANDING workflow_call: inputs: diff --git a/.github/workflows/preview.yaml b/.github/workflows/preview.yaml index 3676c23619..fab7f24f11 100644 --- a/.github/workflows/preview.yaml +++ b/.github/workflows/preview.yaml @@ -5,6 +5,7 @@ on: types: [opened, synchronize, reopened, labeled] branches: - maintenance-3.1.x + - WEBUI-1935-POC-REBRANDING workflow_dispatch: inputs: diff --git a/.github/workflows/sonar.yaml b/.github/workflows/sonar.yaml index bd3efee2f0..f7bd53cdee 100644 --- a/.github/workflows/sonar.yaml +++ b/.github/workflows/sonar.yaml @@ -5,10 +5,12 @@ on: types: [opened, synchronize, reopened] branches: - maintenance-3.1.x + - WEBUI-1935-POC-REBRANDING push: branches: - maintenance-3.1.x + - WEBUI-1935-POC-REBRANDING workflow_dispatch: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 58030b057a..5637de865c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,6 +4,7 @@ on: pull_request: branches: - maintenance-3.1.x + - WEBUI-1935-POC-REBRANDING workflow_call: inputs: diff --git a/AGENTS.md b/AGENTS.md index 5c8c7109ac..c82e869bef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,6 +37,8 @@ elements/ → ALL Polymer web components live here nuxeo-results/ → Result display addons/ → Optional feature bundles (each has index.js entry) i18n/ → Translation JSON files (16 languages) +themes/ → Visual themes (default, dark) +test/ → Unit tests (test/nuxeo-*.test.js) themes/ → Visual themes (default, dark, light, kawaii) test/ → Unit tests (@web/test-runner + Mocha; sources in test/*.test.js) ftest/features/ → Functional test Gherkin scenarios diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ca3e709fac..642ee0dc2f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -203,14 +203,12 @@ Addon loading is a two-step process. During the build, webpack injects `Nuxeo.UI ## Theming -Four built-in themes in `themes/`: +Two built-in themes in `themes/`: | Theme | Directory | |---|---| | Default | `themes/default/theme.html` | | Dark | `themes/dark/theme.html` | -| Light | `themes/light/theme.html` | -| Kawaii | `themes/kawaii/theme.html` | Base styles are in `themes/base.js` and `themes/loader.js` handles dynamic theme switching. diff --git a/PRODUCT.md b/PRODUCT.md index c9faa821be..b4610a3d46 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -80,11 +80,9 @@ Optional addons extend Web UI with additional capabilities: ## Themes -Four visual themes are bundled: +Two visual themes are bundled: - **Default** — Standard Nuxeo branding - **Dark** — Dark mode interface -- **Light** — Light/minimal interface -- **Kawaii** — Fun/playful theme Users can switch themes from their profile settings. diff --git a/elements/document/nuxeo-collapsible-document-page.js b/elements/document/nuxeo-collapsible-document-page.js index ff715cad49..3c6e98334b 100644 --- a/elements/document/nuxeo-collapsible-document-page.js +++ b/elements/document/nuxeo-collapsible-document-page.js @@ -40,6 +40,13 @@ Polymer({ - - + + + [[i18n(label)]] @@ -148,6 +278,18 @@ Polymer({ type: String, value: 'left', }, + + /** + * Set by the parent `nuxeo-app` to mirror its `sidebar-expanded` state. + * Reflected so CSS can key off `:host([expanded])` (cross-browser; avoids + * non-standard `:host-context()`). + */ + expanded: { + type: Boolean, + value: false, + reflectToAttribute: true, + observer: '_onExpandedChanged', + }, }, observers: ['_srcOrIcon(icon, src)'], @@ -170,6 +312,13 @@ Polymer({ } }, + _onExpandedChanged(expanded) { + const tooltip = this.$?.tooltip; + if (tooltip && expanded && typeof tooltip.hide === 'function') { + tooltip.hide(); + } + }, + _srcOrIcon() { if (this.src && this.src.length > 0) { this.$.button.icon = ''; diff --git a/elements/nuxeo-app/nuxeo-menu-item.js b/elements/nuxeo-app/nuxeo-menu-item.js index d05a0e2159..1f133ff41a 100644 --- a/elements/nuxeo-app/nuxeo-menu-item.js +++ b/elements/nuxeo-app/nuxeo-menu-item.js @@ -32,6 +32,7 @@ Polymer({ diff --git a/elements/nuxeo-app/nuxeo-page-item.js b/elements/nuxeo-app/nuxeo-page-item.js index 5371c519bb..aef605c6d4 100644 --- a/elements/nuxeo-app/nuxeo-page-item.js +++ b/elements/nuxeo-app/nuxeo-page-item.js @@ -33,7 +33,7 @@ Polymer({ outline: none; user-select: none; cursor: pointer; - color: var(--disabled-text-color); + color: var(--sat-page-tabs-unselected-text-color, var(--disabled-text-color)); margin: 0 0 0 16px; padding: 12px 6px 12px 6px; border-bottom: 2px solid transparent; @@ -47,10 +47,10 @@ Polymer({ border-bottom: 2px solid var(--nuxeo-app-header-pill-hover); color: var(--nuxeo-app-header-pill-hover); } - + /* Active tab border and font color */ :host(.iron-selected) { border-bottom: 2px solid var(--nuxeo-app-header-pill-active); - color: var(--nuxeo-app-header-pill-active); + color: var(--nuxeo-text-default, var(--nuxeo-app-header-pill-active)); } :host(:focus) { diff --git a/elements/nuxeo-app/nuxeo-page.js b/elements/nuxeo-app/nuxeo-page.js index 473446b21c..c04982992f 100644 --- a/elements/nuxeo-app/nuxeo-page.js +++ b/elements/nuxeo-app/nuxeo-page.js @@ -38,13 +38,26 @@ Polymer({ height: calc(100vh - (var(--nuxeo-app-top, 0) + var(--nuxeo-app-bottom, 0))); display: flex; flex-direction: column; + /* Scoped page-chrome variable; set only by rebranded hosts (nuxeo-browser, nuxeo-home). + Unset on admin / legacy pages so they keep the original transparent chrome. */ + background-color: var(--nuxeo-page-host-background, transparent); } #content { flex: 1 1 auto; position: relative; overflow-y: auto; - padding: 16px 16px 0 16px; + padding: var(--nuxeo-page-content-padding, 16px 16px 2px 16px); + background-color: var(--sat-page-surface-background, var(--nuxeo-app-content-background)); + } + + .main-section-container { + padding: var(--nuxeo-page-main-section-padding); + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; /* Important for flex children with overflow */ + margin-bottom: var(--nuxeo-page-margin-bottom, 0); } .toolbar { @@ -53,8 +66,8 @@ Polymer({ @apply --layout-center; height: var(--nuxeo-drawer-header-height); color: var(--nuxeo-app-header); - background: var(--nuxeo-app-header-background); - box-shadow: var(--nuxeo-app-header-box-shadow); + background: var(--nuxeo-page-toolbar-background, var(--nuxeo-app-header-background)); + box-shadow: var(--nuxeo-page-toolbar-box-shadow, var(--nuxeo-app-header-box-shadow)); overflow-x: auto; } @@ -64,11 +77,11 @@ Polymer({ #tabs { flex: 0 0 auto; - background: var(--nuxeo-app-header-background); - box-shadow: var(--nuxeo-app-header-box-shadow); + background: var(--nuxeo-page-tabs-background, var(--nuxeo-app-header-background)); margin-top: 1px; overflow-x: auto; z-index: 1; + border-radius: var(--nuxeo-page-tabs-border-radius, 0); } :host([dir='rtl']) #tabs { @@ -114,11 +127,13 @@ Polymer({
- -
- +
+ +
+ +
`, diff --git a/elements/nuxeo-browser.html b/elements/nuxeo-browser.html index 825f5f8351..464f6e3d61 100644 --- a/elements/nuxeo-browser.html +++ b/elements/nuxeo-browser.html @@ -55,6 +55,31 @@ height: 100%; max-width: var(--nuxeo-browser-actions-menu-max-width, 240px); } + + /* Opt this rebranded host into the Satori page chrome. + These scoped vars are read by ; admin / legacy pages do NOT set them + so they keep the original transparent content + white toolbar. */ + :host { + --nuxeo-page-host-background: var(--sat-page-surface-background); + --nuxeo-page-content-background: var(--sat-page-surface-background); + --nuxeo-page-toolbar-background: var(--sat-app-header-background); + --nuxeo-page-toolbar-box-shadow: var(--sat-app-header-shadow); + } + + /* Apply styles only when viewing folders (document listing) */ + :host([document-facets~='Folderish']) nuxeo-page, + :host([document-facets~='Collection']) nuxeo-page { + --nuxeo-page-tabs-border-radius: 16px 16px 0 0; + --nuxeo-page-main-section-padding: 16px 16px 0 16px; + --nuxeo-page-content-padding: 0; + --nuxeo-page-margin-bottom: 10px; /* Add margin bottom only on document listing page*/ + } + + /* Apply background color to tabs when viewing individual documents (NOT folders) */ + :host(:not([document-facets~='Folderish']):not([document-facets~='Collection'])) nuxeo-page { + --nuxeo-page-tabs-background: var(--sat-page-tabs-background); + --nuxeo-page-content-padding: 16px 16px 0 16px; + } @@ -100,6 +125,7 @@ properties: { document: { type: Object, + observer: '_documentChanged', }, selectedTab: { type: String, @@ -135,6 +161,14 @@ this.fire('document-updated'); }, + _documentChanged() { + if (this.document && this.document.facets) { + this.setAttribute('document-facets', this.document.facets.join(' ')); + } else { + this.removeAttribute('document-facets'); + } + }, + download() { window.location.href = this.document.properties['file:content'].data; }, diff --git a/elements/nuxeo-browser/nuxeo-breadcrumb.js b/elements/nuxeo-browser/nuxeo-breadcrumb.js index 3f7d99abb8..e16d89d4e2 100644 --- a/elements/nuxeo-browser/nuxeo-breadcrumb.js +++ b/elements/nuxeo-browser/nuxeo-breadcrumb.js @@ -57,6 +57,10 @@ import { microTask } from '@polymer/polymer/lib/utils/async.js'; width: 100%; white-space: nowrap; overflow: hidden; + font-family: var(--sat-font-family-primary, --nuxeo-app-font); + font-style: normal; + font-weight: 400; + font-size: 17px; } .current { diff --git a/elements/nuxeo-clipboard/nuxeo-clipboard.js b/elements/nuxeo-clipboard/nuxeo-clipboard.js index 5628960e38..58fa1e2b90 100644 --- a/elements/nuxeo-clipboard/nuxeo-clipboard.js +++ b/elements/nuxeo-clipboard/nuxeo-clipboard.js @@ -59,18 +59,18 @@ Polymer({ .list-item { cursor: pointer; - padding: 1em; - border-bottom: 1px solid var(--nuxeo-border); + padding: 0.7em 1em; + @apply --sat-drawer-item; } .list-item:hover { - @apply --nuxeo-block-hover; + @apply --sat-drawer-item-selected; } .list-item.selected, .list-item:focus, .list-item.selected:focus { - @apply --nuxeo-block-selected; + @apply --sat-drawer-item-selected; } .list-item-box { @@ -89,6 +89,7 @@ Polymer({ .list-item-title { @apply --layout-flex; + @apply --sat-drawer-item; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -126,6 +127,10 @@ Polymer({ text-align: center; font-size: 1rem; } + + .header h5 { + @apply --sat-section-header; + } diff --git a/elements/nuxeo-collections/nuxeo-collections.js b/elements/nuxeo-collections/nuxeo-collections.js index e000d607ae..7f03229b37 100644 --- a/elements/nuxeo-collections/nuxeo-collections.js +++ b/elements/nuxeo-collections/nuxeo-collections.js @@ -134,18 +134,22 @@ Polymer({ .list-item { cursor: pointer; - padding: 1em; - border-bottom: 1px solid var(--nuxeo-border); + padding: 0.7em 1em; + @apply --sat-drawer-item; + } + + .list-item-title { + @apply --sat-drawer-item; } .list-item:hover { - @apply --nuxeo-block-hover; + @apply --sat-drawer-item-selected; } .list-item.selected, .list-item:focus, .list-item.selected:focus { - @apply --nuxeo-block-selected; + @apply --sat-drawer-item-selected; } .list-item iron-icon { @@ -173,6 +177,10 @@ Polymer({ @apply --layout-flex; @apply --layout-horizontal; } + + .collection-header { + @apply --sat-section-header; + } @@ -193,7 +201,7 @@ Polymer({ > diff --git a/elements/nuxeo-collections/nuxeo-favorites.js b/elements/nuxeo-collections/nuxeo-favorites.js index 98b891a774..ec1cc9f0be 100644 --- a/elements/nuxeo-collections/nuxeo-favorites.js +++ b/elements/nuxeo-collections/nuxeo-favorites.js @@ -68,8 +68,8 @@ Polymer({ .list-item { cursor: pointer; - padding: 1em; - border-bottom: 1px solid var(--nuxeo-border); + padding: 0.7em 1em; + @apply --sat-drawer-item; } .list-item-box { @@ -88,19 +88,20 @@ Polymer({ .list-item-title { @apply --layout-flex; + @apply --sat-drawer-item; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } .list-item:hover { - @apply --nuxeo-block-hover; + @apply --sat-drawer-item-selected; } .list-item.selected, .list-item:focus, .list-item.selected:focus { - @apply --nuxeo-block-selected; + @apply --sat-drawer-item-selected; } .list-item-property { @@ -130,6 +131,10 @@ Polymer({ margin: 0; background-color: transparent; } + + .header h5 { + @apply --sat-section-header; + } diff --git a/elements/nuxeo-document-create-actions/nuxeo-document-create-shortcut.js b/elements/nuxeo-document-create-actions/nuxeo-document-create-shortcut.js index 897e06793b..caf3c49cea 100644 --- a/elements/nuxeo-document-create-actions/nuxeo-document-create-shortcut.js +++ b/elements/nuxeo-document-create-actions/nuxeo-document-create-shortcut.js @@ -17,9 +17,10 @@ limitations under the License. */ import '@polymer/polymer/polymer-legacy.js'; +import '@nuxeo/nuxeo-ui-elements/nuxeo-icons.js'; +import '@polymer/iron-icon/iron-icon.js'; import '@polymer/paper-fab/paper-fab.js'; import { I18nBehavior } from '@nuxeo/nuxeo-ui-elements/nuxeo-i18n-behavior.js'; -import '@nuxeo/nuxeo-ui-elements/widgets/nuxeo-tooltip.js'; import { Polymer } from '@polymer/polymer/lib/legacy/polymer-fn.js'; import { html } from '@polymer/polymer/lib/utils/html-tag.js'; @@ -31,30 +32,80 @@ import { html } from '@polymer/polymer/lib/utils/html-tag.js'; Polymer({ _template: html` - - [[i18n(label)]] +
+ + [[i18n(label)]] +
`, is: 'nuxeo-document-create-shortcut', @@ -66,6 +117,13 @@ Polymer({ label: String, }, + _handleKeydown(e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this._tap(); + } + }, + _tap() { this.fire('create-document', { type: this.type }); }, diff --git a/elements/nuxeo-document-create-actions/nuxeo-document-create-shortcuts.js b/elements/nuxeo-document-create-actions/nuxeo-document-create-shortcuts.js index 4902c6a564..982c52ede1 100644 --- a/elements/nuxeo-document-create-actions/nuxeo-document-create-shortcuts.js +++ b/elements/nuxeo-document-create-actions/nuxeo-document-create-shortcuts.js @@ -34,8 +34,11 @@ Polymer({ diff --git a/elements/nuxeo-results/nuxeo-results.js b/elements/nuxeo-results/nuxeo-results.js index a6f10539c1..e178c1aafc 100644 --- a/elements/nuxeo-results/nuxeo-results.js +++ b/elements/nuxeo-results/nuxeo-results.js @@ -164,6 +164,7 @@ Polymer({ opacity: 0.8; margin-right: 16px; transition: opacity 100ms ease-in-out; + padding-left: 4px; } paper-icon-button[selected] { diff --git a/elements/nuxeo-suggester/nuxeo-suggester.js b/elements/nuxeo-suggester/nuxeo-suggester.js index f72fa4b508..57e907807a 100644 --- a/elements/nuxeo-suggester/nuxeo-suggester.js +++ b/elements/nuxeo-suggester/nuxeo-suggester.js @@ -249,7 +249,7 @@ Polymer({ @media (max-width: 1024px) { #searchButton { - background-color: var(--nuxeo-app-header-background); + background-color: var(--sat-app-header-box-background-color, var(--nuxeo-app-header-background)); z-index: 100; } diff --git a/elements/nuxeo-tasks/nuxeo-tasks-drawer.js b/elements/nuxeo-tasks/nuxeo-tasks-drawer.js index ea65f99f96..d181751929 100644 --- a/elements/nuxeo-tasks/nuxeo-tasks-drawer.js +++ b/elements/nuxeo-tasks/nuxeo-tasks-drawer.js @@ -36,6 +36,9 @@ Polymer({ display: block; border-top: 1px solid var(--nuxeo-border); } + .header h5 { + @apply --sat-section-header; + }
diff --git a/elements/nuxeo-tasks/nuxeo-tasks-list.js b/elements/nuxeo-tasks/nuxeo-tasks-list.js index 57635901f8..61a78c1ba1 100644 --- a/elements/nuxeo-tasks/nuxeo-tasks-list.js +++ b/elements/nuxeo-tasks/nuxeo-tasks-list.js @@ -67,18 +67,18 @@ Polymer({ .list-item { cursor: pointer; - padding: 1em; - border-bottom: 1px solid var(--nuxeo-border); + padding: 0.7em 1em; + @apply --sat-drawer-item; } .list-item:hover { - @apply --nuxeo-block-hover; + @apply --sat-drawer-item-selected; } .list-item.selected, .list-item:focus, .list-item.selected:focus { - @apply --nuxeo-block-selected; + @apply --sat-drawer-item-selected; } nuxeo-data-list { diff --git a/elements/nuxeo-web-ui-bundle.html b/elements/nuxeo-web-ui-bundle.html index aeaef719c6..494a9731e4 100644 --- a/elements/nuxeo-web-ui-bundle.html +++ b/elements/nuxeo-web-ui-bundle.html @@ -541,7 +541,7 @@ @@ -560,13 +560,8 @@ @@ -615,12 +610,8 @@ @@ -727,18 +718,6 @@ - - - - - - - - diff --git a/elements/search/nuxeo-saved-search-actions.js b/elements/search/nuxeo-saved-search-actions.js index 51138a46ad..fbd6a24e70 100644 --- a/elements/search/nuxeo-saved-search-actions.js +++ b/elements/search/nuxeo-saved-search-actions.js @@ -72,7 +72,7 @@ Polymer({ hidden$="[[!_showOtherSearchActions(searchDoc, isSavedSearch, _dirty, _isSearchFormVisible)]]" > diff --git a/elements/search/nuxeo-search-form.js b/elements/search/nuxeo-search-form.js index 1e50cd7c27..97f819c957 100644 --- a/elements/search/nuxeo-search-form.js +++ b/elements/search/nuxeo-search-form.js @@ -77,7 +77,6 @@ Polymer({ right: 0; margin: 0; padding: 1rem; - background-color: var(--nuxeo-drawer-background); } .actions paper-button { @@ -165,7 +164,6 @@ Polymer({ box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.1) inset; @apply --layout-horizontal; @apply --layout-center; - background-color: var(--nuxeo-drawer-background); } .header h1 { @@ -217,19 +215,22 @@ Polymer({ .list-item { cursor: pointer; - color: var(--nuxeo-drawer-text); - padding: 1em; - border-bottom: 1px solid var(--nuxeo-border); + padding: 0.7em 1em; + @apply --sat-drawer-item; } .list-item:hover { - @apply --nuxeo-block-hover; + @apply --sat-drawer-item-selected; } .list-item.selected, .list-item:focus, .list-item.selected:focus { - @apply --nuxeo-block-selected; + @apply --sat-drawer-item-selected; + } + + .list-item-title { + @apply --sat-drawer-item; } .list-item-info { diff --git a/fonts/figtree-latin-500-normal.woff b/fonts/figtree-latin-500-normal.woff new file mode 100644 index 0000000000..10761c3b5a Binary files /dev/null and b/fonts/figtree-latin-500-normal.woff differ diff --git a/fonts/figtree-latin-500-normal.woff2 b/fonts/figtree-latin-500-normal.woff2 new file mode 100644 index 0000000000..6ab5588321 Binary files /dev/null and b/fonts/figtree-latin-500-normal.woff2 differ diff --git a/fonts/noto-sans-latin-300-normal.woff b/fonts/noto-sans-latin-300-normal.woff new file mode 100644 index 0000000000..038ad2c4c5 Binary files /dev/null and b/fonts/noto-sans-latin-300-normal.woff differ diff --git a/fonts/noto-sans-latin-300-normal.woff2 b/fonts/noto-sans-latin-300-normal.woff2 new file mode 100644 index 0000000000..60ff5b9a08 Binary files /dev/null and b/fonts/noto-sans-latin-300-normal.woff2 differ diff --git a/fonts/noto-sans-latin-400-normal.woff b/fonts/noto-sans-latin-400-normal.woff new file mode 100644 index 0000000000..ee9eb7f8bb Binary files /dev/null and b/fonts/noto-sans-latin-400-normal.woff differ diff --git a/fonts/noto-sans-latin-400-normal.woff2 b/fonts/noto-sans-latin-400-normal.woff2 new file mode 100644 index 0000000000..f9e0a65f88 Binary files /dev/null and b/fonts/noto-sans-latin-400-normal.woff2 differ diff --git a/fonts/noto-sans-latin-500-normal.woff b/fonts/noto-sans-latin-500-normal.woff new file mode 100644 index 0000000000..84fa6d8e6b Binary files /dev/null and b/fonts/noto-sans-latin-500-normal.woff differ diff --git a/fonts/noto-sans-latin-500-normal.woff2 b/fonts/noto-sans-latin-500-normal.woff2 new file mode 100644 index 0000000000..94ebd9f11e Binary files /dev/null and b/fonts/noto-sans-latin-500-normal.woff2 differ diff --git a/fonts/noto-sans-latin-600-normal.woff b/fonts/noto-sans-latin-600-normal.woff new file mode 100644 index 0000000000..80de2018c0 Binary files /dev/null and b/fonts/noto-sans-latin-600-normal.woff differ diff --git a/fonts/noto-sans-latin-600-normal.woff2 b/fonts/noto-sans-latin-600-normal.woff2 new file mode 100644 index 0000000000..456247bd7c Binary files /dev/null and b/fonts/noto-sans-latin-600-normal.woff2 differ diff --git a/fonts/noto-sans-latin-700-normal.woff b/fonts/noto-sans-latin-700-normal.woff new file mode 100644 index 0000000000..654b0b5344 Binary files /dev/null and b/fonts/noto-sans-latin-700-normal.woff differ diff --git a/fonts/noto-sans-latin-700-normal.woff2 b/fonts/noto-sans-latin-700-normal.woff2 new file mode 100644 index 0000000000..feff1bf1f5 Binary files /dev/null and b/fonts/noto-sans-latin-700-normal.woff2 differ diff --git a/fonts/noto-sans-latin-900-normal.woff b/fonts/noto-sans-latin-900-normal.woff new file mode 100644 index 0000000000..890ba31f98 Binary files /dev/null and b/fonts/noto-sans-latin-900-normal.woff differ diff --git a/fonts/noto-sans-latin-900-normal.woff2 b/fonts/noto-sans-latin-900-normal.woff2 new file mode 100644 index 0000000000..377e9e2a66 Binary files /dev/null and b/fonts/noto-sans-latin-900-normal.woff2 differ diff --git a/i18n/messages-ar.json b/i18n/messages-ar.json index 5d128215e9..e4d504bf84 100644 --- a/i18n/messages-ar.json +++ b/i18n/messages-ar.json @@ -1418,8 +1418,6 @@ "themes.current": "حالي", "themes.dark": "داكن", "themes.default": "Nuxeo", - "themes.kawaii": "محبوب", - "themes.light": "فاتح", "threeDViewLayout.delete.confirm.message": "هل تريد فعلا حذف هذا المحتوى ثلاثي الأبعاد؟", "threeDViewLayout.delete.tooltip": "حذف محتوى ثلاثي الأبعاد", "threeDViewLayout.deleted.message": "تم حذف محتوى ثلاثي الأبعاد", diff --git a/i18n/messages-cs.json b/i18n/messages-cs.json index 2e4502c5d3..5001417375 100644 --- a/i18n/messages-cs.json +++ b/i18n/messages-cs.json @@ -1418,8 +1418,6 @@ "themes.current": "Current", "themes.dark": "Dark", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Light", "threeDViewLayout.delete.confirm.message": "Do you really want to delete this 3D content?", "threeDViewLayout.delete.tooltip": "Delete 3D Content", "threeDViewLayout.deleted.message": "3D content deleted", diff --git a/i18n/messages-de.json b/i18n/messages-de.json index 5aac1d5f00..540d343b44 100644 --- a/i18n/messages-de.json +++ b/i18n/messages-de.json @@ -1418,8 +1418,6 @@ "themes.current": "Aktuell", "themes.dark": "Dunkel", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Hell", "threeDViewLayout.delete.confirm.message": "Wollen Sie dieses 3D wirklich löschen?", "threeDViewLayout.delete.tooltip": "3D-Inhalt löschen", "threeDViewLayout.deleted.message": "3D gelöscht", diff --git a/i18n/messages-es-ES.json b/i18n/messages-es-ES.json index d164e4c8c6..1df2dd121a 100644 --- a/i18n/messages-es-ES.json +++ b/i18n/messages-es-ES.json @@ -1418,8 +1418,6 @@ "themes.current": "Actual", "themes.dark": "Oscuro", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Luz", "threeDViewLayout.delete.confirm.message": "¿Esta seguro de que desea eliminar este 3D?", "threeDViewLayout.delete.tooltip": "Borrar contenido 3D", "threeDViewLayout.deleted.message": "3D eliminado", diff --git a/i18n/messages-eu.json b/i18n/messages-eu.json index 7823ef8481..8cfe9a54d9 100644 --- a/i18n/messages-eu.json +++ b/i18n/messages-eu.json @@ -1418,8 +1418,6 @@ "themes.current": "Unekoa", "themes.dark": "Iluna", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Argia", "threeDViewLayout.delete.confirm.message": "Ziur 3D eduki hau ezabatu nahi duzula?", "threeDViewLayout.delete.tooltip": "Ezabatu 3D edukia", "threeDViewLayout.deleted.message": "3D edukia ezabatuta", diff --git a/i18n/messages-fr.json b/i18n/messages-fr.json index 042c642e38..4d1f8eeb70 100644 --- a/i18n/messages-fr.json +++ b/i18n/messages-fr.json @@ -1418,8 +1418,6 @@ "themes.current": "Appliqué", "themes.dark": "Dark", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Light", "threeDViewLayout.delete.confirm.message": "Voulez-vous vraiment supprimer ce document 3D ?", "threeDViewLayout.delete.tooltip": "Supprimer le document 3D", "threeDViewLayout.deleted.message": "Document 3D supprimé", diff --git a/i18n/messages-he.json b/i18n/messages-he.json index 8898e9ad70..f87da205c8 100644 --- a/i18n/messages-he.json +++ b/i18n/messages-he.json @@ -1418,8 +1418,6 @@ "themes.current": "נוכחי", "themes.dark": "כהה", "themes.default": "Nuxeo", - "themes.kawaii": "קוואי", - "themes.light": "לייט", "threeDViewLayout.delete.confirm.message": "האם אתה באמת רוצה למחוק את התוכן התלת-ממדי הזה?", "threeDViewLayout.delete.tooltip": "מחיקת תוכן תלת-ממדי", "threeDViewLayout.deleted.message": "התוכן התלת-ממדי נמחק", diff --git a/i18n/messages-id.json b/i18n/messages-id.json index 7a3d09997e..12c0d32e49 100644 --- a/i18n/messages-id.json +++ b/i18n/messages-id.json @@ -1418,8 +1418,6 @@ "themes.current": "Arus", "themes.dark": "Gelap", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Cahaya", "threeDViewLayout.delete.confirm.message": "Apakah anda benar-benar ingin menghapus konten 3D ini?", "threeDViewLayout.delete.tooltip": "Hapus Konten 3D", "threeDViewLayout.deleted.message": "Konten 3D dihapus", diff --git a/i18n/messages-it.json b/i18n/messages-it.json index 264168d560..b06ed2c007 100644 --- a/i18n/messages-it.json +++ b/i18n/messages-it.json @@ -1418,8 +1418,6 @@ "themes.current": "Corrente", "themes.dark": "Scuro", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Luminoso", "threeDViewLayout.delete.confirm.message": "Eliminare il contenuto 3D?", "threeDViewLayout.delete.tooltip": "Elimina contenuto 3D", "threeDViewLayout.deleted.message": "Contenuto 3D eliminato", diff --git a/i18n/messages-ja.json b/i18n/messages-ja.json index 29fcca2095..eb65bd6c1f 100644 --- a/i18n/messages-ja.json +++ b/i18n/messages-ja.json @@ -1418,8 +1418,6 @@ "themes.current": "現行", "themes.dark": "ダーク", "themes.default": "Nuxeo", - "themes.kawaii": "カワイイ", - "themes.light": "ライト", "threeDViewLayout.delete.confirm.message": "この3Dを削除しますか?", "threeDViewLayout.delete.tooltip": "3Dコンテンツを削除", "threeDViewLayout.deleted.message": "3D削除済み", diff --git a/i18n/messages-nl.json b/i18n/messages-nl.json index bd5c0bb2fd..700d800859 100644 --- a/i18n/messages-nl.json +++ b/i18n/messages-nl.json @@ -1418,8 +1418,6 @@ "themes.current": "Huidig", "themes.dark": "Donker", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Licht", "threeDViewLayout.delete.confirm.message": "Weet u zeker dat u deze 3D-content wilt wissen?", "threeDViewLayout.delete.tooltip": "3D-content wissen", "threeDViewLayout.deleted.message": "3D-content gewist", diff --git a/i18n/messages-pl.json b/i18n/messages-pl.json index 4562f6db28..7afa6dfcb9 100644 --- a/i18n/messages-pl.json +++ b/i18n/messages-pl.json @@ -1418,8 +1418,6 @@ "themes.current": "Bieżący", "themes.dark": "Ciemny", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Jasny", "threeDViewLayout.delete.confirm.message": "Czy na pewno chcesz usunąć tę zawartość 3D?", "threeDViewLayout.delete.tooltip": "Usuń zawartość 3D", "threeDViewLayout.deleted.message": "Usunięto zawartość 3D", diff --git a/i18n/messages-pt-PT.json b/i18n/messages-pt-PT.json index bfdb7e5529..70d1cecd5a 100644 --- a/i18n/messages-pt-PT.json +++ b/i18n/messages-pt-PT.json @@ -1418,8 +1418,6 @@ "themes.current": "Atual", "themes.dark": "Escuro", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Suave", "threeDViewLayout.delete.confirm.message": "Deseja mesmo excluir este 3D?", "threeDViewLayout.delete.tooltip": "Excluir conteúdo 3D", "threeDViewLayout.deleted.message": "3D deletado", diff --git a/i18n/messages-sv-SE.json b/i18n/messages-sv-SE.json index 957a8c0899..cd9f4c76bc 100644 --- a/i18n/messages-sv-SE.json +++ b/i18n/messages-sv-SE.json @@ -1418,8 +1418,6 @@ "themes.current": "Aktuellt", "themes.dark": "Mörkt", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Ljust", "threeDViewLayout.delete.confirm.message": "Vill du verkligen ta bort detta 3D-innehåll?", "threeDViewLayout.delete.tooltip": "Ta bort 3D-innehåll", "threeDViewLayout.deleted.message": "3D-innehåll borttaget", diff --git a/i18n/messages-zh-CN.json b/i18n/messages-zh-CN.json index d9448b43f3..bd4ddcfef6 100644 --- a/i18n/messages-zh-CN.json +++ b/i18n/messages-zh-CN.json @@ -1418,8 +1418,6 @@ "themes.current": "当前", "themes.dark": "Dark", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Light", "threeDViewLayout.delete.confirm.message": "是否确实要删除此3D 内容?", "threeDViewLayout.delete.tooltip": "删除3D 内容", "threeDViewLayout.deleted.message": "3D 内容已删除", diff --git a/i18n/messages.json b/i18n/messages.json index becc4a49f0..4ae311241d 100644 --- a/i18n/messages.json +++ b/i18n/messages.json @@ -31,6 +31,8 @@ "activity.versionRemoved": "Version removed", "activity.documentRestored": "Document restored", "accessibility.logo": "Application Logo", + "accessibility.sidebar.logoCollapsed": "{0} logo. Open menu", + "accessibility.sidebar.logoExpanded": "{0} logo, expanded. Close menu", "addToCollectionDocumentsButton.dialog.add": "Add to Collection", "addToCollectionDocumentsButton.dialog.cancel": "Cancel", "addToCollectionDocumentsButton.dialog.collections": "Collections", @@ -48,6 +50,7 @@ "analytics.workflow": "Workflow", "app.account": "User Settings", "app.administration": "Administration", + "app.brandedProductName": "Hyland {0}", "app.browse": "Browse", "app.clipboard": "Clipboard", "app.collections": "Collections", @@ -83,6 +86,7 @@ "app.expiredSearch": "Expired", "app.expiredSession.message": "Your session has expired! Click here to login again.", "app.favorites": "Favorites", + "app.home": "Home", "app.homeNotFound": "Home layout not found", "app.nxqlSearch": "NXQL Search", "app.offlineBanner.message": "Your network is unavailable. Please check your connection.", @@ -1420,8 +1424,6 @@ "themes.current": "Current", "themes.dark": "Dark", "themes.default": "Nuxeo", - "themes.kawaii": "Kawaii", - "themes.light": "Light", "threeDViewLayout.delete.confirm.message": "Do you really want to delete this 3D content?", "threeDViewLayout.delete.tooltip": "Delete 3D Content", "threeDViewLayout.deleted.message": "3D content deleted", diff --git a/images/doctypes/folder.svg b/images/doctypes/folder.svg index c445101cda..cc1511f93b 100644 --- a/images/doctypes/folder.svg +++ b/images/doctypes/folder.svg @@ -1,6 +1,7 @@ - - + + + \ No newline at end of file diff --git a/images/hyland-wordmark.svg b/images/hyland-wordmark.svg new file mode 100644 index 0000000000..4866ff82f9 --- /dev/null +++ b/images/hyland-wordmark.svg @@ -0,0 +1,9 @@ + + Hyland + + + + + + + diff --git a/images/icons/add.svg b/images/icons/add.svg new file mode 100644 index 0000000000..4863e63bc5 --- /dev/null +++ b/images/icons/add.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/icons/administration.svg b/images/icons/administration.svg new file mode 100644 index 0000000000..655bf4d97d --- /dev/null +++ b/images/icons/administration.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/nuxeo-web-ui-ftest/pages/ui/drawer.js b/packages/nuxeo-web-ui-ftest/pages/ui/drawer.js index 66407a635a..bdf155e0ab 100644 --- a/packages/nuxeo-web-ui-ftest/pages/ui/drawer.js +++ b/packages/nuxeo-web-ui-ftest/pages/ui/drawer.js @@ -74,7 +74,10 @@ export default class Drawer extends BasePage { const isVisible = await section.isVisible(); if (!isVisible) { const menu = await this.menu; - const buttonToclick = await menu.$(`nuxeo-menu-icon[name='${name}']`); + // The profile avatar was rebranded from to + // ; all other items remain nuxeo-menu-icon. + const selector = name === 'profile' ? '#profileWrapper' : `nuxeo-menu-icon[name='${name}']`; + const buttonToclick = await menu.$(selector); await buttonToclick.click(); // Wait for the opened panel to be visible after the click await driver.waitUntil( diff --git a/test/nuxeo-app.test.js b/test/nuxeo-app.test.js index b151c4ff15..2499f10524 100644 --- a/test/nuxeo-app.test.js +++ b/test/nuxeo-app.test.js @@ -140,6 +140,32 @@ suite('nuxeo-app', () => { localStorage.getItem.restore(); }); + test('_onProfileAvatarClick prevents navigation', () => { + const e = { preventDefault: sinon.spy() }; + app._onProfileAvatarClick(e); + expect(e.preventDefault).to.have.been.calledOnce; + }); + + test('_userInitials derives display initials from user properties', () => { + expect(app._userInitials(null)).to.equal(''); + expect(app._userInitials({ properties: { firstName: 'Jane', lastName: 'Doe' } })).to.equal('JD'); + expect(app._userInitials({ properties: { firstName: 'Al' } })).to.equal('AL'); + expect(app._userInitials({ properties: { firstName: 'Q' } })).to.equal('Q'); + expect(app._userInitials({ properties: { lastName: 'Ng' } })).to.equal('NG'); + expect(app._userInitials({ properties: { lastName: 'X' } })).to.equal('X'); + expect(app._userInitials({ id: 'administrator', properties: {} })).to.equal('AD'); + expect(app._userInitials({ properties: {} })).to.equal('??'); + }); + + test('_resizeDuringAnimation schedules resize animation loop', () => { + app._resizeDuringAnimation(); + expect(app._resizeLoop).to.exist; + if (app._resizeLoop) { + cancelAnimationFrame(app._resizeLoop); + app._resizeLoop = null; + } + }); + test('_baseUrlChanged assigns RoutingBehavior.baseUrl', () => { app.baseUrl = 'https://example/nuxeo/'; app._baseUrlChanged(); @@ -473,6 +499,381 @@ suite('nuxeo-app', () => { expect(app.toggleChevronIcon).to.equal('icons:chevron-left'); }); + suite('keyboard navigation helpers', () => { + test('home ArrowUp returns focus to logo', () => { + const homeLink = app.shadowRoot.querySelector('.home-link'); + const logo = app.$.logo; + sinon.spy(logo, 'focus'); + + const event = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true, cancelable: true }); + homeLink.dispatchEvent(event); + + expect(event.defaultPrevented).to.be.true; + expect(logo.focus).to.have.been.calledOnce; + logo.focus.restore(); + }); + + test('logo ArrowDown focuses home link when available', () => { + const logo = app.$.logo; + const homeLink = app.shadowRoot.querySelector('.home-link'); + sinon.spy(homeLink, 'focus'); + + logo.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); + + expect(homeLink.focus).to.have.been.calledOnce; + homeLink.focus.restore(); + }); + + test('home ArrowDown focuses first menu item via _setFocusedItem', () => { + const menu = app.$.menu; + const homeLink = app.shadowRoot.querySelector('.home-link'); + sinon.stub(menu, '_setFocusedItem'); + + homeLink.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true })); + + expect(menu._setFocusedItem).to.have.been.calledOnce; + menu._setFocusedItem.restore(); + }); + + test('home ArrowDown with no visible menu items exits early', () => { + const homeLink = app.shadowRoot.querySelector('.home-link'); + const menu = app.$.menu; + + const prevItems = menu.items; + menu.items = []; + + homeLink.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true })); + + menu.items = prevItems; + }); + + test('logo ArrowDown fallback path focuses first item when no home link exists', () => { + const logo = document.createElement('div'); + const menu = document.createElement('div'); + const menuContainer = document.createElement('div'); + const item = document.createElement('div'); + item.setAttribute('name', 'synthetic-item'); + + // Simulate paper-listbox public API consumed by logoToMenuNavigation. + menu.items = [item]; + menu._setFocusedItem = undefined; + sinon.spy(item, 'setAttribute'); + sinon.spy(item, 'focus'); + + app.logoToMenuNavigation.call({ + $: { + logo, + menu, + menuContainer: { + querySelector: () => null, + addEventListener: menuContainer.addEventListener.bind(menuContainer), + }, + }, + }); + + logo.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true })); + + expect(item.setAttribute).to.have.been.calledWith('tabindex', '0'); + expect(item.focus).to.have.been.calledOnce; + item.setAttribute.restore(); + item.focus.restore(); + }); + + test('menu ArrowUp on first item focuses home link and stops further handling', () => { + const menu = app.$.menu; + const homeLink = app.shadowRoot.querySelector('.home-link'); + const visibleItems = (menu.items || []).filter((el) => !el.hasAttribute('hidden')); + const firstItem = visibleItems[0]; + sinon.spy(homeLink, 'focus'); + + if (firstItem) { + firstItem.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true, cancelable: true })); + } + + expect(homeLink.focus).to.have.been.calledOnce; + homeLink.focus.restore(); + }); + + test('menu ArrowDown on last item returns focus to logo', () => { + const menu = app.$.menu; + const logo = app.$.logo; + const visibleItems = (menu.items || []).filter((el) => !el.hasAttribute('hidden')); + const lastItem = visibleItems[visibleItems.length - 1]; + sinon.spy(logo, 'focus'); + + if (lastItem) { + lastItem.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true })); + } + + expect(logo.focus).to.have.been.calledOnce; + logo.focus.restore(); + }); + + test('logo Enter toggles sidebar expanded state', () => { + const logo = app.$.logo; + expect(app.sidebarExpanded).to.be.false; + logo.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })); + expect(app.sidebarExpanded).to.be.true; + logo.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })); + expect(app.sidebarExpanded).to.be.false; + }); + }); + + suite('expandable sidebar', () => { + test('_sidebarProductTitle uses productName', () => { + const fakeI18n = (key, ...args) => (key === 'app.brandedProductName' ? `Hyland ${args[0]}` : key); + expect(app._sidebarProductTitle('Nuxeo', fakeI18n)).to.equal('Hyland Nuxeo'); + }); + + test('_toggleDrawer collapses expanded sidebar when opening drawer', () => { + app.sidebarExpanded = true; + app.drawerOpened = false; + app._selected = ''; + app._toggleDrawer({ type: 'iron-activate' }, { detail: { selected: 'browser' } }); + expect(app.sidebarExpanded).to.be.false; + expect(app.drawerOpened).to.be.true; + }); + + test('_toggleDrawer collapses expanded sidebar when clicking the same item', async () => { + app.sidebarExpanded = true; + app.drawerOpened = true; + app._selected = 'browser'; + app.selectedTab = 'browser'; + app._toggleDrawer({ type: 'iron-activate', detail: { selected: 'browser' } }); + await new Promise((resolve) => requestAnimationFrame(resolve)); + expect(app.drawerOpened).to.be.true; + expect(app.sidebarExpanded).to.be.false; + }); + + test('logo keeps dynamic theme img src', () => { + const img = app.$.logo.querySelector('img'); + expect(img.getAttribute('src')).to.include('themes/'); + }); + + test('sidebarRail is fixed for width transition', () => { + const rail = app.$.sidebarRail; + expect(rail).to.exist; + expect(getComputedStyle(rail).position).to.equal('fixed'); + }); + + test('sidebar scrim is presentational and stays hidden from AT', () => { + const scrim = app.$.sidebarExpandScrim; + expect(scrim.getAttribute('role')).to.equal('presentation'); + expect(scrim.getAttribute('aria-hidden')).to.equal('true'); + }); + + test('_logoAriaLabel uses the collapsed key with product title placeholder', () => { + app.productName = 'Nuxeo'; + const fakeI18n = (key, ...args) => + key === 'app.brandedProductName' ? `Hyland ${args[0]}` : `${key}|${args.join(',')}`; + + expect(app._logoAriaLabel(false, app.productName, fakeI18n)).to.equal( + 'accessibility.sidebar.logoCollapsed|Hyland Nuxeo', + ); + }); + + test('_logoAriaLabel uses the expanded key with product title placeholder', () => { + app.productName = 'Nuxeo'; + const fakeI18n = (key, ...args) => + key === 'app.brandedProductName' ? `Hyland ${args[0]}` : `${key}|${args.join(',')}`; + + expect(app._logoAriaLabel(true, app.productName, fakeI18n)).to.equal( + 'accessibility.sidebar.logoExpanded|Hyland Nuxeo', + ); + }); + + test('_syncSidebarExpandedTooltips hides profile tooltip on expand', () => { + sinon.stub(app, '_syncSidebarMenuTooltips'); + const profileTooltip = app.$.profileTooltip; + sinon.stub(profileTooltip, 'hide'); + + app._syncSidebarExpandedTooltips(true); + + expect(app._syncSidebarMenuTooltips).to.have.been.calledWith(true); + expect(profileTooltip.hide).to.have.been.calledOnce; + + app._syncSidebarMenuTooltips.restore(); + profileTooltip.hide.restore(); + }); + + test('_syncSidebarExpandedTooltips does not call hide when collapsing', () => { + sinon.stub(app, '_syncSidebarMenuTooltips'); + const profileTooltip = app.$.profileTooltip; + sinon.stub(profileTooltip, 'hide'); + + app._syncSidebarExpandedTooltips(false); + + expect(profileTooltip.hide).to.not.have.been.called; + app._syncSidebarMenuTooltips.restore(); + profileTooltip.hide.restore(); + }); + + test('_syncSidebarMenuTooltips propagates expanded state to all icons', () => { + const iconA = {}; + const iconB = {}; + const originalMenuContainer = app.$.menuContainer; + app.$.menuContainer = { querySelectorAll: sinon.stub().returns([iconA, iconB]) }; + + app._syncSidebarMenuTooltips(true); + expect(iconA.expanded).to.be.true; + expect(iconB.expanded).to.be.true; + + app._syncSidebarMenuTooltips(false); + expect(iconA.expanded).to.be.false; + expect(iconB.expanded).to.be.false; + + app.$.menuContainer = originalMenuContainer; + }); + + test('_syncSidebarMenuTooltips returns early when menu container is missing', () => { + const ctx = { $: null }; + expect(() => app._syncSidebarMenuTooltips.call(ctx, true)).to.not.throw(); + }); + + test('_onSidebarNavClick collapses sidebar and resets task selection', () => { + sinon.stub(app, '_collapseSidebar'); + sinon.stub(app, '_resetTaskSelection'); + + app._onSidebarNavClick(); + + expect(app._collapseSidebar).to.have.been.calledOnce; + expect(app._resetTaskSelection).to.have.been.calledOnce; + app._collapseSidebar.restore(); + app._resetTaskSelection.restore(); + }); + + test('_onLogoClick prevents default, stops propagation and toggles sidebar', () => { + sinon.stub(app, '_toggleSidebarExpanded'); + const event = { preventDefault: sinon.spy(), stopPropagation: sinon.spy() }; + + app._onLogoClick(event); + + expect(event.preventDefault).to.have.been.calledOnce; + expect(event.stopPropagation).to.have.been.calledOnce; + expect(app._toggleSidebarExpanded).to.have.been.calledOnce; + app._toggleSidebarExpanded.restore(); + }); + + test('_onSidebarScrimClick collapses sidebar', () => { + sinon.stub(app, '_collapseSidebar'); + + app._onSidebarScrimClick(); + + expect(app._collapseSidebar).to.have.been.calledOnce; + app._collapseSidebar.restore(); + }); + + test('_onSidebarEscape collapses sidebar when Escape is pressed and expanded', () => { + sinon.stub(app, '_collapseSidebar'); + app.sidebarExpanded = true; + + app._onSidebarEscape({ key: 'Escape' }); + + expect(app._collapseSidebar).to.have.been.calledOnce; + app._collapseSidebar.restore(); + }); + + test('menu keyup listener delegates to _toggleDrawer', () => { + sinon.stub(app, '_toggleDrawer'); + app.$.menu.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true, cancelable: true })); + + expect(app._toggleDrawer.called).to.be.true; + app._toggleDrawer.restore(); + }); + + test('drawer transition listeners call _resizeDuringAnimation', () => { + sinon.stub(app, '_resizeDuringAnimation'); + + app.$.drawer.dispatchEvent(new Event('transitionrun')); + app.$.drawer.dispatchEvent(new Event('transitionstart')); + + expect(app._resizeDuringAnimation.callCount).to.equal(2); + app._resizeDuringAnimation.restore(); + }); + + test('toast opening listener applies panel style tweaks', async () => { + app.$.toast.labelText = 'opening'; + app.$.toast.show(); + await app.$.toast.updateComplete; + + const root = app.$.toast.mdcRoot; + const label = root && root.querySelector('.mdc-snackbar__label'); + const surface = root && root.querySelector('.mdc-snackbar__surface'); + expect(root).to.exist; + expect(root.style.position).to.equal('relative'); + expect(label.style.webkitFontSmoothing).to.equal('auto'); + expect(surface.style.width).to.equal('344px'); + app.$.toast.close(); + }); + + test('unhandledrejection listener reports 404 errors via showError', () => { + const originalAddEventListener = window.addEventListener; + let rejectionHandler; + window.addEventListener = (type, handler, ...rest) => { + if (type === 'unhandledrejection') { + rejectionHandler = handler; + } + return originalAddEventListener.call(window, type, handler, ...rest); + }; + + return fixture(html``) + .then((app2) => { + sinon.stub(app2, 'i18n').callsFake((key) => key); + sinon.stub(app2, 'showError'); + sinon.stub(app2, '_errorUrl').returns('https://example/error-url'); + + rejectionHandler({ reason: { status: 404, message: 'Not found' } }); + + expect(app2.showError).to.have.been.calledWith(404, 'Not found', 'https://example/error-url'); + app2.showError.restore(); + app2._errorUrl.restore(); + app2.i18n.restore(); + }) + .finally(() => { + window.addEventListener = originalAddEventListener; + }); + }); + + test('menu mutation observer syncs tooltips when sidebar is expanded', async () => { + sinon.stub(app, '_syncSidebarMenuTooltips'); + app.sidebarExpanded = true; + + app.$.menu.appendChild(document.createElement('div')); + await flush(); + + expect(app._syncSidebarMenuTooltips).to.have.been.calledWith(true); + app._syncSidebarMenuTooltips.restore(); + }); + }); + + suite('skip link behavior', () => { + test('click on skip link focuses and scrolls main content', () => { + const { skipLink, mainContent } = app.$; + sinon.spy(mainContent, 'focus'); + sinon.stub(mainContent, 'scrollIntoView'); + + skipLink.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + + expect(mainContent.focus).to.have.been.calledOnce; + expect(mainContent.scrollIntoView).to.have.been.calledWith({ behavior: 'smooth' }); + mainContent.focus.restore(); + mainContent.scrollIntoView.restore(); + }); + + test('Enter on skip link activates main content', () => { + const { skipLink, mainContent } = app.$; + sinon.spy(mainContent, 'focus'); + sinon.stub(mainContent, 'scrollIntoView'); + + skipLink.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })); + + expect(mainContent.focus).to.have.been.calledOnce; + expect(mainContent.scrollIntoView).to.have.been.calledWith({ behavior: 'smooth' }); + mainContent.focus.restore(); + mainContent.scrollIntoView.restore(); + }); + }); + test('_documentRemovedFromCollection toasts', () => { sinon.stub(app, '_toast'); app._documentRemovedFromCollection(); @@ -1120,19 +1521,45 @@ suite('nuxeo-app', () => { }); suite('logo menu keyboard navigation', () => { - test('ArrowDown on logo focuses first menu item', () => { + test('ArrowDown on logo focuses home link when present', () => { + const logo = app.$.logo; + const menuContainer = app.$.menuContainer; + if (!logo || !menuContainer) { + return; + } + const homeLink = menuContainer.querySelector('.home-link'); + if (!homeLink) { + return; + } + const focusSpy = sinon.spy(homeLink, 'focus'); + logo.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); + expect(focusSpy).to.have.been.called; + focusSpy.restore(); + }); + + test('ArrowDown from home link focuses first menu item', () => { const logo = app.$.logo; const menu = app.$.menu; - if (!logo || !menu) { + const menuContainer = app.$.menuContainer; + if (!logo || !menu || !menuContainer) { + return; + } + const homeLink = menuContainer.querySelector('.home-link'); + if (!homeLink) { return; } const item = document.createElement('div'); - item.setAttribute('name', 'browse'); const focusSpy = sinon.spy(item, 'focus'); - sinon.stub(menu, 'querySelector').returns(item); - logo.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); + sinon.stub(menu, '_setFocusedItem').callsFake(() => item.focus()); + Object.defineProperty(menu, 'items', { value: [item], configurable: true }); + const evt = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, composed: true }); + Object.defineProperty(evt, 'composedPath', { + value: () => [homeLink, menuContainer, app], + configurable: true, + }); + menuContainer.dispatchEvent(evt); expect(focusSpy).to.have.been.called; - menu.querySelector.restore(); + menu._setFocusedItem.restore(); focusSpy.restore(); }); }); @@ -1508,16 +1935,14 @@ suite('nuxeo-app', () => { if (!logo || !menu) { return; } - app.logoToMenuNavigation(); const first = document.createElement('div'); const last = document.createElement('div'); - sinon.stub(menu, 'querySelectorAll').returns([first, last]); + Object.defineProperty(menu, 'items', { value: [first, last], configurable: true }); const focusSpy = sinon.spy(logo, 'focus'); const evt = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }); Object.defineProperty(evt, 'target', { value: last, configurable: true }); menu.dispatchEvent(evt); expect(focusSpy).to.have.been.called; - menu.querySelectorAll.restore(); focusSpy.restore(); }); }); diff --git a/test/nuxeo-browser.test.js b/test/nuxeo-browser.test.js new file mode 100644 index 0000000000..2810b6bcaa --- /dev/null +++ b/test/nuxeo-browser.test.js @@ -0,0 +1,55 @@ +/** +@license +©2023 Hyland Software, Inc. and its affiliates. All rights reserved. +All Hyland product names are registered or unregistered trademarks of Hyland Software, Inc. or its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import '../elements/nuxeo-app.js'; + +suite('nuxeo-browser', () => { + let documentChanged; + + setup(() => { + const BrowserElement = customElements.get('nuxeo-browser'); + documentChanged = BrowserElement.prototype._documentChanged; + }); + + test('_documentChanged sets document-facets when facets exist', () => { + const setAttribute = sinon.spy(); + const removeAttribute = sinon.spy(); + + documentChanged.call({ + document: { facets: ['Folderish', 'Collection'] }, + setAttribute, + removeAttribute, + }); + + expect(setAttribute).to.have.been.calledWith('document-facets', 'Folderish Collection'); + expect(removeAttribute).to.not.have.been.called; + }); + + test('_documentChanged removes document-facets when facets are missing', () => { + const setAttribute = sinon.spy(); + const removeAttribute = sinon.spy(); + + documentChanged.call({ + document: null, + setAttribute, + removeAttribute, + }); + + expect(removeAttribute).to.have.been.calledWith('document-facets'); + expect(setAttribute).to.not.have.been.called; + }); +}); diff --git a/test/nuxeo-document-create-shortcut.test.js b/test/nuxeo-document-create-shortcut.test.js new file mode 100644 index 0000000000..352528b988 --- /dev/null +++ b/test/nuxeo-document-create-shortcut.test.js @@ -0,0 +1,68 @@ +/** +@license +©2023 Hyland Software, Inc. and its affiliates. All rights reserved. +All Hyland product names are registered or unregistered trademarks of Hyland Software, Inc. or its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import { fixture, flush, html } from '@nuxeo/testing-helpers'; +import '../elements/nuxeo-document-create-actions/nuxeo-document-create-shortcut.js'; + +suite('nuxeo-document-create-shortcut', () => { + let element; + + setup(async () => { + element = await fixture( + html``, + ); + sinon.stub(element, 'i18n').callsFake((key) => key); + await flush(); + }); + + test('_tap fires create-document with shortcut type', () => { + const fireSpy = sinon.spy(element, 'fire'); + element._tap(); + expect(fireSpy).to.have.been.calledWith('create-document', { type: 'File' }); + fireSpy.restore(); + }); + + test('_handleKeydown activates shortcut on Enter or Space', () => { + const tapSpy = sinon.spy(element, '_tap'); + const preventDefault = sinon.spy(); + + element._handleKeydown({ key: 'Enter', preventDefault }); + expect(preventDefault).to.have.been.calledOnce; + expect(tapSpy).to.have.been.calledOnce; + + element._handleKeydown({ key: ' ', preventDefault }); + expect(tapSpy).to.have.been.calledTwice; + + element._handleKeydown({ key: 'Tab', preventDefault }); + expect(tapSpy).to.have.been.calledTwice; + tapSpy.restore(); + }); + + test('exposes a single accessible name via the visible label', () => { + const control = element.shadowRoot.querySelector('.shortcut-container'); + const label = element.shadowRoot.querySelector('#shortcutLabel'); + expect(control.getAttribute('role')).to.equal('button'); + expect(control.getAttribute('aria-labelledby')).to.equal('shortcutLabel'); + expect(label.textContent.trim()).to.equal('label.file'); + expect(element.shadowRoot.querySelector('nuxeo-tooltip')).to.be.null; + expect(element.shadowRoot.querySelector('#createBtn').getAttribute('aria-hidden')).to.equal('true'); + }); +}); diff --git a/test/nuxeo-document-tree.test.js b/test/nuxeo-document-tree.test.js index 0b06a37b3f..df440e9bee 100644 --- a/test/nuxeo-document-tree.test.js +++ b/test/nuxeo-document-tree.test.js @@ -550,3 +550,247 @@ suite('nuxeo-document-tree', () => { }); }); }); + +suite('nuxeo-document-tree unit behavior', () => { + let element; + let clock; + + setup(async () => { + element = await fixture(html``); + sinon.stub(element, 'i18n').callsFake((key) => key); + await flush(); + }); + + teardown(() => { + if (clock) { + clock.restore(); + clock = null; + } + sessionStorage.removeItem('nuxeo.tree.selectedPath'); + if (element._treeObserver) { + element._treeObserver.disconnect(); + element._treeObserver = null; + } + }); + + test('_checkRtl and _onRtlChange follow document direction', () => { + document.documentElement.setAttribute('dir', 'rtl'); + element._checkRtl(); + expect(element._isRtl).to.be.true; + expect(element.toggleChevronIcon).to.equal('icons:chevron-right'); + + document.documentElement.setAttribute('dir', 'ltr'); + element._checkRtl(); + element._onRtlChange(); + expect(element._isRtl).to.be.false; + expect(element.toggleChevronIcon).to.equal('icons:chevron-left'); + }); + + test('_expandIcon and _leafClass helpers', () => { + element._isRtl = false; + expect(element._expandIcon(true)).to.equal('hardware:keyboard-arrow-down'); + expect(element._expandIcon(false)).to.equal('hardware:keyboard-arrow-right'); + + element._isRtl = true; + expect(element._expandIcon(false)).to.equal('hardware:keyboard-arrow-left'); + expect(element._leafClass(true)).to.equal('leaf'); + expect(element._leafClass(false)).to.equal(''); + }); + + test('_title uses i18n for Root type', () => { + expect(element._title({ type: 'Root', title: 'ignored' })).to.equal('browse.root'); + expect(element._title({ type: 'Folder', title: 'My folder' })).to.equal('My folder'); + }); + + test('_handleNodeClick stores path and debounces highlight update', () => { + const debounceSpy = sinon.spy(element, '_debounceHighlightUpdate'); + const link = document.createElement('a'); + link.setAttribute('data-path', '/ws/doc'); + + element._handleNodeClick({ currentTarget: link }); + + expect(sessionStorage.getItem('nuxeo.tree.selectedPath')).to.equal('/ws/doc'); + expect(debounceSpy).to.have.been.calledWith('/ws/doc'); + debounceSpy.restore(); + }); + + test('_handleKeydown toggles treeitem and dispatches tree-node-toggled', () => { + const treeItem = document.createElement('div'); + treeItem.setAttribute('role', 'treeitem'); + treeItem.setAttribute('aria-expanded', 'false'); + + const icon = document.createElement('iron-icon'); + icon.click = sinon.spy(); + treeItem.appendChild(icon); + + const preventDefault = sinon.spy(); + const eventSpy = sinon.spy(element, 'dispatchEvent'); + + element._handleKeydown({ + key: 'Enter', + target: icon, + preventDefault, + }); + + expect(treeItem.getAttribute('aria-expanded')).to.equal('true'); + expect(icon.click).to.have.been.calledOnce; + expect(preventDefault).to.have.been.calledOnce; + expect(eventSpy).to.have.been.called; + const customEvent = eventSpy.getCalls().find((c) => c.args[0] && c.args[0].type === 'tree-node-toggled'); + expect(customEvent).to.exist; + expect(customEvent.args[0].detail.expanded).to.be.true; + eventSpy.restore(); + }); + + test('_debounceHighlightUpdate runs highlight after debounce', () => { + clock = sinon.useFakeTimers(); + const highlightSpy = sinon.spy(element, '_updateSelectionHighlight'); + + element._debounceHighlightUpdate('/path'); + expect(highlightSpy).to.not.have.been.called; + + clock.tick(50); + expect(highlightSpy).to.have.been.calledWith('/path'); + highlightSpy.restore(); + }); + + test('_retryHighlightUpdate retries until highlight succeeds', () => { + clock = sinon.useFakeTimers(); + const highlightStub = sinon.stub(element, '_updateSelectionHighlight'); + highlightStub.onCall(0).returns(false); + highlightStub.onCall(1).returns(true); + + element._retryHighlightUpdate('/path', 0); + clock.tick(100); + expect(highlightStub).to.have.been.calledOnce; + + clock.tick(200); + expect(highlightStub).to.have.been.calledTwice; + highlightStub.restore(); + }); + + test('_retryHighlightUpdate stops after max attempts', () => { + clock = sinon.useFakeTimers(); + const highlightStub = sinon.stub(element, '_updateSelectionHighlight').returns(false); + + element._retryHighlightUpdate('/missing', 0); + for (let i = 0; i < 12; i += 1) { + clock.tick(250); + } + expect(highlightStub.callCount).to.be.at.most(11); + highlightStub.restore(); + }); + + test('_updateSelectionHighlight marks parent link and tree row', () => { + const parents = element.shadowRoot.querySelector('.parents'); + const stale = document.createElement('a'); + stale.setAttribute('data-path', '/other'); + stale.classList.add('selected'); + parents.appendChild(stale); + + const target = document.createElement('a'); + target.setAttribute('data-path', '/ws'); + parents.appendChild(target); + + const treeItem = document.createElement('div'); + treeItem.setAttribute('role', 'treeitem'); + const treeLink = document.createElement('a'); + treeLink.setAttribute('data-path', '/Folder1'); + treeItem.appendChild(treeLink); + element.shadowRoot.querySelector('.content').appendChild(treeItem); + + expect(element._updateSelectionHighlight('/ws')).to.be.true; + expect(stale.classList.contains('selected')).to.be.false; + expect(target.classList.contains('selected')).to.be.true; + + expect(element._updateSelectionHighlight('/Folder1')).to.be.true; + expect(treeLink.classList.contains('selected')).to.be.true; + expect(treeItem.classList.contains('selected')).to.be.true; + }); + + test('_updateSelectionHighlight uses sessionStorage when no argument', () => { + sessionStorage.setItem('nuxeo.tree.selectedPath', '/stored'); + + const parents = element.shadowRoot.querySelector('.parents'); + const link = document.createElement('a'); + link.setAttribute('data-path', '/stored'); + parents.appendChild(link); + + expect(element._updateSelectionHighlight()).to.be.true; + expect(link.classList.contains('selected')).to.be.true; + }); + + test('_updateSelectionHighlight searches tree shadow root links', () => { + const treeHost = document.createElement('div'); + const treeShadow = treeHost.attachShadow({ mode: 'open' }); + const treeItem = document.createElement('div'); + treeItem.setAttribute('role', 'treeitem'); + const link = document.createElement('a'); + link.setAttribute('data-path', '/nested'); + treeItem.appendChild(link); + treeShadow.appendChild(treeItem); + element.shadowRoot.querySelector('.content').appendChild(treeHost); + element.$ = { tree: treeHost }; + + expect(element._updateSelectionHighlight('/nested')).to.be.true; + expect(link.classList.contains('selected')).to.be.true; + expect(treeItem.classList.contains('selected')).to.be.true; + }); + + test('_updateSelectionHighlight returns false when no path is available', () => { + element.currentDocument = null; + sessionStorage.removeItem('nuxeo.tree.selectedPath'); + expect(element._updateSelectionHighlight()).to.be.false; + }); + + test('_handlePopState and _handleLocationChanged schedule highlight retry', () => { + const retrySpy = sinon.spy(element, '_retryHighlightUpdate'); + element._handlePopState(); + element._handleLocationChanged(); + expect(retrySpy).to.have.been.calledTwice; + retrySpy.restore(); + }); + + test('_currentDocumentChanged handles Root documents and syncs selected path', () => { + sinon.stub(element, 'hasFacet').returns(false); + const retrySpy = sinon.spy(element, '_retryHighlightUpdate'); + element.docPath = '/old'; + element.currentDocument = { + type: 'Root', + path: '/', + contextParameters: { breadcrumb: { entries: [{ path: '/' }] } }, + }; + + element._currentDocumentChanged(); + + expect(element.docPath).to.equal('/'); + expect(sessionStorage.getItem('nuxeo.tree.selectedPath')).to.equal('/'); + expect(retrySpy).to.have.been.calledWith('/'); + element.hasFacet.restore(); + retrySpy.restore(); + }); + + test('_setupTreeObserver creates observer when missing and is idempotent otherwise', () => { + element._treeObserver = null; + element._setupTreeObserver(); + const created = element._treeObserver; + expect(created).to.exist; + + element._setupTreeObserver(); + expect(element._treeObserver).to.equal(created); + }); + + test('detached disconnects navigation listeners and tree observer', () => { + const popSpy = sinon.spy(window, 'removeEventListener'); + element._boundPopStateHandler = () => {}; + element._boundLocationChangedHandler = () => {}; + element._treeObserver = { disconnect: sinon.spy() }; + + element.detached(); + + expect(popSpy).to.have.been.calledWith('popstate', element._boundPopStateHandler); + expect(popSpy).to.have.been.calledWith('location-changed', element._boundLocationChangedHandler); + expect(element._treeObserver.disconnect).to.have.been.calledOnce; + popSpy.restore(); + }); +}); diff --git a/test/nuxeo-menu-icon.test.js b/test/nuxeo-menu-icon.test.js index b55e88a56f..c377993650 100644 --- a/test/nuxeo-menu-icon.test.js +++ b/test/nuxeo-menu-icon.test.js @@ -70,6 +70,47 @@ suite('nuxeo-menu-icon', () => { }); }); + suite('expanded property', () => { + test('defaults to false and is reflected as attribute', async () => { + expect(element.expanded).to.be.false; + expect(element.hasAttribute('expanded')).to.be.false; + + element.expanded = true; + await flush(); + expect(element.hasAttribute('expanded')).to.be.true; + + element.expanded = false; + await flush(); + expect(element.hasAttribute('expanded')).to.be.false; + }); + + test('hides tooltip when toggled to true', () => { + sinon.stub(element.$.tooltip, 'hide'); + element.expanded = true; + expect(element.$.tooltip.hide).to.have.been.calledOnce; + element.$.tooltip.hide.restore(); + }); + + test('does not call tooltip.hide when toggled to false', () => { + element.expanded = true; + sinon.stub(element.$.tooltip, 'hide'); + element.expanded = false; + expect(element.$.tooltip.hide).to.not.have.been.called; + element.$.tooltip.hide.restore(); + }); + + test('binds tooltip hidden attribute to expanded', async () => { + const tooltip = element.$.tooltip; + element.expanded = true; + await flush(); + expect(tooltip.hasAttribute('hidden')).to.be.true; + + element.expanded = false; + await flush(); + expect(tooltip.hasAttribute('hidden')).to.be.false; + }); + }); + suite('_srcOrIcon', () => { test('should prefer src over icon when src is set', async () => { element.icon = 'icons:home'; @@ -117,5 +158,24 @@ suite('nuxeo-menu-icon', () => { expect(element._href()).to.equal('/stubbed-url'); expect(element.urlFor.calledWith('document', 'uid1')).to.be.true; }); + + test('returns undefined when route is set but urlFor is not available', () => { + element.urlFor = undefined; + element.link = ''; + element.route = 'document:uid1'; + expect(element._href()).to.equal(undefined); + }); + }); + + test('_srcOrIcon keeps existing button src when src is empty', () => { + element.src = ''; + element.icon = 'icons:folder'; + element.$.button.src = 'https://cdn.example/existing.png'; + element.$.button.icon = 'icons:existing'; + + element._srcOrIcon(); + + expect(element.$.button.src).to.equal('https://cdn.example/existing.png'); + expect(element.$.button.icon).to.equal('icons:existing'); }); }); diff --git a/test/nuxeo-theme.test.js b/test/nuxeo-theme.test.js index a4f2ceb96d..b4b0073829 100644 --- a/test/nuxeo-theme.test.js +++ b/test/nuxeo-theme.test.js @@ -26,8 +26,8 @@ suite('nuxeo-theme', () => { }); test('uses themes folder when preview is not set', async () => { - const el = await fixture(html``); - expect(el._image('kawaii')).to.equal('themes/kawaii/preview.jpg'); + const el = await fixture(html``); + expect(el._image('dark')).to.equal('themes/dark/preview.jpg'); }); }); @@ -72,8 +72,8 @@ suite('nuxeo-theme', () => { test('returns false for non-default when no theme in storage', async () => { sinon.stub(localStorage, 'getItem').callsFake(() => null); - const el = await fixture(html``); - expect(el._selected('kawaii')).to.be.false; + const el = await fixture(html``); + expect(el._selected('dark')).to.be.false; }); }); @@ -89,9 +89,9 @@ suite('nuxeo-theme', () => { test('uses apply i18n key when theme is not selected', async () => { const lsStub = sinon.stub(localStorage, 'getItem').callsFake(() => null); - const el = await fixture(html``); + const el = await fixture(html``); const i18nStub = sinon.stub(el, 'i18n').callsFake((k) => k); - expect(el._button('kawaii')).to.equal('themes.apply'); + expect(el._button('dark')).to.equal('themes.apply'); i18nStub.restore(); lsStub.restore(); }); @@ -106,14 +106,14 @@ suite('nuxeo-theme', () => { test('persists theme and dispatches theme-changed', async () => { sinon.stub(localStorage, 'setItem'); - const el = await fixture(html``); + const el = await fixture(html``); const listener = sinon.spy(); el.addEventListener('theme-changed', listener); el._apply(); - expect(localStorage.setItem).to.have.been.calledWith('theme', 'light'); + expect(localStorage.setItem).to.have.been.calledWith('theme', 'dark'); expect(listener).to.have.been.calledOnce; const [evt] = listener.firstCall.args; - expect(evt.detail.theme).to.equal('light'); + expect(evt.detail.theme).to.equal('dark'); }); }); }); diff --git a/test/theme-loader.test.js b/test/theme-loader.test.js index 798c6e33b4..e4a18e0ef0 100644 --- a/test/theme-loader.test.js +++ b/test/theme-loader.test.js @@ -40,7 +40,7 @@ suite('theme-loader', () => { }); test('should allow valid theme names', () => { - ['default', 'dark', 'light', 'kawaii', 'my-custom-theme'].forEach((value) => { + ['default', 'dark', 'my-custom-theme'].forEach((value) => { expect(SAFE_THEME_PATTERN.test(value), `expected "${value}" to be allowed`).to.be.true; }); }); @@ -78,6 +78,17 @@ suite('theme-loader', () => { expect(setItemStub).to.have.been.calledWith('theme', 'dark'); }); + test('should map deprecated theme values to default', () => { + ['light', 'Light', 'kawaii', 'Kawaii'].forEach((value) => { + getItemStub.resetHistory(); + setItemStub.resetHistory(); + getItemStub.returns(value); + + expect(getValidTheme()).to.equal('default'); + expect(setItemStub).to.have.been.calledWith('theme', 'default'); + }); + }); + test('should return "default" when localStorage.getItem throws', () => { getItemStub.throws(new Error('SecurityError')); expect(getValidTheme()).to.equal('default'); diff --git a/themes/base.js b/themes/base.js index c19cde66a4..bdb6ef92b5 100644 --- a/themes/base.js +++ b/themes/base.js @@ -135,6 +135,12 @@ const template = html` text-overflow: ellipsis; color: var(--nuxeo-drawer-header); } + /* Responsive adjustments for dashboard header title */ + @media (max-width: 720px) { + .header { + padding-left: 48px; + } + } /* layouts */ div[role='widget'] > div.multiline { @@ -183,6 +189,76 @@ const template = html` url('../fonts/Inter-Bold.woff?v=3.13') format('woff'); } + /* Figtree — consumed only via the --sat-section-header mixin (font-weight: 500). + Other weights / italic variants were dropped in the rebrand cleanup; re-add + here on demand if a new component needs them. */ + @font-face { + font-family: 'Figtree'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('../fonts/figtree-latin-500-normal.woff2') format('woff2'), + url('../fonts/figtree-latin-500-normal.woff') format('woff'); + } + + /* Noto Sans font family */ + @font-face { + font-family: 'Noto Sans'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url('../fonts/noto-sans-latin-300-normal.woff2') format('woff2'), + url('../fonts/noto-sans-latin-300-normal.woff') format('woff'); + } + + @font-face { + font-family: 'Noto Sans'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../fonts/noto-sans-latin-400-normal.woff2') format('woff2'), + url('../fonts/noto-sans-latin-400-normal.woff') format('woff'); + } + + @font-face { + font-family: 'Noto Sans'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('../fonts/noto-sans-latin-500-normal.woff2') format('woff2'), + url('../fonts/noto-sans-latin-500-normal.woff') format('woff'); + } + + @font-face { + font-family: 'Noto Sans'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('../fonts/noto-sans-latin-600-normal.woff2') format('woff2'), + url('../fonts/noto-sans-latin-600-normal.woff') format('woff'); + } + + @font-face { + font-family: 'Noto Sans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('../fonts/noto-sans-latin-700-normal.woff2') format('woff2'), + url('../fonts/noto-sans-latin-700-normal.woff') format('woff'); + } + + /* Noto Sans 900 — referenced only by the nuxeo-template-rendering addon + (font-weight: 900). Kept so that addon retains a real font face + when enabled; safe to drop if/when that usage is migrated to 700. */ + @font-face { + font-family: 'Noto Sans'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url('../fonts/noto-sans-latin-900-normal.woff2') format('woff2'), + url('../fonts/noto-sans-latin-900-normal.woff') format('woff'); + } + html { font-weight: 400; font-size: 13px; @@ -270,16 +346,14 @@ const template = html` } --nuxeo-block-selected: { - background-color: var(--nuxeo-box); + background-color: var(--sat-selection-pill-background, #e2dfff); outline: 0; - box-shadow: 5px 0 0 0 var(--nuxeo-primary-color) inset; } [dir='rtl'] { --nuxeo-block-selected: { - background-color: var(--nuxeo-box); + background-color: var(--sat-selection-pill-background, #e2dfff); outline: 0; - box-shadow: -5px 0 0 0 var(--nuxeo-primary-color) inset; } } @@ -428,6 +502,10 @@ const template = html` font-family: var(--nuxeo-app-font); } + --iron-data-table-row: { + border-bottom: none; + } + --paper-tooltip: { font-size: 1rem; font-family: var(--nuxeo-app-font); @@ -592,6 +670,32 @@ const template = html` --nuxeo-widget: { margin-bottom: 16px; } + + /* SAT drawersection header mixin — @apply --sat-section-header; */ + --sat-section-header: { + font-weight: 500; + font-size: 26px; + color: var(--sat-section-header-text-color, var(--nuxeo-drawer-text)); + font-family: var(--sat-font-family-secondary, 'Figtree'); + }; + + /* SAT drawer list / menu label mixin — @apply --sat-drawer-item; */ + --sat-drawer-item: { + color: var(--nuxeo-drawer-text); + font-family: var(--sat-font-family-primary, var(--nuxeo-app-font)); + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.1px; + }; + + /* SAT drawer active item mixin — @apply --sat-drawer-item-selected; */ + --sat-drawer-item-selected: { + background-color: var(--sat-selection-pill-background, #e2dfff); + border-radius: 54px; + outline: 0; + }; + } @media (max-width: 1024px) { diff --git a/themes/dark/logo.png b/themes/dark/logo.png index a8fe803881..b75d68087f 100644 Binary files a/themes/dark/logo.png and b/themes/dark/logo.png differ diff --git a/themes/dark/theme.html b/themes/dark/theme.html index 0bf91bd081..2ae589345c 100644 --- a/themes/dark/theme.html +++ b/themes/dark/theme.html @@ -15,11 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. --> + + + diff --git a/themes/default/logo.png b/themes/default/logo.png index cdf9fd16df..b75d68087f 100644 Binary files a/themes/default/logo.png and b/themes/default/logo.png differ diff --git a/themes/default/theme.html b/themes/default/theme.html index fc487543f6..d54272ce16 100644 --- a/themes/default/theme.html +++ b/themes/default/theme.html @@ -15,15 +15,18 @@ See the License for the specific language governing permissions and limitations under the License. --> + + + diff --git a/themes/kawaii/README.md b/themes/kawaii/README.md deleted file mode 100644 index d80b22c94d..0000000000 --- a/themes/kawaii/README.md +++ /dev/null @@ -1 +0,0 @@ -All images, icons, fonts, and videos contained in this folder are copyrighted by Hyland Software, all rights reserved. diff --git a/themes/kawaii/background.png b/themes/kawaii/background.png deleted file mode 100644 index 7080f82249..0000000000 Binary files a/themes/kawaii/background.png and /dev/null differ diff --git a/themes/kawaii/logo.png b/themes/kawaii/logo.png deleted file mode 100644 index eb5461c173..0000000000 Binary files a/themes/kawaii/logo.png and /dev/null differ diff --git a/themes/kawaii/preview.jpg b/themes/kawaii/preview.jpg deleted file mode 100644 index 0811be2c54..0000000000 Binary files a/themes/kawaii/preview.jpg and /dev/null differ diff --git a/themes/kawaii/theme.html b/themes/kawaii/theme.html deleted file mode 100644 index 4e9618814c..0000000000 --- a/themes/kawaii/theme.html +++ /dev/null @@ -1,166 +0,0 @@ - - - - diff --git a/themes/light/README.md b/themes/light/README.md deleted file mode 100644 index d80b22c94d..0000000000 --- a/themes/light/README.md +++ /dev/null @@ -1 +0,0 @@ -All images, icons, fonts, and videos contained in this folder are copyrighted by Hyland Software, all rights reserved. diff --git a/themes/light/logo.png b/themes/light/logo.png deleted file mode 100644 index a8fe803881..0000000000 Binary files a/themes/light/logo.png and /dev/null differ diff --git a/themes/light/preview.jpg b/themes/light/preview.jpg deleted file mode 100644 index 8ae440e5d4..0000000000 Binary files a/themes/light/preview.jpg and /dev/null differ diff --git a/themes/light/theme.html b/themes/light/theme.html deleted file mode 100644 index e1884abc9e..0000000000 --- a/themes/light/theme.html +++ /dev/null @@ -1,166 +0,0 @@ - - - - diff --git a/themes/loader.js b/themes/loader.js index 3b03af7ea4..41a54cc96f 100644 --- a/themes/loader.js +++ b/themes/loader.js @@ -33,6 +33,11 @@ export function getValidTheme() { let raw; try { raw = localStorage.getItem('theme'); + if (['light', 'kawaii'].includes(raw?.trim()?.toLowerCase())) { + // Remove deprecated themes from storage, fallback to default. + safeSetTheme('default'); + raw = 'default'; + } } catch (e) { // localStorage may be unavailable (e.g., private browsing) console.warn('Failed to read theme preference:', e.message); diff --git a/themes/satori-design-tokens.css b/themes/satori-design-tokens.css new file mode 100644 index 0000000000..75445f45ef --- /dev/null +++ b/themes/satori-design-tokens.css @@ -0,0 +1,16 @@ +/** + * Shared Satori primitives and cross-theme defaults used by Nuxeo Web UI. + * Keep this file small: only declare tokens with runtime consumers. + * Full theming contract: docs/THEMING.md. + */ + +:root { + /* Tier 2: cross-component semantic role. */ + --sys-outline-variant: #c8c5d3; + + /* Tier 3: cross-theme component defaults (can be overridden per theme). */ + --sat-font-family-secondary: 'Figtree'; + --sat-font-family-primary: 'Noto Sans'; + --sat-profile-avatar-background: rgba(255, 255, 255, 0.15); + --sat-drawer-toggle-background: transparent; +}