From c1a2885a714459edd81609dd174984ab3304b4db Mon Sep 17 00:00:00 2001 From: "@suet-kei.chan" Date: Thu, 6 Aug 2026 17:08:08 +0200 Subject: [PATCH 1/7] fix: adapt JSResourceLocatorExtension to Nextcloud 33 API Nextcloud 33 changed the core OC\Template\JSResourceLocator API, which caused fatal errors on every page load when running nmctheme on NC33: - doFind() gained a typed signature: doFind(string $resource): void. The override here still used the untyped doFind($script), producing "Declaration must be compatible" fatal errors. Update the override to match the new signature. - __construct() now requires an IConfig as argument #2: (LoggerInterface, IConfig, JSCombiner, IAppManager). The previous version-probing chain (V25/V26/V27) had no matching branch, so it fell through to the V25 signature and threw a TypeError. Add an NC33 branch as the first attempt while keeping the older fallbacks for backward compatibility, and inject IConfig at the registration site in Application.php. Restores nmctheme rendering on Nextcloud 33. --- lib/AppInfo/Application.php | 1 + lib/JSResourceLocatorExtension.php | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index fca30742..dafb6e1e 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -121,6 +121,7 @@ protected function registerJSResourceLocatorExtension(IRegistrationContext $cont $this->getContainer()->getServer()->registerService(JSResourceLocator::class, function (ContainerInterface $c) { return new JSResourceLocatorExtension( $c->get(LoggerInterface::class), + $c->get(IConfig::class), $this->getContainer()->getServer()->query(JSCombiner::class), $c->get(IAppManager::class) ); diff --git a/lib/JSResourceLocatorExtension.php b/lib/JSResourceLocatorExtension.php index 018bcee1..cd585f29 100644 --- a/lib/JSResourceLocatorExtension.php +++ b/lib/JSResourceLocatorExtension.php @@ -16,6 +16,7 @@ use OC\Template\JSCombiner; use OC\Template\JSResourceLocator; use OCP\App\IAppManager; +use OCP\IConfig; use Psr\Log\LoggerInterface; class JSResourceLocatorExtension extends JSResourceLocator { @@ -28,9 +29,17 @@ class JSResourceLocatorExtension extends JSResourceLocator { * so we try to handle different backports in this constructor. * */ - public function __construct(LoggerInterface $logger, JSCombiner $jsCombiner, IAppManager $appManager) { + public function __construct(LoggerInterface $logger, IConfig $config, JSCombiner $jsCombiner, IAppManager $appManager) { $this->ownAppManager = $appManager; + // V33 + try { + parent::__construct($logger, $config, $jsCombiner, $appManager); + return; + } catch (\Throwable $eWrongConstructNC33) { + // ignore the exception, try another constructor + } + // later try { parent::__construct($logger, $jsCombiner, $appManager); @@ -59,7 +68,7 @@ public function __construct(LoggerInterface $logger, JSCombiner $jsCombiner, IAp /** * Deviate all language requests to the nmctheme language extension service */ - public function doFind($script) { + public function doFind(string $script): void { // Translation extensions if (str_contains($script, '/l10n/')) { // only add script if corresponding app has a language json From b4b6125945c027ab035021cc2569307a8d24b7e6 Mon Sep 17 00:00:00 2001 From: "@suet-kei.chan" Date: Fri, 7 Aug 2026 02:01:43 +0200 Subject: [PATCH 2/7] fix: render storage quota widget across Nextcloud versions The storage quota widget failed to appear on some Nextcloud versions because its mount ran in a bare DOMContentLoaded handler that: - never fired when the enqueued bundle loaded after DOMContentLoaded had already dispatched, leaving the callback dead, and - returned early whenever `.files-navigation__list` was absent, which also blocked the component mount even though the mount only depends on `.app-navigation-entry__settings`. Rework filessettings.js to: - guard on document.readyState so a late-loading bundle still runs - wait for the Vue-rendered navigation via a MutationObserver and retry until the settings entry exists - decouple the cosmetic "Files settings" button move from the mount, and gate the mount solely on the node it actually needs - match the nav list across versions (.files-navigation__list, then .app-navigation__list) Also cancel the framework's margin-top:auto on pinned nav entries so "Deleted files" groups with the other items instead of pinning to the bottom. --- css/components/ncappnavigation.scss | 5 +++ src/js/filessettings.js | 57 ++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/css/components/ncappnavigation.scss b/css/components/ncappnavigation.scss index 54400a74..0ede619c 100644 --- a/css/components/ncappnavigation.scss +++ b/css/components/ncappnavigation.scss @@ -115,6 +115,11 @@ // default navbar link styling #app-navigation-vue .app-navigation-entry-wrapper { + // Cancel the framework's margin-top:auto so "Deleted files" isn't pinned to the bottom. + &.app-navigation-entry--pinned { + margin-top: 0; + } + .app-navigation-entry { position: relative; border-radius: var(--border-radius-large); diff --git a/src/js/filessettings.js b/src/js/filessettings.js index fcc5d382..85395b91 100644 --- a/src/js/filessettings.js +++ b/src/js/filessettings.js @@ -11,30 +11,51 @@ const View = new StorageQuotaView({ pinia, }) -window.addEventListener('DOMContentLoaded', () => { - // Select the
  • that should be moved - const settingsButton = document.querySelector( - 'li[data-cy-files-navigation-settings-button]' - ) +let isMounted = false - // Target Files navigation list - const filesNavList = document.querySelector('.files-navigation__list') - - if (!settingsButton || !filesNavList) { - return +const setupStorageQuota = () => { + if (isMounted) { + return true } - // Move the settings button into the Files navigation list - filesNavList.appendChild(settingsButton) - - // Create the mount anchor + // Rendered by Vue after DOMContentLoaded; retry via the observer until present. const entrySettings = document.querySelector('.app-navigation-entry__settings') - if (!entrySettings) return + if (!entrySettings) { + return false + } + + // Best-effort: lift the "Files settings" button into the main nav list. + const settingsButton = document.querySelector('li[data-cy-files-navigation-settings-button]') + const filesNavList = document.querySelector('.files-navigation__list, .app-navigation__list') + if (settingsButton && filesNavList) { + filesNavList.appendChild(settingsButton) + } const anchor = document.createElement('div') anchor.id = 'storage-quota-app' entrySettings.appendChild(anchor) - - // Mount the vue component View.$mount('#storage-quota-app') -}) + + isMounted = true + return true +} + +const waitForNavigation = () => { + if (setupStorageQuota()) { + return + } + + const observer = new MutationObserver(() => { + if (setupStorageQuota()) { + observer.disconnect() + } + }) + observer.observe(document.body, { childList: true, subtree: true }) +} + +// Handle the bundle loading after DOMContentLoaded has already fired. +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', waitForNavigation) +} else { + waitForNavigation() +} From f445ac8421eac708941d9e61e3b6807667e46dc8 Mon Sep 17 00:00:00 2001 From: "@suet-kei.chan" Date: Fri, 7 Aug 2026 02:32:00 +0200 Subject: [PATCH 3/7] fix: restore files-list header layout on Nextcloud 33 Nextcloud 33 restructured the files-list header: the "New" button is now a direct child of .files-list__header instead of being nested in .breadcrumb__actions, and the Type/Modified/People filters render inline in the header rather than in the .files-list__filters row below. This left the New button pinned to the left and the filters unstyled and mispositioned. Header (files.scss): - Reorder the header via flexbox so "New" sits on the right, just before the grid-view toggle, matching the previous layout. The New wrapper is matched with :has() so the rule only applies to the NC33 structure and leaves the older breadcrumb-nested layout untouched. - Style the inline filters as the legacy lavender chips (hidden leading icon, trailing caret) and hide the People filter, which the legacy layout lacks. The styling is scoped to the filters element itself so it holds wherever the element is relocated to. Filter relocation (nmcfiles.ts): - Add setupFilterRelocation() to move the filters element out of the header into the .files-list__filters row below, reproducing the two-row layout. A MutationObserver re-runs the move when Vue re-inserts the filters on re-render, and de-duplicates in case Vue recreates the element. --- css/apps/files.scss | 69 +++++++++++++++++++++++++++++++++++++++++++++ src/nmcfiles.ts | 31 ++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/css/apps/files.scss b/css/apps/files.scss index 631ef4b7..1f47e3a8 100644 --- a/css/apps/files.scss +++ b/css/apps/files.scss @@ -421,8 +421,17 @@ table.files-filestable { .files-list__header { margin-block: 15px; margin-inline: 3rem 1.5rem; + display: flex; + align-items: center; + + // NC33 renders "New" as a direct header child; push it to the right. + > div:has(> .files-list__header-upload-button) { + order: 2; + margin-left: auto; + } .files-list__header-grid-button { + order: 3; border: 1px solid var(--color-main-text); border-radius: 50%; width: 40px !important; @@ -576,6 +585,66 @@ table.files-filestable { padding-inline-end: 1rem; } + // Chip styling for the NC33 filters; nmcfiles.ts relocates them here. + [data-test-id="files-list-filters"] { + display: flex; + align-items: center; + gap: 1rem; + + // Hide the People filter (absent in the legacy layout). + > div:nth-child(3) { + display: none; + } + + button.button-vue { + background-color: var(--nmc-ods-blue-primary); + color: var(--nmc-color-text-and-icon-black); + border: none; + border-radius: var(--border-radius-small); + height: 28px; + min-height: 28px; + min-width: unset; + padding: 0 10px; + font-weight: normal; + + &:hover { + background-color: var(--nmc-ods-blue-hover); + } + + &:active, + &[aria-expanded="true"] { + background-color: var(--nmc-ods-blue-active); + } + + // Hide the leading icon; replace with a trailing caret like the chips. + .button-vue__icon { + display: none; + } + + .button-vue__text { + color: var(--nmc-color-text-and-icon-black); + } + + .button-vue__wrapper::after { + content: ''; + display: inline-block; + width: 6px; + height: 6px; + border-left: 1.5px solid var(--nmc-color-text-and-icon-black); + border-bottom: 1.5px solid var(--nmc-color-text-and-icon-black); + transform: rotate(-45deg); + margin-left: 8px; + margin-bottom: 2px; + transition: transform 0.2s ease; + } + + &[aria-expanded="true"] .button-vue__wrapper::after { + transform: rotate(135deg); + margin-bottom: -2px; + } + } + } + .files-list__filters { padding-block: 1rem; padding-inline: 4rem 1rem; diff --git a/src/nmcfiles.ts b/src/nmcfiles.ts index 04a98230..b561abd7 100644 --- a/src/nmcfiles.ts +++ b/src/nmcfiles.ts @@ -294,6 +294,37 @@ if (document.readyState === 'loading') { setupGridViewScrollFix() } +/** + * NC33 renders the Type/Modified filters inline in the header; move them into the + * .files-list__filters row below. Re-runs on Vue re-renders that re-insert them. + */ +function setupFilterRelocation(): void { + const relocate = (): void => { + const target = document.querySelector('.files-list__filters') + if (!target) return + + const headerFilters = document.querySelector( + '.files-list__header [data-test-id="files-list-filters"]', + ) + if (!headerFilters) return + + // Drop any stale copy before moving, in case Vue recreated the element. + target.querySelectorAll('[data-test-id="files-list-filters"]').forEach(el => el.remove()) + target.appendChild(headerFilters) + } + + relocate() + + const observer = new MutationObserver(() => relocate()) + observer.observe(document.body, { childList: true, subtree: true }) +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupFilterRelocation) +} else { + setupFilterRelocation() +} + window.addEventListener('DOMContentLoaded', function() { const breadcrumb = document.querySelector('.breadcrumb') const empty = document.querySelector('.files-list__empty') From c4ade641564d7a36308480c696cb5909f2c1c187 Mon Sep 17 00:00:00 2001 From: "@suet-kei.chan" Date: Tue, 11 Aug 2026 15:55:39 +0200 Subject: [PATCH 4/7] fix: repair account menu rendering on Nextcloud 33 The NC33 account menu was reworked to use NcListItem components, which broke three assumptions in the user-menu styling: - Log out floated to the top because the flex list assigns order to profile/admin/apps/accounts but never to logout, leaving it at the default order 0. Give logout order: 3 so it renders last. - The profile row (and its oversized avatar) was no longer hidden: NC33 moved id="profile" from the
  • onto the inner , so li#profile stopped matching. Target li:has(a#profile) instead. - Administration settings, Apps and Accounts showed blank icons: the theme hides the real and paints icons via ::after, but only mapped logout and news. Add the missing admin/apps/users mappings using the existing --icon-admin-dark/--icon-apps-dark/--icon-users-dark variables. --- css/components/ncusermenu.scss | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/css/components/ncusermenu.scss b/css/components/ncusermenu.scss index 86ff68f9..376c6977 100644 --- a/css/components/ncusermenu.scss +++ b/css/components/ncusermenu.scss @@ -24,7 +24,11 @@ order: 1; } - li#profile { + li:has(a#logout) { + order: 3; + } + + li:has(a#profile) { display: none; } @@ -150,6 +154,18 @@ background-image: var(--icon-logout-dark); } + > a#admin_settings::after { + background-image: var(--icon-admin-dark); + } + + > a#core_apps::after { + background-image: var(--icon-apps-dark); + } + + > a#core_users::after { + background-image: var(--icon-users-dark); + } + > a#nmc_welcome_popup-about { .loading-icon { From 979b7577bc110c7702310384fe31903ca1f1aa59 Mon Sep 17 00:00:00 2001 From: "@suet-kei.chan" Date: Tue, 11 Aug 2026 16:41:08 +0200 Subject: [PATCH 5/7] fix: restore themed styling for NC33 file-type filter popover NC33 replaces the legacy NcActions "Type" filter menu (ul/li NcActionButton) with the FileListFilterType component built from NcButton items, which escaped the existing popover theming and rendered with core defaults. Style the new popover to match the legacy action menu: - monochrome the file-type icons instead of core's colored SVGs - shrink the over-wide popover to fit content with a modest min-width gutter - cap height with overflow scroll for long lists - remove the inter-item flex gap for compact 44px rows - use the small menu-item radius instead of the NcButton pill - grey highlight on hover/keyboard-focus/selected only, not on the programmatic focus given to the first item on open - normal-weight labels and trimmed left padding to align the icons --- css/components/ncactions.scss | 51 +++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/css/components/ncactions.scss b/css/components/ncactions.scss index f7bc1eb3..e0f62765 100644 --- a/css/components/ncactions.scss +++ b/css/components/ncactions.scss @@ -59,6 +59,57 @@ padding: 1rem; } + // NC33 file-type filter uses NcButton items (no ul/li); reshape the + // popover to match the legacy action menu below. + files-file-list-filter-type .icon-vue svg path { + fill: var(--color-main-text); + } + + &:has(files-file-list-filter-type) { + width: fit-content; + min-width: 12rem; + max-height: min(312px, 60vh); + overflow-y: auto; + + > div, + > div > div, + files-file-list-filter-type, + files-file-list-filter-type > div, + .button-vue { + min-width: 0; + } + + > div, + > div > div { + padding: 0; + } + + files-file-list-filter-type > div { + display: flex; + flex-direction: column; + gap: 0; + padding: 0; + } + + .button-vue { + margin: 0; + border-radius: var(--border-radius); + padding-inline: 4px 8px; + + // Grey on hover / keyboard focus / selected, but not on the + // programmatic focus the first item gets when the menu opens. + &:hover, + &:focus-visible, + &[aria-pressed="true"] { + background-color: var(--color-background-hover); + } + + .button-vue__text { + font-weight: normal; + } + } + } + ul { display: flex; flex-direction: column; From 68d4751e3907b3f154c2a6b53e9de0ec311c18e0 Mon Sep 17 00:00:00 2001 From: "@suet-kei.chan" Date: Tue, 11 Aug 2026 17:06:04 +0200 Subject: [PATCH 6/7] fix: match selected-item styling in NC33 file-type filter The NC33 FileListFilterType NcButton renders its selected (pressed) state with the primary magenta fill and a white label, and shows no checkmark, unlike the legacy action menu. Align the selected state with the legacy menu: - strip the magenta pressed fill, keeping the row plain (grey on hover only) - keep the label dark instead of white - add a right-aligned checkmark so ticks line up in one column - widen the popover slightly (min-width 213px) to match the reference --- css/components/ncactions.scss | 36 +++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/css/components/ncactions.scss b/css/components/ncactions.scss index e0f62765..4ca7ae00 100644 --- a/css/components/ncactions.scss +++ b/css/components/ncactions.scss @@ -67,7 +67,7 @@ &:has(files-file-list-filter-type) { width: fit-content; - min-width: 12rem; + min-width: 213px; max-height: min(312px, 60vh); overflow-y: auto; @@ -94,19 +94,43 @@ .button-vue { margin: 0; border-radius: var(--border-radius); - padding-inline: 4px 8px; + padding-inline: 4px 16px; - // Grey on hover / keyboard focus / selected, but not on the - // programmatic focus the first item gets when the menu opens. + // Grey on hover only, not the first item's focus-on-open. &:hover, - &:focus-visible, - &[aria-pressed="true"] { + &:focus-visible { background-color: var(--color-background-hover); } .button-vue__text { font-weight: normal; } + + // Selected: strip NcButton's magenta pressed fill, keep a dark + // label and add a checkmark like the legacy menu. + &[aria-pressed="true"] { + color: var(--color-main-text); + + &:not(:hover):not(:focus-visible) { + background-color: transparent; + } + + .button-vue__text { + color: var(--color-main-text); + } + + .button-vue__wrapper::after { + content: ""; + display: inline-block; + flex: 0 0 auto; + width: 16px; + height: 16px; + margin-inline-start: auto; + background-color: currentColor; + -webkit-mask: var(--icon-check-dark) no-repeat center / contain; + mask: var(--icon-check-dark) no-repeat center / contain; + } + } } } From d92b565b6963984e7e22d3cd1094e229e984e2d0 Mon Sep 17 00:00:00 2001 From: "@suet-kei.chan" Date: Tue, 11 Aug 2026 17:42:25 +0200 Subject: [PATCH 7/7] fix: align NC33 filter bar layout with the legacy design On NC33 the active filter chips render before the toggles in the DOM and the toggles container stretches full-width, so the bar showed chips on the left and toggles pushed to the right edge, with no divider between them. Lay the filter bar out like the legacy design: - make the row a flex container and move the toggles (Type/Modified) first, capping their container width so it no longer pushes the chips off-screen - place the active chips on the right after the toggles - add a vertical divider between the toggles and chips, anchored to the toggles' trailing edge so both gaps stay equal, shown only when active - tighten the Type/Modified spacing to match the reference --- css/apps/files.scss | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/css/apps/files.scss b/css/apps/files.scss index 1f47e3a8..8cecee00 100644 --- a/css/apps/files.scss +++ b/css/apps/files.scss @@ -589,7 +589,7 @@ table.files-filestable { [data-test-id="files-list-filters"] { display: flex; align-items: center; - gap: 1rem; + gap: 0.5rem; // Hide the People filter (absent in the legacy layout). > div:nth-child(3) { @@ -648,6 +648,44 @@ table.files-filestable { .files-list__filters { padding-block: 1rem; padding-inline: 4rem 1rem; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 0.5rem; + + // NC33 renders the toggles after the chips; show them first (left) and + // cap their container so it can't stretch and push the chips off-screen. + > [data-test-id="files-list-filters"] { + order: -1; + flex: 0 0 auto; + width: max-content; + max-width: max-content; + } + + > ul[aria-label="Active filters"] { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 0.5rem; + list-style: none; + margin: 0; + padding: 0; + } + + // Divider anchored as the toggles' last flex item so it sits a fixed gap + // after "Modified"; only shown when filters are active. + &:has(> ul[aria-label="Active filters"] li) { + gap: 0.25rem; + + > [data-test-id="files-list-filters"]::after { + content: ""; + flex: 0 0 auto; + width: 1px; + height: 1.5rem; + margin-inline-start: -0.25rem; + background-color: var(--color-border); + } + } .file-list-filters { display: flex;