@design.estate/dees-catalog 3.70.0 → 3.71.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.
@@ -1,5 +1,4 @@
1
1
  import * as plugins from '../../00plugins.js';
2
- import * as colors from '../../00colors.js';
3
2
 
4
3
  import {
5
4
  DeesElement,
@@ -16,10 +15,16 @@ import {
16
15
  import * as domtools from '@design.estate/dees-domtools';
17
16
  import { stepperDemo } from './dees-stepper.demo.js';
18
17
  import { themeDefaultStyles } from '../../00theme.js';
18
+ import { cssGeistFontFamily } from '../../00fonts.js';
19
+ import { zIndexRegistry } from '../../00zindex.js';
20
+ import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
21
+ import type { DeesForm } from '../../00group-form/dees-form/dees-form.js';
22
+ import '../dees-tile/dees-tile.js';
19
23
 
20
24
  export interface IStep {
21
25
  title: string;
22
26
  content: TemplateResult;
27
+ menuOptions?: plugins.tsclass.website.IMenuItem<DeesStepper>[];
23
28
  validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
24
29
  onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
25
30
  validationFuncCalled?: boolean;
@@ -34,9 +39,32 @@ declare global {
34
39
 
35
40
  @customElement('dees-stepper')
36
41
  export class DeesStepper extends DeesElement {
42
+ // STATIC
37
43
  public static demo = stepperDemo;
38
44
  public static demoGroups = ['Layout', 'Form'];
39
45
 
46
+ public static async createAndShow(optionsArg: {
47
+ steps: IStep[];
48
+ }): Promise<DeesStepper> {
49
+ const body = document.body;
50
+ const stepper = new DeesStepper();
51
+ stepper.steps = optionsArg.steps;
52
+ stepper.overlay = true;
53
+ stepper.windowLayer = await DeesWindowLayer.createAndShow({ blur: true });
54
+ stepper.windowLayer.addEventListener('click', async () => {
55
+ await stepper.destroy();
56
+ });
57
+ body.append(stepper.windowLayer);
58
+ body.append(stepper);
59
+
60
+ // Get z-index for stepper (should be above window layer)
61
+ stepper.stepperZIndex = zIndexRegistry.getNextZIndex();
62
+ zIndexRegistry.register(stepper, stepper.stepperZIndex);
63
+
64
+ return stepper;
65
+ }
66
+
67
+ // INSTANCE
40
68
  @property({
41
69
  type: Array,
42
70
  })
@@ -47,6 +75,25 @@ export class DeesStepper extends DeesElement {
47
75
  })
48
76
  accessor selectedStep!: IStep;
49
77
 
78
+ @property({
79
+ type: Boolean,
80
+ reflect: true,
81
+ })
82
+ accessor overlay: boolean = false;
83
+
84
+ @property({ type: Number, attribute: false })
85
+ accessor stepperZIndex: number = 1000;
86
+
87
+ @property({ type: Object, attribute: false })
88
+ accessor activeForm: DeesForm | null = null;
89
+
90
+ @property({ type: Boolean, attribute: false })
91
+ accessor activeFormValid: boolean = true;
92
+
93
+ private activeFormSubscription?: { unsubscribe: () => void };
94
+
95
+ private windowLayer?: DeesWindowLayer;
96
+
50
97
  constructor() {
51
98
  super();
52
99
  }
@@ -60,7 +107,24 @@ export class DeesStepper extends DeesElement {
60
107
  position: absolute;
61
108
  width: 100%;
62
109
  height: 100%;
110
+ font-family: ${cssGeistFontFamily};
111
+ color: var(--dees-color-text-primary);
112
+ }
113
+
114
+ /*
115
+ * In overlay mode the host is "transparent" to layout — the inner
116
+ * .stepperContainer.overlay is what pins to the viewport and carries the
117
+ * z-index. Keeping :host unpositioned avoids nesting the stacking context
118
+ * under an auto-z-index parent (which was trapping .stepperContainer
119
+ * below DeesWindowLayer's sibling layers). This mirrors how dees-modal
120
+ * keeps its own :host unpositioned and lets .modalContainer drive layout.
121
+ */
122
+ :host([overlay]) {
123
+ position: static;
124
+ width: 0;
125
+ height: 0;
63
126
  }
127
+
64
128
  .stepperContainer {
65
129
  position: absolute;
66
130
  width: 100%;
@@ -68,101 +132,120 @@ export class DeesStepper extends DeesElement {
68
132
  overflow: hidden;
69
133
  }
70
134
 
71
- .step {
135
+ .stepperContainer.overlay {
136
+ position: fixed;
137
+ top: 0;
138
+ left: 0;
139
+ width: 100vw;
140
+ height: 100vh;
141
+ }
142
+
143
+ .stepperContainer.predestroy {
144
+ opacity: 0;
145
+ transition: opacity 0.2s ease-in;
146
+ }
147
+
148
+ dees-tile.step {
72
149
  position: relative;
73
150
  pointer-events: none;
74
- overflow: hidden;
75
- transition: transform 0.7s cubic-bezier(0.87, 0, 0.13, 1), box-shadow 0.7s cubic-bezier(0.87, 0, 0.13, 1), filter 0.7s cubic-bezier(0.87, 0, 0.13, 1), border 0.7s cubic-bezier(0.87, 0, 0.13, 1);
76
151
  max-width: 500px;
77
152
  min-height: 300px;
78
- border-radius: 12px;
79
- background: ${cssManager.bdTheme('#ffffff', '#0f0f11')};
80
- border: 1px solid ${cssManager.bdTheme('#e2e8f0', '#272729')};
81
- color: ${cssManager.bdTheme('#0f172a', '#f5f5f5')};
82
153
  margin: auto;
83
154
  margin-bottom: 20px;
84
155
  filter: opacity(0.55) saturate(0.85);
85
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
156
+ transition: transform 0.7s cubic-bezier(0.87, 0, 0.13, 1), filter 0.7s cubic-bezier(0.87, 0, 0.13, 1);
86
157
  user-select: none;
87
158
  }
88
159
 
89
- .step.selected {
160
+ dees-tile.step.selected {
90
161
  pointer-events: all;
91
162
  filter: opacity(1) saturate(1);
92
163
  user-select: auto;
93
164
  }
94
165
 
95
- .step.hiddenStep {
166
+ dees-tile.step.hiddenStep {
96
167
  filter: opacity(0);
97
168
  }
98
169
 
99
- .step.entrance {
100
- transition: transform 0.35s ease, box-shadow 0.35s ease, filter 0.35s ease, border 0.35s ease;
170
+ dees-tile.step.entrance {
171
+ transition: transform 0.35s ease, filter 0.35s ease;
101
172
  }
102
173
 
103
- .step.entrance.hiddenStep {
174
+ dees-tile.step.entrance.hiddenStep {
104
175
  transform: translateY(16px);
105
176
  }
106
177
 
107
- .step:last-child {
178
+ dees-tile.step:last-child {
108
179
  margin-bottom: 100vh;
109
180
  }
110
181
 
111
- .step .stepCounter {
112
- color: ${cssManager.bdTheme('#64748b', '#a1a1aa')};
113
- position: absolute;
114
- top: 12px;
115
- right: 12px;
116
- padding: 6px 14px;
117
- font-size: 12px;
118
- border-radius: 999px;
119
- background: ${cssManager.bdTheme('rgba(226, 232, 240, 0.5)', 'rgba(63, 63, 70, 0.45)')};
120
- border: 1px solid ${cssManager.bdTheme('rgba(226, 232, 240, 0.7)', 'rgba(63, 63, 70, 0.6)')};
182
+ .stepperContainer.overlay dees-tile.step::part(outer) {
183
+ box-shadow:
184
+ 0 0 0 1px ${cssManager.bdTheme('hsl(0 0% 0% / 0.03)', 'hsl(0 0% 100% / 0.03)')},
185
+ 0 8px 40px ${cssManager.bdTheme('hsl(0 0% 0% / 0.12)', 'hsl(0 0% 0% / 0.5)')},
186
+ 0 2px 8px ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 0% / 0.25)')};
121
187
  }
122
188
 
123
- .step .goBack {
124
- position: absolute;
125
- top: 12px;
126
- left: 12px;
189
+ .step-header {
190
+ height: 36px;
191
+ display: flex;
192
+ align-items: center;
193
+ justify-content: space-between;
194
+ padding: 0 8px 0 12px;
195
+ gap: 12px;
196
+ }
197
+
198
+ .goBack-spacer {
199
+ width: 1px;
200
+ }
201
+
202
+ .step-header .goBack {
127
203
  display: inline-flex;
128
204
  align-items: center;
129
205
  gap: 6px;
130
- padding: 6px 12px;
206
+ height: 24px;
207
+ padding: 0 8px;
131
208
  font-size: 12px;
132
209
  font-weight: 500;
133
- border-radius: 999px;
134
- border: 1px solid ${cssManager.bdTheme('rgba(226, 232, 240, 0.9)', 'rgba(63, 63, 70, 0.85)')};
135
- background: ${cssManager.bdTheme('rgba(255, 255, 255, 0.9)', 'rgba(39, 39, 42, 0.85)')};
136
- color: ${cssManager.bdTheme('#475569', '#d4d4d8')};
210
+ line-height: 1;
211
+ border: none;
212
+ background: transparent;
213
+ color: var(--dees-color-text-muted);
214
+ border-radius: 4px;
137
215
  cursor: pointer;
138
- transition: border 0.2s ease, color 0.2s ease, background 0.2s ease, transform 0.2s ease;
216
+ transition: background 0.15s ease, color 0.15s ease, transform 0.2s ease;
139
217
  }
140
218
 
141
- .step .goBack:hover {
142
- color: ${cssManager.bdTheme('#0f172a', '#fafafa')};
143
- border-color: ${cssManager.bdTheme(colors.dark.blue, colors.dark.blue)};
144
- background: ${cssManager.bdTheme('rgba(226, 232, 240, 0.95)', 'rgba(63, 63, 70, 0.7)')};
219
+ .step-header .goBack:hover {
220
+ background: var(--dees-color-hover);
221
+ color: var(--dees-color-text-secondary);
145
222
  transform: translateX(-2px);
146
223
  }
147
224
 
148
- .step .goBack:active {
149
- color: ${cssManager.bdTheme('#0f172a', '#fafafa')};
150
- border-color: ${cssManager.bdTheme(colors.dark.blueActive, colors.dark.blueActive)};
151
- background: ${cssManager.bdTheme('rgba(226, 232, 240, 0.85)', 'rgba(63, 63, 70, 0.6)')};
225
+ .step-header .goBack:active {
226
+ background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 15%)')};
152
227
  }
153
228
 
154
- .step .goBack span {
229
+ .step-header .goBack span {
155
230
  transition: transform 0.2s ease;
156
231
  display: inline-block;
157
232
  }
158
233
 
159
- .step .goBack:hover span {
234
+ .step-header .goBack:hover span {
160
235
  transform: translateX(-2px);
161
236
  }
162
237
 
163
- .step .title {
238
+ .step-header .stepCounter {
239
+ color: var(--dees-color-text-muted);
240
+ font-size: 12px;
241
+ font-weight: 500;
242
+ letter-spacing: -0.01em;
243
+ padding: 0 8px;
244
+ }
245
+
246
+ .step-body .title {
164
247
  text-align: center;
165
- padding-top: 64px;
248
+ padding-top: 32px;
166
249
  font-family: 'Geist Sans', sans-serif;
167
250
  font-size: 24px;
168
251
  font-weight: 600;
@@ -170,35 +253,142 @@ export class DeesStepper extends DeesElement {
170
253
  color: inherit;
171
254
  }
172
255
 
173
- .step .content {
256
+ .step-body .content {
174
257
  padding: 32px;
175
258
  }
259
+
260
+ /* --- Footer: modal-style bottom buttons --- */
261
+ .bottomButtons {
262
+ display: flex;
263
+ flex-direction: row;
264
+ justify-content: flex-end;
265
+ align-items: center;
266
+ gap: 0;
267
+ height: 36px;
268
+ width: 100%;
269
+ box-sizing: border-box;
270
+ }
271
+
272
+ .bottomButtons .bottomButton {
273
+ padding: 0 16px;
274
+ height: 100%;
275
+ text-align: center;
276
+ font-size: 12px;
277
+ font-weight: 500;
278
+ cursor: pointer;
279
+ user-select: none;
280
+ transition: all 0.15s ease;
281
+ background: transparent;
282
+ border: none;
283
+ border-left: 1px solid var(--dees-color-border-subtle);
284
+ color: var(--dees-color-text-muted);
285
+ white-space: nowrap;
286
+ display: flex;
287
+ align-items: center;
288
+ }
289
+
290
+ .bottomButtons .bottomButton:first-child {
291
+ border-left: none;
292
+ }
293
+
294
+ .bottomButtons .bottomButton:hover {
295
+ background: var(--dees-color-hover);
296
+ color: var(--dees-color-text-primary);
297
+ }
298
+
299
+ .bottomButtons .bottomButton:active {
300
+ background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(0 0% 13%)')};
301
+ }
302
+
303
+ .bottomButtons .bottomButton.primary {
304
+ color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
305
+ font-weight: 600;
306
+ }
307
+
308
+ .bottomButtons .bottomButton.primary:hover {
309
+ background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
310
+ color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
311
+ }
312
+
313
+ .bottomButtons .bottomButton.primary:active {
314
+ background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.12)', 'hsl(213.1 93.9% 67.8% / 0.12)')};
315
+ }
316
+
317
+ .bottomButtons .bottomButton.disabled {
318
+ pointer-events: none;
319
+ opacity: 0.4;
320
+ cursor: not-allowed;
321
+ }
322
+
323
+ .bottomButtons .bottomButton.disabled:hover {
324
+ background: transparent;
325
+ color: var(--dees-color-text-muted);
326
+ }
327
+
328
+ /* Hint shown on the left of the footer when the active step's form has
329
+ unfilled required fields. Uses margin-right: auto to push right-aligned
330
+ buttons to the right while keeping the hint flush-left. */
331
+ .bottomButtons .stepHint {
332
+ margin-right: auto;
333
+ padding: 0 16px;
334
+ font-size: 11px;
335
+ line-height: 1;
336
+ letter-spacing: -0.01em;
337
+ color: var(--dees-color-text-muted);
338
+ display: flex;
339
+ align-items: center;
340
+ user-select: none;
341
+ }
176
342
  `,
177
343
  ];
178
344
 
179
345
  public render() {
180
346
  return html`
181
- <div class="stepperContainer">
182
- ${this.steps.map(
183
- (stepArg) =>
184
- html`<div
185
- class="step ${stepArg === this.selectedStep
186
- ? 'selected'
187
- : null} ${this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep)
188
- ? 'hiddenStep'
189
- : ''} ${this.getIndexOfStep(stepArg) === 0 ? 'entrance' : ''}"
190
- >
191
- ${this.getIndexOfStep(stepArg) > 0
192
- ? html`<div class="goBack" @click=${this.goBack}><span style="font-family: Inter"><-</span> go to previous step</div>`
193
- : ``}
347
+ <div
348
+ class="stepperContainer ${this.overlay ? 'overlay' : ''}"
349
+ style=${this.overlay ? `z-index: ${this.stepperZIndex};` : ''}
350
+ >
351
+ ${this.steps.map((stepArg, stepIndex) => {
352
+ const isSelected = stepArg === this.selectedStep;
353
+ const isHidden =
354
+ this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep);
355
+ const isFirst = stepIndex === 0;
356
+ return html`<dees-tile
357
+ class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''} ${isFirst ? 'entrance' : ''}"
358
+ >
359
+ <div slot="header" class="step-header">
360
+ ${!isFirst
361
+ ? html`<div class="goBack" @click=${this.goBack}>
362
+ <span style="font-family: Inter"><-</span> go to previous step
363
+ </div>`
364
+ : html`<div class="goBack-spacer"></div>`}
194
365
  <div class="stepCounter">
195
- Step ${this.steps.findIndex((elementArg) => elementArg === stepArg) + 1} of
196
- ${this.steps.length}
366
+ Step ${stepIndex + 1} of ${this.steps.length}
197
367
  </div>
368
+ </div>
369
+ <div class="step-body">
198
370
  <div class="title">${stepArg.title}</div>
199
371
  <div class="content">${stepArg.content}</div>
200
- </div> `
201
- )}
372
+ </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
+ </dees-tile>`;
391
+ })}
202
392
  </div>
203
393
  `;
204
394
  }
@@ -230,6 +420,7 @@ export class DeesStepper extends DeesElement {
230
420
  if (!selectedStepElement) {
231
421
  return;
232
422
  }
423
+ this.scanActiveForm(selectedStepElement);
233
424
  if (!stepperContainer.style.paddingTop) {
234
425
  stepperContainer.style.paddingTop = `${
235
426
  stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2
@@ -296,4 +487,93 @@ export class DeesStepper extends DeesElement {
296
487
  nextStep.validationFuncCalled = false;
297
488
  this.selectedStep = nextStep;
298
489
  }
490
+
491
+ /**
492
+ * Scans the currently selected step for a <dees-form> in its content. When
493
+ * found, subscribes to the form's RxJS changeSubject so the primary
494
+ * menuOption button can auto-enable/disable as required fields are filled.
495
+ *
496
+ * If the form reference is the same as the previous activation (e.g. on a
497
+ * same-step re-render), we just recompute validity without re-subscribing.
498
+ */
499
+ private scanActiveForm(selectedStepElement: HTMLElement) {
500
+ const form = selectedStepElement.querySelector('dees-form') as DeesForm | null;
501
+ if (form === this.activeForm) {
502
+ this.recomputeFormValid();
503
+ return;
504
+ }
505
+ this.activeFormSubscription?.unsubscribe();
506
+ this.activeFormSubscription = undefined;
507
+ this.activeForm = form;
508
+ if (!form) {
509
+ this.activeFormValid = true;
510
+ return;
511
+ }
512
+ // Initial check before subscribing, in case the form's firstUpdated fires
513
+ // synchronously between scan and subscribe.
514
+ this.recomputeFormValid();
515
+ this.activeFormSubscription = form.changeSubject.subscribe(() => {
516
+ this.recomputeFormValid();
517
+ });
518
+ }
519
+
520
+ /**
521
+ * Recomputes activeFormValid by checking every required field in the active
522
+ * form for a non-empty value. Mirrors dees-form.updateRequiredStatus's logic
523
+ * but stores the result on the stepper instead of mutating a submit button.
524
+ */
525
+ private recomputeFormValid() {
526
+ const form = this.activeForm;
527
+ if (!form) {
528
+ this.activeFormValid = true;
529
+ return;
530
+ }
531
+ const fields = form.getFormElements();
532
+ this.activeFormValid = fields.every(
533
+ (field) => !field.required || (field.value !== null && field.value !== undefined && field.value !== ''),
534
+ );
535
+ }
536
+
537
+ /**
538
+ * Click handler for menuOption buttons in the footer. For the primary (last)
539
+ * button, if an active form is present, gates on required-field validity and
540
+ * triggers the form's gatherAndDispatch() before running the action. The
541
+ * action is awaited so any async work (e.g. goNext → scroll animation)
542
+ * completes before the click handler returns.
543
+ */
544
+ private async handleMenuOptionClick(
545
+ optionArg: plugins.tsclass.website.IMenuItem<DeesStepper>,
546
+ isPrimary: boolean,
547
+ ) {
548
+ const form = this.activeForm;
549
+ if (isPrimary && form) {
550
+ if (!this.activeFormValid) return;
551
+ await new Promise<void>((resolve) => {
552
+ form.addEventListener('formData', () => resolve(), { once: true });
553
+ form.gatherAndDispatch();
554
+ });
555
+ }
556
+ await optionArg.action(this);
557
+ }
558
+
559
+ public async destroy() {
560
+ const domtools = await this.domtoolsPromise;
561
+ const container = this.shadowRoot!.querySelector('.stepperContainer');
562
+ container?.classList.add('predestroy');
563
+ await domtools.convenience.smartdelay.delayFor(200);
564
+ if (this.parentElement) {
565
+ this.parentElement.removeChild(this);
566
+ }
567
+ if (this.windowLayer) {
568
+ await this.windowLayer.destroy();
569
+ }
570
+
571
+ // Tear down form subscription to avoid leaks when the overlay closes.
572
+ this.activeFormSubscription?.unsubscribe();
573
+ this.activeFormSubscription = undefined;
574
+ this.activeForm = null;
575
+
576
+ // Unregister from z-index registry
577
+ zIndexRegistry.unregister(this);
578
+ }
299
579
  }
@@ -268,9 +268,7 @@ export class DeesModal extends DeesElement {
268
268
  }
269
269
 
270
270
  .heading .header-button dees-icon {
271
- width: 14px;
272
- height: 14px;
273
- display: block;
271
+ font-size: 14px;
274
272
  }
275
273
 
276
274
  .content {