@design.estate/dees-catalog 9.8.0 → 11.0.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.
Files changed (67) hide show
  1. package/dist_bundle/bundle.js +5626 -5343
  2. package/dist_bundle/bundle.js.map +1 -1
  3. package/dist_bundle/third-party-notices.md +783 -10
  4. package/dist_ts_web/00_commitinfo_data.js +2 -2
  5. package/dist_ts_web/elements/00actionerror.d.ts +32 -0
  6. package/dist_ts_web/elements/00actionerror.js +45 -0
  7. package/dist_ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.d.ts +16 -0
  8. package/dist_ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.js +40 -3
  9. package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.d.ts +7 -0
  10. package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.js +17 -17
  11. package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.d.ts +14 -3
  12. package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.demo.js +23 -4
  13. package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.js +77 -5
  14. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +62 -48
  15. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.js +89 -51
  16. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +528 -662
  17. package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.d.ts +45 -6
  18. package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.js +184 -27
  19. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +55 -20
  20. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.d.ts +23 -3
  21. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.js +71 -11
  22. package/dist_ts_web/elements/00group-input/index.d.ts +0 -1
  23. package/dist_ts_web/elements/00group-input/index.js +1 -2
  24. package/dist_ts_web/elements/00group-overlay/dees-modal/dees-modal.d.ts +7 -0
  25. package/dist_ts_web/elements/00group-overlay/dees-modal/dees-modal.js +13 -3
  26. package/dist_ts_web/elements/index.d.ts +1 -2
  27. package/dist_ts_web/elements/index.js +2 -4
  28. package/dist_ts_web/services/DeesServiceLibLoader.d.ts +7 -0
  29. package/dist_ts_web/services/DeesServiceLibLoader.js +45 -22
  30. package/package.json +3 -4
  31. package/readme.md +105 -18
  32. package/scripts/check-third-party-notices.mjs +261 -0
  33. package/third-party-notices.md +783 -10
  34. package/ts_web/00_commitinfo_data.ts +1 -1
  35. package/ts_web/elements/00actionerror.ts +66 -0
  36. package/ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.ts +38 -2
  37. package/ts_web/elements/00group-dataview/dees-table/dees-table.ts +18 -16
  38. package/ts_web/elements/00group-feedback/dees-hint/dees-hint.demo.ts +22 -3
  39. package/ts_web/elements/00group-feedback/dees-hint/dees-hint.ts +71 -3
  40. package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.ts +99 -49
  41. package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +607 -752
  42. package/ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.ts +196 -24
  43. package/ts_web/elements/00group-harness/interfaces.ts +59 -19
  44. package/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts +73 -13
  45. package/ts_web/elements/00group-input/index.ts +0 -1
  46. package/ts_web/elements/00group-overlay/dees-modal/dees-modal.ts +13 -2
  47. package/ts_web/elements/index.ts +1 -4
  48. package/ts_web/services/DeesServiceLibLoader.ts +150 -102
  49. package/.smartconfig.json +0 -86
  50. package/dist_ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.d.ts +0 -1
  51. package/dist_ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.js +0 -2
  52. package/dist_ts_web/elements/00group-input/dees-input-searchselect/index.d.ts +0 -1
  53. package/dist_ts_web/elements/00group-input/dees-input-searchselect/index.js +0 -2
  54. package/dist_ts_web/elements/dees-search/dees-search.d.ts +0 -1
  55. package/dist_ts_web/elements/dees-search/dees-search.js +0 -2
  56. package/dist_ts_web/elements/dees-search/index.d.ts +0 -1
  57. package/dist_ts_web/elements/dees-search/index.js +0 -2
  58. package/dist_ts_web/elements/dees-tooltip/dees-tooltip.d.ts +0 -1
  59. package/dist_ts_web/elements/dees-tooltip/dees-tooltip.js +0 -2
  60. package/dist_ts_web/elements/dees-tooltip/index.d.ts +0 -1
  61. package/dist_ts_web/elements/dees-tooltip/index.js +0 -2
  62. package/ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.ts +0 -0
  63. package/ts_web/elements/00group-input/dees-input-searchselect/index.ts +0 -1
  64. package/ts_web/elements/dees-search/dees-search.ts +0 -0
  65. package/ts_web/elements/dees-search/index.ts +0 -1
  66. package/ts_web/elements/dees-tooltip/dees-tooltip.ts +0 -1
  67. package/ts_web/elements/dees-tooltip/index.ts +0 -1
@@ -29,13 +29,46 @@ export interface ITerminalResizeDetail {
29
29
  cols: number;
30
30
  }
31
31
 
32
+ /**
33
+ * Detail of `terminal-error`: xterm could not be loaded, or the live terminal
34
+ * refused output. The element stays usable and keeps buffering.
35
+ */
36
+ export interface ITerminalErrorDetail {
37
+ error: unknown;
38
+ }
39
+
40
+ /**
41
+ * Upper bound on output buffered while no terminal is live, in bytes.
42
+ * `Uint8Array` chunks count their `byteLength`, strings their UTF-16 memory
43
+ * (two bytes per code unit). Beyond the bound the oldest chunks are dropped,
44
+ * and the flush then emits RIS before the survivors so a truncated prefix is
45
+ * never rendered as if it were contiguous output.
46
+ */
47
+ export const maxPendingWriteBytes = 8 * 1024 * 1024;
48
+
49
+ /**
50
+ * RIS (ESC c), a hard reset carried *inside* the write stream: xterm parses it
51
+ * in order and routes it to the very same `Terminal.reset()` (InputHandler's
52
+ * ESC-c handler calls `fullReset()`, which fires `onRequestReset`, which
53
+ * `Terminal` handles with `reset()`), whereas calling `reset()` directly is
54
+ * documented as out of band with the pending write queue.
55
+ */
56
+ const RIS = '\x1bc';
57
+
58
+ /** Memory cost of one buffered chunk, in bytes. */
59
+ const chunkByteLength = (chunkArg: string | Uint8Array): number =>
60
+ typeof chunkArg === 'string' ? chunkArg.length * 2 : chunkArg.byteLength;
61
+
32
62
  /**
33
63
  * Transport-agnostic xterm.js surface: the host pipes remote PTY output into
34
64
  * `write()` and receives keystrokes via `terminal-input` — no process,
35
65
  * runtime, or socket coupling. `terminal-resize` reports the grid size after
36
- * every refit so the host can negotiate a PTY size. xterm loads through
37
- * DeesServiceLibLoader; CSP-locked hosts can pre-register bundled modules
38
- * with `provideXtermModules()` to avoid the CDN.
66
+ * every refit so the host can negotiate a PTY size. Output that arrives while
67
+ * no terminal is live is buffered up to `maxPendingWriteBytes` and replayed in
68
+ * order on the next connection, so a replay may start before the element is
69
+ * ready and survives being detached. xterm loads through DeesServiceLibLoader;
70
+ * CSP-locked hosts can pre-register bundled modules with
71
+ * `provideXtermModules()` to avoid the CDN.
39
72
  */
40
73
  @customElement('dees-terminal-view')
41
74
  export class DeesTerminalView extends DeesElement {
@@ -54,9 +87,37 @@ export class DeesTerminalView extends DeesElement {
54
87
  private resizeObserver: ResizeObserver | undefined;
55
88
  private inputDisposable: { dispose(): void } | undefined;
56
89
  private lastReportedSize = '';
57
- // Output that arrives before xterm finished loading is replayed in order.
90
+ // Output that arrives before xterm is live is replayed in order, bounded by
91
+ // maxPendingWriteBytes and retained across a disconnect.
58
92
  private pendingWrites: Array<string | Uint8Array> = [];
93
+ private pendingWriteBytes = 0;
94
+ private pendingWritesDropped = false;
59
95
  private setupPromise: Promise<void> | undefined;
96
+ private readyPending = true;
97
+ private readyResolve: () => void = () => undefined;
98
+ private readyReject: (reasonArg: unknown) => void = () => undefined;
99
+
100
+ /**
101
+ * Resolves once the terminal of the current connection is live; a fresh
102
+ * promise is created for every connection. It rejects with the load error
103
+ * when xterm cannot be loaded (`terminal-error` carries the same error), and
104
+ * when the element is disconnected before the terminal came up.
105
+ */
106
+ public ready: Promise<void>;
107
+
108
+ constructor() {
109
+ super();
110
+ this.ready = this.createReadyPromise();
111
+ // The house lifecycle: startup runs on every connection with an abort
112
+ // signal, garbage on every disconnection, so a reattached element is a
113
+ // live surface again instead of a permanently dead one.
114
+ this.registerStartupFunction(async (signalArg) => {
115
+ await this.ensureTerminal(signalArg);
116
+ });
117
+ this.registerGarbageFunction(() => {
118
+ this.teardownTerminal();
119
+ });
120
+ }
60
121
 
61
122
  public static styles = [
62
123
  themeDefaultStyles,
@@ -209,43 +270,133 @@ export class DeesTerminalView extends DeesElement {
209
270
  return html`<div class="terminalHost"></div>`;
210
271
  }
211
272
 
212
- public async firstUpdated(): Promise<void> {
213
- this.setupPromise = this.setupTerminal();
214
- await this.setupPromise;
273
+ public connectedCallback(): Promise<void> {
274
+ // A consumer awaiting readiness after reattaching must not be handed the
275
+ // settled promise of the previous connection.
276
+ if (!this.readyPending) this.ready = this.createReadyPromise();
277
+ return super.connectedCallback();
215
278
  }
216
279
 
217
280
  /** Feed remote output into the terminal; safe to call before setup finishes. */
218
281
  public write(dataArg: string | Uint8Array): void {
219
- if (this.terminal) {
220
- this.terminal.write(dataArg);
221
- } else {
222
- this.pendingWrites.push(dataArg);
282
+ // While detached the terminal is about to be disposed, so output belongs in
283
+ // the buffer rather than in a surface nobody will see again.
284
+ if (this.terminal && this.isConnected) {
285
+ this.writeToTerminal(this.terminal, dataArg);
286
+ return;
223
287
  }
288
+ this.bufferWrite(dataArg);
224
289
  }
225
290
 
226
291
  /** Reset the screen and scrollback, e.g. before a full replay. */
227
292
  public clear(): void {
228
- this.pendingWrites = [];
229
- this.terminal?.reset();
293
+ const terminal = this.terminal;
294
+ if (terminal && this.isConnected) {
295
+ this.writeToTerminal(terminal, RIS);
296
+ return;
297
+ }
298
+ // Without a terminal the reset is expressed as a buffered sentinel, so the
299
+ // flush of the next connection still performs it in the right order.
300
+ this.pendingWrites = [RIS];
301
+ this.pendingWriteBytes = chunkByteLength(RIS);
302
+ this.pendingWritesDropped = false;
230
303
  }
231
304
 
232
305
  public focus(): void {
233
306
  this.terminal?.focus();
234
307
  }
235
308
 
236
- /** Current grid size, or undefined while xterm is still loading. */
309
+ /** Current grid size, or undefined while no terminal is live. */
237
310
  public get currentSize(): ITerminalResizeDetail | undefined {
238
311
  if (!this.terminal) return undefined;
239
312
  return { rows: this.terminal.rows, cols: this.terminal.cols };
240
313
  }
241
314
 
242
- private async setupTerminal(): Promise<void> {
315
+ private createReadyPromise(): Promise<void> {
316
+ this.readyPending = true;
317
+ const promise = new Promise<void>((resolveArg, rejectArg) => {
318
+ this.readyResolve = () => {
319
+ this.readyPending = false;
320
+ resolveArg();
321
+ };
322
+ this.readyReject = (reasonArg) => {
323
+ this.readyPending = false;
324
+ rejectArg(reasonArg);
325
+ };
326
+ });
327
+ // A readiness promise nobody awaits must never surface as an unhandled
328
+ // rejection; consumers that do await it still observe the reason.
329
+ promise.catch(() => undefined);
330
+ return promise;
331
+ }
332
+
333
+ private bufferWrite(chunkArg: string | Uint8Array): void {
334
+ this.pendingWrites.push(chunkArg);
335
+ this.pendingWriteBytes += chunkByteLength(chunkArg);
336
+ while (this.pendingWriteBytes > maxPendingWriteBytes && this.pendingWrites.length > 1) {
337
+ const droppedChunk = this.pendingWrites.shift()!;
338
+ this.pendingWriteBytes -= chunkByteLength(droppedChunk);
339
+ this.pendingWritesDropped = true;
340
+ }
341
+ }
342
+
343
+ private flushPendingWrites(terminalArg: Terminal): void {
344
+ const pending = this.pendingWrites;
345
+ const dropped = this.pendingWritesDropped;
346
+ this.pendingWrites = [];
347
+ this.pendingWriteBytes = 0;
348
+ this.pendingWritesDropped = false;
349
+ if (dropped && !this.writeToTerminal(terminalArg, RIS)) return;
350
+ for (const chunk of pending) {
351
+ if (!this.writeToTerminal(terminalArg, chunk)) return;
352
+ }
353
+ }
354
+
355
+ /** xterm throws at its own flow-control watermark; that has to reach the host. */
356
+ private writeToTerminal(terminalArg: Terminal, chunkArg: string | Uint8Array): boolean {
357
+ try {
358
+ terminalArg.write(chunkArg);
359
+ return true;
360
+ } catch (error) {
361
+ this.reportError(error);
362
+ return false;
363
+ }
364
+ }
365
+
366
+ private reportError(errorArg: unknown): void {
367
+ if (this.readyPending) this.readyReject(errorArg);
368
+ const detail: ITerminalErrorDetail = { error: errorArg };
369
+ this.dispatchEvent(
370
+ new CustomEvent('terminal-error', { detail, bubbles: true, composed: true }),
371
+ );
372
+ }
373
+
374
+ private ensureTerminal(signalArg: AbortSignal): Promise<void> {
375
+ if (this.terminal) return Promise.resolve();
376
+ if (!this.setupPromise) this.setupPromise = this.setupTerminal(signalArg);
377
+ return this.setupPromise;
378
+ }
379
+
380
+ private async setupTerminal(signalArg: AbortSignal): Promise<void> {
381
+ try {
382
+ await this.createTerminal(signalArg);
383
+ } catch (error) {
384
+ this.reportError(error);
385
+ } finally {
386
+ // Nothing came up: release the guard so the next connection retries.
387
+ if (!this.terminal) this.setupPromise = undefined;
388
+ }
389
+ }
390
+
391
+ private async createTerminal(signalArg: AbortSignal): Promise<void> {
392
+ await this.updateComplete;
243
393
  const libLoader = DeesServiceLibLoader.getInstance();
244
394
  const [xterm, fit] = await Promise.all([
245
395
  libLoader.loadXterm(),
246
396
  libLoader.loadXtermFitAddon(),
247
397
  ]);
248
- if (!this.isConnected) return;
398
+ // The load was awaited: the element may have been detached meanwhile.
399
+ if (signalArg.aborted || !this.isConnected) return;
249
400
  const host = this.shadowRoot?.querySelector<HTMLElement>('.terminalHost');
250
401
  if (!host) return;
251
402
  const computed = getComputedStyle(this);
@@ -265,9 +416,21 @@ export class DeesTerminalView extends DeesElement {
265
416
  cursor: themeColor('--dees-color-accent-primary', '#e6e6e6'),
266
417
  },
267
418
  });
268
- const fitAddon = new fit.FitAddon();
269
- terminal.loadAddon(fitAddon);
270
- terminal.open(host);
419
+ let fitAddon: FitAddon;
420
+ try {
421
+ fitAddon = new fit.FitAddon();
422
+ terminal.loadAddon(fitAddon);
423
+ terminal.open(host);
424
+ } catch (error) {
425
+ // The element does not own this instance yet, so teardown cannot reach
426
+ // it; a half-opened terminal would otherwise leak with its DOM attached.
427
+ try {
428
+ terminal.dispose();
429
+ } catch {
430
+ // Disposing a half-opened terminal must not mask the real failure.
431
+ }
432
+ throw error;
433
+ }
271
434
  this.terminal = terminal;
272
435
  this.fitAddon = fitAddon;
273
436
  this.inputDisposable = terminal.onData((dataArg) => {
@@ -277,11 +440,11 @@ export class DeesTerminalView extends DeesElement {
277
440
  new CustomEvent('terminal-input', { detail, bubbles: true, composed: true }),
278
441
  );
279
442
  });
280
- for (const pending of this.pendingWrites) terminal.write(pending);
281
- this.pendingWrites = [];
443
+ this.flushPendingWrites(terminal);
282
444
  this.refit();
283
445
  this.resizeObserver = new ResizeObserver(() => this.refit());
284
446
  this.resizeObserver.observe(host);
447
+ this.readyResolve();
285
448
  }
286
449
 
287
450
  private refit(): void {
@@ -303,8 +466,7 @@ export class DeesTerminalView extends DeesElement {
303
466
  );
304
467
  }
305
468
 
306
- public async disconnectedCallback(): Promise<void> {
307
- await super.disconnectedCallback();
469
+ private teardownTerminal(): void {
308
470
  this.resizeObserver?.disconnect();
309
471
  this.resizeObserver = undefined;
310
472
  this.inputDisposable?.dispose();
@@ -312,6 +474,16 @@ export class DeesTerminalView extends DeesElement {
312
474
  this.terminal?.dispose();
313
475
  this.terminal = undefined;
314
476
  this.fitAddon = undefined;
315
- this.pendingWrites = [];
477
+ // Instance state of the finished connection: a reattached element has to
478
+ // re-announce its grid and may retry a load that failed.
479
+ this.lastReportedSize = '';
480
+ this.setupPromise = undefined;
481
+ // Buffered output deliberately survives, so a replay that arrived while the
482
+ // element was detached still reaches the next connection.
483
+ if (this.readyPending) {
484
+ this.readyReject(
485
+ new Error('dees-terminal-view was disconnected before the terminal became ready'),
486
+ );
487
+ }
316
488
  }
317
489
  }
@@ -361,6 +361,23 @@ export interface IHarnessSessionMeta {
361
361
  draggable?: boolean;
362
362
  }
363
363
 
364
+ /** Discriminates the two kinds of rows the session list orders as peers. */
365
+ export type THarnessSessionListItemKind = 'session' | 'resource';
366
+
367
+ /**
368
+ * Stable reference to one ordered row. Session ids and resource ids are
369
+ * separate namespaces, so ordering always carries the kind alongside the id.
370
+ */
371
+ export interface IHarnessSessionListItemRef {
372
+ kind: THarnessSessionListItemKind;
373
+ id: string;
374
+ }
375
+
376
+ /** Detail of `harness-session-select` and `harness-session-open`. */
377
+ export interface IHarnessSessionEventDetail {
378
+ session: IHarnessSessionMeta;
379
+ }
380
+
364
381
  /** Host-owned metadata for a resource that can be associated with a harness session. */
365
382
  export interface IHarnessResourceMeta<TState extends string = string> {
366
383
  /** Stable identity used by selection and association records. */
@@ -375,41 +392,51 @@ export interface IHarnessResourceMeta<TState extends string = string> {
375
392
  updatedAt?: number;
376
393
  /** Optional host-defined display state. */
377
394
  state?: TState;
395
+ /** Set false for pinned, non-sortable entries. Defaults to true. */
396
+ draggable?: boolean;
378
397
  }
379
398
 
380
- /** A controlled zero-or-one session association for one resource. */
399
+ /**
400
+ * A controlled zero-or-one association for one resource. The target is any
401
+ * other list item — a session or another resource such as a terminal.
402
+ */
381
403
  export interface IHarnessResourceAssociation {
382
404
  resourceId: string;
383
- sessionId: string;
405
+ target: IHarnessSessionListItemRef;
384
406
  }
385
407
 
386
408
  export interface IHarnessResourceEventDetail<TState extends string = string> {
387
409
  resource: IHarnessResourceMeta<TState>;
388
- sessionId: string | null;
410
+ /** The controlled association target, or null when the resource is unattached. */
411
+ target: IHarnessSessionListItemRef | null;
389
412
  }
390
413
 
391
- export type THarnessResourceAssociationInteractionSource = 'drop' | 'action' | 'keyboard';
414
+ /**
415
+ * Association is an explicit host action, never a drop: dragging a resource
416
+ * only moves it inside the ordered list.
417
+ */
418
+ export type THarnessResourceAssociationInteractionSource = 'action' | 'keyboard';
392
419
 
393
420
  export type THarnessResourceAssociationRequestDetail<TState extends string = string> =
394
421
  | {
395
422
  intent: 'attach';
396
423
  resource: IHarnessResourceMeta<TState>;
397
- fromSessionId: null;
398
- toSessionId: string;
424
+ fromTarget: null;
425
+ toTarget: IHarnessSessionListItemRef;
399
426
  source: THarnessResourceAssociationInteractionSource;
400
427
  }
401
428
  | {
402
429
  intent: 'detach';
403
430
  resource: IHarnessResourceMeta<TState>;
404
- fromSessionId: string;
405
- toSessionId: null;
431
+ fromTarget: IHarnessSessionListItemRef;
432
+ toTarget: null;
406
433
  source: THarnessResourceAssociationInteractionSource;
407
434
  }
408
435
  | {
409
436
  intent: 'reassign';
410
437
  resource: IHarnessResourceMeta<TState>;
411
- fromSessionId: string;
412
- toSessionId: string;
438
+ fromTarget: IHarnessSessionListItemRef;
439
+ toTarget: IHarnessSessionListItemRef;
413
440
  source: THarnessResourceAssociationInteractionSource;
414
441
  };
415
442
 
@@ -423,7 +450,7 @@ export interface IHarnessResourceContextDetail<TState extends string = string>
423
450
  source: THarnessResourceContextSource;
424
451
  originalEvent: MouseEvent | KeyboardEvent;
425
452
  /** Routes a host menu choice through the component's validated controlled request event. */
426
- requestAssociation: (toSessionId: string | null) => boolean;
453
+ requestAssociation: (toTarget: IHarnessSessionListItemRef | null) => boolean;
427
454
  /** Closes the external-menu disclosure state and restores focus to its invoking control. */
428
455
  close: () => void;
429
456
  }
@@ -438,22 +465,35 @@ export interface IHarnessSessionContextDetail {
438
465
  originalEvent: MouseEvent;
439
466
  }
440
467
 
441
- /** A user-defined session group; ordering of `sessionIds` is the display order. */
468
+ /**
469
+ * A user-defined group of list items; ordering of `itemIds` is the display
470
+ * order. Sessions and resources may be members in any interleaving.
471
+ */
442
472
  export interface IHarnessSessionGroup {
443
473
  id: string;
444
474
  name: string;
445
- sessionIds: string[];
475
+ itemIds: IHarnessSessionListItemRef[];
446
476
  }
447
477
 
448
- /** Detail of `harness-session-move`: a session was dropped into a group slot. */
449
- export interface IHarnessSessionMoveDetail {
450
- sessionId: string;
478
+ export type THarnessItemMoveSource = 'pointer' | 'keyboard';
479
+
480
+ /** Detail of `harness-item-move`: one item landed in a new ordered position. */
481
+ export interface IHarnessItemMoveDetail {
482
+ item: IHarnessSessionListItemRef;
483
+ /** Group the item came from, or null for the ungrouped section. */
484
+ fromGroupId: string | null;
451
485
  /** Target group, or null for the ungrouped section. */
486
+ toGroupId: string | null;
487
+ /** Stable target anchor in the dragged-item-excluded order; null means append. */
488
+ beforeItem: IHarnessSessionListItemRef | null;
489
+ source: THarnessItemMoveSource;
490
+ }
491
+
492
+ /** The open insertion slot a drag currently targets. */
493
+ export interface IDropSlot {
452
494
  groupId: string | null;
453
- /** Compatibility insertion index using the pre-drag order for same-section moves. */
495
+ /** Index within the section's sortable, dragged-item-excluded order. */
454
496
  index: number;
455
- /** Stable target anchor in the dragged-card-excluded order; absent means append. */
456
- beforeSessionId?: string;
457
497
  }
458
498
 
459
499
  /** Detail of `harness-group-context`, fired on right-click of a group header. */
@@ -21,17 +21,44 @@ declare global {
21
21
  }
22
22
  }
23
23
 
24
+ /** Native input types this field maps to. Anything richer has its own element. */
25
+ export type TTextInputType = 'text' | 'number' | 'date' | 'email' | 'url' | 'tel' | 'password';
26
+
27
+ /** Keyboard hint per type; `inputMode` on the host still wins. */
28
+ const defaultInputModes: Record<TTextInputType, string> = {
29
+ text: 'text',
30
+ number: 'decimal',
31
+ date: 'text',
32
+ email: 'email',
33
+ url: 'url',
34
+ tel: 'tel',
35
+ password: 'text',
36
+ };
37
+
24
38
  @customElement('dees-input-text')
25
39
  export class DeesInputText extends DeesInputBase {
26
40
  public static demo = demoFunc;
27
41
  public static demoGroups = ['Input'];
28
42
 
29
43
  // INSTANCE
44
+ /**
45
+ * The native input type. `number` keeps the field's value numeric, so a form
46
+ * collects a number rather than a numeric string; every other type keeps the
47
+ * value a string. `password` masks the value; `isPasswordBool` additionally
48
+ * renders the reveal button.
49
+ */
50
+ @property({
51
+ type: String,
52
+ reflect: true,
53
+ })
54
+ accessor type: TTextInputType = 'text';
55
+
56
+ /** A string for every type except `number`, which holds a number or null while empty. */
30
57
  @property({
31
58
  type: String,
32
59
  reflect: true,
33
60
  })
34
- accessor value: string = '';
61
+ accessor value: string | number | null = '';
35
62
 
36
63
  @property({
37
64
  type: Boolean,
@@ -315,8 +342,8 @@ export class DeesInputText extends DeesInputBase {
315
342
  <div class="maincontainer">
316
343
  <input
317
344
  class=${this.vintegrated ? '' : 'field-control'}
318
- type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}"
319
- .value=${this.value}
345
+ type="${this.nativeInputType}"
346
+ .value=${this.valueAsText}
320
347
  @input="${this.updateValue}"
321
348
  .disabled=${this.disabled}
322
349
  ?required=${this.required}
@@ -324,7 +351,7 @@ export class DeesInputText extends DeesInputBase {
324
351
  aria-invalid=${this.validationState === 'invalid' ? 'true' : 'false'}
325
352
  aria-describedby="field-description field-validation"
326
353
  placeholder=${this.placeholder || (this.label ? '' : 'Enter text…')}
327
- inputmode=${this.inputMode || 'text'}
354
+ inputmode=${this.inputMode || defaultInputModes[this.type] || 'text'}
328
355
  />
329
356
  ${this.isPasswordBool
330
357
  ? html`
@@ -352,6 +379,39 @@ export class DeesInputText extends DeesInputBase {
352
379
  `;
353
380
  }
354
381
 
382
+ /** The type handed to the native input; masking wins over the declared type. */
383
+ private get nativeInputType(): string {
384
+ if (this.isPasswordBool || this.type === 'password') {
385
+ return this.showPasswordBool ? 'text' : 'password';
386
+ }
387
+ return this.type;
388
+ }
389
+
390
+ /** The value as text, for validators, the context menu and the native element. */
391
+ private get valueAsText(): string {
392
+ return this.value === null || this.value === undefined ? '' : String(this.value);
393
+ }
394
+
395
+ /**
396
+ * Keeps `value` in the shape the current type promises, whatever a host
397
+ * binds: a number (or null while empty) for `number`, a string otherwise.
398
+ */
399
+ private normalizeValue(valueArg: string | number | null | undefined): string | number | null {
400
+ if (this.type !== 'number') {
401
+ return valueArg === null || valueArg === undefined ? '' : String(valueArg);
402
+ }
403
+ if (valueArg === null || valueArg === undefined || valueArg === '') return null;
404
+ const parsed = typeof valueArg === 'number' ? valueArg : Number(valueArg);
405
+ return Number.isNaN(parsed) ? null : parsed;
406
+ }
407
+
408
+ public willUpdate(changedProperties: Map<PropertyKey, unknown>): void {
409
+ super.willUpdate(changedProperties);
410
+ if (!changedProperties.has('value') && !changedProperties.has('type')) return;
411
+ const normalized = this.normalizeValue(this.value);
412
+ if (normalized !== this.value) this.value = normalized;
413
+ }
414
+
355
415
  /**
356
416
  * `autocomplete` is applied imperatively rather than bound in the template so that the
357
417
  * default renders no attribute at all. Binding it would emit `autocomplete=""` on every
@@ -372,8 +432,8 @@ export class DeesInputText extends DeesInputBase {
372
432
  }
373
433
 
374
434
  firstUpdated() {
375
- if (this.validationFunction && this.value) {
376
- const result = this.validationFunction(this.value);
435
+ if (this.validationFunction && this.value !== null && this.value !== '') {
436
+ const result = this.validationFunction(this.valueAsText);
377
437
  this.validationState = result.valid ? 'valid' : 'invalid';
378
438
  this.validationText = result.message || '';
379
439
  if (result.valid) {
@@ -387,10 +447,10 @@ export class DeesInputText extends DeesInputBase {
387
447
  }
388
448
 
389
449
  public async updateValue(eventArg: Event) {
390
- const target: any = eventArg.target;
391
- this.value = target.value;
450
+ const target = eventArg.target as HTMLInputElement;
451
+ this.value = this.normalizeValue(target.value);
392
452
  if (this.validationFunction) {
393
- const result = this.validationFunction(this.value);
453
+ const result = this.validationFunction(this.valueAsText);
394
454
  this.validationState = result.valid ? 'valid' : 'invalid';
395
455
  this.validationText = result.message || '';
396
456
  if (result.valid) {
@@ -411,16 +471,16 @@ export class DeesInputText extends DeesInputBase {
411
471
  public getContextMenuItems() {
412
472
  return textInputContextMenuItems({
413
473
  input: this.shadowRoot!.querySelector('input')!, disabled: this.disabled,
414
- commit: value => { this.value = value; this.changeSubject.next(this); },
474
+ commit: value => { this.value = this.normalizeValue(value); this.changeSubject.next(this); },
415
475
  });
416
476
  }
417
477
 
418
- public getValue(): string {
478
+ public getValue(): string | number | null {
419
479
  return this.value;
420
480
  }
421
481
 
422
- public setValue(value: string): void {
423
- this.value = value;
482
+ public setValue(value: string | number | null): void {
483
+ this.value = this.normalizeValue(value);
424
484
  }
425
485
 
426
486
  public async togglePasswordView() {
@@ -12,7 +12,6 @@ export * from './dees-input-phone/index.js';
12
12
  export * from './dees-input-quantityselector/index.js';
13
13
  export * from './dees-input-radiogroup/index.js';
14
14
  export * from './dees-input-richtext/index.js';
15
- export * from './dees-input-searchselect/index.js';
16
15
  export * from './dees-input-tags/index.js';
17
16
  export * from './dees-input-text/index.js';
18
17
  export * from './dees-input-textarea/index.js';
@@ -1,5 +1,6 @@
1
1
  import * as plugins from '../../00plugins.js';
2
2
  import { zIndexRegistry } from '../../00zindex.js';
3
+ import { runAction } from '../../00actionerror.js';
3
4
 
4
5
  import { demoFunc } from './dees-modal.demo.js';
5
6
  import {
@@ -49,7 +50,7 @@ export class DeesModal extends DeesElement {
49
50
  let modal: DeesModal;
50
51
  const confirm = async () => {
51
52
  if (modal.promptPending) return;
52
- const value = input.value.trim();
53
+ const value = String(input.value ?? '').trim();
53
54
  const error = value ? options.validate?.(value) : 'Enter a name.';
54
55
  if (error) {
55
56
  input.validationState = 'invalid'; input.validationText = error;
@@ -332,7 +333,7 @@ export class DeesModal extends DeesElement {
332
333
  <dees-button class="modal-action" .text=${action.name}
333
334
  .disabled=${this.promptPending}
334
335
  .type=${index === this.menuOptions.length - 1 ? 'accent' : 'secondary'}
335
- @clicked=${() => action.action(this)}></dees-button>
336
+ @clicked=${() => this.runMenuAction(action)}></dees-button>
336
337
  `)}
337
338
  </div>
338
339
  ` : ''}
@@ -385,4 +386,14 @@ export class DeesModal extends DeesElement {
385
386
  private async handleHelp() {
386
387
  if (this.onHelp) await this.onHelp();
387
388
  }
389
+
390
+ /**
391
+ * A menu action is awaited so a rejected callback surfaces as a visible
392
+ * failure instead of an unhandled rejection. The error is announced on the
393
+ * modal through the cancelable `dees-action-error` event and, unless a
394
+ * listener cancels it, shown as a notification inside this dialog.
395
+ */
396
+ private async runMenuAction(actionArg: plugins.tsclass.website.IMenuItem<DeesModal>) {
397
+ await runAction(this, { action: actionArg, source: 'modal-menu', ownerElement: this.dialog }, () => actionArg.action(this));
398
+ }
388
399
  }
@@ -1,3 +1,4 @@
1
+ export * from './00actionerror.js';
1
2
  export * from './00zindex.js';
2
3
  export * from './00theme.js';
3
4
 
@@ -17,7 +18,3 @@ export * from './00group-runtime/index.js';
17
18
  export * from './00group-simple/index.js';
18
19
  export * from './00group-utility/index.js';
19
20
  export * from './00group-workspace/index.js';
20
-
21
- // Standalone Components
22
- export * from './dees-search/index.js';
23
- export * from './dees-tooltip/index.js';