@smilodon/core 1.4.9 → 1.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -610,6 +610,11 @@ Dark mode is **opt-in only** and can be enabled by adding a class or data attrib
610
610
 
611
611
  <!-- Using data attribute -->
612
612
  <enhanced-select data-theme="dark"></enhanced-select>
613
+
614
+ <!-- Also supported (attribute aliases) -->
615
+ <enhanced-select dark-mode></enhanced-select>
616
+ <enhanced-select darkmode></enhanced-select>
617
+ <enhanced-select theme="dark"></enhanced-select>
613
618
  ```
614
619
 
615
620
  ```css
@@ -631,6 +636,8 @@ enhanced-select.dark-mode {
631
636
  **Light Mode (Default)**
632
637
  ```css
633
638
  --select-options-bg /* Options container background (white) */
639
+ --select-width /* Host width (100% default) */
640
+ --select-height /* Host height (auto default) */
634
641
  --select-option-color /* Option text color (#1f2937) */
635
642
  --select-option-bg /* Option background (white) */
636
643
  --select-option-padding /* Option padding (8px 12px) */
@@ -643,9 +650,41 @@ enhanced-select.dark-mode {
643
650
  --select-option-selected-hover-border /* Selected+hover border (inherits selected border by default) */
644
651
  --select-option-active-bg /* Active background (#f3f4f6) */
645
652
  --select-option-active-color /* Active text color (#1f2937) */
653
+ --select-input-width /* Input field width */
654
+ --select-input-height /* Input container height */
646
655
  --select-dropdown-bg /* Dropdown background (white) */
647
656
  --select-dropdown-border /* Dropdown border color (#ccc) */
648
657
  --select-dropdown-shadow /* Dropdown shadow */
658
+ --select-empty-padding /* Empty/no-results container padding */
659
+ --select-empty-color /* Text color for empty/no-results models */
660
+ --select-empty-font-size /* Font size */
661
+ --select-empty-bg /* Background for empty/no-results state */
662
+ --select-empty-min-height /* Minimum height of empty state box */
663
+
664
+ /* Arrow/button */
665
+ --select-arrow-size /* Width & height of SVG icon (16px default) */
666
+ --select-arrow-color /* Icon color (#667eea) */
667
+ --select-arrow-hover-color /* Icon color when hovered (#667eea) */
668
+ --select-arrow-hover-bg /* Background when hover (rgba(102,126,234,0.08)) */
669
+ --select-arrow-width /* Container width (40px) */
670
+ --select-arrow-border-radius /* Container border radius */
671
+
672
+ /* Group headers (when using groupedItems or flat items with `group` property) */
673
+ --select-group-header-padding /* Padding inside header (8px 12px) */
674
+ --select-group-header-color /* Text color (#6b7280) */
675
+ --select-group-header-bg /* Background (#f3f4f6) */
676
+ --select-group-header-font-size
677
+ --select-group-header-text-align /* Header text alignment (left default) */
678
+ --select-group-header-text-transform
679
+ --select-group-header-letter-spacing
680
+ --select-group-header-border-bottom
681
+
682
+ /* Option content and checkmark hooks */
683
+ --select-option-content-overflow
684
+ --select-option-content-text-overflow
685
+ --select-option-content-white-space
686
+ --select-checkmark-margin-left
687
+ --select-checkmark-color
649
688
  ```
650
689
 
651
690
  **Dark Mode (Opt-in)**
@@ -659,6 +698,8 @@ enhanced-select.dark-mode {
659
698
  --select-dark-option-bg /* Dark option background (#1f2937) */
660
699
  --select-dark-option-hover-bg /* Dark hover background (#374151) */
661
700
  --select-dark-option-selected-bg /* Dark selected bg (#3730a3) */
701
+ --select-dark-group-header-color /* Dark header text */
702
+ --select-dark-group-header-bg /* Dark header background */
662
703
  ```
663
704
 
664
705
  **Complete CSS Variables List (60+ variables)**
@@ -693,11 +734,26 @@ enhanced-select {
693
734
  transparent 100%
694
735
  );
695
736
  }
737
+
738
+ /* Typo-compatible aliases are also accepted */
739
+ enhanced-select {
740
+ --select-seperator-width: 2px;
741
+ --select-seperator-height: 80%;
742
+ --select-seperator-position: 40px;
743
+ --select-seperator-gradient: linear-gradient(to bottom, transparent 0%, #3b82f6 20%, #3b82f6 80%, transparent 100%);
744
+ }
696
745
  ```
697
746
 
747
+ **Gradient Dropdown + Hover/Selected States**
748
+ If your dropdown uses a gradient background (for example via `--select-dropdown-bg`), option hover/selected colors still work as expected. The component intentionally uses the `background` shorthand for hover/selected option states so any inherited `background-image` layers are cleared correctly.
749
+
698
750
  **Badge Remove/Delete Button (Multi-Select)**
699
751
  The × button that removes selected items in multi-select mode is fully customizable:
700
752
 
753
+ **Group Header & No‑Results Parts**
754
+ Both the group header and the no-results message are exposed as shadow parts (`group-header` and `no-results`) so you can target them with `::part()` selectors or CSS variables. This makes it straightforward to match the look of your host framework or UI kit.
755
+
756
+
701
757
  ```css
702
758
  enhanced-select {
703
759
  /* Customize badge appearance */
package/dist/index.cjs CHANGED
@@ -1534,6 +1534,8 @@ class SelectOption extends HTMLElement {
1534
1534
  padding: var(--select-option-padding, 8px 12px);
1535
1535
  cursor: pointer;
1536
1536
  user-select: none;
1537
+ color: var(--select-option-color, var(--select-text-color, #1f2937));
1538
+ background: var(--select-option-bg, var(--select-dropdown-bg, var(--select-bg, white)));
1537
1539
  transition: var(--select-option-transition, background-color 0.2s ease);
1538
1540
  border: var(--select-option-border, none);
1539
1541
  border-bottom: var(--select-option-border-bottom, none);
@@ -1578,9 +1580,20 @@ class SelectOption extends HTMLElement {
1578
1580
 
1579
1581
  .option-content {
1580
1582
  flex: 1;
1581
- overflow: hidden;
1582
- text-overflow: ellipsis;
1583
- white-space: nowrap;
1583
+ overflow: var(--select-option-content-overflow, hidden);
1584
+ text-overflow: var(--select-option-content-text-overflow, ellipsis);
1585
+ white-space: var(--select-option-content-white-space, nowrap);
1586
+ }
1587
+
1588
+ .checkmark-icon {
1589
+ display: none;
1590
+ margin-left: var(--select-checkmark-margin-left, 8px);
1591
+ color: var(--select-checkmark-color, currentColor);
1592
+ }
1593
+
1594
+ :host([aria-selected="true"]) .checkmark-icon,
1595
+ .option-container.selected .checkmark-icon {
1596
+ display: inline-flex;
1584
1597
  }
1585
1598
 
1586
1599
  .remove-button {
@@ -1708,16 +1721,6 @@ class SelectOption extends HTMLElement {
1708
1721
  <path d="M4 8.5L6.5 11L12 5.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1709
1722
  </svg>
1710
1723
  `;
1711
- // Visibility control via CSS or inline style
1712
- // We set it to display: none unless selected.
1713
- // User can override this behavior via part styling if they want transitions
1714
- if (!selected) {
1715
- checkmark.style.display = 'none';
1716
- }
1717
- else {
1718
- checkmark.style.marginLeft = '8px';
1719
- checkmark.style.color = 'currentColor';
1720
- }
1721
1724
  this._container.appendChild(checkmark);
1722
1725
  }
1723
1726
  // Data Attributes Contract on Host
@@ -1875,7 +1878,22 @@ class EnhancedSelect extends HTMLElement {
1875
1878
  }
1876
1879
  set classMap(map) {
1877
1880
  this._classMap = map;
1878
- this._setGlobalStylesMirroring(Boolean(this._optionRenderer || map));
1881
+ this._setGlobalStylesMirroring(Boolean(this._optionRenderer || map || this._groupHeaderRenderer));
1882
+ if (!this.isConnected)
1883
+ return;
1884
+ this._renderOptions();
1885
+ }
1886
+ /**
1887
+ * DOM-based renderer for group headers. When provided, the component will
1888
+ * call this function for each group during rendering. The returned element
1889
+ * will receive `.group-header` and `part="group-header"` automatically.
1890
+ */
1891
+ get groupHeaderRenderer() {
1892
+ return this._groupHeaderRenderer;
1893
+ }
1894
+ set groupHeaderRenderer(renderer) {
1895
+ this._groupHeaderRenderer = renderer;
1896
+ this._setGlobalStylesMirroring(Boolean(this._optionRenderer || this._classMap || renderer));
1879
1897
  if (!this.isConnected)
1880
1898
  return;
1881
1899
  this._renderOptions();
@@ -1934,11 +1952,12 @@ class EnhancedSelect extends HTMLElement {
1934
1952
  this._initializeObservers();
1935
1953
  }
1936
1954
  connectedCallback() {
1955
+ // register instance
1956
+ EnhancedSelect._instances.add(this);
1937
1957
  // WORKAROUND: Force display style on host element for Angular compatibility
1938
1958
  // Angular's rendering seems to not apply :host styles correctly in some cases
1939
1959
  // Must be done in connectedCallback when element is attached to DOM
1940
1960
  this.style.display = 'block';
1941
- this.style.width = '100%';
1942
1961
  if (this._optionRenderer) {
1943
1962
  this._setGlobalStylesMirroring(true);
1944
1963
  }
@@ -1952,6 +1971,8 @@ class EnhancedSelect extends HTMLElement {
1952
1971
  }
1953
1972
  }
1954
1973
  disconnectedCallback() {
1974
+ // unregister instance
1975
+ EnhancedSelect._instances.delete(this);
1955
1976
  // Cleanup observers
1956
1977
  this._resizeObserver?.disconnect();
1957
1978
  this._intersectionObserver?.disconnect();
@@ -2179,7 +2200,8 @@ class EnhancedSelect extends HTMLElement {
2179
2200
  :host {
2180
2201
  display: block;
2181
2202
  position: relative;
2182
- width: 100%;
2203
+ width: var(--select-width, 100%);
2204
+ height: var(--select-height, auto);
2183
2205
  }
2184
2206
 
2185
2207
  .select-container {
@@ -2195,6 +2217,7 @@ class EnhancedSelect extends HTMLElement {
2195
2217
  flex-wrap: wrap;
2196
2218
  gap: var(--select-input-gap, 6px);
2197
2219
  padding: var(--select-input-padding, 6px 52px 6px 8px);
2220
+ height: var(--select-input-height, auto);
2198
2221
  min-height: var(--select-input-min-height, 44px);
2199
2222
  max-height: var(--select-input-max-height, 160px);
2200
2223
  overflow-y: var(--select-input-overflow-y, auto);
@@ -2216,17 +2239,17 @@ class EnhancedSelect extends HTMLElement {
2216
2239
  content: '';
2217
2240
  position: absolute;
2218
2241
  top: 50%;
2219
- right: var(--select-separator-position, 40px);
2242
+ right: var(--select-separator-position, var(--select-seperator-position, 40px));
2220
2243
  transform: translateY(-50%);
2221
- width: var(--select-separator-width, 1px);
2222
- height: var(--select-separator-height, 60%);
2223
- background: var(--select-separator-bg, var(--select-separator-gradient, linear-gradient(
2244
+ width: var(--select-separator-width, var(--select-seperator-width, 1px));
2245
+ height: var(--select-separator-height, var(--select-seperator-height, 60%));
2246
+ background: var(--select-separator-bg, var(--select-seperator-bg, var(--select-separator-gradient, var(--select-seperator-gradient, linear-gradient(
2224
2247
  to bottom,
2225
2248
  transparent 0%,
2226
2249
  rgba(0, 0, 0, 0.1) 20%,
2227
2250
  rgba(0, 0, 0, 0.1) 80%,
2228
2251
  transparent 100%
2229
- )));
2252
+ ))));
2230
2253
  pointer-events: none;
2231
2254
  z-index: 1;
2232
2255
  }
@@ -2237,6 +2260,8 @@ class EnhancedSelect extends HTMLElement {
2237
2260
  right: 0;
2238
2261
  bottom: 0;
2239
2262
  width: var(--select-arrow-width, 40px);
2263
+ /* allow explicit height override even though container normally stretches */
2264
+ height: var(--select-arrow-height, auto);
2240
2265
  display: flex;
2241
2266
  align-items: center;
2242
2267
  justify-content: center;
@@ -2251,7 +2276,7 @@ class EnhancedSelect extends HTMLElement {
2251
2276
  }
2252
2277
 
2253
2278
  .input-container.has-clear-control::after {
2254
- right: var(--select-separator-position-with-clear, 72px);
2279
+ right: var(--select-separator-position-with-clear, var(--select-seperator-position-with-clear, 72px));
2255
2280
  }
2256
2281
 
2257
2282
  .dropdown-arrow-container.with-clear-control {
@@ -2302,9 +2327,14 @@ class EnhancedSelect extends HTMLElement {
2302
2327
  background-color: var(--select-arrow-hover-bg, rgba(102, 126, 234, 0.08));
2303
2328
  }
2304
2329
 
2330
+ .dropdown-arrow:hover {
2331
+ /* legacy alias --select-arrow-hover for icon color */
2332
+ color: var(--select-arrow-hover, var(--select-arrow-hover-color, #667eea));
2333
+ }
2334
+
2305
2335
  .dropdown-arrow {
2306
- width: var(--select-arrow-size, 16px);
2307
- height: var(--select-arrow-size, 16px);
2336
+ width: var(--select-arrow-width, var(--select-arrow-size, 16px));
2337
+ height: var(--select-arrow-height, var(--select-arrow-size, 16px));
2308
2338
  color: var(--select-arrow-color, #667eea);
2309
2339
  transition: transform 0.2s ease, color 0.2s ease;
2310
2340
  transform: translateY(0);
@@ -2324,6 +2354,7 @@ class EnhancedSelect extends HTMLElement {
2324
2354
 
2325
2355
  .select-input {
2326
2356
  flex: 1;
2357
+ width: var(--select-input-width, auto);
2327
2358
  min-width: var(--select-input-min-width, 120px);
2328
2359
  padding: var(--select-input-field-padding, 4px);
2329
2360
  border: none;
@@ -2418,6 +2449,7 @@ class EnhancedSelect extends HTMLElement {
2418
2449
  font-weight: var(--select-group-header-weight, 600);
2419
2450
  color: var(--select-group-header-color, #6b7280);
2420
2451
  background-color: var(--select-group-header-bg, #f3f4f6);
2452
+ text-align: var(--select-group-header-text-align, left);
2421
2453
  font-size: var(--select-group-header-font-size, 12px);
2422
2454
  text-transform: var(--select-group-header-text-transform, uppercase);
2423
2455
  letter-spacing: var(--select-group-header-letter-spacing, 0.05em);
@@ -2602,10 +2634,25 @@ class EnhancedSelect extends HTMLElement {
2602
2634
 
2603
2635
  /* Dark mode - Opt-in via class, data attribute, or ancestor context */
2604
2636
  :host(.dark-mode),
2637
+ :host([dark-mode]),
2638
+ :host([darkmode]),
2605
2639
  :host([data-theme="dark"]),
2640
+ :host([theme="dark"]),
2606
2641
  :host-context(.dark-mode),
2607
2642
  :host-context(.dark),
2608
- :host-context([data-theme="dark"]) {
2643
+ :host-context([dark-mode]),
2644
+ :host-context([darkmode]),
2645
+ :host-context([data-theme="dark"]),
2646
+ :host-context([theme="dark"]) {
2647
+ /* map dark tokens to base option tokens so nested <select-option>
2648
+ components also pick up dark mode via inherited CSS variables */
2649
+ --select-option-bg: var(--select-dark-option-bg, #1f2937);
2650
+ --select-option-color: var(--select-dark-option-color, #f9fafb);
2651
+ --select-option-hover-bg: var(--select-dark-option-hover-bg, #374151);
2652
+ --select-option-hover-color: var(--select-dark-option-hover-color, #f9fafb);
2653
+ --select-option-selected-bg: var(--select-dark-option-selected-bg, #3730a3);
2654
+ --select-option-selected-color: var(--select-dark-option-selected-text, #e0e7ff);
2655
+
2609
2656
  .input-container {
2610
2657
  background: var(--select-dark-bg, #1f2937);
2611
2658
  border-color: var(--select-dark-border, #4b5563);
@@ -2662,6 +2709,12 @@ class EnhancedSelect extends HTMLElement {
2662
2709
  outline: var(--select-dark-option-active-outline, 2px solid rgba(129, 140, 248, 0.55));
2663
2710
  }
2664
2711
 
2712
+ /* Group header in dark mode */
2713
+ .group-header {
2714
+ color: var(--select-dark-group-header-color, var(--select-group-header-color, #6b7280));
2715
+ background-color: var(--select-dark-group-header-bg, var(--select-group-header-bg, #374151));
2716
+ }
2717
+
2665
2718
  .option.selected.active {
2666
2719
  background-color: var(--select-dark-option-selected-active-bg, var(--select-dark-option-selected-bg, #3730a3));
2667
2720
  color: var(--select-dark-option-selected-active-color, var(--select-dark-option-selected-text, #e0e7ff));
@@ -2740,19 +2793,14 @@ class EnhancedSelect extends HTMLElement {
2740
2793
  this._boundArrowClick = (e) => {
2741
2794
  e.stopPropagation();
2742
2795
  e.preventDefault();
2743
- const wasOpen = this._state.isOpen;
2744
- this._state.isOpen = !this._state.isOpen;
2745
- this._updateDropdownVisibility();
2746
- this._updateArrowRotation();
2747
- if (this._state.isOpen && this._config.callbacks.onOpen) {
2748
- this._config.callbacks.onOpen();
2749
- }
2750
- else if (!this._state.isOpen && this._config.callbacks.onClose) {
2751
- this._config.callbacks.onClose();
2796
+ // delegate to the existing open/close helpers so we don't accidentally
2797
+ // drift out of sync with the logic in those methods (focus, events,
2798
+ // scroll-to-selected, etc.)
2799
+ if (this._state.isOpen) {
2800
+ this._handleClose();
2752
2801
  }
2753
- // Scroll to selected when opening
2754
- if (!wasOpen && this._state.isOpen && this._state.selectedIndices.size > 0) {
2755
- setTimeout(() => this._scrollToSelected(), 50);
2802
+ else {
2803
+ this._handleOpen();
2756
2804
  }
2757
2805
  };
2758
2806
  this._arrowContainer.addEventListener('click', this._boundArrowClick);
@@ -2786,6 +2834,16 @@ class EnhancedSelect extends HTMLElement {
2786
2834
  if (wasClosed) {
2787
2835
  this._handleOpen();
2788
2836
  }
2837
+ else {
2838
+ // Keep open while interacting directly with the input so users can
2839
+ // place cursor/type without accidental collapse.
2840
+ if (target === this._input) {
2841
+ this._input.focus();
2842
+ return;
2843
+ }
2844
+ // clicking other parts of the input container while open toggles close
2845
+ this._handleClose();
2846
+ }
2789
2847
  // Focus the input (do not prevent default behavior)
2790
2848
  this._input.focus();
2791
2849
  // If we just opened the dropdown, transfer pointer capture to the
@@ -2961,6 +3019,16 @@ class EnhancedSelect extends HTMLElement {
2961
3019
  _handleOpen() {
2962
3020
  if (!this._config.enabled || this._state.isOpen)
2963
3021
  return;
3022
+ // close any other open selects before proceeding
3023
+ EnhancedSelect._instances.forEach(inst => {
3024
+ if (inst !== this)
3025
+ inst._handleClose();
3026
+ });
3027
+ // Always focus the input when opening so callers (arrow click,
3028
+ // programmatic `open()`, etc.) get the keyboard cursor. This was a
3029
+ // frequent source of confusion in #14 where people opened the dropdown
3030
+ // but the text field never received focus.
3031
+ this._input.focus();
2964
3032
  this._markOpenStart();
2965
3033
  this._state.isOpen = true;
2966
3034
  this._dropdown.style.display = 'block';
@@ -3934,10 +4002,25 @@ class EnhancedSelect extends HTMLElement {
3934
4002
  const query = this._state.searchQuery.toLowerCase();
3935
4003
  // Handle Grouped Items Rendering (when no search query)
3936
4004
  if (this._state.groupedItems.length > 0 && !query) {
3937
- this._state.groupedItems.forEach(group => {
3938
- const header = document.createElement('div');
3939
- header.className = 'group-header';
3940
- header.textContent = group.label;
4005
+ this._state.groupedItems.forEach((group, groupIndex) => {
4006
+ let header;
4007
+ if (this.groupHeaderRenderer) {
4008
+ header = this.groupHeaderRenderer(group, groupIndex);
4009
+ // make sure the returned element has the correct semantics so
4010
+ // people can style it. we add the class/part even if the renderer
4011
+ // returned something else to ensure backward compatibility.
4012
+ if (!(header instanceof HTMLElement)) {
4013
+ // fall back to default if API is misused
4014
+ header = document.createElement('div');
4015
+ header.textContent = String(group.label);
4016
+ }
4017
+ }
4018
+ else {
4019
+ header = document.createElement('div');
4020
+ header.textContent = group.label;
4021
+ }
4022
+ header.classList.add('group-header');
4023
+ header.setAttribute('part', 'group-header');
3941
4024
  this._optionsContainer.appendChild(header);
3942
4025
  group.options.forEach(item => {
3943
4026
  // Find original index for correct ID generation and selection
@@ -4210,6 +4293,8 @@ class EnhancedSelect extends HTMLElement {
4210
4293
  }
4211
4294
  }
4212
4295
  }
4296
+ /** live set of all connected instances; used to auto-close siblings */
4297
+ EnhancedSelect._instances = new Set();
4213
4298
  // Register custom element
4214
4299
  if (!customElements.get('enhanced-select')) {
4215
4300
  customElements.define('enhanced-select', EnhancedSelect);