import * as plugins from '../../00plugins.js'; import { DeesElement, customElement, html, css, unsafeCSS, type CSSResult, cssManager, property, type TemplateResult, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; import { stepperDemo } from './dees-stepper.demo.js'; import { themeDefaultStyles } from '../../00theme.js'; import { cssGeistFontFamily } from '../../00fonts.js'; import { zIndexRegistry } from '../../00zindex.js'; import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js'; import '../dees-tile/dees-tile.js'; export interface IStep { title: string; content: TemplateResult; footerContent?: TemplateResult; validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise; onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise; validationFuncCalled?: boolean; abortController?: AbortController; } declare global { interface HTMLElementTagNameMap { 'dees-stepper': DeesStepper; } } @customElement('dees-stepper') export class DeesStepper extends DeesElement { // STATIC public static demo = stepperDemo; public static demoGroups = ['Layout', 'Form']; public static async createAndShow(optionsArg: { steps: IStep[]; }): Promise { const body = document.body; const stepper = new DeesStepper(); stepper.steps = optionsArg.steps; stepper.overlay = true; stepper.windowLayer = await DeesWindowLayer.createAndShow({ blur: true }); stepper.windowLayer.addEventListener('click', async () => { await stepper.destroy(); }); body.append(stepper.windowLayer); body.append(stepper); // Get z-index for stepper (should be above window layer) stepper.stepperZIndex = zIndexRegistry.getNextZIndex(); zIndexRegistry.register(stepper, stepper.stepperZIndex); return stepper; } // INSTANCE @property({ type: Array, }) accessor steps: IStep[] = []; @property({ type: Object, }) accessor selectedStep!: IStep; @property({ type: Boolean, reflect: true, }) accessor overlay: boolean = false; @property({ type: Number, attribute: false }) accessor stepperZIndex: number = 1000; private windowLayer?: DeesWindowLayer; constructor() { super(); } public static styles = [ themeDefaultStyles, cssManager.defaultStyles, css` /* TODO: Migrate hardcoded values to --dees-* CSS variables */ :host { position: absolute; width: 100%; height: 100%; font-family: ${cssGeistFontFamily}; color: var(--dees-color-text-primary); } :host([overlay]) { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; } .stepperContainer { position: absolute; width: 100%; height: 100%; overflow: hidden; } .stepperContainer.overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; } .stepperContainer.predestroy { opacity: 0; transition: opacity 0.2s ease-in; } dees-tile.step { position: relative; pointer-events: none; max-width: 500px; min-height: 300px; margin: auto; margin-bottom: 20px; filter: opacity(0.55) saturate(0.85); transition: transform 0.7s cubic-bezier(0.87, 0, 0.13, 1), filter 0.7s cubic-bezier(0.87, 0, 0.13, 1); user-select: none; } dees-tile.step.selected { pointer-events: all; filter: opacity(1) saturate(1); user-select: auto; } dees-tile.step.hiddenStep { filter: opacity(0); } dees-tile.step.entrance { transition: transform 0.35s ease, filter 0.35s ease; } dees-tile.step.entrance.hiddenStep { transform: translateY(16px); } dees-tile.step:last-child { margin-bottom: 100vh; } .stepperContainer.overlay dees-tile.step::part(outer) { box-shadow: 0 0 0 1px ${cssManager.bdTheme('hsl(0 0% 0% / 0.03)', 'hsl(0 0% 100% / 0.03)')}, 0 8px 40px ${cssManager.bdTheme('hsl(0 0% 0% / 0.12)', 'hsl(0 0% 0% / 0.5)')}, 0 2px 8px ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 0% / 0.25)')}; } .step-header { height: 36px; display: flex; align-items: center; justify-content: space-between; padding: 0 8px 0 12px; gap: 12px; } .goBack-spacer { width: 1px; } .step-header .goBack { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 8px; font-size: 12px; font-weight: 500; line-height: 1; border: none; background: transparent; color: var(--dees-color-text-muted); border-radius: 4px; cursor: pointer; transition: background 0.15s ease, color 0.15s ease, transform 0.2s ease; } .step-header .goBack:hover { background: var(--dees-color-hover); color: var(--dees-color-text-secondary); transform: translateX(-2px); } .step-header .goBack:active { background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 15%)')}; } .step-header .goBack span { transition: transform 0.2s ease; display: inline-block; } .step-header .goBack:hover span { transform: translateX(-2px); } .step-header .stepCounter { color: var(--dees-color-text-muted); font-size: 12px; font-weight: 500; letter-spacing: -0.01em; padding: 0 8px; } .step-body .title { text-align: center; padding-top: 32px; font-family: 'Geist Sans', sans-serif; font-size: 24px; font-weight: 600; letter-spacing: -0.01em; color: inherit; } .step-body .content { padding: 32px; } .step-footer { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 12px 16px; } `, ]; public render() { return html`
${this.steps.map((stepArg, stepIndex) => { const isSelected = stepArg === this.selectedStep; const isHidden = this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep); const isFirst = stepIndex === 0; return html`
${!isFirst ? html`
<- go to previous step
` : html`
`}
Step ${stepIndex + 1} of ${this.steps.length}
${stepArg.title}
${stepArg.content}
${stepArg.footerContent ? html`` : ''}
`; })}
`; } public getIndexOfStep = (stepArg: IStep): number => { return this.steps.findIndex((stepArg2) => stepArg === stepArg2); }; public async firstUpdated() { await this.domtoolsPromise; await this.domtools.convenience.smartdelay.delayFor(0); this.selectedStep = this.steps[0]; this.setScrollStatus(); // Remove entrance class after initial animation completes await this.domtools.convenience.smartdelay.delayFor(350); this.shadowRoot!.querySelector('.step.entrance')?.classList.remove('entrance'); } public async updated() { this.setScrollStatus(); } public scroller!: typeof domtools.plugins.SweetScroll.prototype; public async setScrollStatus() { const stepperContainer = this.shadowRoot!.querySelector('.stepperContainer') as HTMLElement; const firstStepElement = this.shadowRoot!.querySelector('.step') as HTMLElement; const selectedStepElement = this.shadowRoot!.querySelector('.selected') as HTMLElement; if (!selectedStepElement) { return; } if (!stepperContainer.style.paddingTop) { stepperContainer.style.paddingTop = `${ stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2 }px`; } console.log('Setting scroll status'); console.log(selectedStepElement); const scrollPosition = selectedStepElement.offsetTop - stepperContainer.offsetHeight / 2 + selectedStepElement.offsetHeight / 2; console.log(scrollPosition); const domtoolsInstance = await domtools.DomTools.setupDomTools(); if (!this.scroller) { this.scroller = new domtools.plugins.SweetScroll( { vertical: true, horizontal: false, easing: 'easeInOutExpo', duration: 700, }, stepperContainer ); } if (!this.selectedStep.validationFuncCalled && this.selectedStep.validationFunc) { this.selectedStep.abortController = new AbortController(); this.selectedStep.validationFuncCalled = true; await this.selectedStep.validationFunc(this, selectedStepElement, this.selectedStep.abortController.signal); } this.scroller.to(scrollPosition); } public async goBack() { const currentIndex = this.steps.findIndex((stepArg) => stepArg === this.selectedStep); if (currentIndex <= 0) { return; } // Abort any active listeners on current step if (this.selectedStep.abortController) { this.selectedStep.abortController.abort(); } const currentStep = this.steps[currentIndex]; currentStep.validationFuncCalled = false; const previousStep = this.steps[currentIndex - 1]; previousStep.validationFuncCalled = false; this.selectedStep = previousStep; await this.domtoolsPromise; await this.domtools.convenience.smartdelay.delayFor(100); this.selectedStep.onReturnToStepFunc?.(this, this.shadowRoot!.querySelector('.selected') as HTMLElement); } public goNext() { const currentIndex = this.steps.findIndex((stepArg) => stepArg === this.selectedStep); if (currentIndex < 0 || currentIndex >= this.steps.length - 1) { return; } // Abort any active listeners on current step if (this.selectedStep.abortController) { this.selectedStep.abortController.abort(); } const currentStep = this.steps[currentIndex]; currentStep.validationFuncCalled = false; const nextStep = this.steps[currentIndex + 1]; nextStep.validationFuncCalled = false; this.selectedStep = nextStep; } public async destroy() { const domtools = await this.domtoolsPromise; const container = this.shadowRoot!.querySelector('.stepperContainer'); container?.classList.add('predestroy'); await domtools.convenience.smartdelay.delayFor(200); if (this.parentElement) { this.parentElement.removeChild(this); } if (this.windowLayer) { await this.windowLayer.destroy(); } // Unregister from z-index registry zIndexRegistry.unregister(this); } }