// ============================================================================ // Laundry Management — POS / Backend Design System // 8px grid, soft shadows, premium pill components. // ============================================================================ // ── Tokens ───────────────────────────────────────────────────────────────── $lm-space-1: 4px; $lm-space-2: 8px; $lm-space-3: 12px; $lm-space-4: 16px; $lm-space-5: 24px; $lm-radius-sm: 6px; $lm-radius-md: 10px; $lm-radius-lg: 14px; $lm-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06), 0 1px 1px rgba(15, 23, 42, 0.04); $lm-shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08), 0 2px 4px rgba(15, 23, 42, 0.05); $lm-shadow-lg: 0 10px 30px rgba(15, 23, 42, 0.12), 0 4px 8px rgba(15, 23, 42, 0.06); $lm-color-urgent: #EF4444; $lm-color-delivery: #10B981; $lm-color-vip: #8B5CF6; $lm-color-normal: #6B7280; $lm-color-type: #4F46E5; $lm-font-xs: 11px; $lm-font-sm: 13px; $lm-font-md: 14px; $lm-font-lg: 16px; $lm-font-xl: 18px; $lm-bg-card: #FFFFFF; $lm-bg-soft: #F8FAFC; $lm-border: #E2E8F0; $lm-text: #0F172A; $lm-text-muted: #64748B; // ── Pill component (shared across panel / kanban / receipt) ─────────────── .laundry-pill { display: inline-flex; align-items: center; padding: $lm-space-1 $lm-space-3; border-radius: 9999px; font-size: $lm-font-sm; font-weight: 600; line-height: 1.2; color: #fff; background-color: $lm-color-normal; box-shadow: $lm-shadow-sm; white-space: nowrap; transition: transform 0.12s ease, box-shadow 0.12s ease; &:hover { transform: translateY(-1px); box-shadow: $lm-shadow-md; } .fa { font-size: 0.95em; } &--type { background-color: $lm-color-type; } &--attr { background-color: $lm-color-normal; } &--urgent { background-color: $lm-color-urgent; } &--delivery { background-color: $lm-color-delivery; } &--vip { background-color: $lm-color-vip; } &--deferred { background-color: #F59E0B; } // Semantic data overrides — win over default --attr &[data-priority="urgent"] { background-color: $lm-color-urgent; } &[data-delivery="1"] { background-color: $lm-color-delivery; } } // ── Settle Due Banner (POS Mode indicator) ──────────────────────────────── .laundry-settle-banner { margin: $lm-space-2 $lm-space-3 0 $lm-space-3; padding: $lm-space-3 $lm-space-4; background: linear-gradient(135deg, #F59E0B 0%, #F97316 100%); color: #fff; border-radius: $lm-radius-md; box-shadow: $lm-shadow-md; display: flex; align-items: center; justify-content: space-between; gap: $lm-space-3; animation: laundry-settle-banner-pulse 2.4s ease-in-out infinite; &__lead { display: flex; align-items: center; gap: $lm-space-3; min-width: 0; } &__icon { font-size: $lm-font-xl; flex-shrink: 0; } &__text { display: flex; flex-direction: column; min-width: 0; } &__title { font-size: $lm-font-md; font-weight: 800; letter-spacing: 0.06em; line-height: 1.15; text-transform: uppercase; } &__meta { display: flex; align-items: baseline; gap: $lm-space-3; margin-top: 3px; min-width: 0; } &__partner { font-size: $lm-font-sm; font-weight: 600; opacity: 0.95; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 40ch; } &__amount { font-size: $lm-font-md; font-weight: 800; letter-spacing: 0.01em; padding: 1px 8px; background: rgba(255, 255, 255, 0.2); border-radius: $lm-radius-sm; white-space: nowrap; } &__hint { font-size: $lm-font-xs; opacity: 0.85; margin-top: 2px; font-style: italic; } &__exit { background: rgba(255, 255, 255, 0.18); color: #fff; border: 1px solid rgba(255, 255, 255, 0.4); padding: $lm-space-2 $lm-space-3; border-radius: $lm-radius-sm; font-size: $lm-font-sm; font-weight: 600; cursor: pointer; flex-shrink: 0; transition: background 0.15s ease, transform 0.12s ease; &:hover { background: rgba(255, 255, 255, 0.3); transform: translateY(-1px); } &:active { transform: translateY(0); } &:focus-visible { outline: 2px solid #fff; outline-offset: 2px; } } } @keyframes laundry-settle-banner-pulse { 0%, 100% { box-shadow: 0 4px 12px rgba(245, 158, 11, 0.35); } 50% { box-shadow: 0 4px 20px rgba(245, 158, 11, 0.65); } } // ── Global lock of POS chrome while settle-due mode is active ───────────── // The banner component toggles `pos-laundry-settle-active` on . // CSS is a clarity layer, not the safety layer — JS (pos_store_patch / // order_tabs_patch / navbar_patch) is the real gate. But here we also // disable pointer-events on the peripherals because SINGLE-ORDER MODE // guarantees the only tab is the settle order itself; everything else // is visual chrome that shouldn't respond to clicks. body.pos-laundry-settle-active { // Every non-active tab (should be zero under single-order mode — // but defense in depth): hatched, dim, unclickable. .floating-order-container .btn:not(.active) { pointer-events: none; opacity: 0.35; filter: saturate(0.3); position: relative; &::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: repeating-linear-gradient( 135deg, rgba(245, 158, 11, 0.0) 0 6px, rgba(245, 158, 11, 0.15) 6px 12px ); pointer-events: none; } } // Active settle tab — keep visible & highlighted in warning orange. .floating-order-container .btn.active { outline: 2px solid #F59E0B; outline-offset: 2px; pointer-events: none; // the tab IS the current order; no switch needed } // "+" new-order button inside ListContainer — hard-disabled visually. // JS addNewOrder patch also blocks it. .list-container-add, .o_list_container_add, button[title*="Add a new order" i], button[aria-label*="Add a new order" i] { pointer-events: none; opacity: 0.35; filter: saturate(0.3); } } // ── Order Context Panel (POS right-side card) ───────────────────────────── .laundry-context-panel { margin: $lm-space-2 $lm-space-3; padding: $lm-space-3 $lm-space-4; background: $lm-bg-card; border: 1px solid $lm-border; border-radius: $lm-radius-md; box-shadow: $lm-shadow-sm; display: flex; flex-direction: column; gap: $lm-space-2; transition: box-shadow 0.18s ease, transform 0.18s ease; &:hover { box-shadow: $lm-shadow-md; } &__header { display: flex; align-items: center; justify-content: space-between; } &__title { font-size: $lm-font-xs; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: $lm-text-muted; } &__edit { background: transparent; border: 0; color: $lm-text-muted; padding: $lm-space-1 $lm-space-2; border-radius: $lm-radius-sm; cursor: pointer; transition: background 0.12s ease, color 0.12s ease; &:hover { background: $lm-bg-soft; color: $lm-text; } &:focus-visible { outline: 2px solid rgba(99, 102, 241, 0.45); outline-offset: 2px; } } &__row { display: flex; flex-wrap: wrap; gap: $lm-space-2; } &__attrs .laundry-pill { font-size: $lm-font-xs; padding: 2px $lm-space-2; } &__empty { font-size: $lm-font-sm; color: $lm-text-muted; background: $lm-bg-soft; padding: $lm-space-2 $lm-space-3; border-radius: $lm-radius-sm; text-align: center; } &__cta { display: flex; align-items: center; justify-content: center; gap: $lm-space-2; width: 100%; padding: $lm-space-3 $lm-space-4; background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%); color: #fff; font-size: $lm-font-md; font-weight: 600; border: 0; border-radius: $lm-radius-md; box-shadow: $lm-shadow-sm; cursor: pointer; transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease; .fa { font-size: 1.05em; } &:hover { transform: translateY(-1px); box-shadow: $lm-shadow-md; filter: brightness(1.05); } &:active { transform: translateY(0); filter: brightness(0.95); } &:focus-visible { outline: 2px solid rgba(99, 102, 241, 0.55); outline-offset: 2px; } } &__edit-label { margin-inline-start: $lm-space-1; font-size: $lm-font-xs; font-weight: 600; } &__delivery { display: flex; flex-direction: column; gap: $lm-space-1; padding-top: $lm-space-2; border-top: 1px dashed $lm-border; } &__delivery-row { display: flex; align-items: center; gap: $lm-space-2; font-size: $lm-font-sm; color: $lm-text; .fa { color: $lm-color-delivery; width: 14px; text-align: center; } } &[data-empty="1"] { background: $lm-bg-soft; } &[data-delivery="1"] { border-left: 3px solid $lm-color-delivery; } } // ── Popup polish (shared by all 3 laundry popups) ───────────────────────── .modal .modal-dialog { .btn.btn-outline-primary, .btn.btn-primary { border-radius: $lm-radius-md; transition: transform 0.12s ease, box-shadow 0.12s ease, background-color 0.12s ease, border-color 0.12s ease; } .btn.btn-primary { box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18); } .btn.btn-outline-primary:hover { transform: translateY(-1px); box-shadow: $lm-shadow-md; } .modal-footer .btn-primary, .modal-footer .btn-secondary { position: sticky; bottom: 0; } } // ── Receipt details (printer-friendly) ──────────────────────────────────── .laundry-receipt-details { margin-top: $lm-space-2; text-align: center; &__sep { border-top: 1px dashed #000; margin: $lm-space-2 0; } &__title { font-weight: 700; font-size: $lm-font-md; margin-bottom: $lm-space-1; letter-spacing: 0.04em; } &__row { display: flex; justify-content: space-between; gap: $lm-space-3; font-size: $lm-font-sm; margin: 2px 0; } &__label { font-weight: 600; color: #000; min-width: 80px; text-align: start; } &__value { text-align: end; flex: 1; } &__chip { display: inline-block; padding: 0 $lm-space-2; margin: 0 2px; border: 1px solid #000; border-radius: $lm-radius-sm; font-size: $lm-font-xs; } } // RTL safety [dir="rtl"] .laundry-receipt-details__label { text-align: end; } [dir="rtl"] .laundry-receipt-details__value { text-align: start; } // ── Operational Control Board (laundry.order kanban) ────────────────────── .laundry-board { .laundry-board__card { position: relative; background: $lm-bg-card; border: 1px solid $lm-border; border-radius: $lm-radius-md; box-shadow: $lm-shadow-sm; padding: $lm-space-3 $lm-space-4 $lm-space-3 ($lm-space-4 + 4px); display: flex; flex-direction: column; gap: $lm-space-2; transition: box-shadow 0.18s ease, transform 0.18s ease; &:hover { box-shadow: $lm-shadow-md; transform: translateY(-1px); } } .laundry-board__strip { position: absolute; left: 0; top: 0; bottom: 0; width: 4px; border-top-left-radius: $lm-radius-md; border-bottom-left-radius: $lm-radius-md; background: $lm-color-normal; } .laundry-board__card[data-state="intake"] .laundry-board__strip { background: #3B82F6; } .laundry-board__card[data-state="processing"] .laundry-board__strip { background: #F59E0B; } .laundry-board__card[data-state="ready"] .laundry-board__strip { background: $lm-color-delivery; } .laundry-board__card[data-state="delivered"] .laundry-board__strip { background: $lm-color-normal; } .laundry-board__card[data-priority="urgent"] .laundry-board__strip { background: $lm-color-urgent; } .laundry-board__card[data-priority="urgent"] { border-color: rgba(239, 68, 68, 0.35); } .laundry-board__head { display: flex; flex-direction: column; gap: 2px; } .laundry-board__title-row { display: flex; align-items: center; justify-content: space-between; gap: $lm-space-2; } .laundry-board__name { font-size: $lm-font-md; color: $lm-text; } .laundry-board__total { font-size: $lm-font-md; font-weight: 700; color: $lm-text; } .laundry-board__customer { font-size: $lm-font-sm; color: $lm-text-muted; display: flex; align-items: center; } .laundry-board__badges { display: flex; flex-wrap: wrap; gap: $lm-space-1; } .laundry-board__meta { display: flex; flex-wrap: wrap; gap: $lm-space-3; font-size: $lm-font-xs; color: $lm-text-muted; } .laundry-board__due { color: $lm-color-urgent; font-weight: 700; } .laundry-board__actions { margin-top: $lm-space-1; } } // ── ProductConfiguratorPopup — laundry enhancement ───────────────────── // Scoped exclusively under `.popup-product-configurator.laundry-enhanced`, // applied to the modal-content via Dialog's `contentClass` prop from // xml/product_configurator_popup.xml. Non-laundry configurator popups are // unaffected. .popup-product-configurator.laundry-enhanced { // Attribute group — stronger title, better spacing. .modal-body .attribute { margin-bottom: $lm-space-4 !important; padding-bottom: $lm-space-3; border-bottom: 1px dashed $lm-border; &:last-child { margin-bottom: 0 !important; padding-bottom: 0; border-bottom: 0; } .attribute_name { font-size: $lm-font-sm; font-weight: 800 !important; text-transform: uppercase; letter-spacing: 0.09em; color: $lm-text-muted; margin-bottom: $lm-space-3 !important; } } // Touch-first tiles — applies to both Radio and Pills renderers, // which share the same `.configurator_radio > .attribute-name-cell` // structure in core. .configurator_radio { > .d-flex { flex-wrap: wrap !important; gap: $lm-space-3 !important; } .attribute-name-cell { flex: 1 1 calc(33% - #{$lm-space-3}); min-width: 140px; margin: 0; padding: 0; // Hide the native radio — the label IS the tile. .form-check-input, .radio-check { position: absolute !important; opacity: 0 !important; pointer-events: none !important; width: 0 !important; height: 0 !important; } > label { display: flex !important; align-items: center; justify-content: center; flex-wrap: wrap; gap: $lm-space-2; width: 100%; min-height: 64px; padding: 12px 16px !important; border: 2px solid $lm-border !important; border-radius: 12px !important; background: #fff !important; color: $lm-text !important; font-size: $lm-font-md; font-weight: 700; line-height: 1.25; cursor: pointer; position: relative; box-shadow: none !important; transition: transform 0.14s ease, border-color 0.14s ease, box-shadow 0.16s ease, background 0.14s ease, color 0.14s ease; &:hover { transform: translateY(-1px); box-shadow: $lm-shadow-sm !important; } &:active { transform: translateY(0); } &:focus-visible { outline: 2px solid rgba(79, 70, 229, 0.45); outline-offset: 2px; } // Wrap text span so conflict styling still readable. > span:first-child { word-break: break-word; } } } // Selected state: // - Pills renderer: core toggles `.active` on the