@design.estate/dees-catalog 3.98.1 → 3.99.1

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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-catalog',
6
- version: '3.98.1',
6
+ version: '3.99.1',
7
7
  description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
8
8
  }
@@ -33,6 +33,11 @@ export type TChartPriceLine = 'avg' | 'max';
33
33
 
34
34
  export const CHART_LEGEND_STATS: TChartLegendStat[] = ['latest', 'min', 'max', 'avg'];
35
35
 
36
+ export interface IChartSelectedRange {
37
+ from: number;
38
+ to: number;
39
+ }
40
+
36
41
  declare global {
37
42
  interface HTMLElementTagNameMap {
38
43
  'dees-chart-area': DeesChartArea;
@@ -100,6 +105,12 @@ export class DeesChartArea extends DeesElement {
100
105
  @property({ type: Array })
101
106
  accessor chartLines: TChartPriceLine[] = ['avg', 'max'];
102
107
 
108
+
109
+ @property({ type: Boolean })
110
+ accessor rangeSelectionEnabled = false;
111
+
112
+ @property({ attribute: false })
113
+ accessor selectedRange: IChartSelectedRange | null = null;
103
114
  private internalChartData: ChartSeriesConfig = [];
104
115
  private autoScrollTimer: number | null = null;
105
116
  private lcBundle: ILightweightChartsBundle | null = null;
@@ -110,6 +121,62 @@ export class DeesChartArea extends DeesElement {
110
121
  private pendingRealtimePrune = false;
111
122
  private crosshairMoveHandler: ((param: MouseEventParams) => void) | null = null;
112
123
  private chartContainer: HTMLDivElement | null = null;
124
+ private rangeOverlay: HTMLDivElement | null = null;
125
+ private rangeResizeObserver: ResizeObserver | null = null;
126
+ private rangeDrag: { pointerId: number; startX: number; startMs: number } | null = null;
127
+ private readonly rangePointerDownHandler = (event: PointerEvent): void => {
128
+ if (!this.rangeSelectionEnabled || event.button !== 0 || !this.chartContainer) return;
129
+ const x = this.getRangePointerX(event);
130
+ const timeMs = this.coordinateToEpochMs(x);
131
+ if (timeMs === null) return;
132
+ event.preventDefault();
133
+ this.rangeDrag = { pointerId: event.pointerId, startX: x, startMs: timeMs };
134
+ try { this.chartContainer.setPointerCapture(event.pointerId); } catch { /* synthetic event */ }
135
+ window.addEventListener('keydown', this.rangeKeydownHandler);
136
+ this.paintRangeOverlay(x, x);
137
+ };
138
+ private readonly rangePointerMoveHandler = (event: PointerEvent): void => {
139
+ if (!this.rangeDrag || event.pointerId !== this.rangeDrag.pointerId) return;
140
+ event.preventDefault();
141
+ this.paintRangeOverlay(this.rangeDrag.startX, this.getRangePointerX(event));
142
+ };
143
+ private readonly rangePointerUpHandler = (event: PointerEvent): void => {
144
+ const drag = this.rangeDrag;
145
+ if (!drag || event.pointerId !== drag.pointerId) return;
146
+ const endX = this.getRangePointerX(event);
147
+ const endMs = this.coordinateToEpochMs(endX);
148
+ this.finishRangeDrag(event.pointerId);
149
+ if (endMs === null || Math.abs(endX - drag.startX) < 4) {
150
+ this.syncRangeOverlay();
151
+ return;
152
+ }
153
+ const from = Math.min(drag.startMs, endMs);
154
+ const to = Math.max(drag.startMs, endMs);
155
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from >= to) {
156
+ this.syncRangeOverlay();
157
+ return;
158
+ }
159
+ this.dispatchEvent(new CustomEvent<IChartSelectedRange>('range-change', {
160
+ detail: { from, to },
161
+ bubbles: true,
162
+ composed: true,
163
+ }));
164
+ this.syncRangeOverlay();
165
+ };
166
+ private readonly rangePointerCancelHandler = (event: PointerEvent): void => {
167
+ if (!this.rangeDrag || event.pointerId !== this.rangeDrag.pointerId) return;
168
+ this.finishRangeDrag(event.pointerId);
169
+ this.syncRangeOverlay();
170
+ };
171
+ private readonly rangeKeydownHandler = (event: KeyboardEvent): void => {
172
+ if (event.key !== 'Escape' || !this.rangeDrag) return;
173
+ const pointerId = this.rangeDrag.pointerId;
174
+ this.finishRangeDrag(pointerId);
175
+ this.syncRangeOverlay();
176
+ };
177
+ private readonly rangeVisibleTimeChangeHandler = (): void => {
178
+ this.syncRangeOverlay();
179
+ };
113
180
  private readonly chartMouseLeaveHandler = () => {
114
181
  this.chart?.clearCrosshairPosition();
115
182
  this.handleCrosshairCleared();
@@ -122,11 +189,15 @@ export class DeesChartArea extends DeesElement {
122
189
  this.stopAutoScroll();
123
190
  const chart = this.chart;
124
191
  const crosshairMoveHandler = this.crosshairMoveHandler;
125
- this.chart = null;
126
192
  this.crosshairMoveHandler = null;
127
193
  this.chartContainer?.removeEventListener('mouseleave', this.chartMouseLeaveHandler);
194
+ this.teardownRangeSelection();
195
+ this.chart = null;
196
+ this.rangeResizeObserver?.disconnect();
197
+ this.rangeResizeObserver = null;
128
198
  this.chartContainer = null;
129
199
  this.tooltipEl = null;
200
+ this.rangeOverlay = null;
130
201
  this.crosshairActive = false;
131
202
  this.pendingRealtimePrune = false;
132
203
  if (crosshairMoveHandler && chart) {
@@ -155,6 +226,90 @@ export class DeesChartArea extends DeesElement {
155
226
 
156
227
  // --- Helpers ---
157
228
 
229
+ private getRangePointerX(event: PointerEvent): number {
230
+ const rect = this.chartContainer!.getBoundingClientRect();
231
+ return Math.min(Math.max(event.clientX - rect.left, 0), rect.width);
232
+ }
233
+
234
+ private coordinateToEpochMs(coordinateArg: number): number | null {
235
+ const time = this.chart?.timeScale().coordinateToTime(coordinateArg);
236
+ if (typeof time !== 'number' || !Number.isFinite(time)) return null;
237
+ const epochMs = Math.round(time * 1000);
238
+ return Number.isSafeInteger(epochMs) && epochMs >= 0 ? epochMs : null;
239
+ }
240
+
241
+ private paintRangeOverlay(firstXArg: number, secondXArg: number): void {
242
+ if (!this.rangeOverlay) return;
243
+ const left = Math.min(firstXArg, secondXArg);
244
+ const width = Math.abs(secondXArg - firstXArg);
245
+ this.rangeOverlay.style.display = 'block';
246
+ this.rangeOverlay.style.left = `${left}px`;
247
+ this.rangeOverlay.style.width = `${Math.max(width, 1)}px`;
248
+ }
249
+
250
+ private finishRangeDrag(pointerIdArg: number): void {
251
+ this.rangeDrag = null;
252
+ window.removeEventListener('keydown', this.rangeKeydownHandler);
253
+ try { this.chartContainer?.releasePointerCapture(pointerIdArg); } catch { /* already released */ }
254
+ }
255
+
256
+ private setupRangeSelection(): void {
257
+ if (!this.chartContainer) return;
258
+ this.rangeOverlay = this.chartContainer.querySelector('.rangeSelectionOverlay');
259
+ this.chart?.timeScale().subscribeVisibleTimeRangeChange(this.rangeVisibleTimeChangeHandler);
260
+ this.chartContainer.addEventListener('pointerdown', this.rangePointerDownHandler);
261
+ this.chartContainer.addEventListener('pointermove', this.rangePointerMoveHandler);
262
+ this.chartContainer.addEventListener('pointerup', this.rangePointerUpHandler);
263
+ this.chartContainer.addEventListener('pointercancel', this.rangePointerCancelHandler);
264
+ this.chartContainer.addEventListener('lostpointercapture', this.rangePointerCancelHandler);
265
+ this.rangeResizeObserver = new ResizeObserver(() => this.syncRangeOverlay());
266
+ this.rangeResizeObserver.observe(this.chartContainer);
267
+ this.syncRangeOverlay();
268
+ }
269
+
270
+ private teardownRangeSelection(): void {
271
+ if (!this.chartContainer) return;
272
+ this.chart?.timeScale().unsubscribeVisibleTimeRangeChange(this.rangeVisibleTimeChangeHandler);
273
+ this.chartContainer.removeEventListener('pointerdown', this.rangePointerDownHandler);
274
+ this.chartContainer.removeEventListener('pointermove', this.rangePointerMoveHandler);
275
+ this.chartContainer.removeEventListener('pointerup', this.rangePointerUpHandler);
276
+ this.chartContainer.removeEventListener('pointercancel', this.rangePointerCancelHandler);
277
+ this.chartContainer.removeEventListener('lostpointercapture', this.rangePointerCancelHandler);
278
+ window.removeEventListener('keydown', this.rangeKeydownHandler);
279
+ if (this.rangeDrag) {
280
+ try { this.chartContainer.releasePointerCapture(this.rangeDrag.pointerId); } catch { /* no capture */ }
281
+ }
282
+ this.rangeDrag = null;
283
+ }
284
+
285
+ private syncRangeOverlay(): void {
286
+ if (!this.rangeOverlay || !this.chart || !this.rangeSelectionEnabled || !this.selectedRange) {
287
+ if (this.rangeOverlay) this.rangeOverlay.style.display = 'none';
288
+ return;
289
+ }
290
+ const { from, to } = this.selectedRange;
291
+ if (
292
+ !Number.isSafeInteger(from)
293
+ || !Number.isSafeInteger(to)
294
+ || from < 0
295
+ || to <= from
296
+ ) {
297
+ this.rangeOverlay.style.display = 'none';
298
+ return;
299
+ }
300
+ const fromCoordinate = this.chart.timeScale().timeToCoordinate(
301
+ Math.floor(from / 1000) as UTCTimestamp,
302
+ );
303
+ const toCoordinate = this.chart.timeScale().timeToCoordinate(
304
+ Math.floor(to / 1000) as UTCTimestamp,
305
+ );
306
+ if (fromCoordinate === null || toCoordinate === null) {
307
+ this.rangeOverlay.style.display = 'none';
308
+ return;
309
+ }
310
+ this.paintRangeOverlay(fromCoordinate, toCoordinate);
311
+ }
312
+
158
313
  private convertDataToLC(data: Array<{ x: any; y: number }>): Array<{ time: UTCTimestamp; value: number }> {
159
314
  const pointsByTime = new Map<number, { time: UTCTimestamp; value: number }>();
160
315
  for (const point of data) {
@@ -580,6 +735,7 @@ export class DeesChartArea extends DeesElement {
580
735
  this.internalChartData = canonicalSeries;
581
736
  this.recreateSeries(canonicalSeries);
582
737
  this.setupTooltip();
738
+ this.setupRangeSelection();
583
739
  this.syncAutoScroll();
584
740
  } catch (error) {
585
741
  console.error('Failed to initialize chart:', error);
@@ -620,6 +776,13 @@ export class DeesChartArea extends DeesElement {
620
776
  this.syncAutoScroll();
621
777
  }
622
778
 
779
+ if (
780
+ changedProperties.has('selectedRange')
781
+ || changedProperties.has('rangeSelectionEnabled')
782
+ ) {
783
+ this.syncRangeOverlay();
784
+ }
785
+
623
786
  if (changedProperties.has('chartLines') && this.chart) {
624
787
  this.refreshPriceLines();
625
788
  }
@@ -50,6 +50,16 @@ export const chartAreaStyles = [
50
50
  position: absolute;
51
51
  inset: 0 0 4px 0;
52
52
  }
53
+ .rangeSelectionOverlay {
54
+ position: absolute;
55
+ display: none;
56
+ top: 0;
57
+ bottom: 0;
58
+ z-index: 3;
59
+ pointer-events: none;
60
+ background: color-mix(in srgb, var(--dees-color-accent, #3b82f6) 18%, transparent);
61
+ border-inline: 1px solid color-mix(in srgb, var(--dees-color-accent, #3b82f6) 70%, transparent);
62
+ }
53
63
  .statsBar {
54
64
  min-height: 32px;
55
65
  padding: 4px 16px;
@@ -11,7 +11,9 @@ export const renderChartArea = (component: DeesChartArea): TemplateResult => {
11
11
  <dees-icon .icon=${component.isFullPage ? 'lucide:Minimize2' : 'lucide:Maximize2'} .iconSize=${14}></dees-icon>
12
12
  </button>
13
13
  </div>
14
- <div class="chartContainer"></div>
14
+ <div class="chartContainer">
15
+ <div class="rangeSelectionOverlay" aria-hidden="true"></div>
16
+ </div>
15
17
  ${component.seriesStats.length > 0 && !component.hideLegend ? html`
16
18
  <div slot="footer" class="statsBar">
17
19
  ${component.seriesStats.map(s => html`
@@ -116,6 +116,15 @@ export class DeesTable<T> extends DeesElement {
116
116
  type: Array,
117
117
  })
118
118
  accessor dataActions: ITableAction<T>[] = [];
119
+ private readonly __searchAction: ITableAction<T> = {
120
+ name: 'Search',
121
+ iconName: 'lucide:Search',
122
+ type: ['header'],
123
+ actionFunc: async () => {
124
+ const searchGrid = this.shadowRoot!.querySelector('.searchGrid');
125
+ searchGrid!.classList.toggle('hidden');
126
+ },
127
+ };
119
128
 
120
129
  // schema-first columns API
121
130
  @property({ attribute: false })
@@ -854,7 +863,7 @@ export class DeesTable<T> extends DeesElement {
854
863
  ${this.renderSortIndicator(col)}
855
864
  </th>`;
856
865
  })}
857
- ${this.dataActions && this.dataActions.length > 0
866
+ ${this.__getEffectiveDataActions().length > 0
858
867
  ? html`<th class="actionsCol">Actions</th>`
859
868
  : html``}
860
869
  </tr>
@@ -873,7 +882,7 @@ export class DeesTable<T> extends DeesElement {
873
882
  @input=${(e: Event) => this.setColumnFilter(key, (e.target as HTMLInputElement).value)} />
874
883
  </th>`;
875
884
  })}
876
- ${this.dataActions && this.dataActions.length > 0
885
+ ${this.__getEffectiveDataActions().length > 0
877
886
  ? html`<th></th>`
878
887
  : html``}
879
888
  </tr>`
@@ -1512,21 +1521,6 @@ export class DeesTable<T> extends DeesElement {
1512
1521
  this.__syncFloatingHeader();
1513
1522
  }
1514
1523
  if (this.searchable) {
1515
- const existing = this.dataActions.find((actionArg) => actionArg.type?.includes('header') && actionArg.name === 'Search');
1516
- if (!existing) {
1517
- this.dataActions.unshift({
1518
- name: 'Search',
1519
- iconName: 'lucide:Search',
1520
- type: ['header'],
1521
- actionFunc: async () => {
1522
- console.log('open search');
1523
- const searchGrid = this.shadowRoot!.querySelector('.searchGrid');
1524
- searchGrid!.classList.toggle('hidden');
1525
- }
1526
- });
1527
- console.log(this.dataActions);
1528
- this.requestUpdate();
1529
- };
1530
1524
  // wire search inputs
1531
1525
  this.wireSearchInputs();
1532
1526
  }
@@ -1595,7 +1589,9 @@ export class DeesTable<T> extends DeesElement {
1595
1589
  const width = window.getComputedStyle(cell).width;
1596
1590
  if (cell.textContent.includes('Actions')) {
1597
1591
  const neededWidth =
1598
- this.dataActions.filter((actionArg) => actionArg.type?.includes('inRow')).length * 36;
1592
+ this.__getEffectiveDataActions().filter((actionArg) =>
1593
+ actionArg.type?.includes('inRow')
1594
+ ).length * 36;
1599
1595
  cell.style.width = `${Math.max(neededWidth, 68)}px`;
1600
1596
  } else {
1601
1597
  cell.style.width = width;
@@ -2121,7 +2117,7 @@ export class DeesTable<T> extends DeesElement {
2121
2117
  this.startEditing(cell.item, cell.col);
2122
2118
  return;
2123
2119
  }
2124
- const dblAction = this.dataActions.find(
2120
+ const dblAction = this.__getEffectiveDataActions().find(
2125
2121
  (action) =>
2126
2122
  action.type?.includes('doubleClick')
2127
2123
  && this.isActionRelevant(action, cell.item)
@@ -2349,9 +2345,21 @@ export class DeesTable<T> extends DeesElement {
2349
2345
  );
2350
2346
  }
2351
2347
 
2348
+ private __getEffectiveDataActions(): ITableAction<T>[] {
2349
+ if (
2350
+ !this.searchable
2351
+ || this.dataActions.some((actionArg) =>
2352
+ actionArg.type?.includes('header') && actionArg.name === 'Search'
2353
+ )
2354
+ ) {
2355
+ return this.dataActions;
2356
+ }
2357
+ return [this.__searchAction, ...this.dataActions];
2358
+ }
2359
+
2352
2360
  getActionsForType(typeArg: ITableAction['type'][0]) {
2353
2361
  const actions: ITableAction[] = [];
2354
- for (const action of this.dataActions) {
2362
+ for (const action of this.__getEffectiveDataActions()) {
2355
2363
  if (!action.type?.includes(typeArg)) continue;
2356
2364
  actions.push(action);
2357
2365
  }