(Almost the) rest of the redesign (#880)

* Load demo img

* two-up styles

* Back button

* Button size tweak

* Move back btn

* Move options and back button into a single grid

* Simpler max height

* Responsive grid

* Feed index into options

* Option heading themes

* More option styles

* Changing checkbox position

* Theme range input & use transforms

* Range input underline theme

* Checkbox color

* Add toggle

* Reorder

* Arrow revealer

* Round two-up thumb

* Don't bundle CSS urls starting #

* Results in progress

* Fix Safari bugs

* Download blobs

* Loading spinner

* Hook up download button

* Different style for original image

* Mobile design for results

* Remove demo auto-loader

* Remove redundant colors

* Sticky headings
This commit is contained in:
Jake Archibald
2020-12-09 11:47:23 +00:00
committed by GitHub
parent 12889d9d50
commit fec826b106
36 changed files with 903 additions and 497 deletions

View File

@@ -18,5 +18,5 @@
}
.checked {
fill: #34b9eb;
fill: var(--main-theme-color);
}

View File

@@ -1,4 +1,3 @@
import PointerTracker from 'pointer-tracker';
import * as style from './style.css';
import 'add-css:./style.css';
@@ -28,7 +27,7 @@ function getPrescision(value: string): number {
class RangeInputElement extends HTMLElement {
private _input: HTMLInputElement;
private _valueDisplay?: HTMLDivElement;
private _valueDisplay?: HTMLSpanElement;
private _ignoreChange = false;
static get observedAttributes() {
@@ -41,15 +40,22 @@ class RangeInputElement extends HTMLElement {
this._input.type = 'range';
this._input.className = style.input;
const tracker = new PointerTracker(this._input, {
start: (): boolean => {
if (tracker.currentPointers.length !== 0) return false;
this._input.classList.add(style.touchActive);
return true;
},
end: () => {
let activePointer: number | undefined;
// Not using pointer-tracker here due to https://bugs.webkit.org/show_bug.cgi?id=219636.
this.addEventListener('pointerdown', (event) => {
if (activePointer) return;
activePointer = event.pointerId;
this._input.classList.add(style.touchActive);
const pointerUp = (event: PointerEvent) => {
if (event.pointerId !== activePointer) return;
activePointer = undefined;
this._input.classList.remove(style.touchActive);
},
window.removeEventListener('pointerup', pointerUp);
window.removeEventListener('pointercancel', pointerUp);
};
window.addEventListener('pointerup', pointerUp);
window.addEventListener('pointercancel', pointerUp);
});
for (const event of RETARGETED_EVENTS) {
@@ -66,13 +72,13 @@ class RangeInputElement extends HTMLElement {
this.innerHTML =
`<div class="${style.thumbWrapper}">` +
`<div class="${style.thumb}"></div>` +
`<div class="${style.valueDisplay}"></div>` +
`<div class="${style.valueDisplay}"><svg width="32" height="62"><path d="M27.3 27.3C25 29.6 17 35.8 17 43v3c0 3 2.5 5 3.2 5.8a6 6 0 1 1-8.5 0C12.6 51 15 49 15 46v-3c0-7.2-8-13.4-10.3-15.7A16 16 0 0 1 16 0a16 16 0 0 1 11.3 27.3z"/></svg><span></span></div>` +
'</div>';
this.insertBefore(this._input, this.firstChild);
this._valueDisplay = this.querySelector(
'.' + style.valueDisplay,
) as HTMLDivElement;
'.' + style.valueDisplay + ' > span',
) as HTMLSpanElement;
// Set inline styles (this is useful when used with frameworks which might clear inline styles)
this._update();
}

View File

@@ -23,10 +23,8 @@ range-input::before {
width: 100%;
height: 2px;
border-radius: 1px;
box-shadow: 0 -0.5px 0 rgba(0, 0, 0, 0.3),
inset 0 0.5px 0 rgba(255, 255, 255, 0.2), 0 0.5px 0 rgba(255, 255, 255, 0.3);
background: linear-gradient(#34b9eb, #218ab1) 0 / var(--value-percent, 0%)
100% no-repeat #eee;
background: linear-gradient(var(--main-theme-color), var(--main-theme-color))
0 / var(--value-percent, 0%) 100% no-repeat var(--medium-light-gray);
}
.input {
@@ -41,14 +39,12 @@ range-input::before {
pointer-events: none;
position: absolute;
bottom: 3px;
left: var(--value-percent, 0%);
left: 0;
margin-left: -6px;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><circle cx="5" cy="5" r="1" fill="%235D509E" /></svg>')
center no-repeat #34b9eb;
background: var(--main-theme-color);
border-radius: 50%;
width: 12px;
height: 12px;
box-shadow: 0 0.5px 2px rgba(0, 0, 0, 0.3);
}
.thumb-wrapper {
@@ -58,21 +54,19 @@ range-input::before {
bottom: 0;
height: 0;
overflow: visible;
transform: translate(var(--value-percent, 0%), 0);
}
.value-display {
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="62" fill="none"><path fill="%2334B9EB" d="M27.3 27.3C25 29.6 17 35.8 17 43v3c0 3 2.5 5 3.2 5.8a6 6 0 1 1-8.5 0C12.6 51 15 49 15 46v-3c0-7.2-8-13.4-10.3-15.7A16 16 0 0 1 16 0a16 16 0 0 1 11.3 27.3z"/><circle cx="16" cy="56" r="1" fill="%235D509E"/></svg>')
top center no-repeat;
position: absolute;
box-sizing: border-box;
left: var(--value-percent, 0%);
left: 0;
bottom: 3px;
width: 32px;
height: 62px;
text-align: center;
padding: 8px 3px 0;
margin: 0 0 0 -16px;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3));
transform-origin: 50% 90%;
opacity: 0.0001;
transform: scale(0.2);
@@ -86,6 +80,19 @@ range-input::before {
will-change: transform;
pointer-events: none;
overflow: hidden;
> svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
fill: var(--main-theme-color);
}
> span {
position: relative;
}
}
.touch-active + .thumb-wrapper .value-display {

View File

@@ -33,6 +33,7 @@
box-sizing: border-box;
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-color: var(--main-theme-color);
text-underline-position: under;
width: 48px;
position: relative;

View File

@@ -0,0 +1,21 @@
import { h, Component } from 'preact';
import * as style from './style.css';
import 'add-css:./style.css';
import { Arrow } from '../../../icons';
interface Props extends preact.JSX.HTMLAttributes {}
interface State {}
export default class Revealer extends Component<Props, State> {
render(props: Props) {
return (
<div class={style.checkbox}>
{/* @ts-ignore - TS bug https://github.com/microsoft/TypeScript/issues/16019 */}
<input class={style.realCheckbox} type="checkbox" {...props} />
<div class={style.arrow}>
<Arrow />
</div>
</div>
);
}
}

View File

@@ -0,0 +1,29 @@
.checkbox {
display: inline-block;
position: relative;
}
.arrow {
width: 10px;
height: 10px;
fill: var(--white);
transition: transform 200ms ease;
transform: rotate(-90deg);
svg {
width: 100%;
height: 100%;
display: block;
}
}
.real-checkbox {
top: 0;
position: absolute;
opacity: 0;
pointer-events: none;
&:checked + .arrow {
transform: none;
}
}

View File

@@ -1,6 +1,7 @@
import { h, Component } from 'preact';
import * as style from './style.css';
import 'add-css:./style.css';
import { Arrow } from 'client/lazy-app/icons';
interface Props extends preact.JSX.HTMLAttributes {
large?: boolean;
@@ -18,9 +19,9 @@ export default class Select extends Component<Props, State> {
class={`${style.builtinSelect} ${large ? style.large : ''}`}
{...otherProps}
/>
<svg class={style.arrow} viewBox="0 0 10 5">
<path d="M0 0l5 5 5-5z" />
</svg>
<div class={style.arrow}>
<Arrow />
</div>
</div>
);
}

View File

@@ -3,10 +3,12 @@
}
.builtin-select {
background: #2f2f2f;
background: var(--black);
border-radius: 4px;
font: inherit;
padding: 4px 25px 4px 10px;
padding: 7px 0;
padding-right: 25px;
padding-left: 10px;
-webkit-appearance: none;
-moz-appearance: none;
border: none;
@@ -21,11 +23,12 @@
transform: translateY(-50%);
fill: #fff;
width: 10px;
pointer-events: none;
}
.large {
padding: 10px 35px 10px 10px;
background: #151515;
background: var(--dark-gray);
& .arrow {
right: 13px;

View File

@@ -0,0 +1,22 @@
import { h, Component } from 'preact';
import * as style from './style.css';
import 'add-css:./style.css';
interface Props extends preact.JSX.HTMLAttributes {}
interface State {}
export default class Toggle extends Component<Props, State> {
render(props: Props) {
return (
<div class={style.checkbox}>
{/* @ts-ignore - TS bug https://github.com/microsoft/TypeScript/issues/16019 */}
<input class={style.realCheckbox} type="checkbox" {...props} />
<div class={style.track}>
<div class={style.thumbTrack}>
<div class={style.thumb}></div>
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,55 @@
.checkbox {
display: inline-block;
position: relative;
}
.track {
--thumb-size: 14px;
background: var(--black);
border-radius: 1000px;
width: 24px;
padding: 3px calc(var(--thumb-size) / 2 + 3px);
}
.thumb {
position: relative;
width: var(--thumb-size);
height: var(--thumb-size);
background: var(--less-light-gray);
border-radius: 100%;
transform: translateX(calc(var(--thumb-size) / -2));
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--main-theme-color);
opacity: 0;
transition: opacity 200ms ease;
}
}
.thumb-track {
transition: transform 200ms ease;
}
.real-checkbox {
top: 0;
position: absolute;
opacity: 0;
pointer-events: none;
&:checked + .track {
.thumb-track {
transform: translateX(100%);
}
.thumb::before {
opacity: 1;
}
}
}

View File

@@ -14,18 +14,20 @@ import {
} from '../../feature-meta';
import Expander from './Expander';
import Checkbox from './Checkbox';
import Toggle from './Toggle';
import Select from './Select';
import { Options as QuantOptionsComponent } from 'features/processors/quantize/client';
import { Options as ResizeOptionsComponent } from 'features/processors/resize/client';
interface Props {
index: 0 | 1;
mobileView: boolean;
source?: SourceImage;
encoderState?: EncoderState;
processorState: ProcessorState;
onEncoderTypeChange(newType: OutputType): void;
onEncoderOptionsChange(newOptions: EncoderOptions): void;
onProcessorOptionsChange(newOptions: ProcessorState): void;
onEncoderTypeChange(index: 0 | 1, newType: OutputType): void;
onEncoderOptionsChange(index: 0 | 1, newOptions: EncoderOptions): void;
onProcessorOptionsChange(index: 0 | 1, newOptions: ProcessorState): void;
}
interface State {
@@ -73,7 +75,7 @@ export default class Options extends Component<Props, State> {
// The select element only has values matching encoder types,
// so 'as' is safe here.
const type = el.value as OutputType;
this.props.onEncoderTypeChange(type);
this.props.onEncoderTypeChange(this.props.index, type);
};
private onProcessorEnabledChange = (event: Event) => {
@@ -81,24 +83,31 @@ export default class Options extends Component<Props, State> {
const processor = el.name.split('.')[0] as keyof ProcessorState;
this.props.onProcessorOptionsChange(
this.props.index,
cleanSet(this.props.processorState, `${processor}.enabled`, el.checked),
);
};
private onQuantizerOptionsChange = (opts: ProcessorOptions['quantize']) => {
this.props.onProcessorOptionsChange(
this.props.index,
cleanMerge(this.props.processorState, 'quantize', opts),
);
};
private onResizeOptionsChange = (opts: ProcessorOptions['resize']) => {
this.props.onProcessorOptionsChange(
this.props.index,
cleanMerge(this.props.processorState, 'resize', opts),
);
};
private onEncoderOptionsChange = (newOptions: EncoderOptions) => {
this.props.onEncoderOptionsChange(this.props.index, newOptions);
};
render(
{ source, encoderState, processorState, onEncoderOptionsChange }: Props,
{ source, encoderState, processorState }: Props,
{ supportedEncoderMap }: State,
) {
const encoder = encoderState && encoderMap[encoderState.type];
@@ -106,18 +115,24 @@ export default class Options extends Component<Props, State> {
encoder && 'Options' in encoder ? encoder.Options : undefined;
return (
<div class={style.optionsScroller}>
<div
class={
style.optionsScroller +
' ' +
(encoderState ? '' : style.originalImage)
}
>
<Expander>
{!encoderState ? null : (
<div>
<h3 class={style.optionsTitle}>Edit</h3>
<label class={style.sectionEnabler}>
<Checkbox
Resize
<Toggle
name="resize.enable"
checked={!!processorState.resize.enabled}
onChange={this.onProcessorEnabledChange}
/>
Resize
</label>
<Expander>
{processorState.resize.enabled ? (
@@ -132,12 +147,12 @@ export default class Options extends Component<Props, State> {
</Expander>
<label class={style.sectionEnabler}>
<Checkbox
Reduce palette
<Toggle
name="quantize.enable"
checked={!!processorState.quantize.enabled}
onChange={this.onProcessorEnabledChange}
/>
Reduce palette
</label>
<Expander>
{processorState.quantize.enabled ? (
@@ -180,7 +195,7 @@ export default class Options extends Component<Props, State> {
// the correct type, but typescript isn't smart enough.
encoderState!.options as any
}
onChange={onEncoderOptionsChange}
onChange={this.onEncoderOptionsChange}
/>
)}
</Expander>

View File

@@ -1,58 +1,83 @@
.options-scroller {
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
--horizontal-padding: 15px;
border-radius: var(--scroller-radius);
/* At smaller widths, the multi-panel handles the scrolling */
@media (min-width: 600px) {
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
}
.options-title {
background: rgba(0, 0, 0, 0.9);
background-color: var(--main-theme-color);
color: var(--header-text-color);
margin: 0;
padding: 10px var(--horizontal-padding);
font-weight: normal;
font-weight: bold;
font-size: 1.4rem;
border-bottom: 1px solid #000;
border-bottom: 1px solid var(--off-black);
transition: all 300ms ease-in-out;
transition-property: background-color, color;
position: sticky;
top: 0;
z-index: 1;
}
.original-image .options-title {
background-color: var(--black);
color: var(--white);
}
.option-text-first {
display: grid;
align-items: center;
grid-template-columns: 87px 1fr;
grid-gap: 0.7em;
gap: 0.7em;
padding: 10px var(--horizontal-padding);
}
.option-toggle {
cursor: pointer;
display: grid;
align-items: center;
grid-template-columns: 1fr auto;
gap: 0.7em;
padding: 10px var(--horizontal-padding);
}
.option-reveal {
composes: option-toggle;
grid-template-columns: auto 1fr;
gap: 1em;
}
.option-one-cell {
display: grid;
grid-template-columns: 1fr;
padding: 10px var(--horizontal-padding);
}
.option-input-first,
.section-enabler {
cursor: pointer;
display: grid;
align-items: center;
grid-template-columns: auto 1fr;
grid-gap: 0.7em;
padding: 10px var(--horizontal-padding);
}
.section-enabler {
background: rgba(0, 0, 0, 0.8);
composes: option-toggle;
background: var(--dark-gray);
padding: 15px var(--horizontal-padding);
border-bottom: 1px solid var(--off-black);
}
.options-section {
background: rgba(0, 0, 0, 0.7);
background: var(--off-black);
}
.text-field {
background: #fff;
color: #000;
background: var(--white);
color: var(--black);
font: inherit;
border: none;
padding: 2px 0 2px 10px;
padding: 6px 0 6px 10px;
width: 100%;
box-sizing: border-box;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
border-radius: 4px;
}