@design.estate/dees-catalog 3.97.0 → 3.98.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 (37) hide show
  1. package/dist_bundle/bundle.js +4701 -3485
  2. package/dist_ts_web/00_commitinfo_data.js +1 -1
  3. package/dist_ts_web/elements/00group-button/dees-button/dees-button.d.ts +35 -0
  4. package/dist_ts_web/elements/00group-button/dees-button/dees-button.js +128 -5
  5. package/dist_ts_web/elements/00group-dataview/dees-statsgrid/dees-statsgrid.js +5 -6
  6. package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.d.ts +1 -0
  7. package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.js +21 -11
  8. package/dist_ts_web/elements/00group-feedback/dees-spinner/dees-spinner.js +12 -3
  9. package/dist_ts_web/elements/00group-form/dees-form-submit/dees-form-submit.d.ts +9 -0
  10. package/dist_ts_web/elements/00group-form/dees-form-submit/dees-form-submit.js +25 -2
  11. package/dist_ts_web/elements/00group-input/dees-input-multitoggle/dees-input-multitoggle.js +20 -4
  12. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.d.ts +15 -0
  13. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.js +36 -2
  14. package/dist_ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.js +2 -1
  15. package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.d.ts +246 -0
  16. package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.demo.js +439 -21
  17. package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.js +857 -23
  18. package/dist_ts_web/elements/00group-utility/dees-icon/dees-icon.d.ts +2 -1
  19. package/dist_ts_web/elements/00group-utility/dees-icon/dees-icon.js +52 -80
  20. package/dist_ts_web/elements/00theme.d.ts +2 -0
  21. package/dist_ts_web/elements/00theme.js +7 -1
  22. package/package.json +3 -3
  23. package/readme.hints.md +35 -0
  24. package/readme.md +85 -11
  25. package/ts_web/00_commitinfo_data.ts +1 -1
  26. package/ts_web/elements/00group-button/dees-button/dees-button.ts +128 -2
  27. package/ts_web/elements/00group-dataview/dees-statsgrid/dees-statsgrid.ts +5 -6
  28. package/ts_web/elements/00group-dataview/dees-table/dees-table.ts +43 -27
  29. package/ts_web/elements/00group-feedback/dees-spinner/dees-spinner.ts +11 -2
  30. package/ts_web/elements/00group-form/dees-form-submit/dees-form-submit.ts +18 -0
  31. package/ts_web/elements/00group-input/dees-input-multitoggle/dees-input-multitoggle.ts +19 -3
  32. package/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts +29 -0
  33. package/ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.ts +1 -0
  34. package/ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.demo.ts +418 -19
  35. package/ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.ts +969 -20
  36. package/ts_web/elements/00group-utility/dees-icon/dees-icon.ts +55 -91
  37. package/ts_web/elements/00theme.ts +8 -0
@@ -78,6 +78,15 @@ export class DeesButton extends DeesElement {
78
78
  })
79
79
  accessor insideForm: boolean = false;
80
80
 
81
+ /**
82
+ * stretches the inner button face to the host width.
83
+ * The host is `inline-block`, so `width: 100%` on the host alone leaves the
84
+ * `inline-flex` face shrink-wrapped. Opt in for stacked, full-bleed actions
85
+ * (login cards, sheets, empty states).
86
+ */
87
+ @property({ type: Boolean, reflect: true, attribute: 'full-width' })
88
+ accessor fullWidth: boolean = false;
89
+
81
90
  @property({ type: String, reflect: true })
82
91
  accessor icon!: string;
83
92
 
@@ -124,6 +133,26 @@ export class DeesButton extends DeesElement {
124
133
  display: none;
125
134
  }
126
135
 
136
+ :host([full-width]) {
137
+ display: block;
138
+ width: 100%;
139
+ }
140
+
141
+ /* A full-width button is sized by its container, so its label has to yield rather
142
+ than spill out of the face when the container is narrower than the text. */
143
+ :host([full-width]) .button {
144
+ width: 100%;
145
+ min-width: 0;
146
+ overflow: hidden;
147
+ }
148
+
149
+ :host([full-width]) .textbox {
150
+ min-width: 0;
151
+ overflow: hidden;
152
+ text-overflow: ellipsis;
153
+ white-space: nowrap;
154
+ }
155
+
127
156
  /* Form spacing styles */
128
157
  :host([inside-form]) {
129
158
  margin-bottom: var(--dees-spacing-lg);
@@ -363,10 +392,30 @@ export class DeesButton extends DeesElement {
363
392
  display: none;
364
393
  }
365
394
 
366
- /* Focus state */
395
+ /* Focus state.
396
+ The base .button rule sets outline:none for the pointer path; the ring is restored
397
+ here for :focus-visible only, so a mouse press never leaves a ring behind while
398
+ keyboard focus is always visible. This rule is the focus affordance keyboard users
399
+ actually see — do not remove it alongside the base outline:none. */
367
400
  .button:focus-visible {
368
- outline: 2px solid var(--dees-color-focus-ring);
401
+ /* Deliberately NOT --dees-color-focus-ring: that token is a 0.45/0.55 alpha, which
402
+ composites to 1.85:1 (bright) and 2.31:1 (dark) against the surface behind the
403
+ button — under the 3:1 that WCAG 2.2 SC 1.4.11 requires of a focus indicator, and
404
+ effectively invisible as a navy hairline on a dark canvas. The opaque accent
405
+ measures 4.02:1 bright / 5.76:1 dark. Raising the alpha on the shared token would
406
+ fix every other control too and is the wider change. */
407
+ outline: 2px solid var(--dees-color-accent-primary);
369
408
  outline-offset: 2px;
409
+ /* The indicator has to appear at once. The base .button rule carries
410
+ transition:all, which animates outline-color up from its initial currentColor —
411
+ measured as ~50ms of a near-white ring on a bright accent face, i.e. briefly
412
+ invisible right when the user needs it. Only the focused state opts out. */
413
+ transition: none;
414
+ }
415
+
416
+ /* A disabled face is removed from the tab order, so it should never show a ring. */
417
+ .button.disabled:focus-visible {
418
+ outline: none;
370
419
  }
371
420
 
372
421
  /* Loading spinner */
@@ -488,7 +537,14 @@ export class DeesButton extends DeesElement {
488
537
  class="button ${this.isHidden ? 'hidden' : ''} ${actualType} size-${actualSize} shape-${this.shape} ${this.status} ${this.disabled
489
538
  ? 'disabled'
490
539
  : ''}"
540
+ role="button"
541
+ tabindex=${this.disabled ? -1 : 0}
542
+ aria-disabled=${this.disabled ? 'true' : 'false'}
543
+ aria-busy=${this.status === 'pending' ? 'true' : 'false'}
544
+ .ariaLabel=${showText ? null : this.text || null}
491
545
  @click="${this.dispatchClick}"
546
+ @keydown=${this.handleKeydown}
547
+ @keyup=${this.handleKeyup}
492
548
  >
493
549
  ${this.status === 'normal' ? html``: html`
494
550
  <dees-spinner
@@ -519,6 +575,76 @@ export class DeesButton extends DeesElement {
519
575
  );
520
576
  }
521
577
 
578
+ /** the inner face — the element that actually carries focus */
579
+ private get faceElement(): HTMLElement | null {
580
+ return (this.shadowRoot?.querySelector('.button') as HTMLElement | null) || null;
581
+ }
582
+
583
+ /**
584
+ * States in which the button must not react to input. `disabled` is obvious; the status
585
+ * faces matter too, because `.button.pending / .success / .error` already set
586
+ * `pointer-events: none` and keyboard activation has to refuse the same interactions a
587
+ * mouse cannot make.
588
+ */
589
+ private get isInert(): boolean {
590
+ return this.disabled || this.status !== 'normal';
591
+ }
592
+
593
+ /**
594
+ * Focus lives on the inner face inside the shadow root, so `host.focus()` has to forward
595
+ * or it is a silent no-op. Note that `dees-form-submit.focus()` is deliberately *not*
596
+ * wired to this — that method submits the form, and dees-form depends on it.
597
+ */
598
+ public focus(optionsArg?: FocusOptions): void {
599
+ const face = this.faceElement;
600
+ if (face) {
601
+ face.focus(optionsArg);
602
+ return;
603
+ }
604
+ super.focus(optionsArg);
605
+ }
606
+
607
+ public blur(): void {
608
+ this.faceElement?.blur();
609
+ super.blur();
610
+ }
611
+
612
+ private handleKeydown = (eventArg: KeyboardEvent): void => {
613
+ if (eventArg.key === ' ' || eventArg.key === 'Spacebar') {
614
+ // Native buttons activate Space on keyup. Swallow the keydown so the page cannot
615
+ // scroll underneath the focused button.
616
+ eventArg.preventDefault();
617
+ return;
618
+ }
619
+ if (eventArg.key === 'Enter') {
620
+ eventArg.preventDefault();
621
+ this.activateFromKeyboard();
622
+ }
623
+ };
624
+
625
+ private handleKeyup = (eventArg: KeyboardEvent): void => {
626
+ if (eventArg.key === ' ' || eventArg.key === 'Spacebar') {
627
+ eventArg.preventDefault();
628
+ this.activateFromKeyboard();
629
+ }
630
+ };
631
+
632
+ /**
633
+ * Keyboard activation goes through a real click rather than dispatching `clicked`
634
+ * directly, exactly as a native button does. Several catalog components bind `@click` on
635
+ * dees-button instead of `@clicked` (dees-workspace-diff-editor, the profile picture
636
+ * modal), and both listener styles have to keep working from the keyboard.
637
+ *
638
+ * `pointer-events: none` does not block a programmatic `click()`, so inertness is
639
+ * enforced here rather than left to CSS.
640
+ */
641
+ private activateFromKeyboard(): void {
642
+ if (this.isInert) {
643
+ return;
644
+ }
645
+ this.faceElement?.click();
646
+ }
647
+
522
648
  public async firstUpdated() {
523
649
  // Extract light DOM content (icon + text) and set as properties
524
650
  this.extractLightDom();
@@ -823,14 +823,13 @@ export class DeesStatsGrid extends DeesElement {
823
823
  <div class="grid-title"></div>
824
824
  <div class="grid-actions">
825
825
  ${this.gridActions.map(action => html`
826
- <dees-button
826
+ <dees-button
827
827
  @clicked=${() => this.handleGridAction(action)}
828
828
  type="outline"
829
829
  size="sm"
830
- >
831
- ${action.iconName ? html`<dees-icon .icon=${action.iconName} size="small"></dees-icon>` : ''}
832
- ${action.name}
833
- </dees-button>
830
+ .text=${action.name}
831
+ .icon=${action.iconName || ''}
832
+ ></dees-button>
834
833
  `)}
835
834
  </div>
836
835
  </div>
@@ -1249,4 +1248,4 @@ export class DeesStatsGrid extends DeesElement {
1249
1248
  // open an empty menu.)
1250
1249
  DeesContextmenu.openContextMenuWithOptions(event, tile.actions as any);
1251
1250
  }
1252
- }
1251
+ }
@@ -735,22 +735,24 @@ export class DeesTable<T> extends DeesElement {
735
735
  return html`
736
736
  <td class="actionsCol">
737
737
  <div class="actionsContainer">
738
- ${inRowActions.map(
739
- (actionArg) => html`
740
- <div
741
- class="action"
742
- @click=${() =>
743
- actionArg.actionFunc({
744
- item: itemArg,
745
- table: this,
746
- })}
747
- >
748
- ${actionArg.iconName
749
- ? html` <dees-icon .icon=${actionArg.iconName}></dees-icon> `
750
- : actionArg.name}
751
- </div>
752
- `
753
- )}
738
+ ${inRowActions
739
+ .filter((actionArg) => this.isActionRelevant(actionArg, itemArg))
740
+ .map(
741
+ (actionArg) => html`
742
+ <div
743
+ class="action"
744
+ @click=${() =>
745
+ actionArg.actionFunc({
746
+ item: itemArg,
747
+ table: this,
748
+ })}
749
+ >
750
+ ${actionArg.iconName
751
+ ? html` <dees-icon .icon=${actionArg.iconName}></dees-icon> `
752
+ : actionArg.name}
753
+ </div>
754
+ `
755
+ )}
754
756
  </div>
755
757
  </td>
756
758
  `;
@@ -2119,7 +2121,11 @@ export class DeesTable<T> extends DeesElement {
2119
2121
  this.startEditing(cell.item, cell.col);
2120
2122
  return;
2121
2123
  }
2122
- const dblAction = this.dataActions.find((a) => a.type?.includes('doubleClick'));
2124
+ const dblAction = this.dataActions.find(
2125
+ (action) =>
2126
+ action.type?.includes('doubleClick')
2127
+ && this.isActionRelevant(action, cell.item)
2128
+ );
2123
2129
  if (dblAction) dblAction.actionFunc({ item: cell.item, table: this });
2124
2130
  };
2125
2131
 
@@ -2145,16 +2151,19 @@ export class DeesTable<T> extends DeesElement {
2145
2151
  this.emitSelectionChange();
2146
2152
  this.requestUpdate();
2147
2153
  }
2148
- const userItems: plugins.tsclass.website.IMenuItem[] = this.getActionsForType('contextmenu').map(
2149
- (action) => ({
2150
- name: action.name,
2151
- iconName: action.iconName as any,
2152
- action: async () => {
2153
- await action.actionFunc({ item, table: this });
2154
- return null;
2155
- },
2156
- })
2157
- );
2154
+ const userItems: plugins.tsclass.website.IMenuItem[] = this
2155
+ .getActionsForType('contextmenu')
2156
+ .filter((action) => this.isActionRelevant(action, item))
2157
+ .map(
2158
+ (action) => ({
2159
+ name: action.name,
2160
+ iconName: action.iconName as any,
2161
+ action: async () => {
2162
+ await action.actionFunc({ item, table: this });
2163
+ return null;
2164
+ },
2165
+ })
2166
+ );
2158
2167
  const defaultItems: plugins.tsclass.website.IMenuItem[] = [
2159
2168
  {
2160
2169
  name:
@@ -2333,6 +2342,13 @@ export class DeesTable<T> extends DeesElement {
2333
2342
  );
2334
2343
  }
2335
2344
 
2345
+ private isActionRelevant(actionArg: ITableAction<T>, itemArg: T): boolean {
2346
+ return (
2347
+ !actionArg.actionRelevancyCheckFunc
2348
+ || actionArg.actionRelevancyCheckFunc(itemArg)
2349
+ );
2350
+ }
2351
+
2336
2352
  getActionsForType(typeArg: ITableAction['type'][0]) {
2337
2353
  const actions: ITableAction[] = [];
2338
2354
  for (const action of this.dataActions) {
@@ -55,6 +55,10 @@ export class DeesSpinner extends DeesElement {
55
55
  display: block;
56
56
  }
57
57
 
58
+ /* --dees-spinner-color lets a spinner on a coloured surface (an accent button face,
59
+ a toast) match that surface's foreground. It is read as an inherited fallback
60
+ rather than declared on :host, because a :host declaration would outrank the
61
+ value an ancestor sets. Unset consumers keep the previous token. */
58
62
  #loading {
59
63
  position: relative;
60
64
  transition: none;
@@ -62,9 +66,14 @@ export class DeesSpinner extends DeesElement {
62
66
  justify-content: center;
63
67
  align-content: center;
64
68
  background: transparent;
65
- border: 3px solid color-mix(in srgb, var(--dees-color-text-primary) 20%, transparent);
69
+ border: 3px solid
70
+ color-mix(
71
+ in srgb,
72
+ var(--dees-spinner-color, var(--dees-color-text-primary)) 20%,
73
+ transparent
74
+ );
66
75
  border-radius: 50%;
67
- border-top-color: var(--dees-color-text-primary);
76
+ border-top-color: var(--dees-spinner-color, var(--dees-color-text-primary));
68
77
  animation: spin 1s ease-in-out infinite;
69
78
  -webkit-animation: spin 1s ease-in-out infinite;
70
79
  }
@@ -47,6 +47,10 @@ export class DeesFormSubmit extends DeesElement {
47
47
  @property({ type: String })
48
48
  accessor iconPosition: 'left' | 'right' = 'left';
49
49
 
50
+ /** stretches the button face to the full host width — see dees-button.fullWidth */
51
+ @property({ type: Boolean, reflect: true, attribute: 'full-width' })
52
+ accessor fullWidth: boolean = false;
53
+
50
54
  constructor() {
51
55
  super();
52
56
  }
@@ -58,6 +62,12 @@ export class DeesFormSubmit extends DeesElement {
58
62
  :host {
59
63
  display: inline-block;
60
64
  }
65
+ /* The host has to stretch too, or forwarding fullWidth only widens the face inside a
66
+ still shrink-wrapped host and the property looks broken. */
67
+ :host([full-width]) {
68
+ display: block;
69
+ width: 100%;
70
+ }
61
71
  dees-button {
62
72
  width: 100%;
63
73
  }
@@ -73,6 +83,7 @@ export class DeesFormSubmit extends DeesElement {
73
83
  .icon=${this.icon}
74
84
  .iconPosition=${this.iconPosition}
75
85
  .text=${this.text}
86
+ .fullWidth=${this.fullWidth}
76
87
  ?disabled=${this.disabled}
77
88
  @clicked=${this.submit}
78
89
  ></dees-button>
@@ -103,6 +114,13 @@ export class DeesFormSubmit extends DeesElement {
103
114
  }
104
115
  }
105
116
 
117
+ /**
118
+ * NOT a focus method despite the name — it submits. `dees-form.addBehaviours()` calls
119
+ * `getSubmitButton()?.focus()` when Enter is pressed in the last field, and that is the
120
+ * catalog's "Enter submits the form" behaviour. Renaming it would break every form, so it
121
+ * stays; keyboard *focus* reaches the submit through the inner dees-button face, which
122
+ * carries its own tabindex.
123
+ */
106
124
  public async focus() {
107
125
  const domtools = await this.domtoolsPromise;
108
126
  if (!this.disabled) {
@@ -89,8 +89,8 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
89
89
 
90
90
  .option {
91
91
  position: relative;
92
- display: flex;
93
- align-items: center;
92
+ display: grid;
93
+ place-items: center;
94
94
  height: 100%;
95
95
  padding: 0 var(--dees-spacing-lg);
96
96
  border-radius: var(--dees-radius-sm);
@@ -104,6 +104,21 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
104
104
  z-index: 2;
105
105
  }
106
106
 
107
+ .option-label,
108
+ .option::after {
109
+ grid-area: 1 / 1;
110
+ }
111
+
112
+ /* Reserve the selected label's semibold width in every state so moving
113
+ the thumb never changes the segmented control's intrinsic width. */
114
+ .option::after {
115
+ content: attr(data-option);
116
+ visibility: hidden;
117
+ font-weight: 600;
118
+ pointer-events: none;
119
+ user-select: none;
120
+ }
121
+
107
122
  /* Hairline separators between unselected segments; they fade out next to
108
123
  the thumb. */
109
124
  .option::before {
@@ -197,8 +212,9 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
197
212
  index === 0 || isSelected || index - 1 === selectedIndex;
198
213
  return html`<div
199
214
  class="option ${isSelected ? 'selected' : ''} ${hideSeparator ? 'no-sep' : ''}"
215
+ data-option=${option}
200
216
  @click=${() => this.handleSelection(option)}
201
- >${option}</div>`;
217
+ ><span class="option-label">${option}</span></div>`;
202
218
  })}
203
219
  </div>
204
220
  </div>
@@ -67,6 +67,16 @@ export class DeesInputText extends DeesInputBase {
67
67
  @property({ attribute: false })
68
68
  accessor validConfirmed: boolean = false;
69
69
 
70
+ /**
71
+ * autofill hint forwarded to the inner input element.
72
+ * Empty by default, which renders no `autocomplete` attribute at all — byte-identical to
73
+ * this component before the property existed. Set it to a concrete token list to help
74
+ * password managers (`'username'`, `'current-password'`, `'one-time-code'`) or to opt an
75
+ * input into WebAuthn conditional mediation (`'username webauthn'`).
76
+ */
77
+ @property({ type: String })
78
+ accessor autocomplete: string = '';
79
+
70
80
  private validTimeout: ReturnType<typeof setTimeout> | undefined;
71
81
 
72
82
  public static styles = [
@@ -324,6 +334,25 @@ export class DeesInputText extends DeesInputBase {
324
334
  `;
325
335
  }
326
336
 
337
+ /**
338
+ * `autocomplete` is applied imperatively rather than bound in the template so that the
339
+ * default renders no attribute at all. Binding it would emit `autocomplete=""` on every
340
+ * text input in the catalog, and an empty or `on` value is not universally equivalent to
341
+ * an absent one across browser and password-manager heuristics.
342
+ */
343
+ public updated(_changedProperties: Map<string, any>): void {
344
+ super.updated(_changedProperties);
345
+ const input = this.shadowRoot?.querySelector('input');
346
+ if (!input) {
347
+ return;
348
+ }
349
+ if (this.autocomplete) {
350
+ input.setAttribute('autocomplete', this.autocomplete);
351
+ } else {
352
+ input.removeAttribute('autocomplete');
353
+ }
354
+ }
355
+
327
356
  firstUpdated() {
328
357
  if (this.validationFunction && this.value) {
329
358
  const result = this.validationFunction(this.value);
@@ -373,6 +373,7 @@ export class DeesSimpleAppDash extends DeesElement {
373
373
  bottom: 24px;
374
374
  width: calc(100% - 240px);
375
375
  overflow: auto;
376
+ scrollbar-gutter: stable;
376
377
  background: var(--dees-color-bg-canvas);
377
378
  overscroll-behavior: contain;
378
379
  }