@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.umd.js CHANGED
@@ -1407,6 +1407,18 @@
1407
1407
  icon: '×',
1408
1408
  },
1409
1409
  callbacks: {},
1410
+ tracking: {
1411
+ enabled: false,
1412
+ events: true,
1413
+ styling: true,
1414
+ limitations: true,
1415
+ emitDiagnostics: false,
1416
+ maxEntries: 200,
1417
+ },
1418
+ limitations: {
1419
+ policies: {},
1420
+ autoMitigateRuntimeModeSwitch: true,
1421
+ },
1410
1422
  enabled: true,
1411
1423
  searchable: false,
1412
1424
  placeholder: 'Select an option...',
@@ -1538,6 +1550,8 @@
1538
1550
  padding: var(--select-option-padding, 8px 12px);
1539
1551
  cursor: pointer;
1540
1552
  user-select: none;
1553
+ color: var(--select-option-color, var(--select-text-color, #1f2937));
1554
+ background: var(--select-option-bg, var(--select-dropdown-bg, var(--select-bg, white)));
1541
1555
  transition: var(--select-option-transition, background-color 0.2s ease);
1542
1556
  border: var(--select-option-border, none);
1543
1557
  border-bottom: var(--select-option-border-bottom, none);
@@ -1582,9 +1596,20 @@
1582
1596
 
1583
1597
  .option-content {
1584
1598
  flex: 1;
1585
- overflow: hidden;
1586
- text-overflow: ellipsis;
1587
- white-space: nowrap;
1599
+ overflow: var(--select-option-content-overflow, hidden);
1600
+ text-overflow: var(--select-option-content-text-overflow, ellipsis);
1601
+ white-space: var(--select-option-content-white-space, nowrap);
1602
+ }
1603
+
1604
+ .checkmark-icon {
1605
+ display: none;
1606
+ margin-left: var(--select-checkmark-margin-left, 8px);
1607
+ color: var(--select-checkmark-color, currentColor);
1608
+ }
1609
+
1610
+ :host([aria-selected="true"]) .checkmark-icon,
1611
+ .option-container.selected .checkmark-icon {
1612
+ display: inline-flex;
1588
1613
  }
1589
1614
 
1590
1615
  .remove-button {
@@ -1712,16 +1737,6 @@
1712
1737
  <path d="M4 8.5L6.5 11L12 5.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1713
1738
  </svg>
1714
1739
  `;
1715
- // Visibility control via CSS or inline style
1716
- // We set it to display: none unless selected.
1717
- // User can override this behavior via part styling if they want transitions
1718
- if (!selected) {
1719
- checkmark.style.display = 'none';
1720
- }
1721
- else {
1722
- checkmark.style.marginLeft = '8px';
1723
- checkmark.style.color = 'currentColor';
1724
- }
1725
1740
  this._container.appendChild(checkmark);
1726
1741
  }
1727
1742
  // Data Attributes Contract on Host
@@ -1880,6 +1895,10 @@
1880
1895
  set classMap(map) {
1881
1896
  this._classMap = map;
1882
1897
  this._setGlobalStylesMirroring(Boolean(this._optionRenderer || map || this._groupHeaderRenderer));
1898
+ this._track('style', 'classMapChanged', {
1899
+ hasClassMap: Boolean(map),
1900
+ keys: map ? Object.keys(map) : [],
1901
+ });
1883
1902
  if (!this.isConnected)
1884
1903
  return;
1885
1904
  this._renderOptions();
@@ -1895,6 +1914,7 @@
1895
1914
  set groupHeaderRenderer(renderer) {
1896
1915
  this._groupHeaderRenderer = renderer;
1897
1916
  this._setGlobalStylesMirroring(Boolean(this._optionRenderer || this._classMap || renderer));
1917
+ this._track('style', 'groupHeaderRendererChanged', { enabled: Boolean(renderer) });
1898
1918
  if (!this.isConnected)
1899
1919
  return;
1900
1920
  this._renderOptions();
@@ -1913,6 +1933,7 @@
1913
1933
  this._mirrorGlobalStylesForCustomOptions = false;
1914
1934
  this._globalStylesObserver = null;
1915
1935
  this._globalStylesContainer = null;
1936
+ this._tracking = { events: [], styles: [], limitations: [] };
1916
1937
  this._shadow = this.attachShadow({ mode: 'open' });
1917
1938
  this._uniqueId = `enhanced-select-${Math.random().toString(36).substr(2, 9)}`;
1918
1939
  this._rendererHelpers = this._buildRendererHelpers();
@@ -1959,7 +1980,6 @@
1959
1980
  // Angular's rendering seems to not apply :host styles correctly in some cases
1960
1981
  // Must be done in connectedCallback when element is attached to DOM
1961
1982
  this.style.display = 'block';
1962
- this.style.width = '100%';
1963
1983
  if (this._optionRenderer) {
1964
1984
  this._setGlobalStylesMirroring(true);
1965
1985
  }
@@ -1998,6 +2018,7 @@
1998
2018
  return;
1999
2019
  }
2000
2020
  this._mirrorGlobalStylesForCustomOptions = enabled;
2021
+ this._track('style', 'globalStylesMirroringChanged', { enabled });
2001
2022
  if (enabled) {
2002
2023
  this._setupGlobalStylesMirroring();
2003
2024
  }
@@ -2202,7 +2223,8 @@
2202
2223
  :host {
2203
2224
  display: block;
2204
2225
  position: relative;
2205
- width: 100%;
2226
+ width: var(--select-width, 100%);
2227
+ height: var(--select-height, auto);
2206
2228
  }
2207
2229
 
2208
2230
  .select-container {
@@ -2218,6 +2240,7 @@
2218
2240
  flex-wrap: wrap;
2219
2241
  gap: var(--select-input-gap, 6px);
2220
2242
  padding: var(--select-input-padding, 6px 52px 6px 8px);
2243
+ height: var(--select-input-height, auto);
2221
2244
  min-height: var(--select-input-min-height, 44px);
2222
2245
  max-height: var(--select-input-max-height, 160px);
2223
2246
  overflow-y: var(--select-input-overflow-y, auto);
@@ -2239,17 +2262,17 @@
2239
2262
  content: '';
2240
2263
  position: absolute;
2241
2264
  top: 50%;
2242
- right: var(--select-separator-position, 40px);
2265
+ right: var(--select-separator-position, var(--select-seperator-position, 40px));
2243
2266
  transform: translateY(-50%);
2244
- width: var(--select-separator-width, 1px);
2245
- height: var(--select-separator-height, 60%);
2246
- background: var(--select-separator-bg, var(--select-separator-gradient, linear-gradient(
2267
+ width: var(--select-separator-width, var(--select-seperator-width, 1px));
2268
+ height: var(--select-separator-height, var(--select-seperator-height, 60%));
2269
+ background: var(--select-separator-bg, var(--select-seperator-bg, var(--select-separator-gradient, var(--select-seperator-gradient, linear-gradient(
2247
2270
  to bottom,
2248
2271
  transparent 0%,
2249
2272
  rgba(0, 0, 0, 0.1) 20%,
2250
2273
  rgba(0, 0, 0, 0.1) 80%,
2251
2274
  transparent 100%
2252
- )));
2275
+ ))));
2253
2276
  pointer-events: none;
2254
2277
  z-index: 1;
2255
2278
  }
@@ -2276,7 +2299,7 @@
2276
2299
  }
2277
2300
 
2278
2301
  .input-container.has-clear-control::after {
2279
- right: var(--select-separator-position-with-clear, 72px);
2302
+ right: var(--select-separator-position-with-clear, var(--select-seperator-position-with-clear, 72px));
2280
2303
  }
2281
2304
 
2282
2305
  .dropdown-arrow-container.with-clear-control {
@@ -2354,6 +2377,7 @@
2354
2377
 
2355
2378
  .select-input {
2356
2379
  flex: 1;
2380
+ width: var(--select-input-width, auto);
2357
2381
  min-width: var(--select-input-min-width, 120px);
2358
2382
  padding: var(--select-input-field-padding, 4px);
2359
2383
  border: none;
@@ -2448,6 +2472,7 @@
2448
2472
  font-weight: var(--select-group-header-weight, 600);
2449
2473
  color: var(--select-group-header-color, #6b7280);
2450
2474
  background-color: var(--select-group-header-bg, #f3f4f6);
2475
+ text-align: var(--select-group-header-text-align, left);
2451
2476
  font-size: var(--select-group-header-font-size, 12px);
2452
2477
  text-transform: var(--select-group-header-text-transform, uppercase);
2453
2478
  letter-spacing: var(--select-group-header-letter-spacing, 0.05em);
@@ -2632,10 +2657,25 @@
2632
2657
 
2633
2658
  /* Dark mode - Opt-in via class, data attribute, or ancestor context */
2634
2659
  :host(.dark-mode),
2660
+ :host([dark-mode]),
2661
+ :host([darkmode]),
2635
2662
  :host([data-theme="dark"]),
2663
+ :host([theme="dark"]),
2636
2664
  :host-context(.dark-mode),
2637
2665
  :host-context(.dark),
2638
- :host-context([data-theme="dark"]) {
2666
+ :host-context([dark-mode]),
2667
+ :host-context([darkmode]),
2668
+ :host-context([data-theme="dark"]),
2669
+ :host-context([theme="dark"]) {
2670
+ /* map dark tokens to base option tokens so nested <select-option>
2671
+ components also pick up dark mode via inherited CSS variables */
2672
+ --select-option-bg: var(--select-dark-option-bg, #1f2937);
2673
+ --select-option-color: var(--select-dark-option-color, #f9fafb);
2674
+ --select-option-hover-bg: var(--select-dark-option-hover-bg, #374151);
2675
+ --select-option-hover-color: var(--select-dark-option-hover-color, #f9fafb);
2676
+ --select-option-selected-bg: var(--select-dark-option-selected-bg, #3730a3);
2677
+ --select-option-selected-color: var(--select-dark-option-selected-text, #e0e7ff);
2678
+
2639
2679
  .input-container {
2640
2680
  background: var(--select-dark-bg, #1f2937);
2641
2681
  border-color: var(--select-dark-border, #4b5563);
@@ -2688,6 +2728,8 @@
2688
2728
 
2689
2729
  .option.active:not(.selected) {
2690
2730
  background-color: var(--select-dark-option-active-bg, #374151);
2731
+ color: var(--select-dark-option-active-color, #f9fafb);
2732
+ outline: var(--select-dark-option-active-outline, 2px solid rgba(129, 140, 248, 0.55));
2691
2733
  }
2692
2734
 
2693
2735
  /* Group header in dark mode */
@@ -2695,9 +2737,6 @@
2695
2737
  color: var(--select-dark-group-header-color, var(--select-group-header-color, #6b7280));
2696
2738
  background-color: var(--select-dark-group-header-bg, var(--select-group-header-bg, #374151));
2697
2739
  }
2698
- color: var(--select-dark-option-active-color, #f9fafb);
2699
- outline: var(--select-dark-option-active-outline, 2px solid rgba(129, 140, 248, 0.55));
2700
- }
2701
2740
 
2702
2741
  .option.selected.active {
2703
2742
  background-color: var(--select-dark-option-selected-active-bg, var(--select-dark-option-selected-bg, #3730a3));
@@ -2819,7 +2858,13 @@
2819
2858
  this._handleOpen();
2820
2859
  }
2821
2860
  else {
2822
- // clicking the input while open should close the dropdown too
2861
+ // Keep open while interacting directly with the input so users can
2862
+ // place cursor/type without accidental collapse.
2863
+ if (target === this._input) {
2864
+ this._input.focus();
2865
+ return;
2866
+ }
2867
+ // clicking other parts of the input container while open toggles close
2823
2868
  this._handleClose();
2824
2869
  }
2825
2870
  // Focus the input (do not prevent default behavior)
@@ -3654,6 +3699,162 @@
3654
3699
  }
3655
3700
  _emit(name, detail) {
3656
3701
  this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
3702
+ if (name !== 'diagnostic') {
3703
+ this._track('event', String(name), detail);
3704
+ }
3705
+ }
3706
+ _track(source, name, detail) {
3707
+ const cfg = this._config.tracking;
3708
+ if (!cfg?.enabled)
3709
+ return;
3710
+ if (source === 'event' && !cfg.events)
3711
+ return;
3712
+ if (source === 'style' && !cfg.styling)
3713
+ return;
3714
+ if (source === 'limitation' && !cfg.limitations)
3715
+ return;
3716
+ const entry = {
3717
+ timestamp: Date.now(),
3718
+ source,
3719
+ name,
3720
+ detail,
3721
+ };
3722
+ const bucket = source === 'event'
3723
+ ? this._tracking.events
3724
+ : source === 'style'
3725
+ ? this._tracking.styles
3726
+ : this._tracking.limitations;
3727
+ bucket.push(entry);
3728
+ const maxEntries = Math.max(10, cfg.maxEntries || 200);
3729
+ if (bucket.length > maxEntries) {
3730
+ bucket.splice(0, bucket.length - maxEntries);
3731
+ }
3732
+ if (cfg.emitDiagnostics) {
3733
+ this.dispatchEvent(new CustomEvent('diagnostic', {
3734
+ detail: entry,
3735
+ bubbles: true,
3736
+ composed: true,
3737
+ }));
3738
+ }
3739
+ }
3740
+ _getKnownLimitationDefinitions() {
3741
+ return [
3742
+ {
3743
+ id: 'variableItemHeight',
3744
+ title: 'Variable item height',
3745
+ description: 'Virtualization assumes fixed or estimated item heights; fully dynamic heights are not yet supported.',
3746
+ workaround: 'Use consistent item heights or set estimatedItemHeight to your dominant row size.',
3747
+ },
3748
+ {
3749
+ id: 'builtInFetchPaginationApi',
3750
+ title: 'Built-in fetch/pagination API',
3751
+ description: 'Core does not include a built-in fetchUrl/searchUrl pagination transport.',
3752
+ workaround: 'Use onSearch/onLoadMore callbacks and update data via setItems().',
3753
+ },
3754
+ {
3755
+ id: 'virtualizationOverheadSmallLists',
3756
+ title: 'Virtualization overhead for small lists',
3757
+ description: 'Virtualization can add slight overhead on very small lists.',
3758
+ workaround: 'Disable virtualization for tiny datasets when micro-latency is critical.',
3759
+ },
3760
+ {
3761
+ id: 'runtimeModeSwitching',
3762
+ title: 'Runtime single/multi mode switching',
3763
+ description: 'Switching between single and multi mode can require state reset for consistency.',
3764
+ workaround: 'Enable autoMitigateRuntimeModeSwitch or recreate/reset component state when toggling modes.',
3765
+ },
3766
+ {
3767
+ id: 'legacyBrowserSupport',
3768
+ title: 'Legacy browser support',
3769
+ description: 'Official support targets modern evergreen browsers.',
3770
+ },
3771
+ {
3772
+ id: 'webkitArchLinux',
3773
+ title: 'Playwright WebKit on Arch-based Linux',
3774
+ description: 'Native WebKit Playwright bundle depends on unavailable legacy system libraries on Arch-based distros.',
3775
+ workaround: 'Run WebKit E2E tests via Playwright Docker image.',
3776
+ },
3777
+ ];
3778
+ }
3779
+ _evaluateLimitationStatus(id) {
3780
+ const policyMode = this._config.limitations?.policies?.[id]?.mode ?? 'default';
3781
+ if (policyMode === 'suppress')
3782
+ return 'suppressed';
3783
+ if (id === 'runtimeModeSwitching' && this._config.limitations?.autoMitigateRuntimeModeSwitch) {
3784
+ return 'mitigated';
3785
+ }
3786
+ return 'active';
3787
+ }
3788
+ getKnownLimitations() {
3789
+ return this._getKnownLimitationDefinitions().map((limitation) => {
3790
+ const mode = this._config.limitations?.policies?.[limitation.id]?.mode ?? 'default';
3791
+ return {
3792
+ ...limitation,
3793
+ mode,
3794
+ status: this._evaluateLimitationStatus(limitation.id),
3795
+ };
3796
+ });
3797
+ }
3798
+ setLimitationPolicies(policies) {
3799
+ const next = {
3800
+ ...(this._config.limitations?.policies || {}),
3801
+ ...policies,
3802
+ };
3803
+ this.updateConfig({
3804
+ limitations: {
3805
+ ...(this._config.limitations || { autoMitigateRuntimeModeSwitch: true, policies: {} }),
3806
+ policies: next,
3807
+ },
3808
+ });
3809
+ this._track('limitation', 'policiesUpdated', { policies: next });
3810
+ }
3811
+ getTrackingSnapshot() {
3812
+ return {
3813
+ events: [...this._tracking.events],
3814
+ styles: [...this._tracking.styles],
3815
+ limitations: [...this._tracking.limitations],
3816
+ };
3817
+ }
3818
+ clearTracking(source) {
3819
+ if (!source || source === 'all') {
3820
+ this._tracking.events = [];
3821
+ this._tracking.styles = [];
3822
+ this._tracking.limitations = [];
3823
+ return;
3824
+ }
3825
+ if (source === 'event')
3826
+ this._tracking.events = [];
3827
+ if (source === 'style')
3828
+ this._tracking.styles = [];
3829
+ if (source === 'limitation')
3830
+ this._tracking.limitations = [];
3831
+ }
3832
+ getCapabilities() {
3833
+ return {
3834
+ styling: {
3835
+ classMap: true,
3836
+ optionRenderer: true,
3837
+ groupHeaderRenderer: true,
3838
+ cssCustomProperties: true,
3839
+ shadowParts: true,
3840
+ globalStyleMirroring: true,
3841
+ },
3842
+ events: {
3843
+ emitted: ['select', 'open', 'close', 'search', 'change', 'loadMore', 'remove', 'clear', 'error', 'diagnostic'],
3844
+ diagnosticEvent: true,
3845
+ },
3846
+ functionality: {
3847
+ multiSelect: true,
3848
+ searchable: true,
3849
+ infiniteScroll: true,
3850
+ loadMore: true,
3851
+ clearControl: true,
3852
+ groupedItems: true,
3853
+ serverSideSelection: true,
3854
+ runtimeModeSwitchMitigation: Boolean(this._config.limitations?.autoMitigateRuntimeModeSwitch),
3855
+ },
3856
+ limitations: this.getKnownLimitations(),
3857
+ };
3657
3858
  }
3658
3859
  _emitChange() {
3659
3860
  const selectedItems = Array.from(this._state.selectedItems.values());
@@ -3671,6 +3872,7 @@
3671
3872
  set optionRenderer(renderer) {
3672
3873
  this._optionRenderer = renderer;
3673
3874
  this._setGlobalStylesMirroring(Boolean(renderer || this._classMap));
3875
+ this._track('style', 'optionRendererChanged', { enabled: Boolean(renderer) });
3674
3876
  this._renderOptions();
3675
3877
  }
3676
3878
  /**
@@ -3835,7 +4037,22 @@
3835
4037
  * Update component configuration
3836
4038
  */
3837
4039
  updateConfig(config) {
3838
- this._config = selectConfig.mergeWithComponentConfig(config);
4040
+ const previousMode = this._config.selection.mode;
4041
+ this._config = this._mergeConfig(this._config, config);
4042
+ if (previousMode !== this._config.selection.mode &&
4043
+ this._config.limitations?.autoMitigateRuntimeModeSwitch) {
4044
+ this.clear();
4045
+ this._track('limitation', 'runtimeModeSwitchMitigated', {
4046
+ from: previousMode,
4047
+ to: this._config.selection.mode,
4048
+ });
4049
+ }
4050
+ else if (previousMode !== this._config.selection.mode) {
4051
+ this._track('limitation', 'runtimeModeSwitchDetected', {
4052
+ from: previousMode,
4053
+ to: this._config.selection.mode,
4054
+ });
4055
+ }
3839
4056
  // Update input state based on new config
3840
4057
  if (this._input) {
3841
4058
  this._input.readOnly = !this._config.searchable;
@@ -3863,6 +4080,22 @@
3863
4080
  this._syncClearControlState();
3864
4081
  this._renderOptions();
3865
4082
  }
4083
+ _mergeConfig(target, source) {
4084
+ const result = { ...target };
4085
+ for (const key in source) {
4086
+ if (!Object.prototype.hasOwnProperty.call(source, key))
4087
+ continue;
4088
+ const sourceValue = source[key];
4089
+ const targetValue = result[key];
4090
+ if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue)) {
4091
+ result[key] = this._mergeConfig(targetValue && typeof targetValue === 'object' ? targetValue : {}, sourceValue);
4092
+ }
4093
+ else {
4094
+ result[key] = sourceValue;
4095
+ }
4096
+ }
4097
+ return result;
4098
+ }
3866
4099
  _handleClearControlClick() {
3867
4100
  const shouldClearSelection = this._config.clearControl.clearSelection !== false;
3868
4101
  const shouldClearSearch = this._config.clearControl.clearSearch !== false;