@smilodon/core 1.4.10 → 1.4.12

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.js CHANGED
@@ -1401,6 +1401,18 @@ const defaultConfig = {
1401
1401
  icon: '×',
1402
1402
  },
1403
1403
  callbacks: {},
1404
+ tracking: {
1405
+ enabled: false,
1406
+ events: true,
1407
+ styling: true,
1408
+ limitations: true,
1409
+ emitDiagnostics: false,
1410
+ maxEntries: 200,
1411
+ },
1412
+ limitations: {
1413
+ policies: {},
1414
+ autoMitigateRuntimeModeSwitch: true,
1415
+ },
1404
1416
  enabled: true,
1405
1417
  searchable: false,
1406
1418
  placeholder: 'Select an option...',
@@ -1532,6 +1544,8 @@ class SelectOption extends HTMLElement {
1532
1544
  padding: var(--select-option-padding, 8px 12px);
1533
1545
  cursor: pointer;
1534
1546
  user-select: none;
1547
+ color: var(--select-option-color, var(--select-text-color, #1f2937));
1548
+ background: var(--select-option-bg, var(--select-dropdown-bg, var(--select-bg, white)));
1535
1549
  transition: var(--select-option-transition, background-color 0.2s ease);
1536
1550
  border: var(--select-option-border, none);
1537
1551
  border-bottom: var(--select-option-border-bottom, none);
@@ -1576,9 +1590,20 @@ class SelectOption extends HTMLElement {
1576
1590
 
1577
1591
  .option-content {
1578
1592
  flex: 1;
1579
- overflow: hidden;
1580
- text-overflow: ellipsis;
1581
- white-space: nowrap;
1593
+ overflow: var(--select-option-content-overflow, hidden);
1594
+ text-overflow: var(--select-option-content-text-overflow, ellipsis);
1595
+ white-space: var(--select-option-content-white-space, nowrap);
1596
+ }
1597
+
1598
+ .checkmark-icon {
1599
+ display: none;
1600
+ margin-left: var(--select-checkmark-margin-left, 8px);
1601
+ color: var(--select-checkmark-color, currentColor);
1602
+ }
1603
+
1604
+ :host([aria-selected="true"]) .checkmark-icon,
1605
+ .option-container.selected .checkmark-icon {
1606
+ display: inline-flex;
1582
1607
  }
1583
1608
 
1584
1609
  .remove-button {
@@ -1706,16 +1731,6 @@ class SelectOption extends HTMLElement {
1706
1731
  <path d="M4 8.5L6.5 11L12 5.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1707
1732
  </svg>
1708
1733
  `;
1709
- // Visibility control via CSS or inline style
1710
- // We set it to display: none unless selected.
1711
- // User can override this behavior via part styling if they want transitions
1712
- if (!selected) {
1713
- checkmark.style.display = 'none';
1714
- }
1715
- else {
1716
- checkmark.style.marginLeft = '8px';
1717
- checkmark.style.color = 'currentColor';
1718
- }
1719
1734
  this._container.appendChild(checkmark);
1720
1735
  }
1721
1736
  // Data Attributes Contract on Host
@@ -1874,6 +1889,10 @@ class EnhancedSelect extends HTMLElement {
1874
1889
  set classMap(map) {
1875
1890
  this._classMap = map;
1876
1891
  this._setGlobalStylesMirroring(Boolean(this._optionRenderer || map || this._groupHeaderRenderer));
1892
+ this._track('style', 'classMapChanged', {
1893
+ hasClassMap: Boolean(map),
1894
+ keys: map ? Object.keys(map) : [],
1895
+ });
1877
1896
  if (!this.isConnected)
1878
1897
  return;
1879
1898
  this._renderOptions();
@@ -1889,6 +1908,7 @@ class EnhancedSelect extends HTMLElement {
1889
1908
  set groupHeaderRenderer(renderer) {
1890
1909
  this._groupHeaderRenderer = renderer;
1891
1910
  this._setGlobalStylesMirroring(Boolean(this._optionRenderer || this._classMap || renderer));
1911
+ this._track('style', 'groupHeaderRendererChanged', { enabled: Boolean(renderer) });
1892
1912
  if (!this.isConnected)
1893
1913
  return;
1894
1914
  this._renderOptions();
@@ -1907,6 +1927,7 @@ class EnhancedSelect extends HTMLElement {
1907
1927
  this._mirrorGlobalStylesForCustomOptions = false;
1908
1928
  this._globalStylesObserver = null;
1909
1929
  this._globalStylesContainer = null;
1930
+ this._tracking = { events: [], styles: [], limitations: [] };
1910
1931
  this._shadow = this.attachShadow({ mode: 'open' });
1911
1932
  this._uniqueId = `enhanced-select-${Math.random().toString(36).substr(2, 9)}`;
1912
1933
  this._rendererHelpers = this._buildRendererHelpers();
@@ -1953,7 +1974,6 @@ class EnhancedSelect extends HTMLElement {
1953
1974
  // Angular's rendering seems to not apply :host styles correctly in some cases
1954
1975
  // Must be done in connectedCallback when element is attached to DOM
1955
1976
  this.style.display = 'block';
1956
- this.style.width = '100%';
1957
1977
  if (this._optionRenderer) {
1958
1978
  this._setGlobalStylesMirroring(true);
1959
1979
  }
@@ -1992,6 +2012,7 @@ class EnhancedSelect extends HTMLElement {
1992
2012
  return;
1993
2013
  }
1994
2014
  this._mirrorGlobalStylesForCustomOptions = enabled;
2015
+ this._track('style', 'globalStylesMirroringChanged', { enabled });
1995
2016
  if (enabled) {
1996
2017
  this._setupGlobalStylesMirroring();
1997
2018
  }
@@ -2196,7 +2217,8 @@ class EnhancedSelect extends HTMLElement {
2196
2217
  :host {
2197
2218
  display: block;
2198
2219
  position: relative;
2199
- width: 100%;
2220
+ width: var(--select-width, 100%);
2221
+ height: var(--select-height, auto);
2200
2222
  }
2201
2223
 
2202
2224
  .select-container {
@@ -2212,6 +2234,7 @@ class EnhancedSelect extends HTMLElement {
2212
2234
  flex-wrap: wrap;
2213
2235
  gap: var(--select-input-gap, 6px);
2214
2236
  padding: var(--select-input-padding, 6px 52px 6px 8px);
2237
+ height: var(--select-input-height, auto);
2215
2238
  min-height: var(--select-input-min-height, 44px);
2216
2239
  max-height: var(--select-input-max-height, 160px);
2217
2240
  overflow-y: var(--select-input-overflow-y, auto);
@@ -2233,17 +2256,17 @@ class EnhancedSelect extends HTMLElement {
2233
2256
  content: '';
2234
2257
  position: absolute;
2235
2258
  top: 50%;
2236
- right: var(--select-separator-position, 40px);
2259
+ right: var(--select-separator-position, var(--select-seperator-position, 40px));
2237
2260
  transform: translateY(-50%);
2238
- width: var(--select-separator-width, 1px);
2239
- height: var(--select-separator-height, 60%);
2240
- background: var(--select-separator-bg, var(--select-separator-gradient, linear-gradient(
2261
+ width: var(--select-separator-width, var(--select-seperator-width, 1px));
2262
+ height: var(--select-separator-height, var(--select-seperator-height, 60%));
2263
+ background: var(--select-separator-bg, var(--select-seperator-bg, var(--select-separator-gradient, var(--select-seperator-gradient, linear-gradient(
2241
2264
  to bottom,
2242
2265
  transparent 0%,
2243
2266
  rgba(0, 0, 0, 0.1) 20%,
2244
2267
  rgba(0, 0, 0, 0.1) 80%,
2245
2268
  transparent 100%
2246
- )));
2269
+ ))));
2247
2270
  pointer-events: none;
2248
2271
  z-index: 1;
2249
2272
  }
@@ -2270,7 +2293,7 @@ class EnhancedSelect extends HTMLElement {
2270
2293
  }
2271
2294
 
2272
2295
  .input-container.has-clear-control::after {
2273
- right: var(--select-separator-position-with-clear, 72px);
2296
+ right: var(--select-separator-position-with-clear, var(--select-seperator-position-with-clear, 72px));
2274
2297
  }
2275
2298
 
2276
2299
  .dropdown-arrow-container.with-clear-control {
@@ -2348,6 +2371,7 @@ class EnhancedSelect extends HTMLElement {
2348
2371
 
2349
2372
  .select-input {
2350
2373
  flex: 1;
2374
+ width: var(--select-input-width, auto);
2351
2375
  min-width: var(--select-input-min-width, 120px);
2352
2376
  padding: var(--select-input-field-padding, 4px);
2353
2377
  border: none;
@@ -2442,6 +2466,7 @@ class EnhancedSelect extends HTMLElement {
2442
2466
  font-weight: var(--select-group-header-weight, 600);
2443
2467
  color: var(--select-group-header-color, #6b7280);
2444
2468
  background-color: var(--select-group-header-bg, #f3f4f6);
2469
+ text-align: var(--select-group-header-text-align, left);
2445
2470
  font-size: var(--select-group-header-font-size, 12px);
2446
2471
  text-transform: var(--select-group-header-text-transform, uppercase);
2447
2472
  letter-spacing: var(--select-group-header-letter-spacing, 0.05em);
@@ -2626,10 +2651,25 @@ class EnhancedSelect extends HTMLElement {
2626
2651
 
2627
2652
  /* Dark mode - Opt-in via class, data attribute, or ancestor context */
2628
2653
  :host(.dark-mode),
2654
+ :host([dark-mode]),
2655
+ :host([darkmode]),
2629
2656
  :host([data-theme="dark"]),
2657
+ :host([theme="dark"]),
2630
2658
  :host-context(.dark-mode),
2631
2659
  :host-context(.dark),
2632
- :host-context([data-theme="dark"]) {
2660
+ :host-context([dark-mode]),
2661
+ :host-context([darkmode]),
2662
+ :host-context([data-theme="dark"]),
2663
+ :host-context([theme="dark"]) {
2664
+ /* map dark tokens to base option tokens so nested <select-option>
2665
+ components also pick up dark mode via inherited CSS variables */
2666
+ --select-option-bg: var(--select-dark-option-bg, #1f2937);
2667
+ --select-option-color: var(--select-dark-option-color, #f9fafb);
2668
+ --select-option-hover-bg: var(--select-dark-option-hover-bg, #374151);
2669
+ --select-option-hover-color: var(--select-dark-option-hover-color, #f9fafb);
2670
+ --select-option-selected-bg: var(--select-dark-option-selected-bg, #3730a3);
2671
+ --select-option-selected-color: var(--select-dark-option-selected-text, #e0e7ff);
2672
+
2633
2673
  .input-container {
2634
2674
  background: var(--select-dark-bg, #1f2937);
2635
2675
  border-color: var(--select-dark-border, #4b5563);
@@ -2682,6 +2722,8 @@ class EnhancedSelect extends HTMLElement {
2682
2722
 
2683
2723
  .option.active:not(.selected) {
2684
2724
  background-color: var(--select-dark-option-active-bg, #374151);
2725
+ color: var(--select-dark-option-active-color, #f9fafb);
2726
+ outline: var(--select-dark-option-active-outline, 2px solid rgba(129, 140, 248, 0.55));
2685
2727
  }
2686
2728
 
2687
2729
  /* Group header in dark mode */
@@ -2689,9 +2731,6 @@ class EnhancedSelect extends HTMLElement {
2689
2731
  color: var(--select-dark-group-header-color, var(--select-group-header-color, #6b7280));
2690
2732
  background-color: var(--select-dark-group-header-bg, var(--select-group-header-bg, #374151));
2691
2733
  }
2692
- color: var(--select-dark-option-active-color, #f9fafb);
2693
- outline: var(--select-dark-option-active-outline, 2px solid rgba(129, 140, 248, 0.55));
2694
- }
2695
2734
 
2696
2735
  .option.selected.active {
2697
2736
  background-color: var(--select-dark-option-selected-active-bg, var(--select-dark-option-selected-bg, #3730a3));
@@ -2813,7 +2852,13 @@ class EnhancedSelect extends HTMLElement {
2813
2852
  this._handleOpen();
2814
2853
  }
2815
2854
  else {
2816
- // clicking the input while open should close the dropdown too
2855
+ // Keep open while interacting directly with the input so users can
2856
+ // place cursor/type without accidental collapse.
2857
+ if (target === this._input) {
2858
+ this._input.focus();
2859
+ return;
2860
+ }
2861
+ // clicking other parts of the input container while open toggles close
2817
2862
  this._handleClose();
2818
2863
  }
2819
2864
  // Focus the input (do not prevent default behavior)
@@ -3648,6 +3693,162 @@ class EnhancedSelect extends HTMLElement {
3648
3693
  }
3649
3694
  _emit(name, detail) {
3650
3695
  this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
3696
+ if (name !== 'diagnostic') {
3697
+ this._track('event', String(name), detail);
3698
+ }
3699
+ }
3700
+ _track(source, name, detail) {
3701
+ const cfg = this._config.tracking;
3702
+ if (!cfg?.enabled)
3703
+ return;
3704
+ if (source === 'event' && !cfg.events)
3705
+ return;
3706
+ if (source === 'style' && !cfg.styling)
3707
+ return;
3708
+ if (source === 'limitation' && !cfg.limitations)
3709
+ return;
3710
+ const entry = {
3711
+ timestamp: Date.now(),
3712
+ source,
3713
+ name,
3714
+ detail,
3715
+ };
3716
+ const bucket = source === 'event'
3717
+ ? this._tracking.events
3718
+ : source === 'style'
3719
+ ? this._tracking.styles
3720
+ : this._tracking.limitations;
3721
+ bucket.push(entry);
3722
+ const maxEntries = Math.max(10, cfg.maxEntries || 200);
3723
+ if (bucket.length > maxEntries) {
3724
+ bucket.splice(0, bucket.length - maxEntries);
3725
+ }
3726
+ if (cfg.emitDiagnostics) {
3727
+ this.dispatchEvent(new CustomEvent('diagnostic', {
3728
+ detail: entry,
3729
+ bubbles: true,
3730
+ composed: true,
3731
+ }));
3732
+ }
3733
+ }
3734
+ _getKnownLimitationDefinitions() {
3735
+ return [
3736
+ {
3737
+ id: 'variableItemHeight',
3738
+ title: 'Variable item height',
3739
+ description: 'Virtualization assumes fixed or estimated item heights; fully dynamic heights are not yet supported.',
3740
+ workaround: 'Use consistent item heights or set estimatedItemHeight to your dominant row size.',
3741
+ },
3742
+ {
3743
+ id: 'builtInFetchPaginationApi',
3744
+ title: 'Built-in fetch/pagination API',
3745
+ description: 'Core does not include a built-in fetchUrl/searchUrl pagination transport.',
3746
+ workaround: 'Use onSearch/onLoadMore callbacks and update data via setItems().',
3747
+ },
3748
+ {
3749
+ id: 'virtualizationOverheadSmallLists',
3750
+ title: 'Virtualization overhead for small lists',
3751
+ description: 'Virtualization can add slight overhead on very small lists.',
3752
+ workaround: 'Disable virtualization for tiny datasets when micro-latency is critical.',
3753
+ },
3754
+ {
3755
+ id: 'runtimeModeSwitching',
3756
+ title: 'Runtime single/multi mode switching',
3757
+ description: 'Switching between single and multi mode can require state reset for consistency.',
3758
+ workaround: 'Enable autoMitigateRuntimeModeSwitch or recreate/reset component state when toggling modes.',
3759
+ },
3760
+ {
3761
+ id: 'legacyBrowserSupport',
3762
+ title: 'Legacy browser support',
3763
+ description: 'Official support targets modern evergreen browsers.',
3764
+ },
3765
+ {
3766
+ id: 'webkitArchLinux',
3767
+ title: 'Playwright WebKit on Arch-based Linux',
3768
+ description: 'Native WebKit Playwright bundle depends on unavailable legacy system libraries on Arch-based distros.',
3769
+ workaround: 'Run WebKit E2E tests via Playwright Docker image.',
3770
+ },
3771
+ ];
3772
+ }
3773
+ _evaluateLimitationStatus(id) {
3774
+ const policyMode = this._config.limitations?.policies?.[id]?.mode ?? 'default';
3775
+ if (policyMode === 'suppress')
3776
+ return 'suppressed';
3777
+ if (id === 'runtimeModeSwitching' && this._config.limitations?.autoMitigateRuntimeModeSwitch) {
3778
+ return 'mitigated';
3779
+ }
3780
+ return 'active';
3781
+ }
3782
+ getKnownLimitations() {
3783
+ return this._getKnownLimitationDefinitions().map((limitation) => {
3784
+ const mode = this._config.limitations?.policies?.[limitation.id]?.mode ?? 'default';
3785
+ return {
3786
+ ...limitation,
3787
+ mode,
3788
+ status: this._evaluateLimitationStatus(limitation.id),
3789
+ };
3790
+ });
3791
+ }
3792
+ setLimitationPolicies(policies) {
3793
+ const next = {
3794
+ ...(this._config.limitations?.policies || {}),
3795
+ ...policies,
3796
+ };
3797
+ this.updateConfig({
3798
+ limitations: {
3799
+ ...(this._config.limitations || { autoMitigateRuntimeModeSwitch: true, policies: {} }),
3800
+ policies: next,
3801
+ },
3802
+ });
3803
+ this._track('limitation', 'policiesUpdated', { policies: next });
3804
+ }
3805
+ getTrackingSnapshot() {
3806
+ return {
3807
+ events: [...this._tracking.events],
3808
+ styles: [...this._tracking.styles],
3809
+ limitations: [...this._tracking.limitations],
3810
+ };
3811
+ }
3812
+ clearTracking(source) {
3813
+ if (!source || source === 'all') {
3814
+ this._tracking.events = [];
3815
+ this._tracking.styles = [];
3816
+ this._tracking.limitations = [];
3817
+ return;
3818
+ }
3819
+ if (source === 'event')
3820
+ this._tracking.events = [];
3821
+ if (source === 'style')
3822
+ this._tracking.styles = [];
3823
+ if (source === 'limitation')
3824
+ this._tracking.limitations = [];
3825
+ }
3826
+ getCapabilities() {
3827
+ return {
3828
+ styling: {
3829
+ classMap: true,
3830
+ optionRenderer: true,
3831
+ groupHeaderRenderer: true,
3832
+ cssCustomProperties: true,
3833
+ shadowParts: true,
3834
+ globalStyleMirroring: true,
3835
+ },
3836
+ events: {
3837
+ emitted: ['select', 'open', 'close', 'search', 'change', 'loadMore', 'remove', 'clear', 'error', 'diagnostic'],
3838
+ diagnosticEvent: true,
3839
+ },
3840
+ functionality: {
3841
+ multiSelect: true,
3842
+ searchable: true,
3843
+ infiniteScroll: true,
3844
+ loadMore: true,
3845
+ clearControl: true,
3846
+ groupedItems: true,
3847
+ serverSideSelection: true,
3848
+ runtimeModeSwitchMitigation: Boolean(this._config.limitations?.autoMitigateRuntimeModeSwitch),
3849
+ },
3850
+ limitations: this.getKnownLimitations(),
3851
+ };
3651
3852
  }
3652
3853
  _emitChange() {
3653
3854
  const selectedItems = Array.from(this._state.selectedItems.values());
@@ -3665,6 +3866,7 @@ class EnhancedSelect extends HTMLElement {
3665
3866
  set optionRenderer(renderer) {
3666
3867
  this._optionRenderer = renderer;
3667
3868
  this._setGlobalStylesMirroring(Boolean(renderer || this._classMap));
3869
+ this._track('style', 'optionRendererChanged', { enabled: Boolean(renderer) });
3668
3870
  this._renderOptions();
3669
3871
  }
3670
3872
  /**
@@ -3829,7 +4031,22 @@ class EnhancedSelect extends HTMLElement {
3829
4031
  * Update component configuration
3830
4032
  */
3831
4033
  updateConfig(config) {
3832
- this._config = selectConfig.mergeWithComponentConfig(config);
4034
+ const previousMode = this._config.selection.mode;
4035
+ this._config = this._mergeConfig(this._config, config);
4036
+ if (previousMode !== this._config.selection.mode &&
4037
+ this._config.limitations?.autoMitigateRuntimeModeSwitch) {
4038
+ this.clear();
4039
+ this._track('limitation', 'runtimeModeSwitchMitigated', {
4040
+ from: previousMode,
4041
+ to: this._config.selection.mode,
4042
+ });
4043
+ }
4044
+ else if (previousMode !== this._config.selection.mode) {
4045
+ this._track('limitation', 'runtimeModeSwitchDetected', {
4046
+ from: previousMode,
4047
+ to: this._config.selection.mode,
4048
+ });
4049
+ }
3833
4050
  // Update input state based on new config
3834
4051
  if (this._input) {
3835
4052
  this._input.readOnly = !this._config.searchable;
@@ -3857,6 +4074,22 @@ class EnhancedSelect extends HTMLElement {
3857
4074
  this._syncClearControlState();
3858
4075
  this._renderOptions();
3859
4076
  }
4077
+ _mergeConfig(target, source) {
4078
+ const result = { ...target };
4079
+ for (const key in source) {
4080
+ if (!Object.prototype.hasOwnProperty.call(source, key))
4081
+ continue;
4082
+ const sourceValue = source[key];
4083
+ const targetValue = result[key];
4084
+ if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue)) {
4085
+ result[key] = this._mergeConfig(targetValue && typeof targetValue === 'object' ? targetValue : {}, sourceValue);
4086
+ }
4087
+ else {
4088
+ result[key] = sourceValue;
4089
+ }
4090
+ }
4091
+ return result;
4092
+ }
3860
4093
  _handleClearControlClick() {
3861
4094
  const shouldClearSelection = this._config.clearControl.clearSelection !== false;
3862
4095
  const shouldClearSearch = this._config.clearControl.clearSearch !== false;