UNPKG

@llselect/core

Version:

Low-level select: a minimal, flexible, framework-agnostic replacement for the native HTML select element.

333 lines (281 loc) • 9.65 kB
/* llselect - Bootstrap 4 theme. BS4 has no CSS custom properties (Sass-only theming), so all values are hard-coded to match BS4 defaults. */ .llselect-root { position: relative; display: inline-block; min-width: 14rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 1rem; color: #495057; line-height: 1.5; /* BS4's reboot hardcodes body { text-align: left } (BS4 ships no RTL support), which would pin the widget's text left inside dir="rtl". `start` re-follows the widget's own direction; identical in LTR. BS3 sets nothing and BS5 leaves --bs-body-text-align undefined, so only this theme needs it. */ text-align: start; } /* --- trigger (matches BS4 .form-control look) ---------------- */ .llselect-trigger { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; background-color: #fff; color: #495057; cursor: pointer; user-select: none; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } .llselect-trigger:focus, .llselect-trigger:focus-visible, .llselect-trigger[data-state='open'] { outline: 0; border-color: #80bdff; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .llselect-trigger-content { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .llselect-trigger[data-empty='true'] .llselect-trigger-content { color: #6c757d; } .llselect-trigger-arrow { display: inline-flex; align-items: center; color: #6c757d; transition: transform 0.15s ease-in-out; } .llselect-trigger-arrow:empty { display: none; } .llselect-trigger[data-state='open'] .llselect-trigger-arrow { transform: rotate(180deg); } /* --- popup (matches BS4 .dropdown-menu look) ----------------- */ .llselect-popup { background-color: #fff; border: 1px solid rgba(0, 0, 0, 0.15); border-radius: 0.25rem; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.175); color: #212529; z-index: 1000; padding: 0.5rem 0; } /* --- items (matches BS4 .dropdown-item look) ----------------- */ .llselect-item { padding: 0.25rem 1.5rem; cursor: pointer; color: #212529; background-color: transparent; } .llselect-item:hover { background-color: #e9ecef; color: #16181b; } .llselect-item-focused { background-color: #007bff; color: #fff; } .llselect-item-focused:hover { background-color: #0069d9; color: #fff; } .llselect-item[aria-selected='true'] { font-weight: 600; color: #007bff; } .llselect-item-focused[aria-selected='true'] { color: #fff; } /* --- choose-all row (multi, opt-in) ----------------------------- */ /* The default row content is just the counting text (its numbers carry the tri-state); data-chosen-state is the hook if a theme wants to draw its own indicator, createChooseAllRowContentElFn the way to add a real one. The row also carries the plain item class, so hover / focused styling comes from there. Its divider is drawn by the block-edge rules under "dividers around the command rows": one line where the block above the items ends, none between the row and a command row that follows it. */ /* --- groups (Phase 10, matches .dropdown-header look) --------- */ /* Static label by default; rect-based scroll math (keyboard.ts) allows a theme to make it `position: sticky; top: 0`. */ .llselect-group-label { padding: 0.5rem 1.5rem; font-size: 0.875rem; font-weight: 600; color: #6c757d; background-color: #f8f9fa; } .llselect-group[data-disabled='true'] .llselect-group-label { opacity: 0.55; cursor: not-allowed; } /* --- filter input (Phase 8) ------------------------------------ */ .llselect-filter-input { flex: none; margin: 0.5rem; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; font: inherit; outline: none; background-color: #fff; color: #495057; } .llselect-filter-input:focus { border-color: #80bdff; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } /* --- no-results message ----------------------------------------- */ .llselect-popup-list-no-results { padding: 0.25rem 1.5rem; color: #6c757d; } /* --- dividers around the command rows --------------------------- */ /* The choose-all row and the action rows form one block above the items and one below; they carry .llselect-item too, so hover / focused / disabled styling comes from there. Exactly one line marks each block's edge with the items, drawn as the next element's border-top through sibling combinators (`:has` is above the browser floor). No line inside a block: choose-all followed by a command row reads as one group, like the header blocks of ng-select, Element Plus and Vuetify's select-all recipe. */ .llselect-choose-all-row + .llselect-item:not(.llselect-popup-list-action-row), .llselect-choose-all-row + .llselect-group, .llselect-popup-list-action-row + .llselect-item:not(.llselect-popup-list-action-row), .llselect-popup-list-action-row + .llselect-group, .llselect-item:not(.llselect-popup-list-action-row):not(.llselect-choose-all-row) + .llselect-popup-list-action-row, .llselect-group + .llselect-popup-list-action-row { border-top: 1px solid rgba(0, 0, 0, 0.12); } /* --- pinned command blocks --------------------------------------- */ /* popupListLeadingRowsPinned / popupListTrailingRowsPinned wrap a block in a presentational div that the library makes sticky. The theme gives it an opaque background (the items scroll under it) and the block's edge line, because the sibling rules above cannot see through the wrapper. The line is drawn only toward items: the library sets data-edge-to-items on every render, so two blocks that touch (nothing listed) draw no line at all. */ .llselect-popup-list-pinned-leading-rows, .llselect-popup-list-pinned-trailing-rows { background: #fff; z-index: 1; } .llselect-popup-list-pinned-leading-rows[data-edge-to-items='true'] { border-bottom: 1px solid rgba(0, 0, 0, 0.12); } .llselect-popup-list-pinned-trailing-rows[data-edge-to-items='true'] { border-top: 1px solid rgba(0, 0, 0, 0.12); } /* --- popup header / footer slots -------------------------------- */ /* Pinned above / below the list, outside the scroll area. The elements exist only when their content settings return an element; the divider matches the choose-all row. */ .llselect-popup-header { padding: 0.25rem 1.5rem; border-bottom: 1px solid rgba(0, 0, 0, 0.12); } .llselect-popup-footer { padding: 0.25rem 1.5rem; border-top: 1px solid rgba(0, 0, 0, 0.12); } /* --- disabled (Phase 9) ---------------------------------------- */ /* aria-disabled + data-disabled, never native `disabled`. Keep pointer events alive (no `pointer-events: none`) so hover/focus tooltips work. */ .llselect-trigger[data-disabled='true'] { opacity: 0.55; cursor: not-allowed; } /* Embedded buttons are inert while the control is disabled: show not-allowed, not the pointer cursor their own rule sets. */ .llselect-trigger[data-disabled='true'] .llselect-tag-remove-button, .llselect-trigger[data-disabled='true'] .llselect-trigger-clear-button { cursor: not-allowed; } .llselect-item-disabled { opacity: 0.45; cursor: not-allowed; } .llselect-item-disabled:hover { background: none; } /* --- tags (triggerDisplay: 'tags') --------------------------- */ /* :has lets the trigger content wrap for chips (nowrap+ellipsis otherwise). */ .llselect-trigger-content:has(.llselect-tags) { white-space: normal; overflow: visible; } .llselect-tags { display: flex; flex-wrap: wrap; gap: 0.25rem; } .llselect-tag { display: inline-flex; align-items: center; gap: 0.2rem; /* Logical: mirrors under dir="rtl" (text side start, x-button side end). */ padding-block: 0.1rem; padding-inline: 0.5rem 0.2rem; /* BS4 ships no tag/chip component; solid .badge-primary read too heavy inside a form control. These are BS4's own .alert-primary pair. */ background-color: #cce5ff; color: #004085; border-radius: 0.2rem; font-size: 0.85rem; line-height: 1.5; } .llselect-tag-remove-button { display: inline-flex; align-items: center; justify-content: center; width: 1.15em; height: 1.15em; padding: 0; border: none; border-radius: 0.2rem; background: transparent; color: inherit; opacity: 0.7; cursor: pointer; font: inherit; line-height: 1; } .llselect-tag-remove-button:empty::before { content: '\00d7'; font-size: 1.15em; } .llselect-tag-remove-button:hover { opacity: 1; } /* A disabled item's chip: greyed, its x inert (the JS click guard is the real block). */ .llselect-tag-disabled { opacity: 0.45; cursor: not-allowed; } .llselect-tag-disabled .llselect-tag-remove-button { cursor: not-allowed; } /* --- clear button (clearable) -------------------------------- */ /* :empty::before draws the x only when createTriggerClearButtonContentElFn gave no icon. */ .llselect-trigger-clear-button { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 1.2em; height: 1.2em; padding: 0; border: none; border-radius: 0.2rem; background: transparent; color: #6c757d; cursor: pointer; font: inherit; line-height: 1; } .llselect-trigger-clear-button:empty::before { content: '\00d7'; font-size: 1.2em; } .llselect-trigger-clear-button:hover { color: #212529; background-color: #f8f9fa; } .llselect-trigger[data-empty='true'] .llselect-trigger-clear-button { display: none; }