resetbutton & almanach edit start

This commit is contained in:
Simon Martens
2025-05-30 19:30:50 +02:00
parent d8e50b27b0
commit 8ea36da40f
28 changed files with 1789 additions and 763 deletions

276
views/transform/form.css Normal file
View File

@@ -0,0 +1,276 @@
@layer components {
.dbform .inputwrapper {
@apply rounded-xs border-2 border-transparent px-3
py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600
bg-slate-200 focus-within:bg-slate-100 transition-all duration-100;
}
.dbform .inputwrapper .inputlabel {
@apply text-sm text-gray-700 font-bold;
}
.inputlabeltext {
@apply text-sm text-gray-700 font-bold;
}
.dbform .inputwrapper .inputselect {
@apply mt-1 block w-full rounded-md focus:border-none focus:outline-none
disabled:opacity-50;
}
.dbform .inputwrapper .inputinput {
@apply mt-1 block w-full focus:border-none focus:outline-none;
}
.dbform .submitbutton {
@apply w-full inline-flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-slate-700 hover:bg-slate-800 cursor-pointer focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 active:bg-slate-900 transition-all duration-75;
}
.dbform .resetbutton {
@apply w-full inline-flex justify-center py-2 px-4 border border-transparent rounded-md text-sm font-medium text-gray-800 bg-stone-200 hover:bg-stone-300 cursor-pointer focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 no-underline;
}
/* Multi-Select-Role example styles */
.msr-selected-items-container {
@apply rounded-md;
}
.msr-placeholder-no-selection-text {
@apply text-sm text-gray-500 italic px-2 py-1;
}
.msr-input-area-wrapper {
@apply p-2 rounded-md;
}
.msr-input-area-wrapper.msr-input-area-default-border {
@apply border border-gray-300;
}
.msr-input-area-wrapper.msr-input-area-default-border:focus-within {
@apply focus-within:border-gray-500 focus-within:ring-1 focus-within:ring-gray-400;
}
.msr-input-area-wrapper.msr-input-area-staged {
@apply border border-transparent;
}
.msr-text-input {
@apply bg-transparent text-sm placeholder-gray-400;
}
.msr-selected-item-pill {
@apply bg-gray-200 text-gray-700 px-3 py-[0.3rem] rounded-md text-sm inline-flex items-center m-0.5;
}
.msr-item-name {
@apply font-medium;
}
.msr-item-additional-data {
@apply text-xs ml-1 text-gray-600;
}
.msr-selected-item-role {
@apply font-semibold text-xs ml-1 text-gray-800;
}
.msr-selected-item-delete-btn {
@apply bg-transparent border-none text-gray-500 text-lg leading-none px-1 cursor-pointer opacity-60 transition-opacity duration-200;
}
.msr-selected-item-delete-btn:hover {
@apply hover:opacity-100 hover:text-gray-900;
}
.msr-staged-item-pill {
@apply bg-gray-100 text-gray-800 px-2 py-1 rounded-md text-sm font-medium;
}
.msr-staged-item-text {
@apply mr-2;
}
.msr-staged-role-select {
@apply px-2 py-1 text-sm rounded-md border border-gray-300 bg-white outline-none text-gray-700;
}
.msr-staged-role-select:focus {
@apply focus:border-gray-500 focus:ring-1 focus:ring-gray-400;
}
.msr-staged-cancel-btn {
@apply w-5 h-5 bg-gray-200 text-gray-600 rounded-full text-sm leading-none cursor-pointer;
}
.msr-staged-cancel-btn:hover {
@apply hover:bg-gray-300 hover:text-gray-800;
}
.msr-pre-add-button {
@apply w-10 h-[42px] text-xl rounded-md bg-gray-50 text-gray-700 border border-gray-300 font-semibold outline-none;
}
.msr-pre-add-button:focus {
@apply focus:border-gray-500 focus:ring-1 focus:ring-gray-400;
}
.msr-pre-add-button:hover {
@apply hover:bg-gray-100;
}
.msr-pre-add-button:disabled {
@apply disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed disabled:border-gray-200;
}
.msr-pre-add-button.hidden {
@apply hidden;
}
.msr-add-button {
@apply px-4 py-2 text-sm rounded-md bg-gray-600 text-white font-medium;
}
.msr-add-button:hover {
@apply hover:bg-gray-700;
}
.msr-add-button:disabled {
@apply disabled:bg-gray-300 disabled:cursor-not-allowed;
}
.msr-add-button.hidden {
@apply hidden;
}
.msr-options-list {
@apply bg-white border border-gray-300 rounded-md shadow-md;
}
.msr-options-list.hidden {
@apply hidden;
}
.msr-option-item {
@apply px-3 py-2 text-sm cursor-pointer transition-colors duration-75;
}
.msr-option-item:hover {
@apply bg-gray-100 text-gray-800;
}
.msr-option-item-highlighted {
@apply bg-gray-100 text-gray-800;
}
.msr-option-item-name {
@apply font-medium;
}
.msr-option-item-detail {
@apply text-xs ml-2 text-gray-500;
}
.msr-option-item-highlighted .msr-option-item-detail,
.msr-option-item:hover .msr-option-item-detail {
/* Ensure detail text color changes on hover too */
@apply text-gray-600;
}
multi-select-role[disabled] {
/* This remains standard CSS as Tailwind's disabled: variant is for native elements */
opacity: 0.6;
cursor: not-allowed;
}
.msr-hidden-select {
/* No specific styling needed as it's visually hidden by JS/inline style */
}
/* --- MultiSelectSimple Component Base Styles (using @apply) --- */
.mss-component-wrapper {
/* 'relative' is set inline for positioning dropdown */
}
.mss-selected-items-container {
@apply border border-gray-300 p-1.5 rounded;
/* Tailwind classes from component: flex flex-wrap gap-1 mb-1 min-h-[38px] */
}
.mss-no-items-text {
@apply italic text-xs text-gray-500 p-1 w-full; /* Adjusted font size slightly to match 'xs' */
}
.mss-selected-item-pill {
@apply bg-gray-200 text-gray-800 py-0.5 px-2 rounded text-xs leading-5; /* Adjusted font size and padding */
/* Tailwind classes from component: flex items-center */
}
.mss-selected-item-text {
/* Base styles for text part of the pill */
}
.mss-selected-item-pill-detail {
@apply ml-1 opacity-75 text-xs text-gray-600;
}
.mss-selected-item-pill-detail.hidden {
@apply hidden;
}
.mss-selected-item-delete-btn {
@apply bg-transparent border-none text-gray-600 opacity-70 cursor-pointer ml-1 text-base leading-none align-middle hover:opacity-100 hover:text-gray-900 disabled:opacity-40 disabled:cursor-not-allowed;
}
.mss-input-controls-container {
/* Tailwind classes from component: flex items-center space-x-2 */
}
.mss-input-wrapper {
@apply border border-gray-300 rounded;
/* Tailwind classes from component: relative flex items-center flex-grow */
}
.mss-input-wrapper-focused {
@apply border-indigo-600 ring-1 ring-indigo-600; /* Using ring for focus shadow */
}
.mss-text-input {
@apply py-1.5 px-2 text-sm;
/* Tailwind classes from component: w-full outline-none bg-transparent */
}
.mss-text-input::placeholder {
@apply text-gray-400 italic;
}
.mss-create-new-button {
@apply bg-gray-100 text-gray-700 border border-gray-300 py-1 px-1.5 text-sm rounded hover:bg-gray-200 hover:border-gray-400 disabled:bg-gray-50 disabled:text-gray-400 disabled:border-gray-200 disabled:opacity-70 disabled:cursor-not-allowed;
}
.mss-create-new-button.hidden {
@apply !hidden; /* Ensure it hides */
}
.mss-options-list {
@apply bg-white border border-gray-300 rounded shadow-md; /* Using shadow-md as a softer default */
/* Tailwind classes from component: absolute z-20 w-full max-h-60 overflow-y-auto mt-1 hidden */
}
.mss-option-item {
@apply text-gray-700 py-1.5 px-2.5 text-sm cursor-pointer transition-colors duration-75 hover:bg-gray-100;
}
.mss-option-item-name {
@apply font-medium;
}
.mss-option-item-detail {
@apply text-gray-500 text-xs ml-1.5;
}
.mss-option-item-highlighted {
@apply bg-indigo-100 text-indigo-800;
}
.mss-option-item-highlighted .mss-option-item-name {
/* @apply font-medium; */ /* Already set by .mss-option-item-name, inherit color from parent */
}
.mss-option-item-highlighted .mss-option-item-detail {
@apply text-indigo-700;
}
.mss-hidden-select {
/* Styles are inline in _render for !important, no change needed here */
}
multi-select-simple[disabled] {
@apply opacity-60; /* Adjusted opacity */
}
multi-select-simple[disabled] .mss-selected-items-container {
@apply bg-gray-100;
}
multi-select-simple[disabled] .mss-selected-item-pill {
@apply bg-gray-300 text-gray-500;
}
multi-select-simple[disabled] .mss-selected-item-delete-btn {
@apply text-gray-400;
}
.rbi-button {
@apply disabled:hidden;
}
select + reset-button .rbi-button {
@apply ml-3;
}
}

View File

@@ -12,6 +12,7 @@ import { IntLink } from "./int-link.js";
import { ImageReel } from "./image-reel.js";
import { MultiSelectRole } from "./multi-select-role.js";
import { MultiSelectSimple } from "./multi-select-simple.js";
import { ResetButton } from "./reset-button.js";
const FILTER_LIST_ELEMENT = "filter-list";
const SCROLL_BUTTON_ELEMENT = "scroll-button";
@@ -24,6 +25,7 @@ const FILTER_PILL_ELEMENT = "filter-pill";
const IMAGE_REEL_ELEMENT = "image-reel";
const MULTI_SELECT_ROLE_ELEMENT = "multi-select-places";
const MULTI_SELECT_SIMPLE_ELEMENT = "multi-select-simple";
const RESET_BUTTON_ELEMENT = "reset-button";
customElements.define(INT_LINK_ELEMENT, IntLink);
customElements.define(ABBREV_TOOLTIPS_ELEMENT, AbbreviationTooltips);
@@ -36,5 +38,118 @@ customElements.define(FILTER_PILL_ELEMENT, FilterPill);
customElements.define(IMAGE_REEL_ELEMENT, ImageReel);
customElements.define(MULTI_SELECT_ROLE_ELEMENT, MultiSelectRole);
customElements.define(MULTI_SELECT_SIMPLE_ELEMENT, MultiSelectSimple);
customElements.define(RESET_BUTTON_ELEMENT, ResetButton);
export { FilterList, ScrollButton, AbbreviationTooltips };
function PathPlusQuery() {
const path = window.location.pathname;
const query = window.location.search;
const fullPath = path + query;
return encodeURIComponent(fullPath);
}
/**
* @param {number} timeout - Maximum time to wait in milliseconds.
* @param {number} interval - How often to check in milliseconds.
* @returns {Promise<Function>} Resolves with the QRCode constructor when available.
*/
function getQRCodeWhenAvailable(timeout = 5000, interval = 100) {
return new Promise((resolve, reject) => {
let elapsedTime = 0;
const checkInterval = setInterval(() => {
if (typeof window.QRCode === "function") {
clearInterval(checkInterval);
resolve(window.QRCode); // Resolve with the QRCode object/function
} else {
elapsedTime += interval;
if (elapsedTime >= timeout) {
clearInterval(checkInterval);
console.error("Timed out waiting for QRCode to become available.");
reject(new Error("QRCode not available after " + timeout + "ms. Check if qrcode.min.js is loaded correctly and sets window.QRCode."));
}
}
}, interval);
});
}
// INFO: We have to wait for the QRCode object to be available. It's messy.
async function GenQRCode(value) {
const QRCode = await getQRCodeWhenAvailable();
const qrElement = document.getElementById("qr");
if (qrElement) {
// INFO: Clear previous QR code if any
// Also hide it initially to prevent flickering
qrElement.innerHTML = "";
qrElement.classList.add("hidden");
new QRCode(qrElement, {
text: value,
width: 1280,
height: 1280,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H,
});
setTimeout(() => {
qrElement.classList.remove("hidden");
}, 20);
}
// Add event listeners to the token input field to select its content on focus or click
}
function SelectableInput(tokenElement) {
if (tokenElement) {
tokenElement.addEventListener("focus", (ev) => {
ev.preventDefault();
tokenElement.select();
});
tokenElement.addEventListener("mousedown", (ev) => {
ev.preventDefault();
tokenElement.select();
});
tokenElement.addEventListener("mouseup", (ev) => {
ev.preventDefault();
tokenElement.select();
});
}
if (tokenElement) {
tokenElement.addEventListener("focus", () => {
tokenElement.select();
});
tokenElement.addEventListener("click", () => {
tokenElement.select();
});
}
}
// TODO: Doesn't work properly.
// Intended to make sure errors from boosted links are shown.
function ShowBoostedErrors() {
document.body.addEventListener("htmx:responseError", function (event) {
const config = event.detail.requestConfig;
if (config.boosted) {
document.body.innerHTML = event.detail.xhr.responseText;
const newUrl = event.detail.xhr.responseURL || config.url;
window.history.pushState(null, "", newUrl);
}
});
}
function FormHasChanged(form) {
if (!form || !(form instanceof HTMLFormElement)) {
return false;
}
const resetButton = form.querySelector("reset-button");
if (resetButton && resetButton.changed) {
return true;
}
return false;
}
window.ShowBoostedErrors = ShowBoostedErrors;
window.GenQRCode = GenQRCode;
window.SelectableInput = SelectableInput;
window.PathPlusQuery = PathPlusQuery;
window.FormHasChanged = FormHasChanged;
export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink };

View File

@@ -0,0 +1,263 @@
const RBI_BUTTON_BASE_CLASS = "rbi-button";
const RBI_ICON_CLASS = "rbi-icon";
export class ResetButton extends HTMLElement {
constructor() {
super();
this.initialStates = new Map();
this._controlledElements = [];
this.button = null;
this.changed = false;
this.handleInputChange = this.handleInputChange.bind(this);
this.handleReset = this.handleReset.bind(this);
}
static get observedAttributes() {
return ["controls", "wrapper-class", "modified-class-suffix", "button-aria-label"];
}
connectedCallback() {
// Use an HTML template literal string to define the button's structure
const buttonHTML = `
<button type="button" class="${RBI_BUTTON_BASE_CLASS} cursor-pointer disabled:cursor-default" aria-label="Reset field">
<tool-tip position="right">
<div class="data-tip">Feld zurücksetzen</div>
<span class="${RBI_ICON_CLASS} ri-arrow-go-back-fill"></span>
</tool-tip>
</button>
`;
this.innerHTML = buttonHTML; // Set the inner HTML of the custom element
this.button = this.querySelector("button"); // Get the button element
if (this.button) {
this.button.addEventListener("click", this.handleReset);
} else {
// This case should ideally not be reached if the HTML string is correct
console.error("ResetButtonIndividual: Button element not found after setting innerHTML.");
}
this.updateControlledElements();
this.updateButtonAriaLabel();
}
disconnectedCallback() {
if (this.button) {
this.button.removeEventListener("click", this.handleReset);
}
this._controlledElements.forEach((el) => {
el.removeEventListener("input", this.handleInputChange);
el.removeEventListener("change", this.handleInputChange);
});
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === "controls") {
this.updateControlledElements();
}
if (name === "controls" || name === "button-aria-label") {
this.updateButtonAriaLabel();
}
}
updateControlledElements() {
this._controlledElements.forEach((el) => {
el.removeEventListener("input", this.handleInputChange);
el.removeEventListener("change", this.handleInputChange);
});
this._controlledElements = [];
this.initialStates.clear();
const controlIds = (this.getAttribute("controls") || "")
.split(",")
.map((id) => id.trim())
.filter((id) => id);
if (!controlIds.length && this.button) {
this.button.disabled = true;
this.button.setAttribute("aria-disabled", "true");
return;
}
const foundElements = [];
controlIds.forEach((id) => {
const element = document.getElementById(id);
if (element) {
foundElements.push(element);
this.storeInitialState(element);
element.addEventListener("input", this.handleInputChange);
element.addEventListener("change", this.handleInputChange);
} else {
console.warn(`ResetButtonIndividual: Element with ID "${id}" not found.`);
}
});
this._controlledElements = foundElements;
if (this.button) {
this.button.disabled = this._controlledElements.length === 0;
this.button.setAttribute("aria-controls", this._controlledElements.map((el) => el.id).join(" "));
if (this.button.disabled) {
this.button.setAttribute("aria-disabled", "true");
} else {
this.button.removeAttribute("aria-disabled");
}
}
this.checkIfModified();
}
storeInitialState(element) {
let state;
switch (element.type) {
case "checkbox":
case "radio":
state = { checked: element.checked };
break;
case "select-multiple":
state = {
selectedOptions: Array.from(element.options)
.filter((o) => o.selected)
.map((o) => o.value),
};
break;
case "select-one":
default:
state = { value: element.value };
break;
}
this.initialStates.set(element.id, state);
}
resetElement(element) {
const initialState = this.initialStates.get(element.id);
if (!initialState) return;
switch (element.type) {
case "checkbox":
case "radio":
element.checked = initialState.checked;
break;
case "select-multiple":
Array.from(element.options).forEach((option) => {
option.selected = initialState.selectedOptions.includes(option.value);
});
break;
case "select-one":
default:
element.value = initialState.value;
break;
}
element.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
element.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
}
handleReset() {
this._controlledElements.forEach((el) => {
this.resetElement(el);
});
this.checkIfModified();
}
handleInputChange(event) {
if (this._controlledElements.includes(event.target)) {
this.checkIfModified();
}
}
isElementModified(element) {
const initialState = this.initialStates.get(element.id);
if (!initialState) return false;
switch (element.type) {
case "checkbox":
case "radio":
return element.checked !== initialState.checked;
case "select-multiple":
const currentSelected = Array.from(element.options)
.filter((o) => o.selected)
.map((o) => o.value);
const initialSelected = initialState.selectedOptions;
return currentSelected.length !== initialSelected.length || currentSelected.some((val) => !initialSelected.includes(val)) || initialSelected.some((val) => !currentSelected.includes(val));
case "select-one":
default:
return element.value !== initialState.value;
}
}
checkIfModified() {
let overallModified = false;
this._controlledElements.forEach((el) => {
const modified = this.isElementModified(el);
if (modified) {
overallModified = true;
el.classList.add("modified-element");
} else {
el.classList.remove("modified-element");
}
});
const wrapperClass = this.getAttribute("wrapper-class");
if (wrapperClass) {
const wrapperElement = this.closest(`.${wrapperClass}`);
if (wrapperElement) {
const modifiedSuffix = this.getAttribute("modified-class-suffix") || "modified";
const modifiedWrapperClassName = `${wrapperClass}-${modifiedSuffix}`;
if (overallModified) {
wrapperElement.classList.add(modifiedWrapperClassName);
} else {
wrapperElement.classList.remove(modifiedWrapperClassName);
}
}
}
if (this.button) {
this.button.disabled = !overallModified || this._controlledElements.length === 0;
if (this.button.disabled) {
this.button.setAttribute("aria-disabled", "true");
} else {
this.button.removeAttribute("aria-disabled");
}
}
if (this.changed !== overallModified) {
const event = new CustomEvent("rbichange", {
bubbles: true,
composed: true,
detail: {
modified: overallModified,
controlledElementIds: this._controlledElements.map((el) => el.id),
instance: this,
},
});
this.dispatchEvent(event);
this.changed = overallModified;
}
}
updateButtonAriaLabel() {
if (!this.button) return;
let labelText = this.getAttribute("button-aria-label");
if (!labelText) {
const controlIds = this._controlledElements.map((el) => el.id);
if (controlIds.length === 1 && this._controlledElements[0]) {
const controlledEl = this._controlledElements[0];
const labelEl = document.querySelector(`label[for="${controlledEl.id}"]`);
let fieldName = controlledEl.name || controlledEl.id;
if (labelEl && labelEl.textContent) {
fieldName = labelEl.textContent.trim().replace(/[:*]$/, "").trim();
} else if (controlledEl.getAttribute("aria-label")) {
fieldName = controlledEl.getAttribute("aria-label");
}
labelText = `Reset ${fieldName}`;
} else if (controlIds.length > 1) {
labelText = "Reset selected fields";
} else {
labelText = "Reset field";
}
}
this.button.setAttribute("aria-label", labelText);
}
}

View File

@@ -1,4 +1,7 @@
@import "tailwindcss";
@import "./form.css";
@import "./usermgmt.css";
@theme {
--font-script: Rancho, ui-serif;
--font-sans: "Source Sans 3", "Merriweather Sans", ui-sans-serif;
@@ -516,55 +519,6 @@
@apply !text-slate-900 bg-stone-50;
}
.user-chooser a {
@apply px-4 py-2 no-underline text-gray-500 hover:text-slate-900 font-serif font-bold border-l-4 border-transparent hover:bg-slate-100 transition-all duration-75 rounded-xs;
}
.user-chooser a[aria-current="page"] {
@apply text-slate-900 font-bold bg-slate-100 border-slate-900 shadow-sm;
}
.user-chooser a[aria-current="page"]:nth-child(1) {
@apply border-blue-500;
}
.user-chooser a[aria-current="page"]:nth-child(2) {
@apply border-orange-600;
}
.user-chooser a[aria-current="page"]:nth-child(3) {
@apply border-red-600;
}
.user-mgmt thead th {
@apply text-left font-bold border-b-2 border-slate-800 py-2 px-3;
}
.user-mgmt tbody tr td {
@apply px-3 py-1.5;
}
.user-mgmt tbody tr:nth-child(odd) td {
@apply bg-slate-100;
}
.user-mgmt tbody tr:nth-child(even) td {
@apply bg-slate-50;
}
.user-mgmt tbody tr td:last-of-type {
@apply align-middle text-right pr-4;
}
.user-mgmt tbody tr.deactivated td:not(:last-of-type) {
@apply text-gray-400 line-through;
}
.user-mgmt form button,
.user-mgmt .edit-button {
@apply bg-slate-700 text-gray-200 text-base rounded-xs font-sans transition-all duration-75 px-3 py-1.5 hover:bg-slate-800 hover:text-white cursor-pointer;
}
@keyframes spin {
0% {
transform: rotate(0deg);
@@ -576,240 +530,4 @@
transform: rotate(360deg);
} /* Pause at the final position */
}
/* Multi-Select-Role example styles */
.msr-selected-items-container {
@apply rounded-md;
}
.msr-placeholder-no-selection-text {
@apply text-sm text-gray-500 italic px-2 py-1;
}
.msr-input-area-wrapper {
@apply p-2 rounded-md;
}
.msr-input-area-wrapper.msr-input-area-default-border {
@apply border border-gray-300;
}
.msr-input-area-wrapper.msr-input-area-default-border:focus-within {
@apply focus-within:border-gray-500 focus-within:ring-1 focus-within:ring-gray-400;
}
.msr-input-area-wrapper.msr-input-area-staged {
@apply border border-transparent;
}
.msr-text-input {
@apply bg-transparent text-sm placeholder-gray-400;
}
.msr-selected-item-pill {
@apply bg-gray-200 text-gray-700 px-3 py-[0.3rem] rounded-md text-sm inline-flex items-center m-0.5;
}
.msr-item-name {
@apply font-medium;
}
.msr-item-additional-data {
@apply text-xs ml-1 text-gray-600;
}
.msr-selected-item-role {
@apply font-semibold text-xs ml-1 text-gray-800;
}
.msr-selected-item-delete-btn {
@apply bg-transparent border-none text-gray-500 text-lg leading-none px-1 cursor-pointer opacity-60 transition-opacity duration-200;
}
.msr-selected-item-delete-btn:hover {
@apply hover:opacity-100 hover:text-gray-900;
}
.msr-staged-item-pill {
@apply bg-gray-100 text-gray-800 px-2 py-1 rounded-md text-sm font-medium;
}
.msr-staged-item-text {
@apply mr-2;
}
.msr-staged-role-select {
@apply px-2 py-1 text-sm rounded-md border border-gray-300 bg-white outline-none text-gray-700;
}
.msr-staged-role-select:focus {
@apply focus:border-gray-500 focus:ring-1 focus:ring-gray-400;
}
.msr-staged-cancel-btn {
@apply w-5 h-5 bg-gray-200 text-gray-600 rounded-full text-sm leading-none cursor-pointer;
}
.msr-staged-cancel-btn:hover {
@apply hover:bg-gray-300 hover:text-gray-800;
}
.msr-pre-add-button {
@apply w-10 h-[42px] text-xl rounded-md bg-gray-50 text-gray-700 border border-gray-300 font-semibold outline-none;
}
.msr-pre-add-button:focus {
@apply focus:border-gray-500 focus:ring-1 focus:ring-gray-400;
}
.msr-pre-add-button:hover {
@apply hover:bg-gray-100;
}
.msr-pre-add-button:disabled {
@apply disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed disabled:border-gray-200;
}
.msr-pre-add-button.hidden {
@apply hidden;
}
.msr-add-button {
@apply px-4 py-2 text-sm rounded-md bg-gray-600 text-white font-medium;
}
.msr-add-button:hover {
@apply hover:bg-gray-700;
}
.msr-add-button:disabled {
@apply disabled:bg-gray-300 disabled:cursor-not-allowed;
}
.msr-add-button.hidden {
@apply hidden;
}
.msr-options-list {
@apply bg-white border border-gray-300 rounded-md shadow-md;
}
.msr-options-list.hidden {
@apply hidden;
}
.msr-option-item {
@apply px-3 py-2 text-sm cursor-pointer transition-colors duration-75;
}
.msr-option-item:hover {
@apply bg-gray-100 text-gray-800;
}
.msr-option-item-highlighted {
@apply bg-gray-100 text-gray-800;
}
.msr-option-item-name {
@apply font-medium;
}
.msr-option-item-detail {
@apply text-xs ml-2 text-gray-500;
}
.msr-option-item-highlighted .msr-option-item-detail,
.msr-option-item:hover .msr-option-item-detail {
/* Ensure detail text color changes on hover too */
@apply text-gray-600;
}
multi-select-role[disabled] {
/* This remains standard CSS as Tailwind's disabled: variant is for native elements */
opacity: 0.6;
cursor: not-allowed;
}
.msr-hidden-select {
/* No specific styling needed as it's visually hidden by JS/inline style */
}
/* --- MultiSelectSimple Component Base Styles (using @apply) --- */
.mss-component-wrapper {
/* 'relative' is set inline for positioning dropdown */
}
.mss-selected-items-container {
@apply border border-gray-300 p-1.5 rounded;
/* Tailwind classes from component: flex flex-wrap gap-1 mb-1 min-h-[38px] */
}
.mss-no-items-text {
@apply italic text-xs text-gray-500 p-1 w-full; /* Adjusted font size slightly to match 'xs' */
}
.mss-selected-item-pill {
@apply bg-gray-200 text-gray-800 py-0.5 px-2 rounded text-xs leading-5; /* Adjusted font size and padding */
/* Tailwind classes from component: flex items-center */
}
.mss-selected-item-text {
/* Base styles for text part of the pill */
}
.mss-selected-item-pill-detail {
@apply ml-1 opacity-75 text-xs text-gray-600;
}
.mss-selected-item-pill-detail.hidden {
@apply hidden;
}
.mss-selected-item-delete-btn {
@apply bg-transparent border-none text-gray-600 opacity-70 cursor-pointer ml-1 text-base leading-none align-middle hover:opacity-100 hover:text-gray-900 disabled:opacity-40 disabled:cursor-not-allowed;
}
.mss-input-controls-container {
/* Tailwind classes from component: flex items-center space-x-2 */
}
.mss-input-wrapper {
@apply border border-gray-300 rounded;
/* Tailwind classes from component: relative flex items-center flex-grow */
}
.mss-input-wrapper-focused {
@apply border-indigo-600 ring-1 ring-indigo-600; /* Using ring for focus shadow */
}
.mss-text-input {
@apply py-1.5 px-2 text-sm;
/* Tailwind classes from component: w-full outline-none bg-transparent */
}
.mss-text-input::placeholder {
@apply text-gray-400 italic;
}
.mss-create-new-button {
@apply bg-gray-100 text-gray-700 border border-gray-300 py-1 px-1.5 text-sm rounded hover:bg-gray-200 hover:border-gray-400 disabled:bg-gray-50 disabled:text-gray-400 disabled:border-gray-200 disabled:opacity-70 disabled:cursor-not-allowed;
}
.mss-create-new-button.hidden {
@apply !hidden; /* Ensure it hides */
}
.mss-options-list {
@apply bg-white border border-gray-300 rounded shadow-md; /* Using shadow-md as a softer default */
/* Tailwind classes from component: absolute z-20 w-full max-h-60 overflow-y-auto mt-1 hidden */
}
.mss-option-item {
@apply text-gray-700 py-1.5 px-2.5 text-sm cursor-pointer transition-colors duration-75 hover:bg-gray-100;
}
.mss-option-item-name {
@apply font-medium;
}
.mss-option-item-detail {
@apply text-gray-500 text-xs ml-1.5;
}
.mss-option-item-highlighted {
@apply bg-indigo-100 text-indigo-800;
}
.mss-option-item-highlighted .mss-option-item-name {
/* @apply font-medium; */ /* Already set by .mss-option-item-name, inherit color from parent */
}
.mss-option-item-highlighted .mss-option-item-detail {
@apply text-indigo-700;
}
.mss-hidden-select {
/* Styles are inline in _render for !important, no change needed here */
}
multi-select-simple[disabled] {
@apply opacity-60; /* Adjusted opacity */
}
multi-select-simple[disabled] .mss-selected-items-container {
@apply bg-gray-100;
}
multi-select-simple[disabled] .mss-selected-item-pill {
@apply bg-gray-300 text-gray-500;
}
multi-select-simple[disabled] .mss-selected-item-delete-btn {
@apply text-gray-400;
}
}

View File

@@ -7,6 +7,7 @@ export class TabList extends HTMLElement {
this.shown = -1;
this._headings = [];
this._contents = [];
this._checkbox = null;
}
connectedCallback() {
@@ -38,6 +39,19 @@ export class TabList extends HTMLElement {
});
}
hookupShowAll(checkbox) {
if (checkbox) {
this._checkbox = checkbox;
checkbox.addEventListener("change", (event) => {
if (event.target.checked) {
this.showAll();
} else {
this.default();
}
});
}
}
hookupEvtHandlers() {
for (let heading of this._headings) {
heading.addEventListener("click", this.handleTabClick.bind(this));

View File

@@ -0,0 +1,50 @@
@layer components {
.user-chooser a {
@apply px-4 py-2 no-underline text-gray-500 hover:text-slate-900 font-serif font-bold border-l-4 border-transparent hover:bg-slate-100 transition-all duration-75 rounded-xs;
}
.user-chooser a[aria-current="page"] {
@apply text-slate-900 font-bold bg-slate-100 border-slate-900 shadow-sm;
}
.user-chooser a[aria-current="page"]:nth-child(1) {
@apply border-blue-500;
}
.user-chooser a[aria-current="page"]:nth-child(2) {
@apply border-orange-600;
}
.user-chooser a[aria-current="page"]:nth-child(3) {
@apply border-red-600;
}
.user-mgmt thead th {
@apply text-left font-bold border-b-2 border-slate-800 py-2 px-3;
}
.user-mgmt tbody tr td {
@apply px-3 py-1.5;
}
.user-mgmt tbody tr:nth-child(odd) td {
@apply bg-slate-100;
}
.user-mgmt tbody tr:nth-child(even) td {
@apply bg-slate-50;
}
.user-mgmt tbody tr td:last-of-type {
@apply align-middle text-right pr-4;
}
.user-mgmt tbody tr.deactivated td:not(:last-of-type) {
@apply text-gray-400 line-through;
}
.user-mgmt form button,
.user-mgmt .edit-button {
@apply bg-slate-700 text-gray-200 text-base rounded-xs font-sans transition-all duration-75 px-3 py-1.5 hover:bg-slate-800 hover:text-white cursor-pointer;
}
}