@design.estate/dees-catalog 3.71.0 → 3.72.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-catalog",
3
- "version": "3.71.0",
3
+ "version": "3.72.0",
4
4
  "private": false,
5
5
  "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
6
6
  "main": "dist_ts_web/index.js",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-catalog',
6
- version: '3.71.0',
6
+ version: '3.72.0',
7
7
  description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
8
8
  }
@@ -18,6 +18,7 @@ import { themeDefaultStyles } from '../../00theme.js';
18
18
  import { cssGeistFontFamily } from '../../00fonts.js';
19
19
  import { zIndexRegistry } from '../../00zindex.js';
20
20
  import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
21
+ import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
21
22
  import type { DeesForm } from '../../00group-form/dees-form/dees-form.js';
22
23
  import '../dees-tile/dees-tile.js';
23
24
 
@@ -45,15 +46,16 @@ export class DeesStepper extends DeesElement {
45
46
 
46
47
  public static async createAndShow(optionsArg: {
47
48
  steps: IStep[];
49
+ cancelable?: boolean;
48
50
  }): Promise<DeesStepper> {
49
51
  const body = document.body;
50
52
  const stepper = new DeesStepper();
51
53
  stepper.steps = optionsArg.steps;
52
54
  stepper.overlay = true;
55
+ if (optionsArg.cancelable !== undefined) {
56
+ stepper.cancelable = optionsArg.cancelable;
57
+ }
53
58
  stepper.windowLayer = await DeesWindowLayer.createAndShow({ blur: true });
54
- stepper.windowLayer.addEventListener('click', async () => {
55
- await stepper.destroy();
56
- });
57
59
  body.append(stepper.windowLayer);
58
60
  body.append(stepper);
59
61
 
@@ -81,6 +83,18 @@ export class DeesStepper extends DeesElement {
81
83
  })
82
84
  accessor overlay: boolean = false;
83
85
 
86
+ /**
87
+ * When true (default), the stepper renders a Cancel button in every step's
88
+ * footer, and clicking the backdrop (overlay mode) triggers the same cancel
89
+ * confirmation flow. Set to false for forced flows where the user must
90
+ * complete the stepper — no Cancel button, no backdrop dismissal.
91
+ */
92
+ @property({
93
+ type: Boolean,
94
+ reflect: true,
95
+ })
96
+ accessor cancelable: boolean = true;
97
+
84
98
  @property({ type: Number, attribute: false })
85
99
  accessor stepperZIndex: number = 1000;
86
100
 
@@ -280,15 +294,17 @@ export class DeesStepper extends DeesElement {
280
294
  transition: all 0.15s ease;
281
295
  background: transparent;
282
296
  border: none;
283
- border-left: 1px solid var(--dees-color-border-subtle);
284
297
  color: var(--dees-color-text-muted);
285
298
  white-space: nowrap;
286
299
  display: flex;
287
300
  align-items: center;
288
301
  }
289
302
 
290
- .bottomButtons .bottomButton:first-child {
291
- border-left: none;
303
+ /* Border-left separator on every button EXCEPT the first one.
304
+ Uses general sibling so the stepHint (if rendered on the left) does
305
+ not shift which button counts as "first" and create a phantom border. */
306
+ .bottomButtons .bottomButton ~ .bottomButton {
307
+ border-left: 1px solid var(--dees-color-border-subtle);
292
308
  }
293
309
 
294
310
  .bottomButtons .bottomButton:hover {
@@ -347,6 +363,7 @@ export class DeesStepper extends DeesElement {
347
363
  <div
348
364
  class="stepperContainer ${this.overlay ? 'overlay' : ''}"
349
365
  style=${this.overlay ? `z-index: ${this.stepperZIndex};` : ''}
366
+ @click=${this.handleOutsideClick}
350
367
  >
351
368
  ${this.steps.map((stepArg, stepIndex) => {
352
369
  const isSelected = stepArg === this.selectedStep;
@@ -370,23 +387,27 @@ export class DeesStepper extends DeesElement {
370
387
  <div class="title">${stepArg.title}</div>
371
388
  <div class="content">${stepArg.content}</div>
372
389
  </div>
373
- ${stepArg.menuOptions && stepArg.menuOptions.length > 0
374
- ? html`<div slot="footer" class="bottomButtons">
375
- ${isSelected && this.activeForm !== null && !this.activeFormValid
376
- ? html`<div class="stepHint">Complete form to continue</div>`
377
- : ''}
378
- ${stepArg.menuOptions.map((actionArg, actionIndex) => {
379
- const isPrimary = actionIndex === stepArg.menuOptions!.length - 1;
380
- const isDisabled = isPrimary && this.activeForm !== null && !this.activeFormValid;
381
- return html`
382
- <div
383
- class="bottomButton ${isPrimary ? 'primary' : ''} ${isDisabled ? 'disabled' : ''}"
384
- @click=${() => this.handleMenuOptionClick(actionArg, isPrimary)}
385
- >${actionArg.name}</div>
386
- `;
387
- })}
388
- </div>`
389
- : ''}
390
+ <div slot="footer" class="bottomButtons">
391
+ ${isSelected && this.activeForm !== null && !this.activeFormValid
392
+ ? html`<div class="stepHint">Complete form to continue</div>`
393
+ : ''}
394
+ ${this.cancelable
395
+ ? html`<div
396
+ class="bottomButton"
397
+ @click=${() => this.handleCancelRequest()}
398
+ >Cancel</div>`
399
+ : ''}
400
+ ${stepArg.menuOptions?.map((actionArg, actionIndex) => {
401
+ const isPrimary = actionIndex === stepArg.menuOptions!.length - 1;
402
+ const isDisabled = isPrimary && this.activeForm !== null && !this.activeFormValid;
403
+ return html`
404
+ <div
405
+ class="bottomButton ${isPrimary ? 'primary' : ''} ${isDisabled ? 'disabled' : ''}"
406
+ @click=${() => this.handleMenuOptionClick(actionArg, isPrimary)}
407
+ >${actionArg.name}</div>
408
+ `;
409
+ }) ?? ''}
410
+ </div>
390
411
  </dees-tile>`;
391
412
  })}
392
413
  </div>
@@ -556,6 +577,76 @@ export class DeesStepper extends DeesElement {
556
577
  await optionArg.action(this);
557
578
  }
558
579
 
580
+ /**
581
+ * Currently-open confirmation modal (if any). Prevents double-stacking when
582
+ * the user clicks the backdrop or the Cancel button while a confirm modal
583
+ * is already visible. The reference may become stale (point to a destroyed
584
+ * modal) if the user dismisses the confirm modal by clicking its own
585
+ * backdrop — so handleCancelRequest() uses isConnected to detect that.
586
+ */
587
+ private cancelConfirmModal?: DeesModal;
588
+
589
+ /**
590
+ * Click handler on .stepperContainer. Mirrors dees-modal.handleOutsideClick:
591
+ * when the user clicks the empty backdrop area (target === stepperContainer,
592
+ * not any descendant tile), trigger the cancel confirmation flow. Clicks
593
+ * that originate inside a step tile have a different event.target and are
594
+ * ignored here.
595
+ */
596
+ private handleOutsideClick(eventArg: MouseEvent) {
597
+ if (!this.overlay) return;
598
+ if (!this.cancelable) return;
599
+ eventArg.stopPropagation();
600
+ const stepperContainer = this.shadowRoot!.querySelector('.stepperContainer');
601
+ if (eventArg.target === stepperContainer) {
602
+ this.handleCancelRequest();
603
+ }
604
+ }
605
+
606
+ /**
607
+ * Shown by both the backdrop click and the Cancel button in the footer.
608
+ * Presents a dees-modal asking the user to confirm cancellation. If they
609
+ * confirm, the stepper and window layer are destroyed; otherwise the
610
+ * confirm modal is dismissed and the stepper stays open.
611
+ *
612
+ * The isConnected check on the cached reference handles the case where the
613
+ * user dismissed the previous confirm modal by clicking ITS OWN backdrop —
614
+ * dees-modal.handleOutsideClick calls destroy() directly, bypassing our
615
+ * action callbacks, so our cached reference would be stale without this
616
+ * fallback check.
617
+ */
618
+ public async handleCancelRequest() {
619
+ if (!this.cancelable) return;
620
+ if (this.cancelConfirmModal && this.cancelConfirmModal.isConnected) return;
621
+ this.cancelConfirmModal = undefined;
622
+ this.cancelConfirmModal = await DeesModal.createAndShow({
623
+ heading: 'Cancel setup?',
624
+ width: 'small',
625
+ content: html`
626
+ <p style="margin: 0;">
627
+ Are you sure you want to cancel? Any progress on the current step will be lost.
628
+ </p>
629
+ `,
630
+ menuOptions: [
631
+ {
632
+ name: 'Continue setup',
633
+ action: async (modal) => {
634
+ this.cancelConfirmModal = undefined;
635
+ await modal!.destroy();
636
+ },
637
+ },
638
+ {
639
+ name: 'Yes, cancel',
640
+ action: async (modal) => {
641
+ this.cancelConfirmModal = undefined;
642
+ await modal!.destroy();
643
+ await this.destroy();
644
+ },
645
+ },
646
+ ],
647
+ });
648
+ }
649
+
559
650
  public async destroy() {
560
651
  const domtools = await this.domtoolsPromise;
561
652
  const container = this.shadowRoot!.querySelector('.stepperContainer');
@@ -87,14 +87,24 @@ export class DeesTile extends DeesElement {
87
87
  color: var(--dees-color-text-secondary);
88
88
  }
89
89
 
90
- /* --- Content: the rounded inset --- */
90
+ /* --- Content: the rounded inset ---
91
+ Uses overflow-y: auto so that when a consumer (e.g. dees-modal) caps
92
+ the tile with max-height, long content scrolls inside the tile
93
+ instead of being clipped. For consumers without max-height
94
+ (e.g. dees-stepper), the tile grows with content and the scroll
95
+ never activates. Horizontal overflow stays clipped to preserve the
96
+ rounded corners. */
91
97
  .tile-content {
92
98
  flex: 1;
93
99
  position: relative;
94
100
  border-radius: 8px;
95
101
  border-top: 1px solid var(--dees-color-border-subtle);
96
102
  border-bottom: 1px solid var(--dees-color-border-subtle);
97
- overflow: hidden;
103
+ overflow-x: hidden;
104
+ overflow-y: auto;
105
+ overscroll-behavior: contain;
106
+ scrollbar-width: thin;
107
+ scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
98
108
  }
99
109
 
100
110
  .tile-content.no-footer {
@@ -352,5 +352,80 @@ export const demoFunc = () => html`
352
352
  });
353
353
  }}>Test Responsive</dees-button>
354
354
  </div>
355
+
356
+ <div class="demo-section">
357
+ <h3>Scrollable Content</h3>
358
+ <p>When content exceeds the modal's max-height (<code>calc(100vh - 80px)</code>), the tile caps at that height and the content area scrolls inside. The heading and bottom buttons stay pinned.</p>
359
+ <div class="button-grid">
360
+ <dees-button @click=${() => {
361
+ DeesModal.createAndShow({
362
+ heading: 'Long Article',
363
+ width: 'medium',
364
+ content: html`
365
+ <h4 style="margin-top: 0;">Lorem ipsum dolor sit amet</h4>
366
+ ${Array.from({ length: 40 }, (_, i) => html`
367
+ <p>
368
+ <strong>§ ${i + 1}.</strong>
369
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
370
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
371
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris
372
+ nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
373
+ in reprehenderit in voluptate velit esse cillum dolore eu fugiat
374
+ nulla pariatur. Excepteur sint occaecat cupidatat non proident,
375
+ sunt in culpa qui officia deserunt mollit anim id est laborum.
376
+ </p>
377
+ `)}
378
+ `,
379
+ menuOptions: [{
380
+ name: 'Cancel',
381
+ action: async (modal) => modal!.destroy()
382
+ }, {
383
+ name: 'Accept',
384
+ action: async (modal) => modal!.destroy()
385
+ }],
386
+ });
387
+ }}>Long Article</dees-button>
388
+
389
+ <dees-button @click=${() => {
390
+ DeesModal.createAndShow({
391
+ heading: 'Long List',
392
+ width: 'small',
393
+ content: html`
394
+ <p>Selected items:</p>
395
+ <ul style="padding-left: 20px; margin: 0;">
396
+ ${Array.from({ length: 80 }, (_, i) => html`
397
+ <li style="padding: 4px 0;">Item ${i + 1} — option label</li>
398
+ `)}
399
+ </ul>
400
+ `,
401
+ menuOptions: [{
402
+ name: 'Done',
403
+ action: async (modal) => modal!.destroy()
404
+ }],
405
+ });
406
+ }}>Long List</dees-button>
407
+
408
+ <dees-button @click=${() => {
409
+ DeesModal.createAndShow({
410
+ heading: 'Tall Form',
411
+ width: 'medium',
412
+ content: html`
413
+ <dees-form>
414
+ ${Array.from({ length: 25 }, (_, i) => html`
415
+ <dees-input-text .label=${`Field ${i + 1}`}></dees-input-text>
416
+ `)}
417
+ </dees-form>
418
+ `,
419
+ menuOptions: [{
420
+ name: 'Cancel',
421
+ action: async (modal) => modal!.destroy()
422
+ }, {
423
+ name: 'Submit',
424
+ action: async (modal) => modal!.destroy()
425
+ }],
426
+ });
427
+ }}>Tall Form</dees-button>
428
+ </div>
429
+ </div>
355
430
  </div>
356
431
  `
@@ -271,13 +271,6 @@ export class DeesModal extends DeesElement {
271
271
  font-size: 14px;
272
272
  }
273
273
 
274
- .content {
275
- overflow-y: auto;
276
- overflow-x: hidden;
277
- overscroll-behavior: contain;
278
- scrollbar-width: thin;
279
- scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
280
- }
281
274
  .bottomButtons {
282
275
  display: flex;
283
276
  flex-direction: row;