Compare commits

..

6 Commits

Author SHA1 Message Date
jkunz 1c4dabc21a v3.82.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-25 06:13:41 +00:00
jkunz fed3247eea feat(workspace-terminal): use environment shell command 2026-05-25 06:13:11 +00:00
jkunz 0eb4611ea6 v3.81.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-17 10:21:53 +00:00
jkunz 5d7f39695a feat(dees-updater): enhance updater progress and completion views with version metadata cards 2026-04-17 10:21:53 +00:00
jkunz 3f5cb4570b v3.80.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-16 14:21:16 +00:00
jkunz 428d2741d1 feat(stepper,updater): add progress-aware stepper flows and updater countdown states 2026-04-16 14:21:16 +00:00
14 changed files with 2391 additions and 1018 deletions
+11 -6
View File
@@ -37,12 +37,17 @@
] ]
}, },
"release": { "release": {
"registries": [ "targets": {
"https://verdaccio.lossless.digital", "npm": {
"https://registry.npmjs.org" "registries": [
], "https://verdaccio.lossless.digital",
"accessLevel": "public" "https://registry.npmjs.org"
} ],
"accessLevel": "public"
}
}
},
"schemaVersion": 2
}, },
"@git.zone/tsdoc": { "@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n" "legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
+31
View File
@@ -1,5 +1,36 @@
# Changelog # Changelog
## Pending
## 2026-05-25 - 3.82.0
### Features
- let execution environments provide their native terminal shell command
- Add shell command metadata to the execution environment contract.
- Keep WebContainer terminals on `jsh` while allowing backend environments to provide a different shell.
- Update workspace terminal shell tab labels and setup prompt handling to use the environment shell metadata.
## 2026-05-21 - 3.81.1 - fix(package)
include postinstall helper script in published package
- add the scripts directory to package files so postinstall can resolve update-monaco-version.cjs after installation
## 2026-04-17 - 3.81.0 - feat(dees-updater)
enhance updater progress and completion views with version metadata cards
- add reusable version metadata rendering for current, incoming, and installed versions
- refresh progress and ready states with clearer headings, descriptive copy, and automatic action summary
- apply monospace styling to displayed version numbers for improved readability
## 2026-04-16 - 3.80.0 - feat(stepper,updater)
add progress-aware stepper flows and updater countdown states
- extend dees-stepper with embedded progressbar rendering, progress state helpers, and automatic progression for async validation steps
- rework dees-updater to run as a non-cancelable two-step flow with live progress updates, optional external links, and configurable close or reload completion actions
- refresh stepper and updater demos plus documentation to showcase auto-advancing progress steps and ready-state countdown behavior
## 2026-04-16 - 3.79.0 - feat(dees-progressbar) ## 2026-04-16 - 3.79.0 - feat(dees-progressbar)
add status panels, terminal output, and legacy progress input support add status panels, terminal output, and legacy progress input support
+7 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "3.79.0", "version": "3.82.0",
"private": false, "private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",
@@ -45,12 +45,12 @@
"xterm-addon-fit": "^0.8.0" "xterm-addon-fit": "^0.8.0"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^4.4.0", "@git.zone/tsbuild": "^4.4.2",
"@git.zone/tsbundle": "^2.10.0", "@git.zone/tsbundle": "^2.10.4",
"@git.zone/tstest": "^3.6.3", "@git.zone/tstest": "^3.6.6",
"@git.zone/tswatch": "^3.3.2", "@git.zone/tswatch": "^3.3.5",
"@push.rocks/projectinfo": "^5.1.0", "@push.rocks/projectinfo": "^5.1.0",
"@types/node": "^25.6.0" "@types/node": "^25.9.1"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",
@@ -60,6 +60,7 @@
"dist_ts/**/*", "dist_ts/**/*",
"dist_ts_web/**/*", "dist_ts_web/**/*",
"assets/**/*", "assets/**/*",
"scripts/**/*",
"cli.js", "cli.js",
".smartconfig.json", ".smartconfig.json",
"readme.md" "readme.md"
+1404 -857
View File
File diff suppressed because it is too large Load Diff
+49 -7
View File
@@ -1508,18 +1508,33 @@ const layer = await DeesWindowLayer.createAndShow({
### Navigation Components ### Navigation Components
#### `DeesStepper` #### `DeesStepper`
Multi-step navigation component for guided user flows. Multi-step navigation component for guided user flows, including optional auto-advancing progress steps that can render `dees-progressbar` status output between form steps.
```typescript ```typescript
<dees-stepper <dees-stepper
.steps=${[ .steps=${[
{ key: 'personal', label: 'Personal Info', content: html`<div>Form 1</div>` }, {
{ key: 'address', label: 'Address', content: html`<div>Form 2</div>` }, title: 'Account Setup',
{ key: 'confirm', label: 'Confirmation', content: html`<div>Review</div>` } content: html`<dees-form>...</dees-form>`,
menuOptions: [{ name: 'Continue', action: async (stepper) => stepper?.goNext() }]
},
{
title: 'Provision Workspace',
content: html`<p>Preparing your environment...</p>`,
progressStep: {
label: 'Workspace setup',
indeterminate: true,
statusRows: 4,
terminalLines: ['Allocating workspace']
},
validationFunc: async (stepper, _element, signal) => {
stepper.updateProgressStep({ percentage: 35, statusText: 'Installing dependencies...' });
stepper.appendProgressStepLine('Installing dependencies');
if (signal?.aborted) return;
stepper.updateProgressStep({ percentage: 100, indeterminate: false, statusText: 'Workspace ready.' });
}
}
]} ]}
currentStep="personal"
@step-change=${handleStepChange}
@complete=${handleComplete}
></dees-stepper> ></dees-stepper>
``` ```
@@ -1580,6 +1595,33 @@ Theme provider component that wraps children and provides CSS custom properties
- Works with dark/light mode - Works with dark/light mode
- Overrides cascade to all child components - Overrides cascade to all child components
#### `DeesUpdater`
Updater controller that opens a non-cancelable `dees-stepper` flow with a progress step and a ready step.
```typescript
const updater = await DeesUpdater.createAndShow({
currentVersion: '3.79.0',
updatedVersion: '3.80.0',
moreInfoUrl: 'https://code.foss.global/design.estate/dees-catalog',
changelogUrl: 'https://code.foss.global/design.estate/dees-catalog/-/blob/main/changelog.md',
successAction: 'reload',
successDelayMs: 10000,
});
updater.updateProgress({
percentage: 35,
statusText: 'Downloading signed bundle...',
terminalLines: ['Checking release manifest', 'Downloading signed bundle']
});
updater.appendProgressLine('Verifying checksum');
updater.updateProgress({ percentage: 72, statusText: 'Verifying checksum...' });
await updater.markUpdateReady();
```
After `markUpdateReady()`, the updater switches to a second countdown step with a determinate progress bar and runs the configured success action when the timer reaches zero.
--- ---
### Workspace / IDE Components 💻 ### Workspace / IDE Components 💻
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@design.estate/dees-catalog', name: '@design.estate/dees-catalog',
version: '3.79.0', version: '3.82.0',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
} }
@@ -1,7 +1,45 @@
import { html } from '@design.estate/dees-element'; import { html } from '@design.estate/dees-element';
import { DeesStepper, type IStep } from './dees-stepper.js'; import { DeesStepper, type IStep } from './dees-stepper.js';
const demoSteps: IStep[] = [ const waitForProgressTick = async (timeoutArg: number, signal?: AbortSignal): Promise<boolean> => {
return new Promise((resolve) => {
let completed = false;
const finish = (result: boolean) => {
if (completed) {
return;
}
completed = true;
if (signal) {
signal.removeEventListener('abort', handleAbort);
}
resolve(result);
};
const handleAbort = () => {
window.clearTimeout(timeoutId);
finish(false);
};
const timeoutId = window.setTimeout(() => {
finish(true);
}, timeoutArg);
if (signal) {
signal.addEventListener('abort', handleAbort, { once: true });
}
});
};
const createContinueMenuOptions = (labelArg = 'Continue') => [
{
name: labelArg,
action: async (stepper?: DeesStepper) => stepper?.goNext(),
},
];
const createDemoSteps = (): IStep[] => [
{ {
title: 'Account Setup', title: 'Account Setup',
content: html` content: html`
@@ -10,9 +48,7 @@ const demoSteps: IStep[] = [
<dees-input-text key="password" label="Create Password" type="password" required></dees-input-text> <dees-input-text key="password" label="Create Password" type="password" required></dees-input-text>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: createContinueMenuOptions(),
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
],
}, },
{ {
title: 'Profile Details', title: 'Profile Details',
@@ -22,9 +58,7 @@ const demoSteps: IStep[] = [
<dees-input-text key="lastName" label="Last Name" required></dees-input-text> <dees-input-text key="lastName" label="Last Name" required></dees-input-text>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: createContinueMenuOptions(),
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
],
}, },
{ {
title: 'Contact Information', title: 'Contact Information',
@@ -34,9 +68,74 @@ const demoSteps: IStep[] = [
<dees-input-text key="company" label="Company"></dees-input-text> <dees-input-text key="company" label="Company"></dees-input-text>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: createContinueMenuOptions(),
{ name: 'Continue', action: async (stepper) => stepper!.goNext() }, },
], {
title: 'Provision Workspace',
content: html`
<dees-panel>
<p>
We are creating your starter workspace, applying your onboarding choices,
and preparing a live preview. This step moves forward automatically when
the environment is ready.
</p>
</dees-panel>
`,
progressStep: {
label: 'Workspace setup',
percentage: 8,
indeterminate: true,
statusRows: 4,
statusText: 'Allocating a clean workspace...',
terminalLines: ['Allocating a clean workspace'],
},
validationFunc: async (stepper, _htmlElement, signal) => {
const progressFrames = [
{ line: 'Allocating a clean workspace', percentage: 8, delay: 500 },
{ line: 'Syncing account preferences', percentage: 24, delay: 650 },
{ line: 'Installing selected integrations', percentage: 47, delay: 700 },
{ line: 'Generating starter project files', percentage: 71, delay: 650 },
{ line: 'Booting the live preview environment', percentage: 92, delay: 700 },
];
stepper.resetProgressStep();
for (const [index, progressFrame] of progressFrames.entries()) {
if (signal?.aborted) {
return;
}
if (index === 0) {
stepper.updateProgressStep({
percentage: progressFrame.percentage,
indeterminate: true,
statusText: `${progressFrame.line}...`,
terminalLines: [progressFrame.line],
});
} else {
stepper.appendProgressStepLine(progressFrame.line);
stepper.updateProgressStep({
percentage: progressFrame.percentage,
indeterminate: true,
statusText: `${progressFrame.line}...`,
});
}
const completed = await waitForProgressTick(progressFrame.delay, signal);
if (!completed) {
return;
}
}
stepper.appendProgressStepLine('Workspace ready');
stepper.updateProgressStep({
percentage: 100,
indeterminate: false,
statusText: 'Workspace ready.',
});
await waitForProgressTick(350, signal);
},
}, },
{ {
title: 'Team Size', title: 'Team Size',
@@ -55,9 +154,7 @@ const demoSteps: IStep[] = [
></dees-input-dropdown> ></dees-input-dropdown>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: createContinueMenuOptions(),
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
],
}, },
{ {
title: 'Goals', title: 'Goals',
@@ -75,52 +172,31 @@ const demoSteps: IStep[] = [
></dees-input-multitoggle> ></dees-input-multitoggle>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: createContinueMenuOptions(),
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
],
},
{
title: 'Brand Preferences',
content: html`
<dees-form>
<dees-input-text key="brandColor" label="Primary brand color"></dees-input-text>
<dees-input-text key="tone" label="Preferred tone (e.g. friendly, formal)"></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
],
},
{
title: 'Integrations',
content: html`
<dees-form>
<dees-input-list
key="integrations"
label="Integrations in use"
placeholder="Add integration"
></dees-input-list>
</dees-form>
`,
menuOptions: [
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
],
}, },
{ {
title: 'Review & Launch', title: 'Review & Launch',
content: html` content: html`
<dees-panel> <dees-panel>
<p>Almost there! Review your selections and launch whenever you're ready.</p> <p>
Your workspace is ready. Review the collected details and launch when
you are ready to start.
</p>
</dees-panel> </dees-panel>
`, `,
menuOptions: [ menuOptions: [
{ name: 'Launch', action: async (stepper) => stepper!.goNext() }, {
name: 'Launch',
action: async (stepper?: DeesStepper) => {
if (stepper?.overlay) {
await stepper.destroy();
}
},
},
], ],
}, },
]; ];
const cloneSteps = (): IStep[] => demoSteps.map((step) => ({ ...step }));
export const stepperDemo = () => html` export const stepperDemo = () => html`
<div style="position: absolute; inset: 0;"> <div style="position: absolute; inset: 0;">
<div <div
@@ -128,10 +204,10 @@ export const stepperDemo = () => html`
> >
<dees-button <dees-button
@click=${async () => { @click=${async () => {
await DeesStepper.createAndShow({ steps: cloneSteps() }); await DeesStepper.createAndShow({ steps: createDemoSteps() });
}} }}
>Open stepper as overlay</dees-button> >Open stepper as overlay</dees-button>
</div> </div>
<dees-stepper .steps=${cloneSteps()}></dees-stepper> <dees-stepper .steps=${createDemoSteps()}></dees-stepper>
</div> </div>
`; `;
@@ -5,8 +5,6 @@ import {
customElement, customElement,
html, html,
css, css,
unsafeCSS,
type CSSResult,
cssManager, cssManager,
property, property,
type TemplateResult, type TemplateResult,
@@ -20,16 +18,33 @@ import { zIndexRegistry } from '../../00zindex.js';
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js'; import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js'; import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
import type { DeesForm } from '../../00group-form/dees-form/dees-form.js'; import type { DeesForm } from '../../00group-form/dees-form/dees-form.js';
import '../../00group-feedback/dees-progressbar/dees-progressbar.js';
import '../dees-tile/dees-tile.js'; import '../dees-tile/dees-tile.js';
export interface IStepProgressState {
label?: string;
percentage?: number;
indeterminate?: boolean;
showPercentage?: boolean;
statusText?: string;
terminalLines?: string[];
statusRows?: number;
}
export interface IStepProgress extends IStepProgressState {
autoAdvance?: boolean;
}
export interface IStep { export interface IStep {
title: string; title: string;
content: TemplateResult; content: TemplateResult;
progressStep?: IStepProgress;
menuOptions?: plugins.tsclass.website.IMenuItem<DeesStepper>[]; menuOptions?: plugins.tsclass.website.IMenuItem<DeesStepper>[];
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>; validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>; onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
validationFuncCalled?: boolean; validationFuncCalled?: boolean;
abortController?: AbortController; abortController?: AbortController;
progressStepState?: IStepProgressState;
} }
declare global { declare global {
@@ -276,6 +291,14 @@ export class DeesStepper extends DeesElement {
padding: 32px; padding: 32px;
} }
.step-body .content.withProgressStep {
padding-top: 20px;
}
.progressStep {
padding: 0 24px;
}
/* --- Footer: modal-style bottom buttons --- */ /* --- Footer: modal-style bottom buttons --- */
.bottomButtons { .bottomButtons {
display: flex; display: flex;
@@ -375,6 +398,7 @@ export class DeesStepper extends DeesElement {
const isHidden = const isHidden =
this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep); this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep);
const isFirst = stepIndex === 0; const isFirst = stepIndex === 0;
const progressStepState = stepArg.progressStep ? this.getProgressStepState(stepArg) : null;
return html`<dees-tile return html`<dees-tile
class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''} ${isFirst ? 'entrance' : ''}" class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''} ${isFirst ? 'entrance' : ''}"
> >
@@ -390,7 +414,20 @@ export class DeesStepper extends DeesElement {
</div> </div>
<div class="step-body"> <div class="step-body">
<div class="title">${stepArg.title}</div> <div class="title">${stepArg.title}</div>
<div class="content">${stepArg.content}</div> ${stepArg.progressStep && progressStepState ? html`
<div class="progressStep">
<dees-progressbar
.label=${progressStepState.label ?? stepArg.title}
.percentage=${progressStepState.percentage ?? 0}
.indeterminate=${progressStepState.indeterminate ?? false}
.showPercentage=${progressStepState.showPercentage ?? true}
.statusText=${progressStepState.statusText ?? ''}
.terminalLines=${progressStepState.terminalLines ?? []}
.statusRows=${progressStepState.statusRows ?? 3}
></dees-progressbar>
</div>
` : ''}
<div class="content ${stepArg.progressStep ? 'withProgressStep' : ''}">${stepArg.content}</div>
</div> </div>
<div slot="footer" class="bottomButtons"> <div slot="footer" class="bottomButtons">
${isSelected && this.activeForm !== null && !this.activeFormValid ${isSelected && this.activeForm !== null && !this.activeFormValid
@@ -426,22 +463,30 @@ export class DeesStepper extends DeesElement {
public async firstUpdated() { public async firstUpdated() {
await this.domtoolsPromise; await this.domtoolsPromise;
await this.domtools.convenience.smartdelay.delayFor(0); await this.domtools.convenience.smartdelay.delayFor(0);
if (!this.steps.length) {
return;
}
this.prepareStepForActivation(this.steps[0]);
this.selectedStep = this.steps[0]; this.selectedStep = this.steps[0];
this.setScrollStatus(); await this.updateComplete;
await this.setScrollStatus();
// Remove entrance class after initial animation completes // Remove entrance class after initial animation completes
await this.domtools.convenience.smartdelay.delayFor(350); await this.domtools.convenience.smartdelay.delayFor(350);
this.shadowRoot!.querySelector('.step.entrance')?.classList.remove('entrance'); this.shadowRoot!.querySelector('.step.entrance')?.classList.remove('entrance');
} }
public async updated() { public async updated(changedProperties: Map<string | number | symbol, unknown>) {
this.setScrollStatus(); if (!changedProperties.has('selectedStep') && !changedProperties.has('steps')) {
return;
}
await this.setScrollStatus();
} }
public scroller!: typeof domtools.plugins.SweetScroll.prototype; public scroller!: typeof domtools.plugins.SweetScroll.prototype;
public async setScrollStatus() { public async setScrollStatus() {
const stepperContainer = this.shadowRoot!.querySelector('.stepperContainer') as HTMLElement; const stepperContainer = this.shadowRoot!.querySelector('.stepperContainer') as HTMLElement;
const firstStepElement = this.shadowRoot!.querySelector('.step') as HTMLElement;
const selectedStepElement = this.shadowRoot!.querySelector('.selected') as HTMLElement; const selectedStepElement = this.shadowRoot!.querySelector('.selected') as HTMLElement;
if (!selectedStepElement) { if (!selectedStepElement) {
return; return;
@@ -452,14 +497,11 @@ export class DeesStepper extends DeesElement {
stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2 stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2
}px`; }px`;
} }
console.log('Setting scroll status');
console.log(selectedStepElement);
const scrollPosition = const scrollPosition =
selectedStepElement.offsetTop - selectedStepElement.offsetTop -
stepperContainer.offsetHeight / 2 + stepperContainer.offsetHeight / 2 +
selectedStepElement.offsetHeight / 2; selectedStepElement.offsetHeight / 2;
console.log(scrollPosition); await domtools.DomTools.setupDomTools();
const domtoolsInstance = await domtools.DomTools.setupDomTools();
if (!this.scroller) { if (!this.scroller) {
this.scroller = new domtools.plugins.SweetScroll( this.scroller = new domtools.plugins.SweetScroll(
{ {
@@ -474,7 +516,11 @@ export class DeesStepper extends DeesElement {
if (!this.selectedStep.validationFuncCalled && this.selectedStep.validationFunc) { if (!this.selectedStep.validationFuncCalled && this.selectedStep.validationFunc) {
this.selectedStep.abortController = new AbortController(); this.selectedStep.abortController = new AbortController();
this.selectedStep.validationFuncCalled = true; this.selectedStep.validationFuncCalled = true;
await this.selectedStep.validationFunc(this, selectedStepElement, this.selectedStep.abortController.signal); void this.runStepValidation(
this.selectedStep,
selectedStepElement,
this.selectedStep.abortController.signal,
);
} }
this.scroller.to(scrollPosition); this.scroller.to(scrollPosition);
} }
@@ -492,6 +538,7 @@ export class DeesStepper extends DeesElement {
currentStep.validationFuncCalled = false; currentStep.validationFuncCalled = false;
const previousStep = this.steps[currentIndex - 1]; const previousStep = this.steps[currentIndex - 1];
previousStep.validationFuncCalled = false; previousStep.validationFuncCalled = false;
this.prepareStepForActivation(previousStep);
this.selectedStep = previousStep; this.selectedStep = previousStep;
await this.domtoolsPromise; await this.domtoolsPromise;
await this.domtools.convenience.smartdelay.delayFor(100); await this.domtools.convenience.smartdelay.delayFor(100);
@@ -511,9 +558,52 @@ export class DeesStepper extends DeesElement {
currentStep.validationFuncCalled = false; currentStep.validationFuncCalled = false;
const nextStep = this.steps[currentIndex + 1]; const nextStep = this.steps[currentIndex + 1];
nextStep.validationFuncCalled = false; nextStep.validationFuncCalled = false;
this.prepareStepForActivation(nextStep);
this.selectedStep = nextStep; this.selectedStep = nextStep;
} }
public resetProgressStep(stepArg: IStep = this.selectedStep) {
if (!stepArg?.progressStep) {
return;
}
stepArg.progressStepState = this.createInitialProgressStepState(stepArg);
this.requestUpdate();
}
public updateProgressStep(
progressStateArg: Partial<IStepProgressState>,
stepArg: IStep = this.selectedStep,
) {
if (!stepArg?.progressStep) {
return;
}
const currentProgressState = this.getProgressStepState(stepArg);
stepArg.progressStepState = {
...currentProgressState,
...progressStateArg,
terminalLines: progressStateArg.terminalLines
? [...progressStateArg.terminalLines]
: [...(currentProgressState.terminalLines ?? [])],
};
this.requestUpdate();
}
public appendProgressStepLine(lineArg: string, stepArg: IStep = this.selectedStep) {
if (!stepArg?.progressStep) {
return;
}
const currentProgressState = this.getProgressStepState(stepArg);
this.updateProgressStep(
{
terminalLines: [...(currentProgressState.terminalLines ?? []), lineArg],
},
stepArg,
);
}
/** /**
* Scans the currently selected step for a <dees-form> in its content. When * Scans the currently selected step for a <dees-form> in its content. When
* found, subscribes to the form's RxJS changeSubject so the primary * found, subscribes to the form's RxJS changeSubject so the primary
@@ -582,6 +672,74 @@ export class DeesStepper extends DeesElement {
await optionArg.action(this); await optionArg.action(this);
} }
private getProgressStepState(stepArg: IStep): IStepProgressState {
if (!stepArg.progressStep) {
return {};
}
if (!stepArg.progressStepState) {
stepArg.progressStepState = this.createInitialProgressStepState(stepArg);
}
return stepArg.progressStepState;
}
private createInitialProgressStepState(stepArg: IStep): IStepProgressState {
return {
label: stepArg.progressStep?.label ?? stepArg.title,
percentage: stepArg.progressStep?.percentage ?? 0,
indeterminate: stepArg.progressStep?.indeterminate ?? false,
showPercentage: stepArg.progressStep?.showPercentage ?? true,
statusText: stepArg.progressStep?.statusText ?? '',
terminalLines: [...(stepArg.progressStep?.terminalLines ?? [])],
statusRows: stepArg.progressStep?.statusRows ?? 3,
};
}
private prepareStepForActivation(stepArg?: IStep) {
if (!stepArg?.progressStep) {
return;
}
stepArg.progressStepState = this.createInitialProgressStepState(stepArg);
}
private async runStepValidation(
stepArg: IStep,
selectedStepElement: HTMLElement,
signal: AbortSignal,
): Promise<void> {
try {
await stepArg.validationFunc?.(this, selectedStepElement, signal);
if (signal.aborted) {
return;
}
if (stepArg.progressStep && stepArg.progressStep.autoAdvance !== false && this.selectedStep === stepArg) {
this.goNext();
}
} catch (error) {
if (signal.aborted) {
return;
}
if (stepArg.progressStep) {
const errorText = error instanceof Error ? error.message : 'Unexpected error';
this.appendProgressStepLine(`Error: ${errorText}`, stepArg);
this.updateProgressStep(
{
indeterminate: false,
statusText: errorText,
},
stepArg,
);
}
console.error(error);
}
}
/** /**
* Currently-open confirmation modal (if any). Prevents double-stacking when * Currently-open confirmation modal (if any). Prevents double-stacking when
* the user clicks the backdrop or the Cancel button while a confirm modal * the user clicks the backdrop or the Cancel button while a confirm modal
@@ -657,6 +815,9 @@ export class DeesStepper extends DeesElement {
public async destroy() { public async destroy() {
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
const container = this.shadowRoot!.querySelector('.stepperContainer'); const container = this.shadowRoot!.querySelector('.stepperContainer');
if (this.selectedStep?.abortController) {
this.selectedStep.abortController.abort();
}
container?.classList.add('predestroy'); container?.classList.add('predestroy');
await domtools.convenience.smartdelay.delayFor(250); await domtools.convenience.smartdelay.delayFor(250);
if (this.parentElement) { if (this.parentElement) {
@@ -1,5 +1,11 @@
import * as webcontainer from '@tempfix/webcontainer__api'; import * as webcontainer from '@tempfix/webcontainer__api';
import type { IExecutionEnvironment, IFileEntry, IFileWatcher, IProcessHandle } from '../interfaces/IExecutionEnvironment.js'; import type {
IExecutionEnvironment,
IFileEntry,
IFileWatcher,
IProcessHandle,
IShellCommand,
} from '../interfaces/IExecutionEnvironment.js';
/** /**
* WebContainer-based execution environment. * WebContainer-based execution environment.
@@ -154,6 +160,14 @@ export class WebContainerEnvironment implements IExecutionEnvironment {
}; };
} }
public getShellCommand(): IShellCommand {
return {
command: 'jsh',
label: 'jsh',
prompt: '~/',
};
}
// ============ WebContainer-specific methods ============ // ============ WebContainer-specific methods ============
/** /**
@@ -29,6 +29,17 @@ export interface IProcessHandle {
kill(): void; kill(): void;
} }
export interface IShellCommand {
/** Executable to start for an interactive shell session. */
command: string;
/** Optional command arguments. */
args?: string[];
/** Optional display label for terminal tabs. */
label?: string;
/** Optional prompt marker used for setup commands. */
prompt?: string;
}
/** /**
* Abstract execution environment interface. * Abstract execution environment interface.
* Implementations can target WebContainer (browser), Backend API (server), or Mock (testing). * Implementations can target WebContainer (browser), Backend API (server), or Mock (testing).
@@ -93,12 +104,18 @@ export interface IExecutionEnvironment {
/** /**
* Spawn a new process * Spawn a new process
* @param command - Command to run (e.g., 'jsh', 'node', 'npm') * @param command - Command to run (e.g., 'node', 'npm')
* @param args - Optional arguments * @param args - Optional arguments
* @returns Process handle with I/O streams * @returns Process handle with I/O streams
*/ */
spawn(command: string, args?: string[]): Promise<IProcessHandle>; spawn(command: string, args?: string[]): Promise<IProcessHandle>;
/**
* Return the environment-native interactive shell command.
* Implementations should provide this when terminal shells need a specific executable.
*/
getShellCommand?(): IShellCommand | Promise<IShellCommand>;
// ============ Lifecycle ============ // ============ Lifecycle ============
/** /**
@@ -2,9 +2,73 @@ import { html } from '@design.estate/dees-element';
import { DeesUpdater } from '../dees-updater/dees-updater.js'; import { DeesUpdater } from '../dees-updater/dees-updater.js';
export const demoFunc = async () => { const waitForDemoStep = async (timeoutArg: number): Promise<void> => {
const updater = await DeesUpdater.createAndShow(); await new Promise<void>((resolve) => {
setTimeout(async () => { window.setTimeout(() => resolve(), timeoutArg);
await updater.destroy(); });
}, 10000); };
}
export const demoFunc = () => {
let updaterRunning = false;
return html`
<div style="display: grid; gap: 16px; place-items: center; padding: 32px; text-align: center;">
<p style="margin: 0; max-width: 540px; line-height: 1.6; color: var(--dees-color-text-secondary);">
Launches the updater as a stepper flow. The first step streams terminal-style
progress updates and then moves automatically to the ready step.
</p>
<dees-button
@click=${async () => {
if (updaterRunning) {
return;
}
updaterRunning = true;
try {
const updater = await DeesUpdater.createAndShow({
currentVersion: '3.79.0',
updatedVersion: '3.80.0',
moreInfoUrl: 'https://code.foss.global/design.estate/dees-catalog',
changelogUrl: 'https://code.foss.global/design.estate/dees-catalog/-/blob/main/changelog.md',
successAction: 'close',
successDelayMs: 10000,
});
const progressFrames = [
{ line: 'Checking release manifest', percentage: 12, delay: 550 },
{ line: 'Downloading signed bundle', percentage: 33, delay: 700 },
{ line: 'Verifying checksum', percentage: 51, delay: 650 },
{ line: 'Applying update files', percentage: 74, delay: 800 },
{ line: 'Cleaning up previous release', percentage: 91, delay: 600 },
];
updater.updateProgress({
statusText: 'Checking release manifest...',
terminalLines: ['Checking release manifest'],
percentage: 12,
indeterminate: true,
});
for (const [index, progressFrame] of progressFrames.entries()) {
if (index > 0) {
updater.appendProgressLine(progressFrame.line);
updater.updateProgress({
percentage: progressFrame.percentage,
statusText: `${progressFrame.line}...`,
});
}
await waitForDemoStep(progressFrame.delay);
}
await updater.markUpdateReady();
await waitForDemoStep(10500);
} finally {
updaterRunning = false;
}
}}
>Show updater flow</dees-button>
</div>
`;
};
@@ -4,14 +4,28 @@ import {
type TemplateResult, type TemplateResult,
html, html,
property, property,
type CSSResult, css,
domtools,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { demoFunc } from './dees-updater.demo.js'; import { demoFunc } from './dees-updater.demo.js';
import {
DeesStepper,
type IStep,
type IStepProgressState,
} from '../../00group-layout/dees-stepper/dees-stepper.js';
import { monoFontFamily } from '../../00fonts.js';
import '../../00group-overlay/dees-windowlayer/dees-windowlayer.js'; export type TDeesUpdaterSuccessAction = 'close' | 'reload';
import { css, cssManager } from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js'; export interface IDeesUpdaterOptions {
currentVersion?: string;
updatedVersion?: string;
moreInfoUrl?: string;
changelogUrl?: string;
successAction?: TDeesUpdaterSuccessAction;
successDelayMs?: number;
successActionLabel?: string;
onSuccessAction?: () => Promise<void> | void;
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -24,91 +38,475 @@ export class DeesUpdater extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroups = ['Utility']; public static demoGroups = ['Utility'];
public static async createAndShow() { public static async createAndShow(optionsArg: IDeesUpdaterOptions = {}) {
const updater = new DeesUpdater(); const updater = new DeesUpdater();
updater.currentVersion = optionsArg.currentVersion ?? '';
updater.updatedVersion = optionsArg.updatedVersion ?? '';
updater.moreInfoUrl = optionsArg.moreInfoUrl ?? '';
updater.changelogUrl = optionsArg.changelogUrl ?? '';
updater.successAction = optionsArg.successAction ?? 'close';
updater.successDelayMs = optionsArg.successDelayMs ?? 10000;
updater.successActionLabel = optionsArg.successActionLabel ?? '';
updater.onSuccessAction = optionsArg.onSuccessAction ?? null;
document.body.appendChild(updater); document.body.appendChild(updater);
await updater.show();
return updater; return updater;
} }
@property({ @property({
type: String, type: String,
}) })
accessor currentVersion!: string; accessor currentVersion = '';
@property({ @property({
type: String, type: String,
}) })
accessor updatedVersion!: string; accessor updatedVersion = '';
constructor() { @property({
super(); type: String,
domtools.elementBasic.setup(); })
} accessor moreInfoUrl = '';
@property({
type: String,
})
accessor changelogUrl = '';
@property({
type: String,
})
accessor successAction: TDeesUpdaterSuccessAction = 'close';
@property({
type: Number,
})
accessor successDelayMs = 10000;
@property({
type: String,
})
accessor successActionLabel = '';
private stepper: DeesStepper | null = null;
private progressStep: IStep | null = null;
private showPromise: Promise<void> | null = null;
private onSuccessAction: (() => Promise<void> | void) | null = null;
public static styles = [ public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css` css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */ :host {
.modalContainer { display: none;
will-change: transform;
position: relative;
background: ${cssManager.bdTheme('#eeeeeb', '#222')};
max-width: 800px;
border-radius: 8px;
border-top: 1px solid ${cssManager.bdTheme('#eeeeeb', '#333')};
}
.headingContainer {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 40px;
}
h1 {
margin: none;
font-size: 20px;
color: ${cssManager.bdTheme('#333', '#fff')};
margin-left: 20px;
font-weight: normal;
}
.buttonContainer {
display: grid;
grid-template-columns: 50% 50%;
} }
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
return html` return html``;
<dees-windowlayer }
@clicked="${this.windowLayerClicked}"
.options=${{ public async connectedCallback(): Promise<void> {
blur: true, await super.connectedCallback();
}} void this.show();
> }
<div class="modalContainer">
<div class="headingContainer"> public async show(): Promise<void> {
<dees-spinner .size=${60}></dees-spinner> if (this.stepper?.isConnected) {
<h1>Updating the application...</h1> return;
</div> }
<div class="progress">
<dees-progressbar .progress=${0.5}></dees-progressbar> if (this.showPromise) {
</div> return this.showPromise;
<div class="buttonContainer"> }
<dees-button>More info</dees-button>
<dees-button>Changelog</dees-button> this.showPromise = this.openStepperFlow();
</div>
</div> </dees-windowlayer try {
>> await this.showPromise;
`; } finally {
this.showPromise = null;
}
}
public updateProgress(progressStateArg: Partial<IStepProgressState>) {
if (!this.stepper || !this.progressStep) {
return;
}
this.stepper.updateProgressStep(progressStateArg, this.progressStep);
}
public appendProgressLine(lineArg: string) {
if (!this.stepper || !this.progressStep) {
return;
}
this.stepper.appendProgressStepLine(lineArg, this.progressStep);
}
public markUpdateError(messageArg: string) {
this.appendProgressLine(`Error: ${messageArg}`);
this.updateProgress({
indeterminate: false,
statusText: messageArg,
});
}
public async markUpdateReady() {
if (!this.stepper || !this.progressStep) {
return;
}
this.stepper.updateProgressStep(
{
percentage: 100,
indeterminate: false,
statusText: 'Update ready.',
},
this.progressStep,
);
this.stepper.appendProgressStepLine('Update ready', this.progressStep);
if (this.stepper.selectedStep === this.progressStep) {
this.stepper.goNext();
}
} }
public async destroy() { public async destroy() {
this.parentElement!.removeChild(this); const stepper = this.stepper;
this.stepper = null;
this.progressStep = null;
if (stepper?.isConnected) {
await stepper.destroy();
}
if (this.parentElement) {
this.parentElement.removeChild(this);
}
} }
private windowLayerClicked() {} private async openStepperFlow() {
const { steps, progressStep } = this.createUpdaterSteps();
this.progressStep = progressStep;
this.stepper = await DeesStepper.createAndShow({
steps,
cancelable: false,
});
}
private createUpdaterSteps(): { steps: IStep[]; progressStep: IStep } {
const infoMenuOptions = this.getLinkMenuOptions();
const progressStep: IStep = {
title: 'Updating the application',
content: this.renderProgressContent(),
progressStep: {
label: this.getProgressLabel(),
percentage: 5,
indeterminate: true,
statusRows: 4,
statusText: 'Preparing update...',
terminalLines: ['Preparing update'],
},
menuOptions: infoMenuOptions.length > 0 ? infoMenuOptions : undefined,
};
const readyStep: IStep = {
title: this.updatedVersion ? `Version ${this.updatedVersion} ready` : 'Update ready',
content: this.renderReadyContent(),
progressStep: {
label: this.getSuccessCountdownLabel(this.getSuccessDelaySeconds()),
percentage: 0,
indeterminate: false,
showPercentage: false,
statusRows: 2,
statusText: this.getSuccessCountdownStatus(this.getSuccessDelaySeconds()),
},
validationFunc: async (stepper, _htmlElement, signal) => {
await this.runSuccessCountdown(stepper, readyStep, signal);
},
};
return {
steps: [progressStep, readyStep],
progressStep,
};
}
private getProgressLabel(): string {
if (this.currentVersion && this.updatedVersion) {
return `${this.currentVersion} -> ${this.updatedVersion}`;
}
if (this.updatedVersion) {
return `Preparing ${this.updatedVersion}`;
}
return 'Application update';
}
private getSuccessDelaySeconds(): number {
return Math.max(1, Math.ceil(this.successDelayMs / 1000));
}
private getSuccessActionDisplayLabel(): string {
if (this.successActionLabel) {
return this.successActionLabel;
}
if (this.onSuccessAction) {
return 'Continuing automatically';
}
if (this.successAction === 'reload') {
return 'Reloading application';
}
return 'Closing updater';
}
private getSuccessCountdownLabel(secondsArg: number): string {
return `${this.getSuccessActionDisplayLabel()} in ${secondsArg}s`;
}
private getSuccessCountdownStatus(secondsArg: number): string {
const secondLabel = secondsArg === 1 ? 'second' : 'seconds';
return `${this.getSuccessActionDisplayLabel()} in ${secondsArg} ${secondLabel}.`;
}
private getSuccessActionNowLabel(): string {
return `${this.getSuccessActionDisplayLabel()} now...`;
}
private getLinkMenuOptions() {
const menuOptions: Array<{ name: string; action: () => Promise<void> }> = [];
if (this.moreInfoUrl) {
menuOptions.push({
name: 'More info',
action: async () => {
this.openExternalUrl(this.moreInfoUrl);
},
});
}
if (this.changelogUrl) {
menuOptions.push({
name: 'Changelog',
action: async () => {
this.openExternalUrl(this.changelogUrl);
},
});
}
return menuOptions;
}
private renderVersionMeta(labelArg: string, valueArg: string): TemplateResult {
return html`
<div
style="display: grid; gap: 4px; min-width: 132px; padding: 12px 14px; border: 1px solid var(--dees-color-border-subtle); border-radius: 8px; background: var(--dees-color-hover);"
>
<div
style="font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--dees-color-text-muted);"
>
${labelArg}
</div>
<div
style=${`font-family: ${monoFontFamily}; font-size: 13px; font-weight: 600; letter-spacing: -0.01em; color: var(--dees-color-text-primary);`}
>
${valueArg}
</div>
</div>
`;
}
private renderProgressContent(): TemplateResult {
return html`
<div style="display: grid; gap: 18px; color: var(--dees-color-text-secondary);">
<div style="display: grid; gap: 6px;">
<div
style="font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dees-color-text-muted);"
>
Application update
</div>
<div
style="font-size: 18px; line-height: 1.35; font-weight: 600; letter-spacing: -0.02em; color: var(--dees-color-text-primary);"
>
Downloading and applying the latest release.
</div>
<p style="margin: 0; font-size: 14px; line-height: 1.65; color: var(--dees-color-text-secondary);">
${this.currentVersion && this.updatedVersion
? html`Moving from <strong>${this.currentVersion}</strong> to <strong>${this.updatedVersion}</strong>.`
: this.updatedVersion
? html`Preparing <strong>${this.updatedVersion}</strong> for installation.`
: 'The updater is fetching the newest build and preparing it for installation.'}
</p>
</div>
${this.currentVersion || this.updatedVersion
? html`
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
${this.currentVersion
? this.renderVersionMeta('Current version', this.currentVersion)
: ''}
${this.updatedVersion
? this.renderVersionMeta('Incoming version', this.updatedVersion)
: ''}
</div>
`
: ''}
<div
style="display: grid; gap: 4px; padding: 14px 16px; border: 1px solid var(--dees-color-border-subtle); border-radius: 8px; background: var(--dees-color-hover);"
>
<div style="font-size: 12px; font-weight: 600; color: var(--dees-color-text-primary);">
Automatic flow
</div>
<div style="font-size: 13px; line-height: 1.6; color: var(--dees-color-text-muted);">
The updater continues on its own once the new build is installed and verified.
</div>
</div>
</div>
`;
}
private renderReadyContent(): TemplateResult {
const successDelaySeconds = this.getSuccessDelaySeconds();
return html`
<div style="display: grid; gap: 18px; color: var(--dees-color-text-secondary);">
<div style="display: grid; gap: 6px;">
<div
style="font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dees-color-text-muted);"
>
Update complete
</div>
<div
style="font-size: 18px; line-height: 1.35; font-weight: 600; letter-spacing: -0.02em; color: var(--dees-color-text-primary);"
>
${this.updatedVersion
? html`Version <span style=${`font-family: ${monoFontFamily}; font-size: 0.95em;`}>${this.updatedVersion}</span> is ready to use.`
: 'The new version is ready to use.'}
</div>
<p style="margin: 0; font-size: 14px; line-height: 1.65; color: var(--dees-color-text-secondary);">
The new build has been installed and verified. You can review release details below while the next action runs automatically.
</p>
</div>
${this.updatedVersion
? html`
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
${this.renderVersionMeta('Installed version', this.updatedVersion)}
</div>
`
: ''}
<div
style="display: grid; gap: 4px; padding: 14px 16px; border: 1px solid var(--dees-color-border-subtle); border-radius: 8px; background: var(--dees-color-hover);"
>
<div style="font-size: 12px; font-weight: 600; color: var(--dees-color-text-primary);">
Next action
</div>
<div style="font-size: 14px; line-height: 1.5; font-weight: 600; color: var(--dees-color-text-primary);">
${this.getSuccessActionDisplayLabel()}
</div>
<div style="font-size: 13px; line-height: 1.6; color: var(--dees-color-text-muted);">
Runs automatically in ${successDelaySeconds} seconds.
</div>
</div>
</div>
`;
}
private async runSuccessCountdown(
stepperArg: DeesStepper,
stepArg: IStep,
signal?: AbortSignal,
): Promise<void> {
const totalDuration = Math.max(1000, this.successDelayMs);
const startTime = Date.now();
while (!signal?.aborted) {
const elapsed = Math.min(totalDuration, Date.now() - startTime);
const remainingMilliseconds = Math.max(0, totalDuration - elapsed);
const remainingSeconds = Math.max(0, Math.ceil(remainingMilliseconds / 1000));
stepperArg.updateProgressStep(
{
label: remainingMilliseconds > 0
? this.getSuccessCountdownLabel(remainingSeconds)
: this.getSuccessActionNowLabel(),
percentage: (elapsed / totalDuration) * 100,
indeterminate: false,
showPercentage: false,
statusText: remainingMilliseconds > 0
? this.getSuccessCountdownStatus(remainingSeconds)
: this.getSuccessActionNowLabel(),
},
stepArg,
);
if (remainingMilliseconds <= 0) {
break;
}
const completed = await this.waitForCountdownTick(100, signal);
if (!completed) {
return;
}
}
await this.runConfiguredSuccessAction();
}
private async waitForCountdownTick(timeoutArg: number, signal?: AbortSignal): Promise<boolean> {
return new Promise((resolve) => {
let completed = false;
const finish = (result: boolean) => {
if (completed) {
return;
}
completed = true;
if (signal) {
signal.removeEventListener('abort', handleAbort);
}
resolve(result);
};
const handleAbort = () => {
window.clearTimeout(timeoutId);
finish(false);
};
const timeoutId = window.setTimeout(() => {
finish(true);
}, timeoutArg);
if (signal) {
signal.addEventListener('abort', handleAbort, { once: true });
}
});
}
private async runConfiguredSuccessAction(): Promise<void> {
if (this.onSuccessAction) {
await this.onSuccessAction();
return;
}
if (this.successAction === 'reload') {
await this.destroy();
window.location.reload();
return;
}
await this.destroy();
}
private openExternalUrl(urlArg: string) {
window.open(urlArg, '_blank', 'noopener,noreferrer');
}
} }
@@ -12,7 +12,7 @@ import * as domtools from '@design.estate/dees-domtools';
import type { Terminal } from 'xterm'; import type { Terminal } from 'xterm';
import { themeDefaultStyles } from '../../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
import type { IExecutionEnvironment } from '../../00group-runtime/index.js'; import type { IExecutionEnvironment, IShellCommand } from '../../00group-runtime/index.js';
import { WebContainerEnvironment } from '../../00group-runtime/index.js'; import { WebContainerEnvironment } from '../../00group-runtime/index.js';
import '../../00group-utility/dees-icon/dees-icon.js'; import '../../00group-utility/dees-icon/dees-icon.js';
import '../../00group-feedback/dees-actionbar/dees-actionbar.js'; import '../../00group-feedback/dees-actionbar/dees-actionbar.js';
@@ -660,6 +660,22 @@ export class DeesWorkspaceTerminal extends DeesElement {
} }
} }
private async getShellCommand(): Promise<IShellCommand> {
if (this.executionEnvironment && !this.executionEnvironment.ready) {
await this.executionEnvironment.init();
}
if (this.executionEnvironment?.getShellCommand) {
return await this.executionEnvironment.getShellCommand();
}
return {
command: 'jsh',
label: 'jsh',
prompt: '~/',
};
}
private handleProcessExit(tabId: string, exitCode: number): void { private handleProcessExit(tabId: string, exitCode: number): void {
const tab = this.tabManager.getTab(tabId); const tab = this.tabManager.getTab(tabId);
if (!tab) return; if (!tab) return;
@@ -723,10 +739,11 @@ export class DeesWorkspaceTerminal extends DeesElement {
* Create a new shell tab * Create a new shell tab
*/ */
public async createShellTab(label?: string): Promise<string> { public async createShellTab(label?: string): Promise<string> {
const shellCommand = await this.getShellCommand();
const tab = this.tabManager.createTab( const tab = this.tabManager.createTab(
{ {
type: 'shell', type: 'shell',
label: label || `bash ${this.tabManager.getTabCount() + 1}`, label: label || `${shellCommand.label || shellCommand.command} ${this.tabManager.getTabCount() + 1}`,
closeable: this.tabManager.getTabCount() > 0, // First tab not closeable closeable: this.tabManager.getTabCount() > 0, // First tab not closeable
}, },
this.isBright this.isBright
@@ -739,11 +756,11 @@ export class DeesWorkspaceTerminal extends DeesElement {
// Wait for DOM update then spawn shell // Wait for DOM update then spawn shell
await this.updateComplete; await this.updateComplete;
await this.spawnProcessForTab(tab, 'jsh'); await this.spawnProcessForTab(tab, shellCommand.command, shellCommand.args || []);
// Run setup command if this is the first tab // Run setup command if this is the first tab
if (this.tabManager.getTabCount() === 1 && this.setupCommand) { if (this.tabManager.getTabCount() === 1 && this.setupCommand) {
await this.waitForPrompt(tab.terminal, '~/'); await this.waitForPrompt(tab.terminal, shellCommand.prompt || '~/');
if (tab.inputWriter) { if (tab.inputWriter) {
tab.inputWriter.write(this.setupCommand); tab.inputWriter.write(this.setupCommand);
} }
@@ -6,7 +6,7 @@ import type { IProcessHandle } from '../../00group-runtime/index.js';
* Type of terminal tab based on its source/purpose * Type of terminal tab based on its source/purpose
*/ */
export type TTerminalTabType = export type TTerminalTabType =
| 'shell' // Default interactive shell (jsh) | 'shell' // Environment-native interactive shell
| 'script' // Script from package.json | 'script' // Script from package.json
| 'package-update' // Package update process | 'package-update' // Package update process
| 'custom'; // External process from API | 'custom'; // External process from API
@@ -75,7 +75,7 @@ export interface ICreateTerminalTabOptions {
/** Whether the tab can be closed (default: true for non-shell) */ /** Whether the tab can be closed (default: true for non-shell) */
closeable?: boolean; closeable?: boolean;
/** Command to spawn (default: 'jsh' for shell type) */ /** Command to spawn (shell type uses the execution environment shell by default) */
command?: string; command?: string;
/** Command arguments */ /** Command arguments */