mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-15 18:19:47 +00:00
(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:
@@ -18,5 +18,5 @@
|
||||
}
|
||||
|
||||
.checked {
|
||||
fill: #34b9eb;
|
||||
fill: var(--main-theme-color);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
21
src/client/lazy-app/Compress/Options/Revealer/index.tsx
Normal file
21
src/client/lazy-app/Compress/Options/Revealer/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
29
src/client/lazy-app/Compress/Options/Revealer/style.css
Normal file
29
src/client/lazy-app/Compress/Options/Revealer/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
22
src/client/lazy-app/Compress/Options/Toggle/index.tsx
Normal file
22
src/client/lazy-app/Compress/Options/Toggle/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
55
src/client/lazy-app/Compress/Options/Toggle/style.css
Normal file
55
src/client/lazy-app/Compress/Options/Toggle/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user