@smilodon/core 1.3.13 → 1.4.0

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/dist/index.cjs CHANGED
@@ -1557,16 +1557,55 @@ class SelectOption extends HTMLElement {
1557
1557
  this._shadow.appendChild(style);
1558
1558
  }
1559
1559
  _render() {
1560
- const { item, index, selected, disabled, active, render, showRemoveButton } = this._config;
1560
+ const { item, index, selected, disabled, active, render, showRemoveButton, classMap } = this._config;
1561
1561
  // Clear container
1562
1562
  this._container.innerHTML = '';
1563
- // Apply state classes
1564
- this._container.classList.toggle('selected', selected);
1565
- this._container.classList.toggle('disabled', disabled || false);
1566
- this._container.classList.toggle('active', active || false);
1563
+ // Add part attribute
1564
+ this._container.setAttribute('part', 'option');
1565
+ // Standard styling hook
1566
+ this._container.classList.add('smilodon-option');
1567
+ // Apply state classes using classMap or defaults
1568
+ const selectedClasses = (classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
1569
+ const activeClasses = (classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
1570
+ const disabledClasses = (classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
1571
+ // Apply classes to both the container (internal styling) and the host (external styling/::part)
1572
+ // This ensures that utility classes are visible via ::part selectors
1573
+ const toggleClasses = (element, classes, add) => {
1574
+ if (add) {
1575
+ element.classList.add(...classes);
1576
+ }
1577
+ else {
1578
+ element.classList.remove(...classes);
1579
+ }
1580
+ };
1581
+ if (selected) {
1582
+ toggleClasses(this._container, [...selectedClasses, 'smilodon-option--selected'], true);
1583
+ toggleClasses(this, [...selectedClasses, 'smilodon-option--selected'], true);
1584
+ }
1585
+ else {
1586
+ toggleClasses(this._container, [...selectedClasses, 'smilodon-option--selected'], false);
1587
+ toggleClasses(this, [...selectedClasses, 'smilodon-option--selected'], false);
1588
+ }
1589
+ if (active) {
1590
+ toggleClasses(this._container, [...activeClasses, 'smilodon-option--active'], true);
1591
+ toggleClasses(this, [...activeClasses, 'smilodon-option--active'], true); // Make focus ring visible on host
1592
+ }
1593
+ else {
1594
+ toggleClasses(this._container, [...activeClasses, 'smilodon-option--active'], false);
1595
+ toggleClasses(this, [...activeClasses, 'smilodon-option--active'], false);
1596
+ }
1597
+ if (disabled) {
1598
+ toggleClasses(this._container, [...disabledClasses, 'smilodon-option--disabled'], true);
1599
+ toggleClasses(this, [...disabledClasses, 'smilodon-option--disabled'], true);
1600
+ }
1601
+ else {
1602
+ toggleClasses(this._container, [...disabledClasses, 'smilodon-option--disabled'], false);
1603
+ toggleClasses(this, [...disabledClasses, 'smilodon-option--disabled'], false);
1604
+ }
1567
1605
  // Custom class name
1568
1606
  if (this._config.className) {
1569
- this._container.className += ' ' + this._config.className;
1607
+ const classes = this._config.className.split(' ').filter(Boolean);
1608
+ this._container.classList.add(...classes);
1570
1609
  }
1571
1610
  // Apply custom styles
1572
1611
  if (this._config.style) {
@@ -1575,12 +1614,13 @@ class SelectOption extends HTMLElement {
1575
1614
  // Render content
1576
1615
  const contentDiv = document.createElement('div');
1577
1616
  contentDiv.className = 'option-content';
1617
+ // contentDiv.setAttribute('part', 'option-content'); // Optional
1578
1618
  if (render) {
1579
1619
  const rendered = render(item, index);
1580
1620
  if (typeof rendered === 'string') {
1581
1621
  contentDiv.innerHTML = rendered;
1582
1622
  }
1583
- else {
1623
+ else if (rendered instanceof HTMLElement) {
1584
1624
  contentDiv.appendChild(rendered);
1585
1625
  }
1586
1626
  }
@@ -1594,16 +1634,56 @@ class SelectOption extends HTMLElement {
1594
1634
  this._removeButton = document.createElement('button');
1595
1635
  this._removeButton.className = 'remove-button';
1596
1636
  this._removeButton.innerHTML = '×';
1637
+ this._removeButton.setAttribute('part', 'chip-remove');
1597
1638
  this._removeButton.setAttribute('aria-label', 'Remove option');
1598
1639
  this._removeButton.setAttribute('type', 'button');
1599
1640
  this._container.appendChild(this._removeButton);
1600
1641
  }
1601
- // Set ARIA attributes
1642
+ // Set ARIA attributes and State attributes on Host
1602
1643
  this.setAttribute('role', 'option');
1603
1644
  this.setAttribute('aria-selected', String(selected));
1604
1645
  if (disabled)
1605
1646
  this.setAttribute('aria-disabled', 'true');
1606
1647
  this.id = this._config.id || `select-option-${index}`;
1648
+ // Add checkmark (part="checkmark") - standard for object mode
1649
+ // Only show if NOT showing remove button (avoid clutter)
1650
+ if (!showRemoveButton) {
1651
+ const checkmark = document.createElement('div');
1652
+ checkmark.setAttribute('part', 'checkmark');
1653
+ checkmark.className = 'checkmark-icon';
1654
+ checkmark.innerHTML = `
1655
+ <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:1em;">
1656
+ <path d="M4 8.5L6.5 11L12 5.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1657
+ </svg>
1658
+ `;
1659
+ // Visibility control via CSS or inline style
1660
+ // We set it to display: none unless selected.
1661
+ // User can override this behavior via part styling if they want transitions
1662
+ if (!selected) {
1663
+ checkmark.style.display = 'none';
1664
+ }
1665
+ else {
1666
+ checkmark.style.marginLeft = '8px';
1667
+ checkmark.style.color = 'currentColor';
1668
+ }
1669
+ this._container.appendChild(checkmark);
1670
+ }
1671
+ // Data Attributes Contract on Host
1672
+ const state = [];
1673
+ if (selected)
1674
+ state.push('selected');
1675
+ if (active)
1676
+ state.push('active');
1677
+ if (state.length) {
1678
+ this.dataset.smState = state.join(' ');
1679
+ }
1680
+ else {
1681
+ delete this.dataset.smState;
1682
+ }
1683
+ this.dataset.smIndex = String(index);
1684
+ if (!this.hasAttribute('data-sm-selectable')) {
1685
+ this.toggleAttribute('data-sm-selectable', true);
1686
+ }
1607
1687
  }
1608
1688
  _attachEventListeners() {
1609
1689
  // Click handler for selection
@@ -1830,10 +1910,12 @@ class EnhancedSelect extends HTMLElement {
1830
1910
  _createInputContainer() {
1831
1911
  const container = document.createElement('div');
1832
1912
  container.className = 'input-container';
1913
+ container.setAttribute('part', 'button');
1833
1914
  return container;
1834
1915
  }
1835
1916
  _createInput() {
1836
1917
  const input = document.createElement('input');
1918
+ input.setAttribute('part', 'input');
1837
1919
  input.type = 'text';
1838
1920
  input.className = 'select-input';
1839
1921
  input.id = `${this._uniqueId}-input`;
@@ -1861,6 +1943,7 @@ class EnhancedSelect extends HTMLElement {
1861
1943
  _createDropdown() {
1862
1944
  const dropdown = document.createElement('div');
1863
1945
  dropdown.className = 'select-dropdown';
1946
+ dropdown.setAttribute('part', 'listbox');
1864
1947
  dropdown.style.display = 'none';
1865
1948
  if (this._config.styles.classNames?.dropdown) {
1866
1949
  dropdown.className += ' ' + this._config.styles.classNames.dropdown;
@@ -1892,7 +1975,7 @@ class EnhancedSelect extends HTMLElement {
1892
1975
  const container = document.createElement('div');
1893
1976
  container.className = 'dropdown-arrow-container';
1894
1977
  container.innerHTML = `
1895
- <svg class="dropdown-arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1978
+ <svg class="dropdown-arrow" part="arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1896
1979
  <path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1897
1980
  </svg>
1898
1981
  `;
@@ -2414,6 +2497,22 @@ class EnhancedSelect extends HTMLElement {
2414
2497
  const query = e.target.value;
2415
2498
  this._handleSearch(query);
2416
2499
  });
2500
+ // Delegated click listener for improved event handling (smart fallback)
2501
+ this._optionsContainer.addEventListener('click', (e) => {
2502
+ const target = e.target;
2503
+ // Handle option clicks
2504
+ const option = target.closest('[data-sm-selectable], [data-selectable], [data-sm-state]');
2505
+ if (option && !option.hasAttribute('aria-disabled')) {
2506
+ const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
2507
+ const index = Number(indexStr);
2508
+ if (!Number.isNaN(index)) {
2509
+ this._selectOption(index, {
2510
+ shiftKey: e.shiftKey,
2511
+ toggleKey: e.ctrlKey || e.metaKey,
2512
+ });
2513
+ }
2514
+ }
2515
+ });
2417
2516
  // Keyboard navigation
2418
2517
  this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
2419
2518
  // Click outside to close
@@ -2986,10 +3085,12 @@ class EnhancedSelect extends HTMLElement {
2986
3085
  selectedEntries.forEach(([index, item]) => {
2987
3086
  const badge = document.createElement('span');
2988
3087
  badge.className = 'selection-badge';
3088
+ badge.setAttribute('part', 'chip');
2989
3089
  badge.textContent = getLabel(item);
2990
3090
  // Add remove button to badge
2991
3091
  const removeBtn = document.createElement('button');
2992
3092
  removeBtn.className = 'badge-remove';
3093
+ removeBtn.setAttribute('part', 'chip-remove');
2993
3094
  removeBtn.innerHTML = '×';
2994
3095
  removeBtn.setAttribute('aria-label', `Remove ${getLabel(item)}`);
2995
3096
  removeBtn.addEventListener('click', (e) => {
@@ -3357,6 +3458,7 @@ class EnhancedSelect extends HTMLElement {
3357
3458
  if (this._state.isSearching) {
3358
3459
  const searching = document.createElement('div');
3359
3460
  searching.className = 'searching-state';
3461
+ searching.setAttribute('part', 'loading');
3360
3462
  searching.textContent = 'Searching...';
3361
3463
  this._optionsContainer.appendChild(searching);
3362
3464
  return;
@@ -3414,6 +3516,7 @@ class EnhancedSelect extends HTMLElement {
3414
3516
  });
3415
3517
  if (!hasRenderedItems && !this._state.isBusy) {
3416
3518
  const empty = document.createElement('div');
3519
+ empty.setAttribute('part', 'no-results');
3417
3520
  empty.className = 'empty-state';
3418
3521
  if (query) {
3419
3522
  empty.textContent = `No results found for "${this._state.searchQuery}"`;
@@ -3427,6 +3530,7 @@ class EnhancedSelect extends HTMLElement {
3427
3530
  // Append Busy Indicator if busy
3428
3531
  if (this._state.isBusy && this._config.busyBucket.enabled) {
3429
3532
  const busyBucket = document.createElement('div');
3533
+ busyBucket.setAttribute('part', 'loading');
3430
3534
  busyBucket.className = 'busy-bucket';
3431
3535
  if (this._config.busyBucket.showSpinner) {
3432
3536
  const spinner = document.createElement('div');
@@ -3475,11 +3579,24 @@ class EnhancedSelect extends HTMLElement {
3475
3579
  getValue,
3476
3580
  getLabel,
3477
3581
  showRemoveButton: this._config.selection.mode === 'multi' && this._config.selection.showRemoveButton,
3582
+ classMap: this.classMap,
3478
3583
  });
3584
+ // Valid part attribute on the web component host itself
3585
+ option.setAttribute('part', 'option');
3479
3586
  option.dataset.index = String(index);
3480
3587
  option.dataset.value = String(getValue(item));
3588
+ // New standard attributes on Host
3589
+ option.dataset.smIndex = String(index);
3590
+ if (!option.hasAttribute('data-sm-selectable')) {
3591
+ option.toggleAttribute('data-sm-selectable', true);
3592
+ }
3593
+ const val = getValue(item);
3594
+ if (val != null) {
3595
+ option.dataset.smValue = String(val);
3596
+ }
3481
3597
  option.id = option.id || optionId;
3482
3598
  option.addEventListener('click', (e) => {
3599
+ e.stopPropagation(); // Prevent duplicate handling by delegation
3483
3600
  const mouseEvent = e;
3484
3601
  this._selectOption(index, {
3485
3602
  shiftKey: mouseEvent.shiftKey,
@@ -3495,35 +3612,71 @@ class EnhancedSelect extends HTMLElement {
3495
3612
  }
3496
3613
  _normalizeCustomOptionElement(element, meta) {
3497
3614
  const optionEl = element instanceof HTMLElement ? element : document.createElement('div');
3615
+ // Add part attribute for styling
3616
+ if (!optionEl.hasAttribute('part')) {
3617
+ optionEl.setAttribute('part', 'option');
3618
+ }
3498
3619
  // Add both semantic namespaced classes and the legacy internal classes that CSS uses
3499
3620
  optionEl.classList.add('smilodon-option', 'option');
3500
- // Toggle state classes
3621
+ // Toggle state classes using classMap if available
3501
3622
  const isSelected = meta.selected;
3502
3623
  const isActive = meta.active;
3503
3624
  const isDisabled = meta.disabled;
3625
+ // Resolve classes from classMap or defaults
3626
+ const selectedClasses = (this.classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
3627
+ const activeClasses = (this.classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
3628
+ const disabledClasses = (this.classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
3504
3629
  if (isSelected) {
3505
- optionEl.classList.add('smilodon-option--selected', 'selected');
3630
+ optionEl.classList.add(...selectedClasses);
3631
+ optionEl.classList.add('smilodon-option--selected');
3506
3632
  }
3507
3633
  else {
3508
- optionEl.classList.remove('smilodon-option--selected', 'selected');
3634
+ optionEl.classList.remove(...selectedClasses);
3635
+ optionEl.classList.remove('smilodon-option--selected');
3509
3636
  }
3510
3637
  if (isActive) {
3511
- optionEl.classList.add('smilodon-option--active', 'active');
3638
+ optionEl.classList.add(...activeClasses);
3639
+ optionEl.classList.add('smilodon-option--active');
3512
3640
  }
3513
3641
  else {
3514
- optionEl.classList.remove('smilodon-option--active', 'active');
3642
+ optionEl.classList.remove(...activeClasses);
3643
+ optionEl.classList.remove('smilodon-option--active');
3515
3644
  }
3516
3645
  if (isDisabled) {
3517
- optionEl.classList.add('smilodon-option--disabled', 'disabled');
3646
+ optionEl.classList.add(...disabledClasses);
3647
+ optionEl.classList.add('smilodon-option--disabled');
3518
3648
  }
3519
3649
  else {
3520
- optionEl.classList.remove('smilodon-option--disabled', 'disabled');
3650
+ optionEl.classList.remove(...disabledClasses);
3651
+ optionEl.classList.remove('smilodon-option--disabled');
3652
+ }
3653
+ // Data Attributes Contract
3654
+ const state = [];
3655
+ if (isSelected)
3656
+ state.push('selected');
3657
+ if (isActive)
3658
+ state.push('active');
3659
+ if (state.length) {
3660
+ optionEl.dataset.smState = state.join(' ');
3521
3661
  }
3662
+ else {
3663
+ delete optionEl.dataset.smState;
3664
+ }
3665
+ // Legacy data attribute support
3522
3666
  if (!optionEl.hasAttribute('data-selectable')) {
3523
3667
  optionEl.setAttribute('data-selectable', '');
3524
3668
  }
3669
+ // New delegation attribute
3670
+ if (!optionEl.hasAttribute('data-sm-selectable')) {
3671
+ optionEl.setAttribute('data-sm-selectable', '');
3672
+ }
3525
3673
  optionEl.dataset.index = String(meta.index);
3526
3674
  optionEl.dataset.value = String(meta.value);
3675
+ // New standard attributes
3676
+ optionEl.dataset.smIndex = String(meta.index);
3677
+ if (meta.value != null) {
3678
+ optionEl.dataset.smValue = String(meta.value);
3679
+ }
3527
3680
  optionEl.id = optionEl.id || meta.id;
3528
3681
  if (!optionEl.getAttribute('role')) {
3529
3682
  optionEl.setAttribute('role', 'option');
@@ -3543,6 +3696,7 @@ class EnhancedSelect extends HTMLElement {
3543
3696
  }
3544
3697
  if (!meta.disabled) {
3545
3698
  optionEl.addEventListener('click', (e) => {
3699
+ e.stopPropagation(); // Prevent duplicate handling by delegation
3546
3700
  const mouseEvent = e;
3547
3701
  this._selectOption(meta.index, {
3548
3702
  shiftKey: mouseEvent.shiftKey,