feat(ocis): dezky whitelabel theme for the files web UI
ci / typecheck (map[dir:apps/booking name:booking]) (push) Has been cancelled
ci / typecheck (map[dir:apps/portal name:portal]) (push) Has been cancelled
ci / typecheck (map[dir:apps/website name:website]) (push) Has been cancelled
ci / typecheck (map[dir:services/platform-api name:platform-api]) (push) Has been cancelled
ci / test (push) Has been cancelled

Skin OCIS web in the dezky brand so users don't see ownCloud/Infinite Scale.

- Custom theme.json (WEB_UI_THEME_PATH + WEB_ASSET_THEMES_PATH): dezky name,
  slogan, logos (light wordmark for the dark top bar, dark wordmark for the
  light login, favicon), and the full dezky palette — carbon chrome, signal
  yellow as a sparing accent, paper/bone surfaces, dezky semantic colours
- Pin the light theme as default (single variant) so OS-dark / auto-system
  always resolves to it
- Override only index.html via WEB_ASSET_CORE_PATH (OCIS falls back to the
  embedded core per-file): hide the ".versions" footer ("Infinite Scale … /
  ownCloud Web UI …") and set the pre-hydration <title>/theme-color to dezky

Apache-2.0 lets us drop the ownCloud marks without trademark fees. NOTE:
index.html pins the built bundle hashes — refresh it after an OCIS image bump.
This commit is contained in:
Ronni Baslund
2026-06-07 12:14:04 +02:00
parent b7f10eb092
commit 8a9fd36f33
6 changed files with 411 additions and 0 deletions
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
<rect x="8" y="8" width="84" height="84" rx="22" fill="#0A0A0A"/>
<path d="M 34.425 52 a 14 14 0 1 0 28 0 a 14 14 0 1 0 -28 0 Z M 41.925 52 a 6.5 6.5 0 1 0 13 0 a 6.5 6.5 0 1 0 -13 0 Z" fill-rule="evenodd" fill="#D4FF3A"/>
<path d="M 58.575 29.5 a 3.5 3.5 0 0 1 7 0 L 65.575 66 L 58.575 66 Z" fill="#D4FF3A"/>
<circle cx="74" cy="26" r="4" fill="#D4FF3A"/>
</svg>

After

Width:  |  Height:  |  Size: 460 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="80" viewBox="0 0 300 80" role="img" aria-label="dezky">
<!-- dezky mark (signal yellow) -->
<g transform="translate(2,12) scale(0.56)" fill="#D4FF3A">
<path d="M 34.425 52 a 14 14 0 1 0 28 0 a 14 14 0 1 0 -28 0 Z M 41.925 52 a 6.5 6.5 0 1 0 13 0 a 6.5 6.5 0 1 0 -13 0 Z" fill-rule="evenodd"/>
<path d="M 58.575 29.5 a 3.5 3.5 0 0 1 7 0 L 65.575 66 L 58.575 66 Z"/>
<circle cx="74" cy="26" r="4"/>
</g>
<!-- wordmark, dark (for the light login page) -->
<text x="66" y="53" font-family="'Inter Tight','Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="44" font-weight="600" letter-spacing="-2" fill="#0A0A0A">dezky</text>
</svg>

After

Width:  |  Height:  |  Size: 734 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="80" viewBox="0 0 300 80" role="img" aria-label="dezky">
<!-- dezky mark (signal yellow) -->
<g transform="translate(2,12) scale(0.56)" fill="#D4FF3A">
<path d="M 34.425 52 a 14 14 0 1 0 28 0 a 14 14 0 1 0 -28 0 Z M 41.925 52 a 6.5 6.5 0 1 0 13 0 a 6.5 6.5 0 1 0 -13 0 Z" fill-rule="evenodd"/>
<path d="M 58.575 29.5 a 3.5 3.5 0 0 1 7 0 L 65.575 66 L 58.575 66 Z"/>
<circle cx="74" cy="26" r="4"/>
</g>
<!-- wordmark, light (for the dark top bar) -->
<text x="66" y="53" font-family="'Inter Tight','Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="44" font-weight="600" letter-spacing="-2" fill="#F4F3EE">dezky</text>
</svg>

After

Width:  |  Height:  |  Size: 731 B

@@ -0,0 +1,186 @@
{
"clients": {
"android": {},
"desktop": {},
"ios": {},
"web": {
"defaults": {
"appBanner": {},
"designTokens": {
"breakpoints": {
"large-default": "",
"large-max": "",
"medium-default": "",
"medium-max": "",
"small-default": "",
"small-max": "",
"xlarge": "",
"xsmall-max": ""
},
"fontSizes": {
"default": "",
"large": "",
"medium": ""
},
"sizes": {
"form-check-default": "",
"height-small": "",
"height-table-row": "",
"icon-default": "",
"max-height-logo": "",
"max-width-logo": "",
"tiles-default": "",
"tiles-resize-step": "",
"width-medium": ""
},
"spacing": {
"large": "",
"medium": "",
"small": "",
"xlarge": "",
"xsmall": "",
"xxlarge": ""
}
},
"loginPage": {
"backgroundImg": ""
},
"logo": {
"topbar": "themes/dezky/assets/logo-light.svg",
"login": "themes/dezky/assets/logo-dark.svg",
"favicon": "themes/dezky/assets/favicon.svg"
}
},
"themes": [
{
"designTokens": {
"colorPalette": {
"background-accentuate": "rgba(212,255,58,0.12)",
"background-default": "#FAFAF7",
"background-highlight": "rgba(212,255,58,0.18)",
"background-hover": "rgba(10,10,10,0.03)",
"background-muted": "#F4F3EE",
"background-secondary": "#FFFFFF",
"border": "#E6E4DC",
"color-components-apptopbar-background": "transparent",
"color-components-apptopbar-border": "rgba(244,243,238,0.12)",
"icon-archive": "#fbbe54",
"icon-audio": "#700460",
"icon-document": "#3b44a6",
"icon-folder": "#0A0A0A",
"icon-image": "#ee6b3b",
"icon-medical": "#0984db",
"icon-pdf": "#ec0d47",
"icon-presentation": "#ee6b3b",
"icon-spreadsheet": "#15c286",
"icon-video": "#045459",
"input-bg": "#FFFFFF",
"input-border": "#D4D2C8",
"input-text-default": "#0A0A0A",
"input-text-muted": "rgba(10,10,10,0.4)",
"swatch-brand-contrast": "#F4F3EE",
"swatch-brand-default": "#0A0A0A",
"swatch-brand-hover": "#1F1F1C",
"swatch-danger-contrast": "#FFFFFF",
"swatch-danger-default": "#E23030",
"swatch-danger-hover": "#C52525",
"swatch-danger-muted": "rgb(204, 117, 117)",
"swatch-inverse-default": "#ffffff",
"swatch-inverse-hover": "#ffffff",
"swatch-inverse-muted": "#bfbfbf",
"swatch-passive-contrast": "#ffffff",
"swatch-passive-default": "#4c5f79",
"swatch-passive-hover": "#43536b",
"swatch-passive-hover-outline": "#f7fafd",
"swatch-passive-muted": "#283e5d",
"swatch-primary-contrast": "#F4F3EE",
"swatch-primary-default": "#0A0A0A",
"swatch-primary-gradient": "#0A0A0A",
"swatch-primary-gradient-hover": "#1F1F1C",
"swatch-primary-hover": "#1F1F1C",
"swatch-primary-muted": "rgba(10,10,10,0.08)",
"swatch-primary-muted-hover": "rgba(10,10,10,0.12)",
"swatch-success-contrast": "#FFFFFF",
"swatch-success-default": "#1F8A5B",
"swatch-success-hover": "#18774E",
"swatch-success-muted": "rgb(83, 150, 10)",
"swatch-warning-contrast": "#0A0A0A",
"swatch-warning-default": "#E89A1F",
"swatch-warning-hover": "#CE8714",
"swatch-warning-muted": "rgba(183, 76, 27, .5)",
"text-default": "#0A0A0A",
"text-inverse": "#F4F3EE",
"text-muted": "rgba(10,10,10,0.55)"
}
},
"isDark": false,
"name": "dezky"
}
]
}
},
"common": {
"logo": "themes/dezky/assets/logo-dark.svg",
"name": "dezky",
"shareRoles": {
"1c996275-f1c9-4e71-abdf-a42f6495e960": {
"iconName": "upload",
"label": "UnifiedRoleEditorLite"
},
"2d00ce52-1fc2-4dbc-8b95-a73b73395f5a": {
"iconName": "pencil",
"label": "UnifiedRoleFileEditor"
},
"312c0871-5ef7-4b3a-85b6-0e4074c64049": {
"iconName": "user-star",
"label": "UnifiedRoleManager"
},
"3284f2d5-0070-4ad8-ac40-c247f7c1fb27": {
"iconName": "pencil",
"label": "UnifiedRoleSpaceEditorWithoutVersions"
},
"58c63c02-1d89-4572-916a-870abc5a1b7d": {
"iconName": "pencil",
"label": "UnifiedRoleSpaceEditor"
},
"63e64e19-8d43-42ec-a738-2b6af2610efa": {
"iconName": "stop-circle",
"label": "UnifiedRoleFullDenial"
},
"a8d5fe5e-96e3-418d-825b-534dbdf22b99": {
"iconName": "eye",
"label": "UnifiedRoleSpaceViewer"
},
"aa97fe03-7980-45ac-9e50-b325749fd7e6": {
"iconName": "shield",
"label": "UnifiedRoleSecureView"
},
"b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5": {
"iconName": "eye",
"name": "UnifiedRoleViewer"
},
"c1235aea-d106-42db-8458-7d5610fb0a67": {
"iconName": "pencil",
"label": "UnifiedRoleFileEditorListGrants"
},
"d5041006-ebb3-4b4a-b6a4-7c180ecfb17d": {
"iconName": "eye",
"name": "UnifiedRoleViewerListGrants"
},
"e8ea8b21-abd4-45d2-b893-8d1546378e9e": {
"iconName": "pencil",
"label": "UnifiedRoleEditorListGrants"
},
"fb6c3e19-e378-47e5-b277-9732f9de6e21": {
"iconName": "pencil",
"label": "UnifiedRoleEditor"
}
},
"slogan": "dezky sikker plads til dine data",
"urls": {
"accessDeniedHelp": "https://dezky.eu",
"imprint": "https://dezky.eu/terms",
"privacy": "https://dezky.eu/privacy"
}
}
}
@@ -0,0 +1,185 @@
<!DOCTYPE html><html lang="en"><head><base href="/"/>
<script>window.WEB_APPS_MAP = {"web-app-activities":"./web-app-activities-C6-nUzaG.mjs","web-app-admin-settings":"./web-app-admin-settings-eamF9H1m.mjs","web-app-epub-reader":"./web-app-epub-reader-CBWsf2Xc.mjs","web-app-external":"./web-app-external-CThGwBtU.mjs","web-app-files":"./web-app-files-jkl0xRBD.mjs","web-app-ocm":"./web-app-ocm-jjhlU3lY.mjs","web-app-pdf-viewer":"./web-app-pdf-viewer-CogKBhpf.mjs","web-app-preview":"./web-app-preview-DpHPMA7r.mjs","web-app-search":"./web-app-search-Bozc2TEu.mjs","web-app-text-editor":"./web-app-text-editor-BogOmQ6Y.mjs","web-app-webfinger":"./web-app-webfinger-CpqRkzON.mjs","web-app-app-store":"./web-app-app-store-C5GXswMi.mjs"}</script>
<meta charset="utf-8"/>
<meta name="viewport" content="initial-scale=1.0, minimum-scale=1.0"/>
<meta name="theme-color" content="#0A0A0A"/>
<meta http-equiv="x-ua-compatible" content="IE=edge"/>
<title>dezky</title>
<link rel="manifest" href="manifest.json" crossorigin="use-credentials"/>
<script src="js/require.js?1738922102603"></script>
<style>
html,
body {
height: 100%;
}
.splash-banner {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0.5rem;
height: 100%;
}
.splash-hide {
display: none;
}
#loading {
display: inline-block;
height: 34px;
width: 34px;
border: 1px solid #4c5f79;
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
-webkit-animation: spin 1s linear infinite;
}
#splash-incompatible button {
margin: 30px 0;
}
@keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
</style>
<script type="module" crossorigin="" src="./js/index.html-BfHVxEh5.mjs"></script>
<link rel="modulepreload" crossorigin="" href="./js/chunks/PortalTarget.vue_vue_type_script_lang-B4CXey58.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/useRouteMeta-Bh7r1NIY.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/ActionMenuItem-B0-kRumC.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/SpaceInfo-D4uuC6fC.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/datetime-B9EP4faS.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/Pagination-CYuct762.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/useScrollTo-Dj6n8_Gk.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/call-DmED1Wyl.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/FileSideBar-DSEMZK9y.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/omit-T5kFvVmu.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/useGroupingSettings-CJ2TLaEi.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/AppLoadingSpinner-DHn2sj0e.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/useAuthService-cMinPPtZ.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/isEmpty-DuLEwVXA.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/useOpenEmptyEditor-CXV5M0VW.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/useAppDefaults-CNbR4RxQ.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/AppWrapperRoute-DNMI9jAd.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/useAppProviderService-CP821-Jy.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/types-BoCZvwvE.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/CompareSaveDialog-CJbQ2zGC.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/index-D6yFbeGx.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/fuse-Cqy8O5rp.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/ItemFilter-D0B0bwfQ.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/NoContentMessage-BpxTDAzR.mjs"/>
<link rel="modulepreload" crossorigin="" href="./js/chunks/SearchBarFilter-On9swWiz.mjs"/>
<link rel="stylesheet" crossorigin="" href="./assets/style-D1bLdTZ9.css"/>
<style id="dezky-overrides">.versions{display:none!important}</style></head>
<body>
<div id="splash-incompatible" class="splash-banner splash-hide">
<div class="oc-card oc-border oc-rounded oc-width-large oc-text-center">
<div class="oc-card-header">
<div class="oc-flex oc-flex-middle oc-flex-center">
<span class="oc-mr-s oc-icon oc-icon-m oc-icon-warning">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
<g xmlns="http://www.w3.org/2000/svg">
<path fill="none" d="M0 0h24v24H0z"></path>
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z"></path>
</g>
</svg>
</span>
<h2>Your browser is not supported</h2>
</div>
</div>
<div class="oc-card-body oc-link-resolve-error-message">
<p>Your browser version is considered old and might not work correctly.</p>
<p>We recommend you update to a newer version.</p>
</div>
</div>
<button class="oc-button oc-button-primary oc-button-primary-filled oc-rounded" onclick="forceOldBrowser()">
I want to continue anyway
</button>
</div>
<div id="splash-loading" class="splash-banner splash-hide">
<div id="loading"></div>
</div>
<div id="owncloud"></div>
<noscript>
<div class="splash-banner"><h3>Please enable JavaScript</h3></div>
</noscript>
<script>
function runtimeLoaded() {}
var loader = document.getElementById('splash-loading')
var browserError = document.getElementById('splash-incompatible')
var loaderTimer = setTimeout(function () {
loader.classList.remove('splash-hide')
}, 500);
function displayError() {
loader.classList.remove('splash-hide')
loader.innerHTML = "<h3>Oops. Something went wrong.</h3>"
}
function displayBrowserError() {
clearTimeout(loaderTimer)
removeLoadingSpinner()
browserError.classList.remove('splash-hide')
}
function forceOldBrowser() {
localStorage.setItem("forceAllowOldBrowser", JSON.stringify({expiry: new Date().getTime() + 30*24*60*60*1000}))
browserError.classList.add('splash-hide')
init()
}
function removeLoadingSpinner() {
if (!loader.classList.contains('splash-hide')) {
loader.classList.add('splash-hide')
}
}
function init() {
if (typeof requirejs === 'undefined') {
displayError()
} else {
window.runtimeLoaded = function(runtime) {
clearTimeout(loaderTimer)
runtime.bootstrapApp('config.json', removeLoadingSpinner).catch((error) => {
removeLoadingSpinner()
runtime.bootstrapErrorApp(error)
})
}
}
}
const supportedBrowsers = /Edge?\/(12[2-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Firefox\/(1{2}[5-9]|1[2-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Chrom(ium|e)\/(109|1[1-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|(Maci|X1{2}).+ Version\/(16\.([6-9]|\d{2,})|(1[7-9]|[2-9]\d|\d{3,})\.\d+)([,.]\d+|)( \(\w+\)|)( Mobile\/\w+|) Safari\/|Chrome.+OPR\/(10[89]|1[1-9]\d|[2-9]\d{2}|\d{4,})\.\d+\.\d+|(CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS|CPU iPad OS)[ +]+(1{2}[._]\d+|(1[2-9]|[2-9]\d|\d{3,})[._]\d+)([._]\d+|)|Android:?[ /-](13[2-9]|1[4-9]\d|[2-9]\d{2}|\d{4,})(\.\d+|)(\.\d+|)|Android.+Firefox\/(13[2-9]|1[4-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Android.+Chrom(ium|e)\/(13[2-9]|1[4-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Android.+(UC? ?Browser|UCWEB|U3)[ /]?(15\.([5-9]|\d{2,})|(1[6-9]|[2-9]\d|\d{3,})\.\d+)\.\d+|SamsungBrowser\/(2[4-9]|[3-9]\d|\d{3,})\.\d+|Android.+MQ{2}Browser\/(14(\.(9|\d{2,})|)|(1[5-9]|[2-9]\d|\d{3,})(\.\d+|))(\.\d+|)/
const forceAllowOldBrowser = localStorage.getItem("forceAllowOldBrowser") || false
const validForceAllowOldBrowser = forceAllowOldBrowser && JSON.parse(localStorage.getItem("forceAllowOldBrowser")).expiry > new Date().getTime()
if (forceAllowOldBrowser && !validForceAllowOldBrowser)
localStorage.removeItem("forceAllowOldBrowser")
if (!validForceAllowOldBrowser && !supportedBrowsers.test(navigator.userAgent)) {
displayBrowserError()
} else {
init()
}
var scriptTags = document.getElementsByTagName('script')
for (let i = 0; i < scriptTags.length; i++) {
if (scriptTags[i].src) {
scriptTags[i].onerror = displayError
}
}
</script>
</body></html>
@@ -357,11 +357,25 @@ services:
OCIS_ADD_RUN_SERVICES: audit
AUDIT_LOG_FILE_PATH: /var/log/ocis/audit.log
AUDIT_LOG_FORMAT: json
# Whitelabel: serve our dezky theme dir under /themes/ and make it the
# active web theme (replaces the built-in ownCloud branding — name, logos,
# favicon, colours). Apache-2.0 lets us drop the ownCloud marks entirely.
WEB_ASSET_THEMES_PATH: /etc/ocis/web-themes
WEB_UI_THEME_PATH: /themes/dezky/theme.json
# Override ONLY index.html (OCIS layers WEB_ASSET_CORE_PATH over the
# embedded core via a fallback FS, so every other asset still serves from
# the binary). Our index hides the version footer (.versions) and sets the
# pre-hydration <title>/theme-color to dezky. NOTE: index.html pins the
# built bundle hashes — refresh it from the live `/` after an OCIS image
# bump (or drop this mount) so the script refs stay valid.
WEB_ASSET_CORE_PATH: /etc/ocis/web-core
volumes:
- ocis_config:/etc/ocis
- ocis_data:/var/lib/ocis
- ocis_audit_log:/var/log/ocis
- ./configs/ocis/csp.yaml:/etc/ocis/csp.yaml:ro
- ./configs/ocis/themes:/etc/ocis/web-themes:ro
- ./configs/ocis/web-core:/etc/ocis/web-core:ro
networks: [dezky]
depends_on:
- authentik-server