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