From b2986390e755d6d7bc9ffe40d93b9c00c9dfd441 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 10 Nov 2026 14:00:28 +0100 Subject: [PATCH 1/7] Restyle labels as tinted pills via dedicated `labelBadge` class Closes #5935 --- .../templates/shared_badgeColorFormField.tpl | 2 +- .../Form/Builder/Field/Controller/Label.ts | 4 +- .../WebComponent/woltlab-core-label-picker.ts | 2 +- wcfsetup/install/files/js/WCF.Label.js | 4 +- .../Form/Builder/Field/Controller/Label.js | 4 +- .../files/js/WoltLabSuite/WebComponent.min.js | 2 +- .../files/lib/acp/form/LabelAddForm.class.php | 1 + .../files/lib/data/label/Label.class.php | 2 +- .../field/BadgeColorFormField.class.php | 22 +++++ .../gridView/admin/LabelGridView.class.php | 6 +- wcfsetup/install/files/style/layout/box.scss | 3 +- .../install/files/style/layout/content.scss | 15 ++- .../install/files/style/layout/print.scss | 3 +- wcfsetup/install/files/style/ui/darkMode.scss | 45 +++++++++ wcfsetup/install/files/style/ui/label.scss | 92 +++++++++++++++---- .../install/files/style/ui/messageGroup.scss | 3 +- .../install/files/style/ui/tabularBox.scss | 3 +- 17 files changed, 169 insertions(+), 44 deletions(-) diff --git a/com.woltlab.wcf/templates/shared_badgeColorFormField.tpl b/com.woltlab.wcf/templates/shared_badgeColorFormField.tpl index b0edbb2fb68..6fcda23b0d0 100644 --- a/com.woltlab.wcf/templates/shared_badgeColorFormField.tpl +++ b/com.woltlab.wcf/templates/shared_badgeColorFormField.tpl @@ -21,7 +21,7 @@ *}> {else} - {$field->getDefaultLabelText()} + {$field->getDefaultLabelText()} {/if} diff --git a/ts/WoltLabSuite/Core/Form/Builder/Field/Controller/Label.ts b/ts/WoltLabSuite/Core/Form/Builder/Field/Controller/Label.ts index 30ab549c174..7a431aaf64f 100644 --- a/ts/WoltLabSuite/Core/Form/Builder/Field/Controller/Label.ts +++ b/ts/WoltLabSuite/Core/Form/Builder/Field/Controller/Label.ts @@ -67,7 +67,7 @@ class Label { listItem.appendChild(span); const label = document.createElement("span"); - label.classList.add("badge", "label"); + label.classList.add("labelBadge"); label.innerHTML = getPhrase("wcf.label.withoutSelection"); span.appendChild(label); } @@ -82,7 +82,7 @@ class Label { listItem.appendChild(span); const label = document.createElement("span"); - label.classList.add("badge", "label"); + label.classList.add("labelBadge"); label.innerHTML = getPhrase("wcf.label.none"); span.appendChild(label); } diff --git a/ts/WoltLabSuite/WebComponent/woltlab-core-label-picker.ts b/ts/WoltLabSuite/WebComponent/woltlab-core-label-picker.ts index a26560ff121..f9403e63e3a 100644 --- a/ts/WoltLabSuite/WebComponent/woltlab-core-label-picker.ts +++ b/ts/WoltLabSuite/WebComponent/woltlab-core-label-picker.ts @@ -422,7 +422,7 @@ #getBadge(phrase: string): string { const badge = document.createElement("span"); - badge.classList.add("badge", "label"); + badge.classList.add("labelBadge"); badge.textContent = window.WoltLabLanguage.getPhrase(phrase); return badge.outerHTML; diff --git a/wcfsetup/install/files/js/WCF.Label.js b/wcfsetup/install/files/js/WCF.Label.js index e625fb49d57..32332592cf6 100644 --- a/wcfsetup/install/files/js/WCF.Label.js +++ b/wcfsetup/install/files/js/WCF.Label.js @@ -121,12 +121,12 @@ WCF.Label.Chooser = Class.extend({ } if (this._showWithoutSelection) { - var buttonWithoutSelection = $('
  • ' + WCF.Language.get('wcf.label.withoutSelection') + '
  • ').data('groupID', $groupID).appendTo($additionalList).click($.proxy(this._click, this)); + var buttonWithoutSelection = $('
  • ' + WCF.Language.get('wcf.label.withoutSelection') + '
  • ').data('groupID', $groupID).appendTo($additionalList).click($.proxy(this._click, this)); blockPageScroll(buttonWithoutSelection[0]); } if (!$group.data('forceSelection')) { - var $buttonEmpty = $('
  • ' + WCF.Language.get('wcf.label.none') + '
  • ').data('groupID', $groupID).appendTo($additionalList); + var $buttonEmpty = $('
  • ' + WCF.Language.get('wcf.label.none') + '
  • ').data('groupID', $groupID).appendTo($additionalList); $buttonEmpty.click($.proxy(this._click, this)); blockPageScroll($buttonEmpty[0]); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/Label.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/Label.js index afb72370b23..91ce7ec2b1f 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/Label.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/Label.js @@ -53,7 +53,7 @@ define(["require", "exports", "tslib", "../../../../Core", "../../../../Dom/Util const span = document.createElement("span"); listItem.appendChild(span); const label = document.createElement("span"); - label.classList.add("badge", "label"); + label.classList.add("labelBadge"); label.innerHTML = (0, Language_1.getPhrase)("wcf.label.withoutSelection"); span.appendChild(label); } @@ -65,7 +65,7 @@ define(["require", "exports", "tslib", "../../../../Core", "../../../../Dom/Util const span = document.createElement("span"); listItem.appendChild(span); const label = document.createElement("span"); - label.classList.add("badge", "label"); + label.classList.add("labelBadge"); label.innerHTML = (0, Language_1.getPhrase)("wcf.label.none"); span.appendChild(label); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/WebComponent.min.js b/wcfsetup/install/files/js/WoltLabSuite/WebComponent.min.js index 7c20b328778..9539b4dfb90 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/WebComponent.min.js +++ b/wcfsetup/install/files/js/WoltLabSuite/WebComponent.min.js @@ -60,7 +60,7 @@ Expecting `+Q.join(", ")+", got '"+(this.terminals_[T]||T)+"'":ae="Parse error o time::after { content: " (" attr(title) ")"; } - }`,v.append(F)}p&&(this.#t.dateTime=m.toISOString(),this.#t.title=x.DateAndTime.format(m));let k;if(this.static)k=this.#t.title;else if(_l?k=this.#a(v,0):m.getTime()>c?k=this.#a(v,-1):k=v.map(V=>V.value).join(""):k=x.DateAndTime.format(m)}else k=x.Date.format(m);k=k.charAt(0).toUpperCase()+k.slice(1),this.#t.textContent=k}#a(p,m){return p.map(k=>k.type==="weekday"?x.TodayOrYesterday.format(m,"day"):k.value).join("")}}window.customElements.define("woltlab-core-date-time",H);let D=()=>{document.querySelectorAll("woltlab-core-date-time").forEach(d=>d.refresh(!1))},b,S=()=>{b=window.setInterval(()=>{g(),D()},6e4)};document.addEventListener("DOMContentLoaded",()=>S(),{once:!0}),document.addEventListener("visibilitychange",()=>{document.hidden?window.clearInterval(b):(D(),S())})}{class s extends HTMLElement{#e;#t;#a;#r=0;constructor(){super(),this.#e=document.createElement("input"),this.#e.type="file",this.#e.classList.add("woltlabCoreFileUpload__input"),this.#e.addEventListener("cancel",r=>{r.stopPropagation()}),this.#e.addEventListener("change",()=>{let{files:r}=this.#e;if(!(r===null||r.length===0)){for(let a of r){let e=new CustomEvent("shouldUpload",{cancelable:!0,detail:a});if(this.dispatchEvent(e),e.defaultPrevented)continue;let t=new CustomEvent("upload",{detail:a});this.dispatchEvent(t)}this.uploadFiles(Array.from(r)),this.#e.value=""}}),this.addEventListener("click",r=>{r.target===this&&this.#e.click()}),this.#t=document.createElement("fa-icon"),this.#a=document.createElement("span")}connectedCallback(){let r=this.dataset.fileExtensions||"";r!==""&&(this.#e.accept=r);let a=this.maximumCount;(a>1||a===-1)&&(this.#e.multiple=!0);let e=document.createElement("button");e.type="button",e.classList.add("button","woltlabCoreFileUpload__button"),e.addEventListener("keydown",t=>{(t.key==="Enter"||t.key===" ")&&this.#e.click()}),e.append(this.#t,this.#a,this.#e),this.#l(),this.append(e)}uploadFiles(r){let a=new CustomEvent("upload:files",{detail:{files:r}});this.dispatchEvent(a)}markAsBusy(){this.#r++,this.#l()}markAsReady(){this.#r--,this.#l()}get maximumCount(){return parseInt(this.dataset.maximumCount||"1")}get maximumSize(){return parseInt(this.dataset.maximumSize||"-1")}get disabled(){return this.#e.disabled}set disabled(r){this.#e.disabled=!!r}#l(){let r=this.#r===0?"upload":"spinner",a=this.#r===0?"wcf.global.button.upload":"wcf.global.loading";this.#t.setIcon(r),this.#a.textContent=window.WoltLabLanguage.getPhrase(a)}}window.customElements.define("woltlab-core-file-upload",s)}{let s=0;class n extends HTMLElement{static formAssociated=!0;static get observedAttributes(){return["required"]}#e;#t=void 0;#a=void 0;#r="";#l=new Map;#i=void 0;#o=new Map;#s=void 0;#n=void 0;#c="";#h=void 0;constructor(){super(),this.#e=this.attachInternals()}connectedCallback(){if(this.#a!==void 0)return;if(this.hasAttribute("labels")&&(this.#l=new Map(JSON.parse(this.getAttribute("labels"))),this.removeAttribute("labels")),this.multiple&&this.invertible)throw new Error("A label picker cannot be invertible if more than one label can be selected.");this.#r=this.getAttribute("value")||"";let a=`labelPicker${++s}`;this.#s=document.createElement("div"),this.#s.id=a,this.#s.popover="auto",this.#s.classList.add("labelPicker__popover"),this.#s.addEventListener("toggle",t=>{t.newState==="open"?this.#x():this.#L()}),this.#s.addEventListener("focusout",t=>{t.relatedTarget instanceof Node&&!this.contains(t.relatedTarget)&&this.#g()&&this.#s.hidePopover()}),this.#a=document.createElement("button"),this.#a.type="button",this.#a.classList.add("labelPicker__toggle"),this.#a.disabled=this.matches(":disabled"),this.#a.popoverTargetElement=this.#s,this.#a.setAttribute("aria-haspopup","listbox"),this.#t=document.createElement("span"),this.#t.classList.add("labelPicker__badges");let e=document.createElement("fa-icon");e.classList.add("labelPicker__chevron"),e.setIcon("chevron-down"),this.#a.append(this.#t,e),this.#a.addEventListener("keydown",t=>{(t.key==="ArrowDown"||t.key==="ArrowUp")&&!this.#g()&&(t.preventDefault(),this.#s.showPopover({source:this.#a}))}),this.#s.append(...this.#E(a)),this.innerHTML="",this.append(this.#a,this.#s),this.addEventListener("click",t=>{t.target===this&&this.#a.focus()}),this.#f()}attributeChangedCallback(a){this.#a!==void 0&&a==="required"&&this.#f()}formDisabledCallback(a){this.#a!==void 0&&(this.#a.disabled=a),a&&this.#g()&&this.#s.hidePopover()}formResetCallback(){this.setAttribute("value",this.#r),this.#f()}formStateRestoreCallback(a){if(typeof a!="string")return;let e=this.#p(a);e.every(t=>this.#d(t))&&(this.setAttribute("value",e.join(",")),this.#f())}#d(a){return a===-1?this.invertible:a===0||this.#l.has(a)}#p(a){return a.split(",").map(e=>parseInt(e)).filter(e=>!Number.isNaN(e)&&e!==0)}#E(a){this.#i=document.createElement("div"),this.#i.classList.add("labelPicker__listbox"),this.#i.setAttribute("role","listbox"),this.#i.addEventListener("keydown",t=>this.#T(t));for(let[t,l]of this.#l)this.#i.append(this.#m(t,l));if(this.multiple){let t=document.createElement("span");t.id=`${a}Limit`,t.classList.add("labelPicker__limit"),t.textContent=window.WoltLabLanguage.getPhrase("wcf.label.picker.maxSelection",{max:this.max}),this.#n=document.createElement("button"),this.#n.type="button",this.#n.classList.add("labelPicker__reset"),this.#n.textContent=window.WoltLabLanguage.getPhrase("wcf.global.button.reset"),this.#n.addEventListener("click",()=>this.#S()),this.#n.addEventListener("keydown",c=>{c.key==="Tab"&&c.shiftKey&&(c.preventDefault(),this.#b())});let l=document.createElement("div");return l.classList.add("labelPicker__footer"),l.append(t,this.#n),this.#i.setAttribute("aria-multiselectable","true"),this.#i.setAttribute("aria-describedby",t.id),[this.#i,l]}let e=[];return this.invertible&&e.push(this.#m(-1,this.#u("wcf.label.withoutSelection"))),this.required||e.push(this.#m(0,this.#u("wcf.label.none"))),e.length>0&&(e[0].classList.add("labelPicker__option--separated"),this.#i.append(...e)),[this.#i]}#m(a,e){let t=document.createElement("span");t.classList.add("labelPicker__indicator",this.multiple?"labelPicker__indicator--checkbox":"labelPicker__indicator--radio");let l=document.createElement("span");l.classList.add("labelPicker__label"),l.innerHTML=e;let c=document.createElement("div");return c.classList.add("labelPicker__option"),c.setAttribute("role","option"),c.tabIndex=-1,c.append(t,l),c.addEventListener("click",()=>this.#k(a)),this.#o.set(a,c),c}#g(){return this.#s?.matches(":popover-open")??!1}#x(){let a=this.#e.labels[0]?.textContent?.trim()??"";a!==""&&this.#i.setAttribute("aria-label",a),this.#a.classList.add("labelPicker__toggle--open"),this.#b()}#b(){let a=Array.from(this.#o.values());(a.find(t=>t.getAttribute("aria-selected")==="true")??a[0])?.focus()}#L(){this.#a.classList.remove("labelPicker__toggle--open"),this.#c="",window.clearTimeout(this.#h)}#w(){this.#s.hidePopover(),this.#a.focus()}#T(a){let e=Array.from(this.#o.values()),t=a.target.closest(".labelPicker__option");if(t===null)return;let l=e.indexOf(t),c=Array.from(this.#o.keys())[l];switch(a.key){case"ArrowDown":e[Math.min(l+1,e.length-1)].focus();break;case"ArrowUp":e[Math.max(l-1,0)].focus();break;case"Home":e[0].focus();break;case"End":e[e.length-1].focus();break;case" ":this.#k(c);break;case"Enter":this.multiple?this.#w():this.#k(c);break;case"Tab":if(!a.shiftKey)return;this.#w();break;default:if(a.key.length!==1||a.altKey||a.ctrlKey||a.metaKey)return;this.#A(a.key,l)?.focus();break}a.preventDefault()}#A(a,e){this.#c+=a.toLowerCase(),window.clearTimeout(this.#h),this.#h=window.setTimeout(()=>{this.#c=""},500);let t=Array.from(this.#o.values()),l=this.#c.length===1?e+1:e;return[...t.slice(l),...t.slice(0,l)].find(g=>g.textContent.trim().toLowerCase().startsWith(this.#c))}#k(a){if(this.multiple){this.#_(a);return}this.value!==a&&(this.value=a,this.#y()),this.#w()}#_(a){let e=this.values,t=e.indexOf(a);if(t===-1){if(e.length>=this.max)return;e.push(a)}else e.splice(t,1);this.values=e,this.#y()}#S(){this.values.length!==0&&(this.values=[],this.#y(),this.#b())}#y(){this.dispatchEvent(new Event("input",{bubbles:!0})),this.dispatchEvent(new Event("change",{bubbles:!0}))}#u(a){let e=document.createElement("span");return e.classList.add("badge","label"),e.textContent=window.WoltLabLanguage.getPhrase(a),e.outerHTML}#f(){this.multiple?this.#M():this.#q()}#q(){let a=this.value;this.#t!==void 0&&(a===-1?this.#t.innerHTML=this.#u("wcf.label.withoutSelection"):this.#t.innerHTML=this.#l.get(a)??this.#u("wcf.label.none"));for(let[e,t]of this.#o)t.setAttribute("aria-selected",(e===a).toString());this.#e.setFormValue(a.toString()),this.#v(a===0)}#M(){let a=this.values;if(this.#t!==void 0){let l=a.map(c=>this.#l.get(c)).join("");this.#t.innerHTML=l||this.#u("wcf.label.none")}this.#n!==void 0&&(this.#n.disabled=a.length===0);let e=a.length>=this.max;for(let[l,c]of this.#o){let g=a.includes(l);c.setAttribute("aria-selected",g.toString()),!g&&e?c.setAttribute("aria-disabled","true"):c.removeAttribute("aria-disabled")}let t=new FormData;for(let l of a)t.append(`${this.name}[]`,l.toString());this.#e.setFormValue(t,a.join(",")),this.#v(a.length===0)}#v(a){this.required&&a?this.#e.setValidity({valueMissing:!0},window.WoltLabLanguage.getPhrase("wcf.global.form.error.empty"),this.#a):this.#e.setValidity({})}get value(){return this.#p(this.getAttribute("value")||"")[0]??0}set value(a){if(!this.#d(a))throw new Error(`There is no label with the id ${a}.`);this.setAttribute("value",a.toString()),this.#f()}get values(){let a=this.#p(this.getAttribute("value")||"");return this.multiple?a:a.slice(0,1)}set values(a){if(a.length>this.max)throw new Error(`Cannot select more than ${this.max} labels.`);if(new Set(a).size!==a.length)throw new Error("The list of labels contains duplicates.");for(let e of a)if(e===0||!this.#d(e))throw new Error(`There is no label with the id ${e}.`);this.setAttribute("value",a.join(",")),this.#f()}get max(){let a=parseInt(this.getAttribute("max")||"1");return Number.isNaN(a)||a<1?1:a}get multiple(){return this.max>1}get name(){return this.getAttribute("name")||""}get form(){return this.#e.form}get disabled(){return this.hasAttribute("disabled")}set disabled(a){this.toggleAttribute("disabled",a)}get required(){return this.hasAttribute("required")}get invertible(){return this.hasAttribute("invertible")}}window.customElements.define("woltlab-core-label-picker",n)}{let n=[24,48,96];class r extends HTMLElement{#e;#t;connectedCallback(){this.#e===void 0&&this.#a()}attributeChangedCallback(e,t,l){if(e==="size"){let c=parseInt(l||"");if(!n.includes(c)){let g=parseInt(t||"");n.includes(g)||(g=24),this.setAttribute(e,g.toString())}}}#a(){this.classList.add("loading-indicator"),this.hasAttribute("size")||this.setAttribute("size","24"),this.#e=document.createElement("fa-icon"),this.#e.size=this.size,this.#e.setIcon("spinner"),this.#t=document.createElement("span"),this.#t.classList.add("loading-indicator__text"),this.#t.textContent=window.WoltLabLanguage.getPhrase("wcf.global.loading"),this.#t.hidden=this.hideText;let e=document.createElement("div");e.classList.add("loading-indicator__wrapper"),e.append(this.#e,this.#t),this.append(e)}get size(){return parseInt(this.getAttribute("size"))}set size(e){if(!n.includes(e))throw new TypeError(`The size ${e} is unrecognized, permitted values are ${n.join(", ")}.`);this.setAttribute("size",e.toString()),this.#e&&(this.#e.size=e)}get hideText(){return this.hasAttribute("hide-text")}set hideText(e){e?this.setAttribute("hide-text",""):this.removeAttribute("hide-text"),this.#t&&(this.#t.hidden=e)}static get observedAttributes(){return["size"]}}window.customElements.define("woltlab-core-loading-indicator",r)}{let s;(l=>(l.Info="info",l.Success="success",l.Warning="warning",l.Error="error"))(s||={});class n extends HTMLElement{#e;#t;constructor(){super(),this.#e=document.createElement("fa-icon"),this.#e.size=24,this.#e.slot="icon"}connectedCallback(){this.#a(),this.#r(),this.querySelector(':scope > fa-icon[slot="icon"]')?.remove(),this.append(this.#e)}#a(){if(this.shadowRoot!==null)return;let a=this.attachShadow({mode:"open"}),e=document.createElement("style");e.textContent=` + }`,v.append(F)}p&&(this.#t.dateTime=m.toISOString(),this.#t.title=x.DateAndTime.format(m));let k;if(this.static)k=this.#t.title;else if(_l?k=this.#a(v,0):m.getTime()>c?k=this.#a(v,-1):k=v.map(V=>V.value).join(""):k=x.DateAndTime.format(m)}else k=x.Date.format(m);k=k.charAt(0).toUpperCase()+k.slice(1),this.#t.textContent=k}#a(p,m){return p.map(k=>k.type==="weekday"?x.TodayOrYesterday.format(m,"day"):k.value).join("")}}window.customElements.define("woltlab-core-date-time",H);let D=()=>{document.querySelectorAll("woltlab-core-date-time").forEach(d=>d.refresh(!1))},b,S=()=>{b=window.setInterval(()=>{g(),D()},6e4)};document.addEventListener("DOMContentLoaded",()=>S(),{once:!0}),document.addEventListener("visibilitychange",()=>{document.hidden?window.clearInterval(b):(D(),S())})}{class s extends HTMLElement{#e;#t;#a;#r=0;constructor(){super(),this.#e=document.createElement("input"),this.#e.type="file",this.#e.classList.add("woltlabCoreFileUpload__input"),this.#e.addEventListener("cancel",r=>{r.stopPropagation()}),this.#e.addEventListener("change",()=>{let{files:r}=this.#e;if(!(r===null||r.length===0)){for(let a of r){let e=new CustomEvent("shouldUpload",{cancelable:!0,detail:a});if(this.dispatchEvent(e),e.defaultPrevented)continue;let t=new CustomEvent("upload",{detail:a});this.dispatchEvent(t)}this.uploadFiles(Array.from(r)),this.#e.value=""}}),this.addEventListener("click",r=>{r.target===this&&this.#e.click()}),this.#t=document.createElement("fa-icon"),this.#a=document.createElement("span")}connectedCallback(){let r=this.dataset.fileExtensions||"";r!==""&&(this.#e.accept=r);let a=this.maximumCount;(a>1||a===-1)&&(this.#e.multiple=!0);let e=document.createElement("button");e.type="button",e.classList.add("button","woltlabCoreFileUpload__button"),e.addEventListener("keydown",t=>{(t.key==="Enter"||t.key===" ")&&this.#e.click()}),e.append(this.#t,this.#a,this.#e),this.#l(),this.append(e)}uploadFiles(r){let a=new CustomEvent("upload:files",{detail:{files:r}});this.dispatchEvent(a)}markAsBusy(){this.#r++,this.#l()}markAsReady(){this.#r--,this.#l()}get maximumCount(){return parseInt(this.dataset.maximumCount||"1")}get maximumSize(){return parseInt(this.dataset.maximumSize||"-1")}get disabled(){return this.#e.disabled}set disabled(r){this.#e.disabled=!!r}#l(){let r=this.#r===0?"upload":"spinner",a=this.#r===0?"wcf.global.button.upload":"wcf.global.loading";this.#t.setIcon(r),this.#a.textContent=window.WoltLabLanguage.getPhrase(a)}}window.customElements.define("woltlab-core-file-upload",s)}{let s=0;class n extends HTMLElement{static formAssociated=!0;static get observedAttributes(){return["required"]}#e;#t=void 0;#a=void 0;#r="";#l=new Map;#i=void 0;#o=new Map;#s=void 0;#n=void 0;#c="";#h=void 0;constructor(){super(),this.#e=this.attachInternals()}connectedCallback(){if(this.#a!==void 0)return;if(this.hasAttribute("labels")&&(this.#l=new Map(JSON.parse(this.getAttribute("labels"))),this.removeAttribute("labels")),this.multiple&&this.invertible)throw new Error("A label picker cannot be invertible if more than one label can be selected.");this.#r=this.getAttribute("value")||"";let a=`labelPicker${++s}`;this.#s=document.createElement("div"),this.#s.id=a,this.#s.popover="auto",this.#s.classList.add("labelPicker__popover"),this.#s.addEventListener("toggle",t=>{t.newState==="open"?this.#x():this.#L()}),this.#s.addEventListener("focusout",t=>{t.relatedTarget instanceof Node&&!this.contains(t.relatedTarget)&&this.#g()&&this.#s.hidePopover()}),this.#a=document.createElement("button"),this.#a.type="button",this.#a.classList.add("labelPicker__toggle"),this.#a.disabled=this.matches(":disabled"),this.#a.popoverTargetElement=this.#s,this.#a.setAttribute("aria-haspopup","listbox"),this.#t=document.createElement("span"),this.#t.classList.add("labelPicker__badges");let e=document.createElement("fa-icon");e.classList.add("labelPicker__chevron"),e.setIcon("chevron-down"),this.#a.append(this.#t,e),this.#a.addEventListener("keydown",t=>{(t.key==="ArrowDown"||t.key==="ArrowUp")&&!this.#g()&&(t.preventDefault(),this.#s.showPopover({source:this.#a}))}),this.#s.append(...this.#E(a)),this.innerHTML="",this.append(this.#a,this.#s),this.addEventListener("click",t=>{t.target===this&&this.#a.focus()}),this.#f()}attributeChangedCallback(a){this.#a!==void 0&&a==="required"&&this.#f()}formDisabledCallback(a){this.#a!==void 0&&(this.#a.disabled=a),a&&this.#g()&&this.#s.hidePopover()}formResetCallback(){this.setAttribute("value",this.#r),this.#f()}formStateRestoreCallback(a){if(typeof a!="string")return;let e=this.#p(a);e.every(t=>this.#d(t))&&(this.setAttribute("value",e.join(",")),this.#f())}#d(a){return a===-1?this.invertible:a===0||this.#l.has(a)}#p(a){return a.split(",").map(e=>parseInt(e)).filter(e=>!Number.isNaN(e)&&e!==0)}#E(a){this.#i=document.createElement("div"),this.#i.classList.add("labelPicker__listbox"),this.#i.setAttribute("role","listbox"),this.#i.addEventListener("keydown",t=>this.#T(t));for(let[t,l]of this.#l)this.#i.append(this.#m(t,l));if(this.multiple){let t=document.createElement("span");t.id=`${a}Limit`,t.classList.add("labelPicker__limit"),t.textContent=window.WoltLabLanguage.getPhrase("wcf.label.picker.maxSelection",{max:this.max}),this.#n=document.createElement("button"),this.#n.type="button",this.#n.classList.add("labelPicker__reset"),this.#n.textContent=window.WoltLabLanguage.getPhrase("wcf.global.button.reset"),this.#n.addEventListener("click",()=>this.#S()),this.#n.addEventListener("keydown",c=>{c.key==="Tab"&&c.shiftKey&&(c.preventDefault(),this.#b())});let l=document.createElement("div");return l.classList.add("labelPicker__footer"),l.append(t,this.#n),this.#i.setAttribute("aria-multiselectable","true"),this.#i.setAttribute("aria-describedby",t.id),[this.#i,l]}let e=[];return this.invertible&&e.push(this.#m(-1,this.#u("wcf.label.withoutSelection"))),this.required||e.push(this.#m(0,this.#u("wcf.label.none"))),e.length>0&&(e[0].classList.add("labelPicker__option--separated"),this.#i.append(...e)),[this.#i]}#m(a,e){let t=document.createElement("span");t.classList.add("labelPicker__indicator",this.multiple?"labelPicker__indicator--checkbox":"labelPicker__indicator--radio");let l=document.createElement("span");l.classList.add("labelPicker__label"),l.innerHTML=e;let c=document.createElement("div");return c.classList.add("labelPicker__option"),c.setAttribute("role","option"),c.tabIndex=-1,c.append(t,l),c.addEventListener("click",()=>this.#k(a)),this.#o.set(a,c),c}#g(){return this.#s?.matches(":popover-open")??!1}#x(){let a=this.#e.labels[0]?.textContent?.trim()??"";a!==""&&this.#i.setAttribute("aria-label",a),this.#a.classList.add("labelPicker__toggle--open"),this.#b()}#b(){let a=Array.from(this.#o.values());(a.find(t=>t.getAttribute("aria-selected")==="true")??a[0])?.focus()}#L(){this.#a.classList.remove("labelPicker__toggle--open"),this.#c="",window.clearTimeout(this.#h)}#w(){this.#s.hidePopover(),this.#a.focus()}#T(a){let e=Array.from(this.#o.values()),t=a.target.closest(".labelPicker__option");if(t===null)return;let l=e.indexOf(t),c=Array.from(this.#o.keys())[l];switch(a.key){case"ArrowDown":e[Math.min(l+1,e.length-1)].focus();break;case"ArrowUp":e[Math.max(l-1,0)].focus();break;case"Home":e[0].focus();break;case"End":e[e.length-1].focus();break;case" ":this.#k(c);break;case"Enter":this.multiple?this.#w():this.#k(c);break;case"Tab":if(!a.shiftKey)return;this.#w();break;default:if(a.key.length!==1||a.altKey||a.ctrlKey||a.metaKey)return;this.#A(a.key,l)?.focus();break}a.preventDefault()}#A(a,e){this.#c+=a.toLowerCase(),window.clearTimeout(this.#h),this.#h=window.setTimeout(()=>{this.#c=""},500);let t=Array.from(this.#o.values()),l=this.#c.length===1?e+1:e;return[...t.slice(l),...t.slice(0,l)].find(g=>g.textContent.trim().toLowerCase().startsWith(this.#c))}#k(a){if(this.multiple){this.#_(a);return}this.value!==a&&(this.value=a,this.#y()),this.#w()}#_(a){let e=this.values,t=e.indexOf(a);if(t===-1){if(e.length>=this.max)return;e.push(a)}else e.splice(t,1);this.values=e,this.#y()}#S(){this.values.length!==0&&(this.values=[],this.#y(),this.#b())}#y(){this.dispatchEvent(new Event("input",{bubbles:!0})),this.dispatchEvent(new Event("change",{bubbles:!0}))}#u(a){let e=document.createElement("span");return e.classList.add("labelBadge"),e.textContent=window.WoltLabLanguage.getPhrase(a),e.outerHTML}#f(){this.multiple?this.#M():this.#q()}#q(){let a=this.value;this.#t!==void 0&&(a===-1?this.#t.innerHTML=this.#u("wcf.label.withoutSelection"):this.#t.innerHTML=this.#l.get(a)??this.#u("wcf.label.none"));for(let[e,t]of this.#o)t.setAttribute("aria-selected",(e===a).toString());this.#e.setFormValue(a.toString()),this.#v(a===0)}#M(){let a=this.values;if(this.#t!==void 0){let l=a.map(c=>this.#l.get(c)).join("");this.#t.innerHTML=l||this.#u("wcf.label.none")}this.#n!==void 0&&(this.#n.disabled=a.length===0);let e=a.length>=this.max;for(let[l,c]of this.#o){let g=a.includes(l);c.setAttribute("aria-selected",g.toString()),!g&&e?c.setAttribute("aria-disabled","true"):c.removeAttribute("aria-disabled")}let t=new FormData;for(let l of a)t.append(`${this.name}[]`,l.toString());this.#e.setFormValue(t,a.join(",")),this.#v(a.length===0)}#v(a){this.required&&a?this.#e.setValidity({valueMissing:!0},window.WoltLabLanguage.getPhrase("wcf.global.form.error.empty"),this.#a):this.#e.setValidity({})}get value(){return this.#p(this.getAttribute("value")||"")[0]??0}set value(a){if(!this.#d(a))throw new Error(`There is no label with the id ${a}.`);this.setAttribute("value",a.toString()),this.#f()}get values(){let a=this.#p(this.getAttribute("value")||"");return this.multiple?a:a.slice(0,1)}set values(a){if(a.length>this.max)throw new Error(`Cannot select more than ${this.max} labels.`);if(new Set(a).size!==a.length)throw new Error("The list of labels contains duplicates.");for(let e of a)if(e===0||!this.#d(e))throw new Error(`There is no label with the id ${e}.`);this.setAttribute("value",a.join(",")),this.#f()}get max(){let a=parseInt(this.getAttribute("max")||"1");return Number.isNaN(a)||a<1?1:a}get multiple(){return this.max>1}get name(){return this.getAttribute("name")||""}get form(){return this.#e.form}get disabled(){return this.hasAttribute("disabled")}set disabled(a){this.toggleAttribute("disabled",a)}get required(){return this.hasAttribute("required")}get invertible(){return this.hasAttribute("invertible")}}window.customElements.define("woltlab-core-label-picker",n)}{let n=[24,48,96];class r extends HTMLElement{#e;#t;connectedCallback(){this.#e===void 0&&this.#a()}attributeChangedCallback(e,t,l){if(e==="size"){let c=parseInt(l||"");if(!n.includes(c)){let g=parseInt(t||"");n.includes(g)||(g=24),this.setAttribute(e,g.toString())}}}#a(){this.classList.add("loading-indicator"),this.hasAttribute("size")||this.setAttribute("size","24"),this.#e=document.createElement("fa-icon"),this.#e.size=this.size,this.#e.setIcon("spinner"),this.#t=document.createElement("span"),this.#t.classList.add("loading-indicator__text"),this.#t.textContent=window.WoltLabLanguage.getPhrase("wcf.global.loading"),this.#t.hidden=this.hideText;let e=document.createElement("div");e.classList.add("loading-indicator__wrapper"),e.append(this.#e,this.#t),this.append(e)}get size(){return parseInt(this.getAttribute("size"))}set size(e){if(!n.includes(e))throw new TypeError(`The size ${e} is unrecognized, permitted values are ${n.join(", ")}.`);this.setAttribute("size",e.toString()),this.#e&&(this.#e.size=e)}get hideText(){return this.hasAttribute("hide-text")}set hideText(e){e?this.setAttribute("hide-text",""):this.removeAttribute("hide-text"),this.#t&&(this.#t.hidden=e)}static get observedAttributes(){return["size"]}}window.customElements.define("woltlab-core-loading-indicator",r)}{let s;(l=>(l.Info="info",l.Success="success",l.Warning="warning",l.Error="error"))(s||={});class n extends HTMLElement{#e;#t;constructor(){super(),this.#e=document.createElement("fa-icon"),this.#e.size=24,this.#e.slot="icon"}connectedCallback(){this.#a(),this.#r(),this.querySelector(':scope > fa-icon[slot="icon"]')?.remove(),this.append(this.#e)}#a(){if(this.shadowRoot!==null)return;let a=this.attachShadow({mode:"open"}),e=document.createElement("style");e.textContent=` :host { align-items: center; display: grid; diff --git a/wcfsetup/install/files/lib/acp/form/LabelAddForm.class.php b/wcfsetup/install/files/lib/acp/form/LabelAddForm.class.php index be0078bab68..e8c0664928a 100644 --- a/wcfsetup/install/files/lib/acp/form/LabelAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/LabelAddForm.class.php @@ -89,6 +89,7 @@ protected function createForm() ->label('wcf.acp.label.cssClassName') ->required() ->textReferenceNodeId('label') + ->previewClassName('labelBadge') ]) ]); } diff --git a/wcfsetup/install/files/lib/data/label/Label.class.php b/wcfsetup/install/files/lib/data/label/Label.class.php index 20b47bc1ea6..9a33f8b1d96 100644 --- a/wcfsetup/install/files/lib/data/label/Label.class.php +++ b/wcfsetup/install/files/lib/data/label/Label.class.php @@ -60,7 +60,7 @@ public function getClassNames() */ public function render(string $additionalClasses = '') { - return 'getClassNames() !== '' ? ' ' . StringUtil::encodeHTML($this->getClassNames()) : '') . ($additionalClasses !== '' ? ' ' . StringUtil::encodeHTML($additionalClasses) : '') . '">' . StringUtil::encodeHTML($this->getTitle()) . ''; diff --git a/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php index 1f5ee51f809..6fe9d02645f 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php @@ -44,6 +44,7 @@ final class BadgeColorFormField extends RadioButtonFormField implements IPattern protected ?string $textReferenceNodeId; protected string $defaultLabelText; protected string $customClassName = ''; + protected string $previewClassName = 'badge'; public function __construct() { @@ -138,6 +139,27 @@ public function textReferenceNodeId(string $id): self return $this; } + /** + * Sets the CSS class of the color previews, `badge` for badges such as user + * ranks and `labelBadge` for labels. + * + * @since 6.3 + */ + public function previewClassName(string $className): self + { + $this->previewClassName = $className; + + return $this; + } + + /** + * @since 6.3 + */ + public function getPreviewClassName(): string + { + return $this->previewClassName; + } + public function getDefaultLabelText(): string { return $this->defaultLabelText; diff --git a/wcfsetup/install/files/lib/system/gridView/admin/LabelGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/LabelGridView.class.php index 7c9cf020819..672a2e14ce2 100644 --- a/wcfsetup/install/files/lib/system/gridView/admin/LabelGridView.class.php +++ b/wcfsetup/install/files/lib/system/gridView/admin/LabelGridView.class.php @@ -55,12 +55,8 @@ public function __construct() public function render(mixed $value, DatabaseObject $row): string { \assert($row instanceof Label); - $badgeClassName = StringUtil::encodeHTML($row->getClassNames()); - $label = StringUtil::encodeHTML($row->getTitle()); - return <<{$label} - HTML; + return $row->render(); } } ) diff --git a/wcfsetup/install/files/style/layout/box.scss b/wcfsetup/install/files/style/layout/box.scss index 3312f732645..76cfbb46a58 100644 --- a/wcfsetup/install/files/style/layout/box.scss +++ b/wcfsetup/install/files/style/layout/box.scss @@ -17,7 +17,8 @@ margin-top: 20px; } - .badge { + .badge, + .labelBadge { top: -2px; } } diff --git a/wcfsetup/install/files/style/layout/content.scss b/wcfsetup/install/files/style/layout/content.scss index d2b2d945518..02e488631ae 100644 --- a/wcfsetup/install/files/style/layout/content.scss +++ b/wcfsetup/install/files/style/layout/content.scss @@ -10,7 +10,8 @@ // deprecated @include wcfFontTitle; - .badge { + .badge, + .labelBadge { top: -2px; @include wcfLineHeight; @@ -149,7 +150,8 @@ } } - .badge { + .badge, + .labelBadge { top: -2px; } } @@ -176,7 +178,8 @@ } } - .badge { + .badge, + .labelBadge { top: -2px; } } @@ -257,7 +260,8 @@ fieldset { } } - .badge { + .badge, + .labelBadge { top: -2px; } @@ -289,7 +293,8 @@ fieldset { > h3 { @include wcfFontHeadline; - > .badge { + > .badge, + > .labelBadge { top: -2px; } } diff --git a/wcfsetup/install/files/style/layout/print.scss b/wcfsetup/install/files/style/layout/print.scss index c1256a7d83e..9877c7a43a9 100644 --- a/wcfsetup/install/files/style/layout/print.scss +++ b/wcfsetup/install/files/style/layout/print.scss @@ -74,7 +74,8 @@ } } - .badge { + .badge, + .labelBadge { padding: 0 !important; &::before { diff --git a/wcfsetup/install/files/style/ui/darkMode.scss b/wcfsetup/install/files/style/ui/darkMode.scss index 739fa0afc06..999690ccdac 100644 --- a/wcfsetup/install/files/style/ui/darkMode.scss +++ b/wcfsetup/install/files/style/ui/darkMode.scss @@ -107,6 +107,51 @@ html[data-color-scheme="dark"] { } } + .labelBadge { + --label-tint: 16%; + --label-tint-border: 42%; + + &.yellow { + --label-color: #e3c645; + } + + &.orange { + --label-color: #ffa04a; + } + + &.brown { + --label-color: #d8a885; + } + + &.red { + --label-color: #ff8f8f; + } + + &.pink { + --label-color: #ff8fce; + } + + &.purple { + --label-color: #d3a9ff; + } + + &.blue { + --label-color: #8fc6f0; + } + + &.green { + --label-color: #74d874; + } + + &.black { + --label-color: #c3c6ca; + } + } + + a.labelBadge:hover { + --label-tint: 24%; + } + .contentItem { border: 1px solid var(--wcfContentBorderInner); } diff --git a/wcfsetup/install/files/style/ui/label.scss b/wcfsetup/install/files/style/ui/label.scss index 50a221d544a..6091e99bd4e 100644 --- a/wcfsetup/install/files/style/ui/label.scss +++ b/wcfsetup/install/files/style/ui/label.scss @@ -1,30 +1,82 @@ /* #### Labels #### */ +/* A label is tinted with its color instead of being filled like a `.badge`, + so that admin-defined categories stand apart from status badges. */ +.labelBadge { + @include wcfFontSmall; + + --label-color: var(--wcfContentText); + --label-tint: 12%; + --label-tint-border: 38%; + + background-color: color-mix(in srgb, var(--label-color) var(--label-tint), transparent); + border-radius: 999px; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--label-color) var(--label-tint-border), transparent); + color: var(--label-color); + display: inline-block; + font-weight: 500; + line-height: 1.28; + max-width: 100%; + overflow: hidden; + padding: 2px 9px; + position: relative; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + word-wrap: normal; + + /* default label colors */ + &.yellow { + --label-color: #7a5f00; + } + + &.orange { + --label-color: #a84200; + } + + &.brown { + --label-color: #8a4b2a; + } + + &.red { + --label-color: #c00000; + } + + &.pink { + --label-color: #b0086a; + } + + &.purple { + --label-color: #8a14b8; + } + + &.blue { + --label-color: #2f6496; + } + + &.green { + --label-color: #0a730a; + } + + &.black { + --label-color: #333; + } +} + +a.labelBadge:hover { + --label-tint: 20%; + + color: var(--label-color); + text-decoration: none; +} + /* label list */ .labelList { @include inlineList; display: inline-flex; - > li { - &:not(:first-child) { - .label { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - } - - &:not(:last-child) { - margin-right: 1px; - - .label { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - } - - .label { - top: -1px; - } + > li .labelBadge { + top: -1px; } } diff --git a/wcfsetup/install/files/style/ui/messageGroup.scss b/wcfsetup/install/files/style/ui/messageGroup.scss index a2ce592cc74..d53d9b37d2c 100644 --- a/wcfsetup/install/files/style/ui/messageGroup.scss +++ b/wcfsetup/install/files/style/ui/messageGroup.scss @@ -83,7 +83,8 @@ @include wcfFontHeadline; } - > .badge.label { + > .badge.label, + > .labelBadge { top: -2px; } } diff --git a/wcfsetup/install/files/style/ui/tabularBox.scss b/wcfsetup/install/files/style/ui/tabularBox.scss index 9e905eb095f..5a0398047fa 100644 --- a/wcfsetup/install/files/style/ui/tabularBox.scss +++ b/wcfsetup/install/files/style/ui/tabularBox.scss @@ -94,7 +94,8 @@ html:not(.touch) .tabularListRow:not(.tabularListRowHead):hover { display: block; } - .badge { + .badge, + .labelBadge { top: -2px; } } From 53434837aa9baa0a4581430bedd46dfd981e4ef4 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 10 Nov 2026 15:00:21 +0100 Subject: [PATCH 2/7] Switch label pills to pastel backgrounds with WCAG AA text contrast --- wcfsetup/install/files/style/ui/darkMode.scss | 34 ++++++++------- wcfsetup/install/files/style/ui/label.scss | 42 +++++++++++-------- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/wcfsetup/install/files/style/ui/darkMode.scss b/wcfsetup/install/files/style/ui/darkMode.scss index 999690ccdac..5b7f0712326 100644 --- a/wcfsetup/install/files/style/ui/darkMode.scss +++ b/wcfsetup/install/files/style/ui/darkMode.scss @@ -108,50 +108,52 @@ html[data-color-scheme="dark"] { } .labelBadge { - --label-tint: 16%; - --label-tint-border: 42%; - &.yellow { - --label-color: #e3c645; + --label-background: #5c4d00; + --label-color: #ffe36b; } &.orange { - --label-color: #ffa04a; + --label-background: #6b3500; + --label-color: #ffbb7a; } &.brown { - --label-color: #d8a885; + --label-background: #4a3627; + --label-color: #e8cbb3; } &.red { - --label-color: #ff8f8f; + --label-background: #6b1a1a; + --label-color: #ffbaba; } &.pink { - --label-color: #ff8fce; + --label-background: #661547; + --label-color: #ffb3e2; } &.purple { - --label-color: #d3a9ff; + --label-background: #472966; + --label-color: #e2c8ff; } &.blue { - --label-color: #8fc6f0; + --label-background: #1a3f63; + --label-color: #b5dbf8; } &.green { - --label-color: #74d874; + --label-background: #1b4d1b; + --label-color: #a0e8a0; } &.black { - --label-color: #c3c6ca; + --label-background: #e1e2e3; + --label-color: #1a1d21; } } - a.labelBadge:hover { - --label-tint: 24%; - } - .contentItem { border: 1px solid var(--wcfContentBorderInner); } diff --git a/wcfsetup/install/files/style/ui/label.scss b/wcfsetup/install/files/style/ui/label.scss index 6091e99bd4e..b6a7ec30b3e 100644 --- a/wcfsetup/install/files/style/ui/label.scss +++ b/wcfsetup/install/files/style/ui/label.scss @@ -1,16 +1,16 @@ /* #### Labels #### */ -/* A label is tinted with its color instead of being filled like a `.badge`, - so that admin-defined categories stand apart from status badges. */ +/* A label is a pastel pill instead of a saturated `.badge`, so that + admin-defined categories stand apart from status badges. Each color pairs a + background with a text color of at least 4.5:1 contrast. */ .labelBadge { @include wcfFontSmall; + --label-background: color-mix(in srgb, var(--wcfContentText) 9%, var(--wcfContentBackground)); --label-color: var(--wcfContentText); - --label-tint: 12%; - --label-tint-border: 38%; - background-color: color-mix(in srgb, var(--label-color) var(--label-tint), transparent); + background-color: var(--label-background); border-radius: 999px; - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--label-color) var(--label-tint-border), transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--label-color) 20%, transparent); color: var(--label-color); display: inline-block; font-weight: 500; @@ -26,45 +26,53 @@ /* default label colors */ &.yellow { - --label-color: #7a5f00; + --label-background: #ffe45c; + --label-color: #574400; } &.orange { - --label-color: #a84200; + --label-background: #ffc28a; + --label-color: #7d3100; } &.brown { - --label-color: #8a4b2a; + --label-background: #d9c0a8; + --label-color: #5a331b; } &.red { - --label-color: #c00000; + --label-background: #ffbdbd; + --label-color: #9a0000; } &.pink { - --label-color: #b0086a; + --label-background: #ffc2e4; + --label-color: #8c0654; } &.purple { - --label-color: #8a14b8; + --label-background: #dcc4ff; + --label-color: #5f0f96; } &.blue { - --label-color: #2f6496; + --label-background: #bfd9f7; + --label-color: #1a456e; } &.green { - --label-color: #0a730a; + --label-background: #bde8b5; + --label-color: #0a520a; } &.black { - --label-color: #333; + --label-background: #3a3a3d; + --label-color: #fff; } } a.labelBadge:hover { - --label-tint: 20%; - + background-color: color-mix(in srgb, var(--label-background) 90%, var(--label-color)); color: var(--label-color); text-decoration: none; } From 4922b5f4593d4b55b23961f57011bb4a2bb36d13 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 10 Nov 2026 15:09:46 +0100 Subject: [PATCH 3/7] Add label-only teal, lime, navy and maroon colors --- .../files/lib/acp/form/LabelAddForm.class.php | 1 + .../field/BadgeColorFormField.class.php | 30 ++++++++++++++++++- wcfsetup/install/files/style/ui/darkMode.scss | 20 +++++++++++++ wcfsetup/install/files/style/ui/label.scss | 21 +++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/wcfsetup/install/files/lib/acp/form/LabelAddForm.class.php b/wcfsetup/install/files/lib/acp/form/LabelAddForm.class.php index e8c0664928a..a81b7acb953 100644 --- a/wcfsetup/install/files/lib/acp/form/LabelAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/LabelAddForm.class.php @@ -90,6 +90,7 @@ protected function createForm() ->required() ->textReferenceNodeId('label') ->previewClassName('labelBadge') + ->additionalColors(BadgeColorFormField::LABEL_CSS_CLASSNAMES) ]) ]); } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php index 6fe9d02645f..1b8654a93c8 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php @@ -36,6 +36,18 @@ final class BadgeColorFormField extends RadioButtonFormField implements IPattern BadgeColorFormField::CUSTOM_CSS_CLASSNAME, /* not a real value */ ]; + /** + * Colors that are only available for labels (`labelBadge`). + * + * @since 6.3 + */ + public const LABEL_CSS_CLASSNAMES = [ + 'teal', + 'lime', + 'navy', + 'maroon', + ]; + /** * @inheritDoc */ @@ -95,7 +107,7 @@ public function validate() #[\Override] public function value(mixed $value) { - if ($this->supportsCustomClassName() && !\in_array($value, self::AVAILABLE_CSS_CLASSNAMES, true)) { + if ($this->supportsCustomClassName() && !\in_array($value, $this->options, true)) { parent::value(BadgeColorFormField::CUSTOM_CSS_CLASSNAME); $this->customClassName = $value; } else { @@ -152,6 +164,22 @@ public function previewClassName(string $className): self return $this; } + /** + * Adds colors in front of the `none` and custom options, e.g. + * `BadgeColorFormField::LABEL_CSS_CLASSNAMES` for labels. + * + * @param list $classNames + * @since 6.3 + */ + public function additionalColors(array $classNames): self + { + $options = \array_keys($this->options); + $index = \array_search('none', $options, true); + \array_splice($options, $index === false ? \count($options) : $index, 0, $classNames); + + return $this->options(\array_combine($options, $options)); + } + /** * @since 6.3 */ diff --git a/wcfsetup/install/files/style/ui/darkMode.scss b/wcfsetup/install/files/style/ui/darkMode.scss index 5b7f0712326..f75423305f7 100644 --- a/wcfsetup/install/files/style/ui/darkMode.scss +++ b/wcfsetup/install/files/style/ui/darkMode.scss @@ -152,6 +152,26 @@ html[data-color-scheme="dark"] { --label-background: #e1e2e3; --label-color: #1a1d21; } + + &.teal { + --label-background: #0b4a4a; + --label-color: #9ff0ee; + } + + &.lime { + --label-background: #3d5400; + --label-color: #d9f78a; + } + + &.navy { + --label-background: #b5ccff; + --label-color: #14285c; + } + + &.maroon { + --label-background: #ffc7d2; + --label-color: #6b0f25; + } } .contentItem { diff --git a/wcfsetup/install/files/style/ui/label.scss b/wcfsetup/install/files/style/ui/label.scss index b6a7ec30b3e..ed2fa112e94 100644 --- a/wcfsetup/install/files/style/ui/label.scss +++ b/wcfsetup/install/files/style/ui/label.scss @@ -69,6 +69,27 @@ --label-background: #3a3a3d; --label-color: #fff; } + + /* label-only colors */ + &.teal { + --label-background: #9fe5e3; + --label-color: #0a4d4b; + } + + &.lime { + --label-background: #d6f57a; + --label-color: #3b5200; + } + + &.navy { + --label-background: #1f3d7a; + --label-color: #fff; + } + + &.maroon { + --label-background: #7a1f35; + --label-color: #fff; + } } a.labelBadge:hover { From 724154ad38d389c94c8dc463d54e0fabd9a91825 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 10 Nov 2026 15:24:47 +0100 Subject: [PATCH 4/7] Replace deprecated `labelList` markup with flex-based `labelBadgeList`/`labelBadgeInlineList` --- .../templates/articleContentHeaderTitle.tpl | 6 +++--- .../templates/articleListItems.tpl | 6 +++--- .../gridView/admin/ArticleGridView.class.php | 6 +++--- wcfsetup/install/files/style/ui/label.scss | 19 ++++++++++++++++++- .../install/files/style/ui/messageGroup.scss | 6 ++++-- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/com.woltlab.wcf/templates/articleContentHeaderTitle.tpl b/com.woltlab.wcf/templates/articleContentHeaderTitle.tpl index cf7211528e5..fdb550c5874 100644 --- a/com.woltlab.wcf/templates/articleContentHeaderTitle.tpl +++ b/com.woltlab.wcf/templates/articleContentHeaderTitle.tpl @@ -38,11 +38,11 @@ {if $article->hasLabels}
  • {icon name='tags'} -
      +
      {foreach from=$article->getLabels() item=label} -
    • {unsafe:$label->render()}
    • + {unsafe:$label->render()} {/foreach} -
    +
  • {/if} diff --git a/com.woltlab.wcf/templates/articleListItems.tpl b/com.woltlab.wcf/templates/articleListItems.tpl index 51dfc8e4854..7af0ab6ed60 100644 --- a/com.woltlab.wcf/templates/articleListItems.tpl +++ b/com.woltlab.wcf/templates/articleListItems.tpl @@ -44,11 +44,11 @@
    {if $article->hasLabels} -
      +
      {foreach from=$article->getLabels() item=label} -
    • {unsafe:$label->render()}
    • + {unsafe:$label->render()} {/foreach} -
    +
    {/if}

    diff --git a/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php index d78910cf6da..a3e0c31a5ba 100644 --- a/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php +++ b/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php @@ -103,11 +103,11 @@ public function render(mixed $value, DatabaseObject $row): string $labels = ''; if ($row->hasLabels === 1) { - $labels = '
      '; + $labels = '
      '; foreach ($row->getLabels() as $label) { - $labels .= '
    • ' . $label->render() . '
    • '; + $labels .= $label->render(); } - $labels .= '
    '; + $labels .= ''; } $badges = ''; if ($row->isDeleted !== 0) { diff --git a/wcfsetup/install/files/style/ui/label.scss b/wcfsetup/install/files/style/ui/label.scss index ed2fa112e94..51484b73809 100644 --- a/wcfsetup/install/files/style/ui/label.scss +++ b/wcfsetup/install/files/style/ui/label.scss @@ -98,7 +98,7 @@ a.labelBadge:hover { text-decoration: none; } -/* label list */ +/* @deprecated 6.3 Use `.labelBadgeList` or `.labelBadgeInlineList` instead */ .labelList { @include inlineList; @@ -332,3 +332,20 @@ a.labelBadge:hover { .labelSelection__custom .labelSelection__span { flex: 1 1 auto; } + +.labelBadgeList, +.labelBadgeInlineList { + align-items: center; + column-gap: 6px; + flex-wrap: wrap; + row-gap: 3px; + white-space: nowrap; +} + +.labelBadgeList { + display: flex; +} + +.labelBadgeInlineList { + display: inline-flex; +} diff --git a/wcfsetup/install/files/style/ui/messageGroup.scss b/wcfsetup/install/files/style/ui/messageGroup.scss index d53d9b37d2c..8cdffa84617 100644 --- a/wcfsetup/install/files/style/ui/messageGroup.scss +++ b/wcfsetup/install/files/style/ui/messageGroup.scss @@ -118,7 +118,8 @@ } } - > .labelList { + > .labelList, + > .labelBadgeInlineList { float: right; padding-left: 7px; } @@ -294,7 +295,8 @@ display: none; } - .labelList { + .labelList, + .labelBadgeInlineList { float: none; padding-bottom: 2px; padding-left: 0; From f26a57933817f93a66696709f30f8169a9f80879 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 10 Nov 2026 16:16:13 +0100 Subject: [PATCH 5/7] Drop obsolete `label` class from status badges --- com.woltlab.wcf/templates/commentList.tpl | 4 ++-- com.woltlab.wcf/templates/commentResponseList.tpl | 4 ++-- com.woltlab.wcf/templates/paidSubscriptionList.tpl | 2 +- ts/WoltLabSuite/Core/Acp/Ui/Option/RewriteTest.ts | 2 +- ts/WoltLabSuite/Core/Ui/Message/Manager.ts | 2 +- .../files/js/WoltLabSuite/Core/Acp/Ui/Option/RewriteTest.js | 2 +- .../install/files/js/WoltLabSuite/Core/Ui/Message/Manager.js | 2 +- .../files/lib/system/gridView/admin/ArticleGridView.class.php | 2 +- .../lib/system/gridView/admin/UserRankGridView.class.php | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/com.woltlab.wcf/templates/commentList.tpl b/com.woltlab.wcf/templates/commentList.tpl index bb0faedaf3c..0f0f803a85b 100644 --- a/com.woltlab.wcf/templates/commentList.tpl +++ b/com.woltlab.wcf/templates/commentList.tpl @@ -34,11 +34,11 @@
    {if $comment->isDisabled} - {lang}wcf.message.status.disabled{/lang} + {lang}wcf.message.status.disabled{/lang} {/if} {if $commentManager->isContentAuthor($comment->getDecoratedObject())} - {lang}wcf.comment.objectAuthor{/lang} + {lang}wcf.comment.objectAuthor{/lang} {/if} {event name='commentStatus'} diff --git a/com.woltlab.wcf/templates/commentResponseList.tpl b/com.woltlab.wcf/templates/commentResponseList.tpl index 40c30f0398e..c0e559d736e 100644 --- a/com.woltlab.wcf/templates/commentResponseList.tpl +++ b/com.woltlab.wcf/templates/commentResponseList.tpl @@ -30,11 +30,11 @@
    {if $response->isDisabled} - {lang}wcf.message.status.disabled{/lang} + {lang}wcf.message.status.disabled{/lang} {/if} {if $commentManager->isContentAuthor($response->getDecoratedObject())} - {lang}wcf.comment.objectAuthor{/lang} + {lang}wcf.comment.objectAuthor{/lang} {/if} {event name='commentResponseStatus'} diff --git a/com.woltlab.wcf/templates/paidSubscriptionList.tpl b/com.woltlab.wcf/templates/paidSubscriptionList.tpl index 40e6e51ebbd..42c9f9049c6 100644 --- a/com.woltlab.wcf/templates/paidSubscriptionList.tpl +++ b/com.woltlab.wcf/templates/paidSubscriptionList.tpl @@ -44,7 +44,7 @@ {foreach from=$subscriptions item=subscription}
  • -

    {$subscription->getTitle()} {unsafe:$subscription->getFormattedCost()}

    +

    {$subscription->getTitle()} {unsafe:$subscription->getFormattedCost()}

    {unsafe:$subscription->getFormattedDescription()}
    diff --git a/ts/WoltLabSuite/Core/Acp/Ui/Option/RewriteTest.ts b/ts/WoltLabSuite/Core/Acp/Ui/Option/RewriteTest.ts index 508c214c42f..bd3ebd3d149 100644 --- a/ts/WoltLabSuite/Core/Acp/Ui/Option/RewriteTest.ts +++ b/ts/WoltLabSuite/Core/Acp/Ui/Option/RewriteTest.ts @@ -119,7 +119,7 @@ class RewriteTest { const testFailureResults = document.getElementById("dialogRewriteTestFailureResults")!; testFailureResults.innerHTML = results .map((result) => { - return `
  • ${getPhrase( + return `
  • ${getPhrase( "wcf.acp.option.url_omit_index_php.test.status." + (result.pass ? "success" : "failure"), )} ${result.app}
  • `; }) diff --git a/ts/WoltLabSuite/Core/Ui/Message/Manager.ts b/ts/WoltLabSuite/Core/Ui/Message/Manager.ts index 9d0575f60a9..b9a070fb083 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Manager.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Manager.ts @@ -236,7 +236,7 @@ class UiMessageManager implements AjaxCallbackObject { } badge = document.createElement("span"); - badge.className = `badge label ${badgeColor} ${className}`; + badge.className = `badge ${badgeColor} ${className}`; badge.textContent = getPhrase(phrase); const listItem = document.createElement("li"); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Option/RewriteTest.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Option/RewriteTest.js index b1bb462bca9..ab049ae0cdd 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Option/RewriteTest.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Option/RewriteTest.js @@ -91,7 +91,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Language", "../../../U const testFailureResults = document.getElementById("dialogRewriteTestFailureResults"); testFailureResults.innerHTML = results .map((result) => { - return `
  • ${(0, Language_1.getPhrase)("wcf.acp.option.url_omit_index_php.test.status." + (result.pass ? "success" : "failure"))} ${result.app}
  • `; + return `
  • ${(0, Language_1.getPhrase)("wcf.acp.option.url_omit_index_php.test.status." + (result.pass ? "success" : "failure"))} ${result.app}
  • `; }) .join(""); this.setStatus("failure"); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Manager.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Manager.js index 4c3c1c825e5..c2aef2e1ed8 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Manager.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Manager.js @@ -172,7 +172,7 @@ define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Dom/Ch return; } badge = document.createElement("span"); - badge.className = `badge label ${badgeColor} ${className}`; + badge.className = `badge ${badgeColor} ${className}`; badge.textContent = (0, Language_1.getPhrase)(phrase); const listItem = document.createElement("li"); listItem.appendChild(badge); diff --git a/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php index a3e0c31a5ba..6f1b7e30f41 100644 --- a/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php +++ b/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php @@ -112,7 +112,7 @@ public function render(mixed $value, DatabaseObject $row): string $badges = ''; if ($row->isDeleted !== 0) { $badges .= \sprintf( - '%s', + '%s', WCF::getLanguage()->get('wcf.message.status.deleted') ); } diff --git a/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php index c5fd49d0534..b0d909828de 100644 --- a/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php +++ b/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php @@ -61,7 +61,7 @@ public function render(mixed $value, DatabaseObject $row): string { \assert($row instanceof UserRank); - return '' + return '' . ($value ?? '') . ''; } From fbf60995cad1e2149cd9c0d179d7708b37256cd0 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 10 Nov 2026 18:10:31 +0100 Subject: [PATCH 6/7] Migrate label group object type availability to MultipleSelectionFormField --- com.woltlab.wcf/acpTemplateDelete.xml | 1 + .../shared_multipleSelectionFormField.tpl | 2 +- .../Core/Acp/Component/Label/Availability.ts | 4 +- .../acp/templates/__labelGroupObjectTypes.tpl | 23 --- .../files/acp/templates/labelGroupAdd.tpl | 6 + .../Core/Acp/Component/Label/Availability.js | 2 +- .../lib/acp/form/LabelGroupAddForm.class.php | 161 ++++++++---------- .../lib/acp/form/LabelGroupEditForm.class.php | 25 --- 8 files changed, 85 insertions(+), 139 deletions(-) delete mode 100644 wcfsetup/install/files/acp/templates/__labelGroupObjectTypes.tpl diff --git a/com.woltlab.wcf/acpTemplateDelete.xml b/com.woltlab.wcf/acpTemplateDelete.xml index faf5bd67a08..d837e1e60cb 100644 --- a/com.woltlab.wcf/acpTemplateDelete.xml +++ b/com.woltlab.wcf/acpTemplateDelete.xml @@ -167,5 +167,6 @@ + diff --git a/com.woltlab.wcf/templates/shared_multipleSelectionFormField.tpl b/com.woltlab.wcf/templates/shared_multipleSelectionFormField.tpl index efd25f6fad6..bce7d85783a 100644 --- a/com.woltlab.wcf/templates/shared_multipleSelectionFormField.tpl +++ b/com.woltlab.wcf/templates/shared_multipleSelectionFormField.tpl @@ -16,7 +16,7 @@
      {foreach from=$field->getNestedOptions() item=__fieldNestedOption} - 0} style="padding-left: {$__fieldNestedOption[depth]*20}px"{/if}> +
    • 0} style="padding-left: {$__fieldNestedOption[depth]*20}px"{/if}>