mdas-jsview-sdk 1.0.23-uat.0 → 1.0.26-uat.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.
@@ -2857,8 +2857,36 @@ const TimeSalesStyles = `
2857
2857
  }
2858
2858
  `;
2859
2859
 
2860
+ /**
2861
+ * @deprecated SharedStyles is deprecated and will be removed in a future version.
2862
+ *
2863
+ * Please migrate to BaseStyles + CommonWidgetPatterns:
2864
+ *
2865
+ * BEFORE:
2866
+ * import { SharedStyles } from './styles/index.js';
2867
+ * export const MyWidgetStyles = `${SharedStyles} ...widget styles...`;
2868
+ *
2869
+ * AFTER:
2870
+ * import { BaseStyles } from './styles/BaseStyles.js';
2871
+ * import { getLoadingOverlayStyles, getErrorStyles } from './styles/CommonWidgetPatterns.js';
2872
+ * export const MyWidgetStyles = `
2873
+ * ${BaseStyles}
2874
+ * ${getLoadingOverlayStyles('my-widget')}
2875
+ * ${getErrorStyles('my-widget')}
2876
+ * ...widget-specific styles...
2877
+ * `;
2878
+ *
2879
+ * Benefits of migration:
2880
+ * - Proper CSS scoping (no global conflicts)
2881
+ * - Reduced code duplication
2882
+ * - Better maintainability
2883
+ * - Smaller bundle size
2884
+ *
2885
+ * See STYLING_CUSTOMIZATION_GUIDE.md for details.
2886
+ */
2860
2887
  const SharedStyles = `
2861
2888
  /* ========================================
2889
+ ⚠️ DEPRECATED - Use BaseStyles.js instead
2862
2890
  FONT SIZE SYSTEM - CSS Variables
2863
2891
  Adjust --mdas-base-font-size to scale all fonts
2864
2892
  ======================================== */
@@ -6638,6 +6666,15 @@ const OptionChainTemplate = `
6638
6666
  </select>
6639
6667
  <button class="fetch-button" disabled>Search</button>
6640
6668
  </div>
6669
+ <div class="filter-section">
6670
+ <label for="strike-filter" class="filter-label">Display:</label>
6671
+ <select class="strike-filter" id="strike-filter">
6672
+ <option value="5">Near the money (±5 strikes)</option>
6673
+ <option value="10">Near the money (±10 strikes)</option>
6674
+ <option value="15">Near the money (±15 strikes)</option>
6675
+ <option value="all">All strikes</option>
6676
+ </select>
6677
+ </div>
6641
6678
  </div>
6642
6679
 
6643
6680
  <!-- Data Grid Section -->
@@ -6694,11 +6731,383 @@ const OptionChainTemplate = `
6694
6731
  </div>
6695
6732
  `;
6696
6733
 
6734
+ // src/widgets/styles/BaseStyles.js
6735
+
6736
+ /**
6737
+ * Base Styles - CSS Variables and Universal Utilities
6738
+ *
6739
+ * This file contains:
6740
+ * 1. CSS Custom Properties (variables) for consistent theming
6741
+ * 2. Responsive font sizing system
6742
+ * 3. Optional utility classes
6743
+ *
6744
+ * Usage:
6745
+ * import { BaseStyles } from './styles/BaseStyles';
6746
+ * export const MyWidgetStyles = `${BaseStyles} ...widget styles...`;
6747
+ */
6748
+
6749
+ const BaseStyles = `
6750
+ /* ============================================
6751
+ MDAS WIDGET CSS VARIABLES
6752
+ ============================================ */
6753
+
6754
+ :root {
6755
+ /* --- FONT SIZE SYSTEM --- */
6756
+ /* Base font size - all other sizes are relative to this */
6757
+ --mdas-base-font-size: 14px;
6758
+
6759
+ /* Component-specific font sizes (em units scale with base) */
6760
+ --mdas-small-text-size: 0.79em; /* 11px at 14px base */
6761
+ --mdas-medium-text-size: 0.93em; /* 13px at 14px base */
6762
+ --mdas-large-text-size: 1.14em; /* 16px at 14px base */
6763
+
6764
+ /* Widget element sizes */
6765
+ --mdas-company-name-size: 1.43em; /* 20px at 14px base */
6766
+ --mdas-symbol-size: 1.79em; /* 25px at 14px base */
6767
+ --mdas-price-size: 2.29em; /* 32px at 14px base */
6768
+ --mdas-change-size: 1.14em; /* 16px at 14px base */
6769
+
6770
+ /* Data display sizes */
6771
+ --mdas-label-size: 0.86em; /* 12px at 14px base */
6772
+ --mdas-value-size: 1em; /* 14px at 14px base */
6773
+ --mdas-footer-size: 0.79em; /* 11px at 14px base */
6774
+
6775
+ /* Chart-specific sizes */
6776
+ --mdas-chart-title-size: 1.29em; /* 18px at 14px base */
6777
+ --mdas-chart-label-size: 0.93em; /* 13px at 14px base */
6778
+ --mdas-chart-value-size: 1.43em; /* 20px at 14px base */
6779
+
6780
+ /* --- COLOR PALETTE (for future theming support) --- */
6781
+ /* Primary colors */
6782
+ --mdas-primary-color: #3b82f6;
6783
+ --mdas-primary-hover: #2563eb;
6784
+
6785
+ /* Status colors */
6786
+ --mdas-color-positive: #059669;
6787
+ --mdas-color-positive-bg: #d1fae5;
6788
+ --mdas-color-negative: #dc2626;
6789
+ --mdas-color-negative-bg: #fee2e2;
6790
+ --mdas-color-neutral: #6b7280;
6791
+
6792
+ /* Background colors */
6793
+ --mdas-bg-primary: #ffffff;
6794
+ --mdas-bg-secondary: #f9fafb;
6795
+ --mdas-bg-tertiary: #f3f4f6;
6796
+
6797
+ /* Text colors */
6798
+ --mdas-text-primary: #111827;
6799
+ --mdas-text-secondary: #6b7280;
6800
+ --mdas-text-tertiary: #9ca3af;
6801
+
6802
+ /* Border colors */
6803
+ --mdas-border-primary: #e5e7eb;
6804
+ --mdas-border-secondary: #d1d5db;
6805
+
6806
+ /* --- SPACING SYSTEM --- */
6807
+ --mdas-spacing-xs: 4px;
6808
+ --mdas-spacing-sm: 8px;
6809
+ --mdas-spacing-md: 16px;
6810
+ --mdas-spacing-lg: 24px;
6811
+ --mdas-spacing-xl: 32px;
6812
+
6813
+ /* --- EFFECTS --- */
6814
+ --mdas-border-radius: 8px;
6815
+ --mdas-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
6816
+ --mdas-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
6817
+ --mdas-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
6818
+
6819
+ /* --- Z-INDEX LAYERS --- */
6820
+ --mdas-z-base: 1;
6821
+ --mdas-z-sticky: 10;
6822
+ --mdas-z-overlay: 100;
6823
+ --mdas-z-modal: 10000;
6824
+ }
6825
+
6826
+ /* ============================================
6827
+ RESPONSIVE FONT SCALING
6828
+ ============================================ */
6829
+
6830
+ /* Tablet breakpoint - slightly smaller fonts */
6831
+ @media (max-width: 768px) {
6832
+ :root {
6833
+ --mdas-base-font-size: 13px;
6834
+ }
6835
+ }
6836
+
6837
+ /* Mobile breakpoint - smaller fonts for small screens */
6838
+ @media (max-width: 480px) {
6839
+ :root {
6840
+ --mdas-base-font-size: 12px;
6841
+ }
6842
+ }
6843
+
6844
+ /* ============================================
6845
+ OPTIONAL UTILITY CLASSES
6846
+ Use these classes for consistent styling
6847
+ ============================================ */
6848
+
6849
+ /* Widget card styling - apply to widget root element */
6850
+ .mdas-card {
6851
+ background: var(--mdas-bg-primary);
6852
+ border-radius: var(--mdas-border-radius);
6853
+ padding: var(--mdas-spacing-lg);
6854
+ box-shadow: var(--mdas-shadow-md);
6855
+ border: 1px solid var(--mdas-border-primary);
6856
+ }
6857
+
6858
+ /* Responsive container */
6859
+ .mdas-container {
6860
+ width: 100%;
6861
+ max-width: 1400px;
6862
+ margin: 0 auto;
6863
+ }
6864
+
6865
+ /* Text utilities */
6866
+ .mdas-text-primary {
6867
+ color: var(--mdas-text-primary);
6868
+ }
6869
+
6870
+ .mdas-text-secondary {
6871
+ color: var(--mdas-text-secondary);
6872
+ }
6873
+
6874
+ .mdas-text-muted {
6875
+ color: var(--mdas-text-tertiary);
6876
+ }
6877
+
6878
+ /* Status colors */
6879
+ .mdas-positive {
6880
+ color: var(--mdas-color-positive);
6881
+ }
6882
+
6883
+ .mdas-negative {
6884
+ color: var(--mdas-color-negative);
6885
+ }
6886
+
6887
+ .mdas-neutral {
6888
+ color: var(--mdas-color-neutral);
6889
+ }
6890
+ `;
6891
+
6892
+ // src/widgets/styles/CommonWidgetPatterns.js
6893
+
6894
+ /**
6895
+ * Common Widget Patterns - Reusable Style Functions
6896
+ *
6897
+ * These functions generate properly scoped CSS for common widget patterns.
6898
+ * Each function takes a widget class name and returns scoped CSS.
6899
+ *
6900
+ * Benefits:
6901
+ * - Eliminates code duplication
6902
+ * - Ensures proper scoping (no global conflicts)
6903
+ * - Consistent styling across widgets
6904
+ * - Easy to maintain and update
6905
+ *
6906
+ * Usage:
6907
+ * import { getLoadingOverlayStyles } from './CommonWidgetPatterns';
6908
+ * const styles = `${getLoadingOverlayStyles('my-widget')} ...other styles...`;
6909
+ */
6910
+
6911
+ /**
6912
+ * Generate loading overlay styles for a widget
6913
+ * @param {string} widgetClass - Widget class name (e.g., 'option-chain-widget')
6914
+ * @returns {string} Scoped CSS for loading overlay
6915
+ */
6916
+ const getLoadingOverlayStyles = widgetClass => `
6917
+ /* Loading Overlay for ${widgetClass} */
6918
+ .${widgetClass} .widget-loading-overlay {
6919
+ position: absolute;
6920
+ top: 0;
6921
+ left: 0;
6922
+ right: 0;
6923
+ bottom: 0;
6924
+ background: rgba(255, 255, 255, 0.95);
6925
+ backdrop-filter: blur(2px);
6926
+ display: flex;
6927
+ flex-direction: column;
6928
+ align-items: center;
6929
+ justify-content: center;
6930
+ z-index: var(--mdas-z-overlay, 100);
6931
+ border-radius: var(--mdas-border-radius, 12px);
6932
+ }
6933
+
6934
+ .${widgetClass} .widget-loading-overlay.hidden {
6935
+ display: none;
6936
+ }
6937
+
6938
+ .${widgetClass} .loading-content {
6939
+ display: flex;
6940
+ flex-direction: column;
6941
+ align-items: center;
6942
+ gap: 12px;
6943
+ }
6944
+
6945
+ .${widgetClass} .loading-spinner {
6946
+ width: 40px;
6947
+ height: 40px;
6948
+ border: 4px solid var(--mdas-border-primary, #e5e7eb);
6949
+ border-top-color: var(--mdas-primary-color, #3b82f6);
6950
+ border-radius: 50%;
6951
+ animation: ${widgetClass}-spin 1s linear infinite;
6952
+ }
6953
+
6954
+ .${widgetClass} .loading-text {
6955
+ color: var(--mdas-text-secondary, #6b7280);
6956
+ font-size: var(--mdas-medium-text-size, 0.93em);
6957
+ font-weight: 500;
6958
+ }
6959
+
6960
+ @keyframes ${widgetClass}-spin {
6961
+ to { transform: rotate(360deg); }
6962
+ }
6963
+ `;
6964
+
6965
+ /**
6966
+ * Generate widget error display styles
6967
+ * @param {string} widgetClass - Widget class name
6968
+ * @returns {string} Scoped CSS for error display
6969
+ */
6970
+ const getErrorStyles = widgetClass => `
6971
+ /* Error Display for ${widgetClass} */
6972
+ .${widgetClass} .widget-error {
6973
+ padding: 16px 20px;
6974
+ background: var(--mdas-color-negative-bg, #fee2e2);
6975
+ border: 1px solid #fecaca;
6976
+ border-radius: var(--mdas-border-radius, 8px);
6977
+ color: var(--mdas-color-negative, #dc2626);
6978
+ font-size: var(--mdas-medium-text-size, 0.93em);
6979
+ margin: 16px;
6980
+ text-align: center;
6981
+ }
6982
+
6983
+ .${widgetClass} .widget-error-container {
6984
+ display: flex;
6985
+ flex-direction: column;
6986
+ align-items: center;
6987
+ gap: 12px;
6988
+ padding: 24px;
6989
+ }
6990
+ `;
6991
+
6992
+ /**
6993
+ * Generate widget footer styles
6994
+ * @param {string} widgetClass - Widget class name
6995
+ * @returns {string} Scoped CSS for widget footer
6996
+ */
6997
+ const getFooterStyles = widgetClass => `
6998
+ /* Footer for ${widgetClass} */
6999
+ .${widgetClass} .widget-footer {
7000
+ display: flex;
7001
+ justify-content: space-between;
7002
+ align-items: center;
7003
+ padding: 8px 16px;
7004
+ background: var(--mdas-bg-secondary, #f9fafb);
7005
+ border-top: 1px solid var(--mdas-border-primary, #e5e7eb);
7006
+ font-size: var(--mdas-footer-size, 0.79em);
7007
+ color: var(--mdas-text-secondary, #6b7280);
7008
+ }
7009
+
7010
+ .${widgetClass} .widget-footer .last-update {
7011
+ color: var(--mdas-text-tertiary, #9ca3af);
7012
+ }
7013
+
7014
+ .${widgetClass} .widget-footer .data-source {
7015
+ color: var(--mdas-text-secondary, #6b7280);
7016
+ font-weight: 500;
7017
+ }
7018
+ `;
7019
+
7020
+ /**
7021
+ * Generate no-data state styles
7022
+ * @param {string} widgetClass - Widget class name
7023
+ * @returns {string} Scoped CSS for no-data state
7024
+ */
7025
+ const getNoDataStyles = widgetClass => `
7026
+ /* No Data State for ${widgetClass} */
7027
+ .${widgetClass} .no-data-state {
7028
+ display: flex;
7029
+ flex-direction: column;
7030
+ align-items: center;
7031
+ justify-content: center;
7032
+ padding: 40px 20px;
7033
+ text-align: center;
7034
+ color: var(--mdas-text-secondary, #6b7280);
7035
+ }
7036
+
7037
+ .${widgetClass} .no-data-content {
7038
+ max-width: 400px;
7039
+ display: flex;
7040
+ flex-direction: column;
7041
+ align-items: center;
7042
+ gap: 16px;
7043
+ }
7044
+
7045
+ .${widgetClass} .no-data-icon {
7046
+ width: 64px;
7047
+ height: 64px;
7048
+ display: flex;
7049
+ align-items: center;
7050
+ justify-content: center;
7051
+ opacity: 0.5;
7052
+ }
7053
+
7054
+ .${widgetClass} .no-data-icon svg {
7055
+ width: 100%;
7056
+ height: 100%;
7057
+ }
7058
+
7059
+ .${widgetClass} .no-data-title {
7060
+ font-size: var(--mdas-large-text-size, 1.14em);
7061
+ font-weight: 600;
7062
+ color: var(--mdas-text-primary, #111827);
7063
+ margin: 0;
7064
+ }
7065
+
7066
+ .${widgetClass} .no-data-description {
7067
+ font-size: var(--mdas-medium-text-size, 0.93em);
7068
+ color: var(--mdas-text-secondary, #6b7280);
7069
+ margin: 0;
7070
+ line-height: 1.5;
7071
+ }
7072
+
7073
+ .${widgetClass} .no-data-description strong {
7074
+ color: var(--mdas-text-primary, #111827);
7075
+ font-weight: 600;
7076
+ }
7077
+
7078
+ .${widgetClass} .no-data-guidance {
7079
+ font-size: var(--mdas-small-text-size, 0.79em);
7080
+ color: var(--mdas-text-tertiary, #9ca3af);
7081
+ margin: 0;
7082
+ }
7083
+
7084
+ /* Error variant of no-data state */
7085
+ .${widgetClass} .no-data-state.error-access {
7086
+ color: var(--mdas-color-negative, #dc2626);
7087
+ }
7088
+
7089
+ .${widgetClass} .no-data-state.error-access .no-data-title {
7090
+ color: var(--mdas-color-negative, #dc2626);
7091
+ }
7092
+
7093
+ .${widgetClass} .no-data-state.error-access .no-data-icon {
7094
+ opacity: 0.8;
7095
+ }
7096
+
7097
+ .${widgetClass} .no-data-state.error-access .no-data-icon svg circle[fill] {
7098
+ fill: var(--mdas-color-negative, #dc2626);
7099
+ }
7100
+ `;
7101
+
6697
7102
  // src/widgets/styles/OptionChainStyles.js
6698
7103
  const OptionChainStyles = `
6699
- ${SharedStyles}
7104
+ ${BaseStyles}
7105
+ ${getLoadingOverlayStyles('option-chain-widget')}
7106
+ ${getErrorStyles('option-chain-widget')}
7107
+ ${getFooterStyles('option-chain-widget')}
7108
+ ${getNoDataStyles('option-chain-widget')}
6700
7109
 
6701
- /* Base styles remain the same until responsive section */
7110
+ /* Option Chain Widget Specific Styles */
6702
7111
  .option-chain-widget {
6703
7112
  border: 1px solid #e5e7eb;
6704
7113
  border-radius: 8px;
@@ -6723,8 +7132,22 @@ ${SharedStyles}
6723
7132
  flex-wrap: wrap;
6724
7133
  }
6725
7134
 
7135
+ .option-chain-widget .filter-section {
7136
+ display: flex;
7137
+ gap: 8px;
7138
+ align-items: center;
7139
+ margin-top: 8px;
7140
+ }
7141
+
7142
+ .option-chain-widget .filter-label {
7143
+ font-size: 13px;
7144
+ font-weight: 500;
7145
+ color: #374151;
7146
+ }
7147
+
6726
7148
  .option-chain-widget .symbol-input,
6727
- .option-chain-widget .date-select {
7149
+ .option-chain-widget .date-select,
7150
+ .option-chain-widget .strike-filter {
6728
7151
  padding: 8px 12px;
6729
7152
  border: 1px solid #d1d5db;
6730
7153
  border-radius: 4px;
@@ -6745,6 +7168,12 @@ ${SharedStyles}
6745
7168
  background: white;
6746
7169
  }
6747
7170
 
7171
+ .option-chain-widget .strike-filter {
7172
+ min-width: 200px;
7173
+ background: white;
7174
+ cursor: pointer;
7175
+ }
7176
+
6748
7177
  .option-chain-widget .fetch-button {
6749
7178
  padding: 8px 16px;
6750
7179
  background: #3b82f6;
@@ -6900,10 +7329,22 @@ ${SharedStyles}
6900
7329
  min-height: 32px;
6901
7330
  }
6902
7331
 
7332
+ /* In-the-money highlighting */
7333
+ .option-chain-widget .calls-data.in-the-money span,
7334
+ .option-chain-widget .puts-data.in-the-money span {
7335
+ background-color: #fffbeb;
7336
+ }
7337
+
6903
7338
  .option-chain-widget .option-row:hover {
6904
7339
  background: #f9fafb;
6905
7340
  }
6906
- .option-chain-widget .option-row:hover .calls-data span,
7341
+
7342
+ .option-chain-widget .option-row:hover .calls-data.in-the-money span,
7343
+ .option-chain-widget .option-row:hover .puts-data.in-the-money span {
7344
+ background-color: #fef3c7;
7345
+ }
7346
+
7347
+ .option-chain-widget .option-row:hover .calls-data span,
6907
7348
  .option-chain-widget .option-row:hover .puts-data span,
6908
7349
  .option-chain-widget .option-row:hover .strike-data {
6909
7350
  background: #f9fafb;
@@ -6927,14 +7368,32 @@ ${SharedStyles}
6927
7368
  .option-chain-widget .puts-data span {
6928
7369
  padding: 6px 4px;
6929
7370
  text-align: center;
6930
- font-size: 12px; /* Changed from 12px to 14px */
6931
- font-weight: 400; /* Added to match data values in other widgets */
6932
- color: #111827; /* Changed from #374151 to match other widgets */
7371
+ font-size: 12px;
7372
+ font-weight: 400;
7373
+ color: #111827;
6933
7374
  overflow: hidden;
6934
7375
  text-overflow: ellipsis;
6935
7376
  white-space: nowrap;
6936
7377
  }
6937
7378
 
7379
+ /* Override color for change values with positive/negative classes */
7380
+ .option-chain-widget .calls-data span.positive,
7381
+ .option-chain-widget .puts-data span.positive {
7382
+ color: #059669 !important;
7383
+ font-weight: 500;
7384
+ }
7385
+
7386
+ .option-chain-widget .calls-data span.negative,
7387
+ .option-chain-widget .puts-data span.negative {
7388
+ color: #dc2626 !important;
7389
+ font-weight: 500;
7390
+ }
7391
+
7392
+ .option-chain-widget .calls-data span.neutral,
7393
+ .option-chain-widget .puts-data span.neutral {
7394
+ color: #6b7280 !important;
7395
+ }
7396
+
6938
7397
  .option-chain-widget .contract-cell {
6939
7398
  font-size: 10px;
6940
7399
  color: #6b7280;
@@ -6946,78 +7405,42 @@ ${SharedStyles}
6946
7405
  white-space: nowrap;
6947
7406
  }
6948
7407
 
6949
- .option-chain-widget .positive {
6950
- color: #059669;
6951
- }
6952
-
6953
- .option-chain-widget .negative {
6954
- color: #dc2626;
6955
- }
6956
-
6957
- .option-chain-widget .neutral {
6958
- color: #6b7280;
6959
- }
6960
-
6961
- .option-chain-widget .widget-footer {
6962
- background: #f9fafb;
6963
- padding: 8px 16px;
6964
- border-top: 1px solid #e5e7eb;
6965
- text-align: center;
6966
- font-size: 11px;
6967
- color: #6b7280;
7408
+ .option-chain-widget .contract-cell.clickable {
7409
+ color: #3b82f6;
7410
+ cursor: pointer;
7411
+ text-decoration: underline;
7412
+ transition: all 0.2s ease;
6968
7413
  }
6969
7414
 
6970
- .option-chain-widget .widget-loading-overlay {
6971
- position: absolute;
6972
- top: 0;
6973
- left: 0;
6974
- right: 0;
6975
- bottom: 0;
6976
- background: rgba(255, 255, 255, 0.9);
6977
- display: flex;
6978
- align-items: center;
6979
- justify-content: center;
6980
- z-index: 20;
7415
+ .option-chain-widget .contract-cell.clickable:hover {
7416
+ color: #2563eb;
7417
+ background-color: #eff6ff;
6981
7418
  }
6982
7419
 
6983
- .option-chain-widget .widget-loading-overlay.hidden {
6984
- display: none;
7420
+ .option-chain-widget .contract-cell.clickable:focus {
7421
+ outline: 2px solid #3b82f6;
7422
+ outline-offset: 2px;
7423
+ border-radius: 2px;
6985
7424
  }
6986
7425
 
6987
- .option-chain-widget .loading-content {
6988
- text-align: center;
7426
+ .option-chain-widget .contract-cell.clickable:active {
7427
+ color: #1e40af;
7428
+ background-color: #dbeafe;
6989
7429
  }
6990
7430
 
6991
- .option-chain-widget .loading-spinner {
6992
- width: 32px;
6993
- height: 32px;
6994
- border: 3px solid #e5e7eb;
6995
- border-top: 3px solid #3b82f6;
6996
- border-radius: 50%;
6997
- animation: spin 1s linear infinite;
6998
- margin: 0 auto 12px;
7431
+ .option-chain-widget .positive {
7432
+ color: #059669;
6999
7433
  }
7000
7434
 
7001
- @keyframes spin {
7002
- 0% { transform: rotate(0deg); }
7003
- 100% { transform: rotate(360deg); }
7435
+ .option-chain-widget .negative {
7436
+ color: #dc2626;
7004
7437
  }
7005
7438
 
7006
- .option-chain-widget .no-data-state {
7007
- padding: 40px;
7008
- text-align: center;
7439
+ .option-chain-widget .neutral {
7009
7440
  color: #6b7280;
7010
7441
  }
7011
7442
 
7012
- .option-chain-widget .widget-error {
7013
- padding: 20px;
7014
- background: #fef2f2;
7015
- color: #dc2626;
7016
- border: 1px solid #fecaca;
7017
- margin: 16px;
7018
- border-radius: 4px;
7019
- text-align: center;
7020
- }
7443
+ /* Loading, Error, Footer, and No-Data styles provided by CommonWidgetPatterns */
7021
7444
 
7022
7445
  /* RESPONSIVE STYLES */
7023
7446
 
@@ -7028,13 +7451,19 @@ ${SharedStyles}
7028
7451
  align-items: stretch;
7029
7452
  gap: 8px;
7030
7453
  }
7031
-
7454
+
7455
+ .option-chain-widget .filter-section {
7456
+ flex-direction: row;
7457
+ margin-top: 0;
7458
+ }
7459
+
7032
7460
  .option-chain-widget .symbol-input,
7033
- .option-chain-widget .date-select {
7461
+ .option-chain-widget .date-select,
7462
+ .option-chain-widget .strike-filter {
7034
7463
  width: 100%;
7035
7464
  max-width: none;
7036
7465
  }
7037
-
7466
+
7038
7467
  .option-chain-widget .fetch-button {
7039
7468
  width: 100%;
7040
7469
  }
@@ -7190,6 +7619,121 @@ ${SharedStyles}
7190
7619
  padding: 2px 1px;
7191
7620
  }
7192
7621
  }
7622
+
7623
+ /* Modal Styles */
7624
+ .option-chain-modal-overlay {
7625
+ position: fixed;
7626
+ top: 0;
7627
+ left: 0;
7628
+ right: 0;
7629
+ bottom: 0;
7630
+ background: rgba(0, 0, 0, 0.6);
7631
+ display: flex;
7632
+ align-items: center;
7633
+ justify-content: center;
7634
+ z-index: 10000;
7635
+ padding: 20px;
7636
+ animation: fadeIn 0.2s ease-out;
7637
+ }
7638
+
7639
+ @keyframes fadeIn {
7640
+ from {
7641
+ opacity: 0;
7642
+ }
7643
+ to {
7644
+ opacity: 1;
7645
+ }
7646
+ }
7647
+
7648
+ .option-chain-modal {
7649
+ background: white;
7650
+ border-radius: 12px;
7651
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
7652
+ max-width: 800px;
7653
+ width: 100%;
7654
+ max-height: 90vh;
7655
+ display: flex;
7656
+ flex-direction: column;
7657
+ animation: slideUp 0.3s ease-out;
7658
+ }
7659
+
7660
+ @keyframes slideUp {
7661
+ from {
7662
+ transform: translateY(20px);
7663
+ opacity: 0;
7664
+ }
7665
+ to {
7666
+ transform: translateY(0);
7667
+ opacity: 1;
7668
+ }
7669
+ }
7670
+
7671
+ .option-chain-modal-header {
7672
+ display: flex;
7673
+ align-items: center;
7674
+ justify-content: space-between;
7675
+ padding: 20px 24px;
7676
+ border-bottom: 1px solid #e5e7eb;
7677
+ }
7678
+
7679
+ .option-chain-modal-header h3 {
7680
+ margin: 0;
7681
+ font-size: 18px;
7682
+ font-weight: 600;
7683
+ color: #111827;
7684
+ }
7685
+
7686
+ .option-chain-modal-close {
7687
+ background: none;
7688
+ border: none;
7689
+ font-size: 28px;
7690
+ line-height: 1;
7691
+ color: #6b7280;
7692
+ cursor: pointer;
7693
+ padding: 0;
7694
+ width: 32px;
7695
+ height: 32px;
7696
+ display: flex;
7697
+ align-items: center;
7698
+ justify-content: center;
7699
+ border-radius: 4px;
7700
+ transition: all 0.2s ease;
7701
+ }
7702
+
7703
+ .option-chain-modal-close:hover {
7704
+ background: #f3f4f6;
7705
+ color: #111827;
7706
+ }
7707
+
7708
+ .option-chain-modal-close:active {
7709
+ background: #e5e7eb;
7710
+ }
7711
+
7712
+ .option-chain-modal-body {
7713
+ flex: 1;
7714
+ overflow-y: auto;
7715
+ padding: 24px;
7716
+ }
7717
+
7718
+ /* Responsive modal */
7719
+ @media (max-width: 768px) {
7720
+ .option-chain-modal {
7721
+ max-width: 95%;
7722
+ max-height: 95vh;
7723
+ }
7724
+
7725
+ .option-chain-modal-header {
7726
+ padding: 16px;
7727
+ }
7728
+
7729
+ .option-chain-modal-header h3 {
7730
+ font-size: 16px;
7731
+ }
7732
+
7733
+ .option-chain-modal-body {
7734
+ padding: 16px;
7735
+ }
7736
+ }
7193
7737
  `;
7194
7738
 
7195
7739
  class OptionChainWidget extends BaseWidget {
@@ -7204,7 +7748,18 @@ class OptionChainWidget extends BaseWidget {
7204
7748
  this.data = null;
7205
7749
  this.isDestroyed = false;
7206
7750
  this.unsubscribe = null;
7751
+ this.unsubscribeUnderlying = null; // For underlying stock price
7752
+ this.underlyingPrice = null; // Current price of underlying stock
7207
7753
  this.loadingTimeout = null;
7754
+ this.renderDebounceTimeout = null; // For debouncing re-renders
7755
+
7756
+ // Separate cache for option chain data and underlying price
7757
+ this.cachedOptionChainData = null;
7758
+ this.cachedUnderlyingPrice = null;
7759
+
7760
+ // Modal for Options widget
7761
+ this.optionsModal = null;
7762
+ this.optionsWidgetInstance = null;
7208
7763
 
7209
7764
  // INPUT VALIDATION: Validate initial symbol if provided
7210
7765
  if (options.symbol) {
@@ -7220,6 +7775,7 @@ class OptionChainWidget extends BaseWidget {
7220
7775
  }
7221
7776
  this.date = '';
7222
7777
  this.availableDates = {};
7778
+ this.strikeFilterRange = 5; // Default: show ±10 strikes from ATM
7223
7779
 
7224
7780
  // RATE LIMITING: Create rate limiter for fetch button (1 second between fetches)
7225
7781
  this.fetchRateLimiter = createRateLimitValidator(1000);
@@ -7250,6 +7806,7 @@ class OptionChainWidget extends BaseWidget {
7250
7806
  this.dateSelect = this.container.querySelector('.date-select');
7251
7807
  this.fetchButton = this.container.querySelector('.fetch-button');
7252
7808
  this.dataGrid = this.container.querySelector('.option-chain-data-grid');
7809
+ this.strikeFilterSelect = this.container.querySelector('.strike-filter');
7253
7810
 
7254
7811
  // Set initial symbol value if provided in options
7255
7812
  if (this.symbol) {
@@ -7324,6 +7881,22 @@ class OptionChainWidget extends BaseWidget {
7324
7881
  }
7325
7882
  this.loadAvailableDates(symbolValidation.sanitized);
7326
7883
  });
7884
+
7885
+ // MEMORY LEAK FIX: Use BaseWidget's addEventListener
7886
+ // Strike filter dropdown
7887
+ this.addEventListener(this.strikeFilterSelect, 'change', e => {
7888
+ const value = e.target.value;
7889
+ if (value === 'all') {
7890
+ this.strikeFilterRange = 'all';
7891
+ } else {
7892
+ this.strikeFilterRange = parseInt(value, 10);
7893
+ }
7894
+
7895
+ // Re-render with new filter if we have data
7896
+ if (this.data) {
7897
+ this.displayOptionChain(this.data);
7898
+ }
7899
+ });
7327
7900
  }
7328
7901
  async loadAvailableDates(symbol) {
7329
7902
  if (!symbol) return;
@@ -7339,7 +7912,9 @@ class OptionChainWidget extends BaseWidget {
7339
7912
  // Use API service from wsManager
7340
7913
  const apiService = this.wsManager.getApiService();
7341
7914
  const data = await apiService.getOptionChainDates(symbol);
7342
- console.log("Available dates:", data.dates_dictionary);
7915
+ if (this.debug) {
7916
+ console.log('[OptionChainWidget] Available dates:', data.dates_dictionary);
7917
+ }
7343
7918
  this.availableDates = data.dates_dictionary || {};
7344
7919
  this.populateDateOptions();
7345
7920
 
@@ -7433,11 +8008,15 @@ class OptionChainWidget extends BaseWidget {
7433
8008
  this.showError(`No data received for ${this.symbol} on ${this.date}. Please try again.`);
7434
8009
  }, 10000); // 10 second timeout
7435
8010
 
7436
- // Unsubscribe from previous subscription if exists
8011
+ // Unsubscribe from previous subscriptions if they exist
7437
8012
  if (this.unsubscribe) {
7438
8013
  this.unsubscribe();
7439
8014
  this.unsubscribe = null;
7440
8015
  }
8016
+ if (this.unsubscribeUnderlying) {
8017
+ this.unsubscribeUnderlying();
8018
+ this.unsubscribeUnderlying = null;
8019
+ }
7441
8020
 
7442
8021
  // Subscribe to option chain data
7443
8022
  this.subscribeToData();
@@ -7447,25 +8026,62 @@ class OptionChainWidget extends BaseWidget {
7447
8026
  }
7448
8027
  }
7449
8028
  subscribeToData() {
7450
- // Subscribe with symbol and date for routing
7451
- this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol,
7452
- // Pass symbol as string
7453
- {
8029
+ // Subscribe to option chain data
8030
+ this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol, {
7454
8031
  date: this.date
7455
- } // Pass date as additional parameter
7456
- );
8032
+ });
7457
8033
 
7458
- // Send subscription message with both symbol and date
7459
- /* this.wsManager.send({
7460
- type: 'queryoptionchain',
7461
- underlying: this.symbol,
7462
- date: this.date
7463
- }); */
8034
+ // Subscribe to underlying stock price for ITM highlighting
8035
+ this.unsubscribeUnderlying = this.wsManager.subscribe(`${this.widgetId}-underlying`, ['queryl1'], this.handleMessage.bind(this), this.symbol);
8036
+ }
8037
+
8038
+ /**
8039
+ * Find the nearest strike price to the underlying price (ATM strike)
8040
+ * @param {Array} sortedStrikes - Array of strike prices sorted ascending
8041
+ * @param {Number} underlyingPrice - Current price of underlying stock
8042
+ * @returns {String} - The nearest strike price
8043
+ */
8044
+ findNearestStrike(sortedStrikes, underlyingPrice) {
8045
+ if (!sortedStrikes || sortedStrikes.length === 0 || !underlyingPrice) {
8046
+ return null;
8047
+ }
8048
+
8049
+ // Find strike closest to underlying price
8050
+ let nearestStrike = sortedStrikes[0];
8051
+ let minDifference = Math.abs(parseFloat(sortedStrikes[0]) - underlyingPrice);
8052
+ for (const strike of sortedStrikes) {
8053
+ const difference = Math.abs(parseFloat(strike) - underlyingPrice);
8054
+ if (difference < minDifference) {
8055
+ minDifference = difference;
8056
+ nearestStrike = strike;
8057
+ }
8058
+ }
8059
+ return nearestStrike;
8060
+ }
8061
+
8062
+ /**
8063
+ * Filter strikes to show only those near the money
8064
+ * @param {Array} sortedStrikes - Array of strike prices sorted ascending
8065
+ * @param {String} atmStrike - The at-the-money strike price
8066
+ * @param {Number} range - Number of strikes to show above and below ATM
8067
+ * @returns {Array} - Filtered array of strikes
8068
+ */
8069
+ filterNearMoneyStrikes(sortedStrikes, atmStrike, range) {
8070
+ if (!atmStrike || !sortedStrikes || sortedStrikes.length === 0) {
8071
+ return sortedStrikes;
8072
+ }
8073
+ const atmIndex = sortedStrikes.indexOf(atmStrike);
8074
+ if (atmIndex === -1) {
8075
+ return sortedStrikes;
8076
+ }
8077
+ const startIndex = Math.max(0, atmIndex - range);
8078
+ const endIndex = Math.min(sortedStrikes.length - 1, atmIndex + range);
8079
+ return sortedStrikes.slice(startIndex, endIndex + 1);
7464
8080
  }
7465
8081
  handleData(message) {
7466
- // Clear loading timeout since we received data
8082
+ // Clear loading timeout since we received data (use BaseWidget method)
7467
8083
  if (this.loadingTimeout) {
7468
- clearTimeout(this.loadingTimeout);
8084
+ this.clearTimeout(this.loadingTimeout);
7469
8085
  this.loadingTimeout = null;
7470
8086
  }
7471
8087
 
@@ -7497,7 +8113,9 @@ class OptionChainWidget extends BaseWidget {
7497
8113
  }
7498
8114
  }
7499
8115
  } else {
7500
- this.data = message; // Store for caching
8116
+ // Cache the option chain data
8117
+ this.data = message;
8118
+ this.cachedOptionChainData = message;
7501
8119
  this.displayOptionChain(message);
7502
8120
  }
7503
8121
  } else if (message.type === 'queryoptionchain' && Array.isArray(message.data)) {
@@ -7512,9 +8130,35 @@ class OptionChainWidget extends BaseWidget {
7512
8130
  }
7513
8131
  }
7514
8132
  } else {
7515
- this.data = message.data; // Store for caching
8133
+ // Cache the option chain data
8134
+ this.data = message.data;
8135
+ this.cachedOptionChainData = message.data;
7516
8136
  this.displayOptionChain(message.data);
7517
8137
  }
8138
+ } else if (message.type === 'queryl1' && message.Data) {
8139
+ if (this.debug) {
8140
+ console.log('[OptionChainWidget] Received underlying price update');
8141
+ }
8142
+ const data = message.Data.find(d => d.Symbol === this.symbol);
8143
+ if (data && data.LastPx) {
8144
+ // Cache the underlying price
8145
+ this.underlyingPrice = parseFloat(data.LastPx);
8146
+ this.cachedUnderlyingPrice = parseFloat(data.LastPx);
8147
+
8148
+ // Debounce re-render to prevent excessive updates
8149
+ // Clear any pending render
8150
+ if (this.renderDebounceTimeout) {
8151
+ this.clearTimeout(this.renderDebounceTimeout);
8152
+ }
8153
+
8154
+ // Schedule a debounced re-render (500ms delay)
8155
+ this.renderDebounceTimeout = this.setTimeout(() => {
8156
+ if (this.data) {
8157
+ this.displayOptionChain(this.data);
8158
+ }
8159
+ this.renderDebounceTimeout = null;
8160
+ }, 500);
8161
+ }
7518
8162
  }
7519
8163
  if (message._cached) {
7520
8164
  this.showConnectionQuality(); // Show cache indicator
@@ -7522,6 +8166,14 @@ class OptionChainWidget extends BaseWidget {
7522
8166
  }
7523
8167
  displayOptionChain(data) {
7524
8168
  if (this.isDestroyed) return;
8169
+
8170
+ // Validate data
8171
+ if (!data || !Array.isArray(data) || data.length === 0) {
8172
+ if (this.debug) {
8173
+ console.warn('[OptionChainWidget] Invalid or empty data passed to displayOptionChain');
8174
+ }
8175
+ return;
8176
+ }
7525
8177
  try {
7526
8178
  // Hide loading overlay
7527
8179
  this.hideLoading();
@@ -7554,9 +8206,18 @@ class OptionChainWidget extends BaseWidget {
7554
8206
  }
7555
8207
  });
7556
8208
 
7557
- // Sort strikes and display
8209
+ // Sort strikes
7558
8210
  const sortedStrikes = Object.keys(optionsByStrike).sort((a, b) => parseFloat(a) - parseFloat(b));
7559
- sortedStrikes.forEach(strike => {
8211
+
8212
+ // Apply ATM filter if not showing all
8213
+ let displayStrikes = sortedStrikes;
8214
+ if (this.strikeFilterRange !== 'all' && this.underlyingPrice) {
8215
+ const atmStrike = this.findNearestStrike(sortedStrikes, this.underlyingPrice);
8216
+ if (atmStrike) {
8217
+ displayStrikes = this.filterNearMoneyStrikes(sortedStrikes, atmStrike, this.strikeFilterRange);
8218
+ }
8219
+ }
8220
+ displayStrikes.forEach(strike => {
7560
8221
  const {
7561
8222
  call,
7562
8223
  put
@@ -7575,7 +8236,7 @@ class OptionChainWidget extends BaseWidget {
7575
8236
  const formatted = parseFloat(change).toFixed(2);
7576
8237
  const className = change > 0 ? 'positive' : change < 0 ? 'negative' : 'neutral';
7577
8238
  const sign = change > 0 ? '+' : '';
7578
- span.className = `option-chain-${className}`;
8239
+ span.className = className;
7579
8240
  span.textContent = `${sign}${formatted}`;
7580
8241
  return span;
7581
8242
  };
@@ -7588,7 +8249,28 @@ class OptionChainWidget extends BaseWidget {
7588
8249
  // Create calls data section
7589
8250
  const callsData = document.createElement('div');
7590
8251
  callsData.className = 'calls-data';
7591
- callsData.appendChild(createElement('span', call ? sanitizeSymbol(call.symbol) : '--', 'contract-cell'));
8252
+
8253
+ // Add ITM highlighting for calls (ITM when strike < underlying price)
8254
+ if (this.underlyingPrice && parseFloat(strike) < this.underlyingPrice) {
8255
+ callsData.classList.add('in-the-money');
8256
+ }
8257
+
8258
+ // Make contract cell clickable
8259
+ const callContractCell = createElement('span', call ? sanitizeSymbol(call.symbol) : '--', 'contract-cell');
8260
+ if (call && call.symbol) {
8261
+ callContractCell.classList.add('clickable');
8262
+ callContractCell.setAttribute('role', 'button');
8263
+ callContractCell.setAttribute('tabindex', '0');
8264
+ callContractCell.setAttribute('data-symbol', call.symbol);
8265
+ this.addEventListener(callContractCell, 'click', () => this.showOptionsModal(call.symbol));
8266
+ this.addEventListener(callContractCell, 'keypress', e => {
8267
+ if (e.key === 'Enter' || e.key === ' ') {
8268
+ e.preventDefault();
8269
+ this.showOptionsModal(call.symbol);
8270
+ }
8271
+ });
8272
+ }
8273
+ callsData.appendChild(callContractCell);
7592
8274
  callsData.appendChild(createElement('span', formatPrice(call?.lastPrice)));
7593
8275
  const callChangeSpan = document.createElement('span');
7594
8276
  if (call) {
@@ -7611,7 +8293,28 @@ class OptionChainWidget extends BaseWidget {
7611
8293
  // Create puts data section
7612
8294
  const putsData = document.createElement('div');
7613
8295
  putsData.className = 'puts-data';
7614
- putsData.appendChild(createElement('span', put ? sanitizeSymbol(put.symbol) : '', 'contract-cell'));
8296
+
8297
+ // Add ITM highlighting for puts (ITM when strike > underlying price)
8298
+ if (this.underlyingPrice && parseFloat(strike) > this.underlyingPrice) {
8299
+ putsData.classList.add('in-the-money');
8300
+ }
8301
+
8302
+ // Make contract cell clickable
8303
+ const putContractCell = createElement('span', put ? sanitizeSymbol(put.symbol) : '', 'contract-cell');
8304
+ if (put && put.symbol) {
8305
+ putContractCell.classList.add('clickable');
8306
+ putContractCell.setAttribute('role', 'button');
8307
+ putContractCell.setAttribute('tabindex', '0');
8308
+ putContractCell.setAttribute('data-symbol', put.symbol);
8309
+ this.addEventListener(putContractCell, 'click', () => this.showOptionsModal(put.symbol));
8310
+ this.addEventListener(putContractCell, 'keypress', e => {
8311
+ if (e.key === 'Enter' || e.key === ' ') {
8312
+ e.preventDefault();
8313
+ this.showOptionsModal(put.symbol);
8314
+ }
8315
+ });
8316
+ }
8317
+ putsData.appendChild(putContractCell);
7615
8318
  putsData.appendChild(createElement('span', formatPrice(put?.lastPrice)));
7616
8319
  const putChangeSpan = document.createElement('span');
7617
8320
  if (put) {
@@ -7635,14 +8338,22 @@ class OptionChainWidget extends BaseWidget {
7635
8338
 
7636
8339
  // Add footer - with array length check
7637
8340
  if (data && data.length > 0) {
7638
- const timestamp = formatTimestampET(data[0].quoteTime || Date.now());
8341
+ const isDelayed = data[0].DataSource === 'OPRA-D';
8342
+
8343
+ // Get timestamp - subtract 20 minutes if delayed data
8344
+ let timestampValue = data[0].quoteTime || Date.now();
8345
+ if (isDelayed) {
8346
+ // Subtract 20 minutes (20 * 60 * 1000 milliseconds)
8347
+ timestampValue = timestampValue - 20 * 60 * 1000;
8348
+ }
8349
+ const timestamp = formatTimestampET(timestampValue);
7639
8350
  const lastUpdateElement = this.container.querySelector('.last-update');
7640
8351
  if (lastUpdateElement) {
7641
8352
  lastUpdateElement.textContent = `Last update: ${timestamp}`;
7642
8353
  }
7643
8354
 
7644
8355
  // Update footer
7645
- const source = data[0].DataSource === 'OPRA-D' ? '20 mins delayed' : 'Real-time';
8356
+ const source = isDelayed ? '20 mins delayed' : 'Real-time';
7646
8357
  const dataSourceElement = this.container.querySelector('.data-source');
7647
8358
  if (dataSourceElement) {
7648
8359
  dataSourceElement.textContent = `Source: ${source}`;
@@ -7665,35 +8376,6 @@ class OptionChainWidget extends BaseWidget {
7665
8376
  loadingOverlay.classList.add('hidden');
7666
8377
  }
7667
8378
  }
7668
-
7669
- /* showInputError(message) {
7670
- const errorDiv = document.createElement('div');
7671
- errorDiv.className = 'symbol-error-message';
7672
- errorDiv.textContent = message;
7673
- errorDiv.style.cssText = `
7674
- color: #dc2626;
7675
- font-size: 12px;
7676
- margin-bottom: 2px;
7677
- position: absolute;
7678
- background: white;
7679
- z-index: 1000;
7680
- transform: translateY(-100%);
7681
- top: -4px;
7682
- left: 0; // Align with input box
7683
- `;
7684
-
7685
- // Ensure the parent is positioned
7686
- const parent = this.symbolInput;
7687
- if (parent) {
7688
- parent.style.position = 'relative';
7689
- // Insert the errorDiv as the first child of the parent
7690
- parent.insertBefore(errorDiv, parent.firstChild);
7691
- }
7692
-
7693
- this.symbolInput.errorMessage = errorDiv;
7694
- this.symbolInput.focus();
7695
- } */
7696
-
7697
8379
  showError(message) {
7698
8380
  this.hideLoading();
7699
8381
  this.clearError();
@@ -7766,19 +8448,135 @@ class OptionChainWidget extends BaseWidget {
7766
8448
  this.container.appendChild(noDataElement);
7767
8449
  }
7768
8450
  }
8451
+
8452
+ /**
8453
+ * Show Options widget in a modal for a specific option contract
8454
+ * @param {string} optionSymbol - The option contract symbol
8455
+ */
8456
+ showOptionsModal(optionSymbol) {
8457
+ if (!optionSymbol || optionSymbol === '--') {
8458
+ return;
8459
+ }
8460
+
8461
+ // Close existing modal if any
8462
+ this.closeOptionsModal();
8463
+
8464
+ // Create modal overlay
8465
+ this.optionsModal = document.createElement('div');
8466
+ this.optionsModal.className = 'option-chain-modal-overlay';
8467
+ this.optionsModal.innerHTML = `
8468
+ <div class="option-chain-modal">
8469
+ <div class="option-chain-modal-header">
8470
+ <h3>Option Details: ${sanitizeSymbol(optionSymbol)}</h3>
8471
+ <button class="option-chain-modal-close" aria-label="Close modal">&times;</button>
8472
+ </div>
8473
+ <div class="option-chain-modal-body">
8474
+ <div id="option-chain-modal-widget"></div>
8475
+ </div>
8476
+ </div>
8477
+ `;
8478
+
8479
+ // Add to body
8480
+ document.body.appendChild(this.optionsModal);
8481
+
8482
+ // Add close event
8483
+ const closeBtn = this.optionsModal.querySelector('.option-chain-modal-close');
8484
+ this.addEventListener(closeBtn, 'click', () => this.closeOptionsModal());
8485
+
8486
+ // Close on overlay click
8487
+ this.addEventListener(this.optionsModal, 'click', e => {
8488
+ if (e.target === this.optionsModal) {
8489
+ this.closeOptionsModal();
8490
+ }
8491
+ });
8492
+
8493
+ // Close on Escape key
8494
+ const escapeHandler = e => {
8495
+ if (e.key === 'Escape') {
8496
+ this.closeOptionsModal();
8497
+ }
8498
+ };
8499
+ document.addEventListener('keydown', escapeHandler);
8500
+ this._escapeHandler = escapeHandler;
8501
+
8502
+ // Create Options widget instance
8503
+ try {
8504
+ const widgetContainer = this.optionsModal.querySelector('#option-chain-modal-widget');
8505
+ this.optionsWidgetInstance = new OptionsWidget(widgetContainer, {
8506
+ symbol: optionSymbol,
8507
+ wsManager: this.wsManager,
8508
+ debug: this.debug,
8509
+ styled: true
8510
+ }, `${this.widgetId}-options-modal`);
8511
+ if (this.debug) {
8512
+ console.log('[OptionChainWidget] Options modal opened for:', optionSymbol);
8513
+ }
8514
+ } catch (error) {
8515
+ console.error('[OptionChainWidget] Error creating Options widget:', error);
8516
+ this.closeOptionsModal();
8517
+ this.showError(`Failed to load option details: ${error.message}`);
8518
+ }
8519
+ }
8520
+
8521
+ /**
8522
+ * Close the Options modal and cleanup
8523
+ */
8524
+ closeOptionsModal() {
8525
+ // Destroy widget instance
8526
+ if (this.optionsWidgetInstance) {
8527
+ this.optionsWidgetInstance.destroy();
8528
+ this.optionsWidgetInstance = null;
8529
+ }
8530
+
8531
+ // Remove modal from DOM
8532
+ if (this.optionsModal) {
8533
+ this.optionsModal.remove();
8534
+ this.optionsModal = null;
8535
+ }
8536
+
8537
+ // Remove escape key handler
8538
+ if (this._escapeHandler) {
8539
+ document.removeEventListener('keydown', this._escapeHandler);
8540
+ this._escapeHandler = null;
8541
+ }
8542
+ if (this.debug) {
8543
+ console.log('[OptionChainWidget] Options modal closed');
8544
+ }
8545
+ }
7769
8546
  destroy() {
7770
8547
  this.isDestroyed = true;
8548
+
8549
+ // Close modal if open
8550
+ this.closeOptionsModal();
7771
8551
  if (this.unsubscribe) {
7772
8552
  this.unsubscribe();
7773
8553
  this.unsubscribe = null;
7774
8554
  }
7775
8555
 
8556
+ // Unsubscribe from underlying price
8557
+ if (this.unsubscribeUnderlying) {
8558
+ this.unsubscribeUnderlying();
8559
+ this.unsubscribeUnderlying = null;
8560
+ }
8561
+
7776
8562
  // MEMORY LEAK FIX: Clear loading timeout using BaseWidget method
7777
8563
  if (this.loadingTimeout) {
7778
8564
  this.clearTimeout(this.loadingTimeout);
7779
8565
  this.loadingTimeout = null;
7780
8566
  }
7781
8567
 
8568
+ // Clear debounce timeout
8569
+ if (this.renderDebounceTimeout) {
8570
+ this.clearTimeout(this.renderDebounceTimeout);
8571
+ this.renderDebounceTimeout = null;
8572
+ }
8573
+
8574
+ // Clear cached data
8575
+ this.cachedOptionChainData = null;
8576
+ this.cachedUnderlyingPrice = null;
8577
+ this.underlyingPrice = null;
8578
+ this.data = null;
8579
+
7782
8580
  // Call parent destroy for automatic cleanup of event listeners, timeouts, intervals
7783
8581
  super.destroy();
7784
8582
  }
@@ -7864,80 +8662,81 @@ const DataStyles = `
7864
8662
  font-family: Arial, sans-serif;
7865
8663
  }
7866
8664
 
7867
- .widget-header {
8665
+ .data-widget .widget-header {
7868
8666
  display: flex;
7869
8667
  flex-direction: column;
7870
8668
  margin-bottom: 10px;
7871
8669
  }
7872
8670
 
7873
- .symbol {
8671
+ .data-widget .symbol {
7874
8672
  font-size: 24px;
7875
8673
  font-weight: bold;
7876
8674
  margin-right: 10px;
7877
8675
  }
7878
8676
 
7879
- .company-info {
8677
+ .data-widget .company-info {
7880
8678
  font-size: 12px;
7881
8679
  color: #666;
7882
8680
  }
7883
8681
 
7884
- .trading-info {
8682
+ .data-widget .trading-info {
7885
8683
  font-size: 12px;
7886
8684
  color: #999;
7887
8685
  }
7888
8686
 
7889
- .price-section {
8687
+ .data-widget .price-section {
7890
8688
  display: flex;
7891
8689
  align-items: baseline;
7892
8690
  margin-bottom: 10px;
7893
8691
  }
7894
8692
 
7895
- .current-price {
8693
+ .data-widget .current-price {
7896
8694
  font-size: 36px;
7897
8695
  font-weight: bold;
7898
8696
  margin-right: 10px;
7899
8697
  }
7900
8698
 
7901
- .price-change {
8699
+ .data-widget .price-change {
7902
8700
  font-size: 18px;
7903
8701
  }
7904
8702
 
7905
- .price-change.positive {
8703
+ .data-widget .price-change.positive {
7906
8704
  color: green;
7907
8705
  }
7908
8706
 
7909
- .price-change.negative {
8707
+ .data-widget .price-change.negative {
7910
8708
  color: red;
7911
8709
  }
7912
8710
 
7913
- .bid-ask-section {
8711
+ .data-widget .bid-ask-section {
7914
8712
  display: flex;
7915
8713
  justify-content: space-between;
7916
8714
  margin-bottom: 10px;
7917
8715
  }
7918
8716
 
7919
- .ask, .bid {
8717
+ .data-widget .ask,
8718
+ .data-widget .bid {
7920
8719
  display: flex;
7921
8720
  align-items: center;
7922
8721
  }
7923
8722
 
7924
- .label {
8723
+ .data-widget .label {
7925
8724
  font-size: 14px;
7926
8725
  margin-right: 5px;
7927
8726
  }
7928
8727
 
7929
- .value {
8728
+ .data-widget .value {
7930
8729
  font-size: 16px;
7931
8730
  font-weight: bold;
7932
8731
  margin-right: 5px;
7933
8732
  }
7934
8733
 
7935
- .size {
8734
+ .data-widget .size {
7936
8735
  font-size: 14px;
7937
8736
  color: red;
7938
8737
  }
7939
8738
 
7940
- .widget-footer {
8739
+ .data-widget .widget-footer {
7941
8740
  font-size: 12px;
7942
8741
  color: #333;
7943
8742
  }
@@ -8348,76 +9147,50 @@ class CombinedMarketWidget extends BaseWidget {
8348
9147
 
8349
9148
  // Subscribe to regular market data (Level 1)
8350
9149
  // Create a wrapper to add data type context
8351
- this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'], messageWrapper => {
8352
- const {
8353
- event,
8354
- data
8355
- } = messageWrapper;
8356
-
8357
- // Handle connection events
8358
- if (event === 'connection') {
8359
- this.handleConnectionStatus(data);
8360
- return;
8361
- }
8362
-
8363
- // For data events, add type context and use base handleMessage pattern
8364
- if (event === 'data') {
8365
- data._dataType = 'market';
8366
- this.handleMessage({
8367
- event,
8368
- data
8369
- });
8370
- }
8371
- }, this.symbol);
9150
+ this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'], this.handleMessage.bind(this), this.symbol);
8372
9151
 
8373
9152
  // Subscribe to night session data (BlueOcean or Bruce)
8374
- this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'], messageWrapper => {
8375
- const {
8376
- event,
8377
- data
8378
- } = messageWrapper;
8379
-
8380
- // Connection already handled by market subscription
8381
- if (event === 'connection') {
8382
- return;
8383
- }
8384
-
8385
- // For data events, add type context and use base handleMessage pattern
8386
- if (event === 'data') {
8387
- data._dataType = 'night';
8388
- this.handleMessage({
8389
- event,
8390
- data
8391
- });
8392
- }
8393
- }, this.symbol);
9153
+ this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'], this.handleMessage.bind(this), this.symbol);
8394
9154
  }
8395
9155
  handleData(message) {
8396
9156
  // Extract data type from metadata
8397
- const dataType = message._dataType;
9157
+ const dataType = message.type;
8398
9158
  if (this.debug) {
8399
9159
  console.log(`[CombinedMarketWidget] handleData called with type: ${dataType}`, message);
8400
9160
  }
8401
9161
 
8402
- // Safety check - if no data type, try to infer from structure
8403
- if (!dataType) {
8404
- if (this.debug) {
8405
- console.warn('[CombinedMarketWidget] No data type specified, attempting to infer from structure');
9162
+ // Clear loading timeout since we received data (use BaseWidget method)
9163
+ if (this.loadingTimeout) {
9164
+ this.clearTimeout(this.loadingTimeout);
9165
+ this.loadingTimeout = null;
9166
+ }
9167
+
9168
+ // Handle option chain data
9169
+ if (Array.isArray(message)) {
9170
+ if (message.length === 0) {
9171
+ // Only show no data state if we don't have cached data
9172
+ if (!this.data) {
9173
+ this.showNoDataState();
9174
+ } else {
9175
+ this.hideLoading();
9176
+ if (this.debug) {
9177
+ console.log('[OptionChainWidget] No new data, keeping cached data visible');
9178
+ }
9179
+ }
8406
9180
  }
8407
- return;
8408
9181
  }
8409
9182
 
8410
9183
  // Handle error messages from server
8411
- if (message.type === 'error' && message.noData) {
9184
+ if (message.type === 'error' || message.error == true) {
8412
9185
  if (this.debug) {
8413
9186
  console.log(`[CombinedMarketWidget] Received no data message for ${dataType}:`, message.message);
8414
9187
  }
8415
9188
  // Keep existing cached data visible, just hide loading
8416
- if (dataType === 'market' && !this.marketData) {
9189
+ if (dataType === 'queryl1' && !this.marketData) {
8417
9190
  if (this.debug) {
8418
9191
  console.log('[CombinedMarketWidget] No market data available');
8419
9192
  }
8420
- } else if (dataType === 'night' && !this.nightSessionData) {
9193
+ } else if (dataType === 'blueoceanl1' && !this.nightSessionData) {
8421
9194
  if (this.debug) {
8422
9195
  console.log('[CombinedMarketWidget] No night session data available');
8423
9196
  }
@@ -8870,7 +9643,7 @@ const IntradayChartStyles = `
8870
9643
  margin: 0 auto;
8871
9644
  }
8872
9645
 
8873
- .chart-header {
9646
+ .intraday-chart-widget .chart-header {
8874
9647
  display: flex;
8875
9648
  justify-content: space-between;
8876
9649
  align-items: center;
@@ -8879,13 +9652,13 @@ const IntradayChartStyles = `
8879
9652
  border-bottom: 1px solid #e5e7eb;
8880
9653
  }
8881
9654
 
8882
- .chart-title-section {
9655
+ .intraday-chart-widget .chart-title-section {
8883
9656
  display: flex;
8884
9657
  flex-direction: column;
8885
9658
  gap: 4px;
8886
9659
  }
8887
9660
 
8888
- .company-market-info {
9661
+ .intraday-chart-widget .company-market-info {
8889
9662
  display: flex;
8890
9663
  gap: 8px;
8891
9664
  align-items: center;
@@ -8893,18 +9666,18 @@ const IntradayChartStyles = `
8893
9666
  color: #6b7280;
8894
9667
  }
8895
9668
 
8896
- .intraday-company-name {
9669
+ .intraday-chart-widget .intraday-company-name {
8897
9670
  font-weight: 500;
8898
9671
  }
8899
9672
 
8900
- .intraday-chart-symbol {
9673
+ .intraday-chart-widget .intraday-chart-symbol {
8901
9674
  font-size: 1.5em;
8902
9675
  font-weight: 700;
8903
9676
  color: #1f2937;
8904
9677
  margin: 0;
8905
9678
  }
8906
9679
 
8907
- .intraday-chart-source {
9680
+ .intraday-chart-widget .intraday-chart-source {
8908
9681
  font-size: 0.75em;
8909
9682
  padding: 4px 8px;
8910
9683
  border-radius: 4px;
@@ -8913,36 +9686,36 @@ const IntradayChartStyles = `
8913
9686
  font-weight: 600;
8914
9687
  }
8915
9688
 
8916
- .chart-change {
9689
+ .intraday-chart-widget .chart-change {
8917
9690
  font-size: 1.1em;
8918
9691
  font-weight: 600;
8919
9692
  padding: 6px 12px;
8920
9693
  border-radius: 6px;
8921
9694
  }
8922
9695
 
8923
- .chart-change.positive {
9696
+ .intraday-chart-widget .chart-change.positive {
8924
9697
  color: #059669;
8925
9698
  background: #d1fae5;
8926
9699
  }
8927
9700
 
8928
- .chart-change.negative {
9701
+ .intraday-chart-widget .chart-change.negative {
8929
9702
  color: #dc2626;
8930
9703
  background: #fee2e2;
8931
9704
  }
8932
9705
 
8933
- .chart-controls {
9706
+ .intraday-chart-widget .chart-controls {
8934
9707
  display: flex;
8935
9708
  justify-content: space-between;
8936
9709
  align-items: center;
8937
9710
  margin-bottom: 15px;
8938
9711
  }
8939
9712
 
8940
- .chart-range-selector {
9713
+ .intraday-chart-widget .chart-range-selector {
8941
9714
  display: flex;
8942
9715
  gap: 8px;
8943
9716
  }
8944
9717
 
8945
- .range-btn {
9718
+ .intraday-chart-widget .range-btn {
8946
9719
  padding: 8px 16px;
8947
9720
  border: 1px solid #e5e7eb;
8948
9721
  background: white;
@@ -8954,28 +9727,28 @@ const IntradayChartStyles = `
8954
9727
  transition: all 0.2s ease;
8955
9728
  }
8956
9729
 
8957
- .range-btn:hover {
9730
+ .intraday-chart-widget .range-btn:hover {
8958
9731
  background: #f9fafb;
8959
9732
  border-color: #d1d5db;
8960
9733
  }
8961
9734
 
8962
- .range-btn.active {
9735
+ .intraday-chart-widget .range-btn.active {
8963
9736
  background: #667eea;
8964
9737
  color: white;
8965
9738
  border-color: #667eea;
8966
9739
  }
8967
9740
 
8968
- .range-btn:focus {
9741
+ .intraday-chart-widget .range-btn:focus {
8969
9742
  outline: none;
8970
9743
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
8971
9744
  }
8972
9745
 
8973
- .chart-type-selector {
9746
+ .intraday-chart-widget .chart-type-selector {
8974
9747
  display: flex;
8975
9748
  gap: 8px;
8976
9749
  }
8977
9750
 
8978
- .type-btn {
9751
+ .intraday-chart-widget .type-btn {
8979
9752
  padding: 8px 16px;
8980
9753
  border: 1px solid #e5e7eb;
8981
9754
  background: white;
@@ -8987,23 +9760,23 @@ const IntradayChartStyles = `
8987
9760
  transition: all 0.2s ease;
8988
9761
  }
8989
9762
 
8990
- .type-btn:hover {
9763
+ .intraday-chart-widget .type-btn:hover {
8991
9764
  background: #f9fafb;
8992
9765
  border-color: #d1d5db;
8993
9766
  }
8994
9767
 
8995
- .type-btn.active {
9768
+ .intraday-chart-widget .type-btn.active {
8996
9769
  background: #10b981;
8997
9770
  color: white;
8998
9771
  border-color: #10b981;
8999
9772
  }
9000
9773
 
9001
- .type-btn:focus {
9774
+ .intraday-chart-widget .type-btn:focus {
9002
9775
  outline: none;
9003
9776
  box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
9004
9777
  }
9005
9778
 
9006
- .zoom-reset-btn {
9779
+ .intraday-chart-widget .zoom-reset-btn {
9007
9780
  display: flex;
9008
9781
  align-items: center;
9009
9782
  gap: 6px;
@@ -9018,34 +9791,34 @@ const IntradayChartStyles = `
9018
9791
  transition: all 0.2s ease;
9019
9792
  }
9020
9793
 
9021
- .zoom-reset-btn:hover {
9794
+ .intraday-chart-widget .zoom-reset-btn:hover {
9022
9795
  background: #f9fafb;
9023
9796
  border-color: #667eea;
9024
9797
  color: #667eea;
9025
9798
  }
9026
9799
 
9027
- .zoom-reset-btn:focus {
9800
+ .intraday-chart-widget .zoom-reset-btn:focus {
9028
9801
  outline: none;
9029
9802
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
9030
9803
  }
9031
9804
 
9032
- .zoom-reset-btn svg {
9805
+ .intraday-chart-widget .zoom-reset-btn svg {
9033
9806
  flex-shrink: 0;
9034
9807
  }
9035
9808
 
9036
- .chart-container {
9809
+ .intraday-chart-widget .chart-container {
9037
9810
  height: 500px;
9038
9811
  margin-bottom: 20px;
9039
9812
  position: relative;
9040
9813
  }
9041
9814
 
9042
- .chart-stats {
9815
+ .intraday-chart-widget .chart-stats {
9043
9816
  padding: 15px;
9044
9817
  background: #f9fafb;
9045
9818
  border-radius: 8px;
9046
9819
  }
9047
9820
 
9048
- .stats-header {
9821
+ .intraday-chart-widget .stats-header {
9049
9822
  font-size: 0.875em;
9050
9823
  font-weight: 700;
9051
9824
  color: #374151;
@@ -9056,19 +9829,19 @@ const IntradayChartStyles = `
9056
9829
  border-bottom: 2px solid #e5e7eb;
9057
9830
  }
9058
9831
 
9059
- .stats-grid {
9832
+ .intraday-chart-widget .stats-grid {
9060
9833
  display: grid;
9061
9834
  grid-template-columns: repeat(5, 1fr);
9062
9835
  gap: 15px;
9063
9836
  }
9064
9837
 
9065
- .stat-item {
9838
+ .intraday-chart-widget .stat-item {
9066
9839
  display: flex;
9067
9840
  flex-direction: column;
9068
9841
  gap: 4px;
9069
9842
  }
9070
9843
 
9071
- .stat-label {
9844
+ .intraday-chart-widget .stat-label {
9072
9845
  font-size: 0.75em;
9073
9846
  color: #6b7280;
9074
9847
  font-weight: 600;
@@ -9076,13 +9849,13 @@ const IntradayChartStyles = `
9076
9849
  letter-spacing: 0.5px;
9077
9850
  }
9078
9851
 
9079
- .stat-value {
9852
+ .intraday-chart-widget .stat-value {
9080
9853
  font-size: 1.1em;
9081
9854
  font-weight: 700;
9082
9855
  color: #1f2937;
9083
9856
  }
9084
9857
 
9085
- .widget-loading-overlay {
9858
+ .intraday-chart-widget .widget-loading-overlay {
9086
9859
  position: absolute;
9087
9860
  top: 0;
9088
9861
  left: 0;
@@ -9101,26 +9874,26 @@ const IntradayChartStyles = `
9101
9874
  backdrop-filter: blur(1px);
9102
9875
  }
9103
9876
 
9104
- .widget-loading-overlay.hidden {
9877
+ .intraday-chart-widget .widget-loading-overlay.hidden {
9105
9878
  display: none;
9106
9879
  }
9107
9880
 
9108
- .loading-spinner {
9881
+ .intraday-chart-widget .loading-spinner {
9109
9882
  width: 20px;
9110
9883
  height: 20px;
9111
9884
  border: 3px solid #e5e7eb;
9112
9885
  border-top-color: #667eea;
9113
9886
  border-radius: 50%;
9114
- animation: spin 0.8s linear infinite;
9887
+ animation: intraday-spin 0.8s linear infinite;
9115
9888
  }
9116
9889
 
9117
- .loading-text {
9890
+ .intraday-chart-widget .loading-text {
9118
9891
  color: #6b7280;
9119
9892
  font-size: 0.875em;
9120
9893
  font-weight: 500;
9121
9894
  }
9122
9895
 
9123
- @keyframes spin {
9896
+ @keyframes intraday-spin {
9124
9897
  to { transform: rotate(360deg); }
9125
9898
  }
9126
9899
 
@@ -9130,26 +9903,26 @@ const IntradayChartStyles = `
9130
9903
  padding: 15px;
9131
9904
  }
9132
9905
 
9133
- .stats-grid {
9906
+ .intraday-chart-widget .stats-grid {
9134
9907
  grid-template-columns: repeat(3, 1fr);
9135
9908
  gap: 10px;
9136
9909
  }
9137
9910
 
9138
- .stats-header {
9911
+ .intraday-chart-widget .stats-header {
9139
9912
  font-size: 0.8em;
9140
9913
  margin-bottom: 10px;
9141
9914
  }
9142
9915
 
9143
- .chart-container {
9916
+ .intraday-chart-widget .chart-container {
9144
9917
  height: 350px;
9145
9918
  }
9146
9919
 
9147
- .intraday-chart-symbol {
9920
+ .intraday-chart-widget .intraday-chart-symbol {
9148
9921
  font-size: 1.2em;
9149
9922
  }
9150
9923
  }
9151
9924
 
9152
- .widget-error {
9925
+ .intraday-chart-widget .widget-error {
9153
9926
  padding: 15px;
9154
9927
  background: #fee2e2;
9155
9928
  border: 1px solid #fecaca;
@@ -42019,8 +42792,8 @@ class WebSocketManager {
42019
42792
  // Pass messageType from parent message, don't extract from each data item
42020
42793
  this._processDataItem(dataItem, relevantWidgets, messageType);
42021
42794
  });
42022
- } else if (item && item[0] && item[0].Strike !== undefined && item[0].Expire && !item[0].underlyingSymbol) {
42023
- this._processOptionChainData(item, relevantWidgets);
42795
+ } else if (item.data && item.data[0] && item.data[0].Strike !== undefined && item.data[0].Expire && !item.data[0].underlyingSymbol) {
42796
+ this._processOptionChainData(item.data, relevantWidgets);
42024
42797
  } else {
42025
42798
  // Process single item - messageType already extracted from message
42026
42799
  this._processDataItem(item, relevantWidgets, messageType);
@@ -42029,6 +42802,7 @@ class WebSocketManager {
42029
42802
 
42030
42803
  // Simplified option chain processing - handle entire array at once
42031
42804
  _processOptionChainData(optionChainArray, relevantWidgets) {
42805
+ //console.log('PROCESSING DATA OPTIONS')
42032
42806
  if (!optionChainArray || optionChainArray.length === 0) return;
42033
42807
  // Extract underlying symbol and date from first option contract
42034
42808
  const firstOption = optionChainArray[0];