Skip to content

Commit

Permalink
DCA custom interval slider
Browse files Browse the repository at this point in the history
  • Loading branch information
jvonasek committed Sep 16, 2024
1 parent 0b5bd1a commit c21a297
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 19 deletions.
6 changes: 6 additions & 0 deletions .changeset/tasty-rats-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@galacticcouncil/apps': minor
'@galacticcouncil/ui': minor
---

Added range slider component for DCA
52 changes: 34 additions & 18 deletions packages/apps/src/app/dca/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,25 +327,41 @@ export class DcaForm extends BaseElement {
}

formFrequencyTemplate() {
const error = this.error['frequencyOutOfRange'];
const isDisabled =
this.error['balanceTooLow'] || this.error['minBudgetTooLow'];
const min = this.order
? Math.min(this.order.frequencyMin, this.order.frequencyOpt)
: 0;

const max = Number.isFinite(this.order?.frequencyOpt)
? Math.max(min, this.order.frequencyOpt)
: 0;

const value = this.frequency ?? max;

const valueMsec = value * 60 * 1000;
const blockTime = 12_000;
const blockCount = Math.floor(valueMsec / blockTime);
const blockHint =
blockCount > 0
? i18n.t('form.advanced.intervalBlocks', {
minutes: value,
blocks: blockCount,
})
: undefined;

return html`
<uigc-textfield
field
number
?disabled=${!!isDisabled}
.disabled=${!!isDisabled}
?error=${error}
.error=${error}
.min=${1}
.placeholder=${this.order?.frequency}
.value=${this.frequency}
@input-change=${(e: CustomEvent) => this.onFrequencyChange(e)}>
<span class="adornment" slot="inputAdornment">
${i18n.t('form.advanced.interval')}
</span>
</uigc-textfield>
<div>
<uigc-slider
label=${i18n.t('form.advanced.interval')}
unit="min"
hint=${blockHint}
.min=${min}
.max=${max}
.value=${value}
.disabled=${!this.order}
@input-change=${(e: CustomEvent) => this.onFrequencyChange(e)}>
>
</uigc-slider>
</div>
`;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/apps/src/app/dca/translation.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

"form.advanced": "Advanced settings",
"form.advanced.desc": "Customize your trades to an even greater extent.",
"form.advanced.interval": "Interval (minutes)",
"form.advanced.interval": "Custom interval",
"form.advanced.intervalBlocks": "{{minutes}} minutes = {{blocks}} blocks",

"form.summary": "Summary",
"form.summary.message": "Swap <1>{{amountIn}} {{assetIn}}</1> for <1>{{assetOut}}</1> every <1>~{{frequency}}</1> with a total budget of <1>{{amountInBudget}} {{assetIn}}</1> over the period of <1>~{{time}}</1>",
Expand Down
126 changes: 126 additions & 0 deletions packages/ui/src/component/Slider.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
.slider-root {
position: relative;
width: 100%;
}

.slider-root:has(input:disabled) .progress,
.slider-root:has(input:disabled) .thumb::before {
background: var(--hex-basic-600);
}

.slider-root .slider {
position: relative;
display: flex;
align-items: center;

width: 100%;
height: var(--thumb-size);
}

.slider-root .slider > input {
appearance: none;
width: 100%;
height: var(--track-size);
margin: 0;

border-radius: var(--track-size);
background: rgba(84, 99, 128, 0.35);

outline: none;
padding: 0;
}

.slider-root input:disabled::-webkit-slider-thumb {
cursor: not-allowed;
}

.slider-root input::-webkit-slider-thumb {
touch-action: auto;
appearance: none;
opacity: 0;

width: var(--thumb-size);
height: var(--thumb-size);

cursor: grab;
}

.slider-root input::-moz-range-thumb {
touch-action: auto;
opacity: 0;
cursor: grab;
}

.slider-root .progress {
position: absolute;
top: 50%;
margin-top: calc(var(--track-size) / 2 * -1);
width: calc(
var(--percentage) + var(--thumb-offset) - (var(--thumb-size) / 2)
);
height: var(--track-size);
border-radius: var(--track-size);
background: var(--hex-bright-blue-300);
pointer-events: none;
}

.slider-root .thumb {
position: absolute;
top: 50%;
left: calc(var(--percentage) + var(--thumb-offset));

width: var(--thumb-size);
height: var(--thumb-size);
border-radius: 50%;

background: rgba(146, 209, 247, 0.2);
backdrop-filter: blur(3px);

transform: translate(-50%, -50%);

pointer-events: none;
}

.slider-root .thumb::before {
content: '';
position: absolute;
width: calc(var(--thumb-size) / 2);
height: calc(var(--thumb-size) / 2);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);

border-radius: 50%;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.5);
background: var(--hex-bright-blue-300);
}

.slider-root .top,
.slider-root .bottom {
display: flex;
align-items: center;
justify-content: space-between;

font-size: 14px;
font-weight: 500;
color: #fff;
}

.slider-root .value {
display: flex;
align-items: center;
gap: 4px;
}

.slider-root .bottom {
font-size: 11px;
color: var(--hex-basic-600);
}

.slider-root .dash {
position: absolute;
height: 4px;
width: 1px;

background-color: rgba(150, 138, 158, 0.24);
}
142 changes: 142 additions & 0 deletions packages/ui/src/component/Slider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { debounce } from 'ts-debounce';

import { UIGCElement } from './base/UIGCElement';
import styles from './Slider.css';

import './Popper';
import './icons/Info';

@customElement('uigc-slider')
export class Slider extends UIGCElement {
_inputHandler = null;

@property({ type: Number }) min: number = 0;
@property({ type: Number }) max: number = 100;
@property({ type: Number }) step: number = 1;
@property({ type: Number }) value: number;
@property({ type: Number }) thumbSize: number = 26;
@property({ type: Number }) trackSize: number = 5;
@property({ type: Number }) dashCount: number = 20;
@property({ type: String }) label: string = '';
@property({ type: String }) unit: string = '';
@property({ type: String }) hint: string = '';
@property({ type: Boolean }) disabled: boolean;

constructor() {
super();
this._inputHandler = debounce(this.onInputChange, 300);
}

static get styles() {
return [UIGCElement.styles, styles];
}

connectedCallback(): void {
super.connectedCallback();
this.value = Math.floor((this.min + this.max) / 2);
this.style.setProperty('--thumb-size', this.thumbSize + 'px');
this.style.setProperty('--track-size', this.trackSize + 'px');

this.updateValue();
}

updateValue() {
const min = Number(this.min);
const max = Number(this.max);
const value = Number(this.value);
const percent = max > min ? (100 * (value - min)) / (max - min) : 0;
const thumbOffset = calculateThumbOffset(percent, this.thumbSize);

this.style.setProperty('--thumb-offset', thumbOffset + 'px');
this.style.setProperty('--percentage', percent + '%');
}

onInputChange() {
const options = {
bubbles: true,
composed: true,
detail: { value: this.value },
};
this.dispatchEvent(new CustomEvent('input-change', options));
}

handleInput(event: Event) {
const input = event.target as HTMLInputElement;
this.value = Number(input.value);
this.updateValue();
this._inputHandler();
}

updated(changedProperties: Map<string, unknown>) {
if (
changedProperties.has('min') ||
changedProperties.has('max') ||
changedProperties.has('step')
) {
this.updateValue();
}
}

dashTemplate() {
return Array.from({ length: this.dashCount + 1 }, (_, index) => {
const position = index * 5;
return html`
<div class="dash" style="top:1px;left:${position}%"></div>
<div class="dash" style="bottom:1px;left:${position}%"></div>
`;
});
}

hintTemplate() {
if (!this.hint) return '';
return html`
<uigc-popper text=${this.hint}>
<uigc-icon-info fit></uigc-icon-info>
</uigc-popper>
`;
}

render() {
return html`
<div class="slider-root">
<div class="top">
<p class="label">${this.label}</p>
<p class="value">${this.value} ${this.unit} ${this.hintTemplate()}</p>
</div>
<div class="slider">
<input
type="range"
min=${this.min}
max=${this.max}
step=${this.step}
value=${this.value}
.disabled=${this.disabled}
@input=${this.handleInput} />
<div class="progress"></div>
<div class="thumb"></div>
${this.dashTemplate()}
</div>
<div class="bottom">
<p>${this.min} ${this.unit}</p>
<p>${this.max} ${this.unit}</p>
</div>
</div>
`;
}
}

function linearScale(input: [number, number], output: [number, number]) {
return (value: number) => {
if (input[0] === input[1] || output[0] === output[1]) return output[0];
const ratio = (output[1] - output[0]) / (input[1] - input[0]);
return output[0] + ratio * (value - input[0]);
};
}

function calculateThumbOffset(percent: number, thumbSize: number) {
const halfWidth = thumbSize / 2;
const offset = linearScale([0, 50], [0, halfWidth])(percent);
return halfWidth - offset;
}
1 change: 1 addition & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export { Progress } from './component/Progress';
export { SearchBar } from './component/SearchBar';
export { Selector } from './component/Selector';
export { Skeleton } from './component/Skeleton';
export { Slider } from './component/Slider';
export { Switch } from './component/Switch';
export { Textfield } from './component/Textfield';
export { Toast } from './component/Toast';
Expand Down

0 comments on commit c21a297

Please sign in to comment.