forked from external-repos/squoosh
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3aaf4a279c | ||
|
|
f42427cf13 | ||
|
|
a27f76397a | ||
|
|
3ba25554d7 | ||
|
|
d216fd4b0b | ||
|
|
51ee851811 | ||
|
|
3cf5221114 | ||
|
|
64da19e2fc |
Binary file not shown.
|
Before Width: | Height: | Size: 20 MiB |
@@ -14,12 +14,9 @@ echo "============================================="
|
|||||||
emcc \
|
emcc \
|
||||||
${OPTIMIZE} \
|
${OPTIMIZE} \
|
||||||
--bind \
|
--bind \
|
||||||
-D WEBP_USE_THREAD=1 \
|
-s ALLOW_MEMORY_GROWTH=1 \
|
||||||
-s USE_PTHREADS=1 \
|
-s MODULARIZE=1 \
|
||||||
-s ASSERTIONS=1 \
|
-s 'EXPORT_NAME="webp_enc"' \
|
||||||
-s PTHREAD_POOL_SIZE=4 \
|
|
||||||
-s TOTAL_MEMORY=268435456 \
|
|
||||||
-s WASM_MEM_MAX=268435456 \
|
|
||||||
--std=c++11 \
|
--std=c++11 \
|
||||||
-I node_modules/libwebp \
|
-I node_modules/libwebp \
|
||||||
-o ./webp_enc.js \
|
-o ./webp_enc.js \
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<script src='webp_enc.js'></script>
|
<script src='webp_enc.js'></script>
|
||||||
<script>
|
<script>
|
||||||
// const Module = webp_enc();
|
const module = webp_enc();
|
||||||
|
|
||||||
async function loadImage(src) {
|
async function loadImage(src) {
|
||||||
// Load image
|
// Load image
|
||||||
@@ -17,11 +17,10 @@
|
|||||||
return ctx.getImageData(0, 0, img.width, img.height);
|
return ctx.getImageData(0, 0, img.width, img.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
Module.onRuntimeInitialized = async _ => {
|
module.onRuntimeInitialized = async _ => {
|
||||||
console.log('Version:', Module.version().toString(16));
|
console.log('Version:', module.version().toString(16));
|
||||||
const image = await loadImage('../really_big.jpg');
|
const image = await loadImage('../example.png');
|
||||||
let start = performance.now();
|
const result = module.encode(image.data, image.width, image.height, {
|
||||||
const result = Module.encode(image.data, image.width, image.height, {
|
|
||||||
quality: 75,
|
quality: 75,
|
||||||
target_size: 0,
|
target_size: 0,
|
||||||
target_PSNR: 0,
|
target_PSNR: 0,
|
||||||
@@ -44,18 +43,16 @@
|
|||||||
exact: 0,
|
exact: 0,
|
||||||
image_hint: 0,
|
image_hint: 0,
|
||||||
emulate_jpeg_size: 0,
|
emulate_jpeg_size: 0,
|
||||||
thread_level: 1,
|
thread_level: 0,
|
||||||
low_memory: 0,
|
low_memory: 0,
|
||||||
near_lossless: 100,
|
near_lossless: 100,
|
||||||
use_delta_palette: 0,
|
use_delta_palette: 0,
|
||||||
use_sharp_yuv: 0,
|
use_sharp_yuv: 0,
|
||||||
});
|
});
|
||||||
let stop = performance.now();
|
|
||||||
console.log('size', result.length);
|
console.log('size', result.length);
|
||||||
console.log('time', stop - start);
|
const blob = new Blob([result], {type: 'image/webp'});
|
||||||
const blob = new Blob([new Uint8Array(result)], {type: 'image/webp'});
|
|
||||||
|
|
||||||
Module.free_result();
|
module.free_result();
|
||||||
|
|
||||||
const blobURL = URL.createObjectURL(blob);
|
const blobURL = URL.createObjectURL(blob);
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
// Copyright 2015 The Emscripten Authors. All rights reserved.
|
|
||||||
// Emscripten is available under two separate licenses, the MIT license and the
|
|
||||||
// University of Illinois/NCSA Open Source License. Both these licenses can be
|
|
||||||
// found in the LICENSE file.
|
|
||||||
|
|
||||||
// Pthread Web Worker startup routine:
|
|
||||||
// This is the entry point file that is loaded first by each Web Worker
|
|
||||||
// that executes pthreads on the Emscripten application.
|
|
||||||
|
|
||||||
// Thread-local:
|
|
||||||
var threadInfoStruct = 0; // Info area for this thread in Emscripten HEAP (shared). If zero, this worker is not currently hosting an executing pthread.
|
|
||||||
var selfThreadId = 0; // The ID of this thread. 0 if not hosting a pthread.
|
|
||||||
var parentThreadId = 0; // The ID of the parent pthread that launched this thread.
|
|
||||||
var tempDoublePtr = 0; // A temporary memory area for global float and double marshalling operations.
|
|
||||||
|
|
||||||
// Thread-local: Each thread has its own allocated stack space.
|
|
||||||
var STACK_BASE = 0;
|
|
||||||
var STACKTOP = 0;
|
|
||||||
var STACK_MAX = 0;
|
|
||||||
|
|
||||||
// These are system-wide memory area parameters that are set at main runtime startup in main thread, and stay constant throughout the application.
|
|
||||||
var buffer; // All pthreads share the same Emscripten HEAP as SharedArrayBuffer with the main execution thread.
|
|
||||||
var DYNAMICTOP_PTR = 0;
|
|
||||||
var TOTAL_MEMORY = 0;
|
|
||||||
var STATICTOP = 0;
|
|
||||||
var staticSealed = true; // When threads are being initialized, the static memory area has been already sealed a long time ago.
|
|
||||||
var DYNAMIC_BASE = 0;
|
|
||||||
|
|
||||||
var ENVIRONMENT_IS_PTHREAD = true;
|
|
||||||
|
|
||||||
// performance.now() is specced to return a wallclock time in msecs since that Web Worker/main thread launched. However for pthreads this can cause
|
|
||||||
// subtle problems in emscripten_get_now() as this essentially would measure time from pthread_create(), meaning that the clocks between each threads
|
|
||||||
// would be wildly out of sync. Therefore sync all pthreads to the clock on the main browser thread, so that different threads see a somewhat
|
|
||||||
// coherent clock across each of them (+/- 0.1msecs in testing)
|
|
||||||
var __performance_now_clock_drift = 0;
|
|
||||||
|
|
||||||
// Cannot use console.log or console.error in a web worker, since that would risk a browser deadlock! https://bugzilla.mozilla.org/show_bug.cgi?id=1049091
|
|
||||||
// Therefore implement custom logging facility for threads running in a worker, which queue the messages to main thread to print.
|
|
||||||
var Module = {};
|
|
||||||
|
|
||||||
// When error objects propagate from Web Worker to main thread, they lose helpful call stack and thread ID information, so print out errors early here,
|
|
||||||
// before that happens.
|
|
||||||
this.addEventListener('error', function(e) {
|
|
||||||
if (e.message.indexOf('SimulateInfiniteLoop') != -1) return e.preventDefault();
|
|
||||||
|
|
||||||
var errorSource = ' in ' + e.filename + ':' + e.lineno + ':' + e.colno;
|
|
||||||
console.error('Pthread ' + selfThreadId + ' uncaught exception' + (e.filename || e.lineno || e.colno ? errorSource : '') + ': ' + e.message + '. Error object:');
|
|
||||||
console.error(e.error);
|
|
||||||
});
|
|
||||||
|
|
||||||
function threadPrint() {
|
|
||||||
var text = Array.prototype.slice.call(arguments).join(' ');
|
|
||||||
console.log(text);
|
|
||||||
}
|
|
||||||
function threadPrintErr() {
|
|
||||||
var text = Array.prototype.slice.call(arguments).join(' ');
|
|
||||||
console.error(text);
|
|
||||||
console.error(new Error().stack);
|
|
||||||
}
|
|
||||||
function threadAlert() {
|
|
||||||
var text = Array.prototype.slice.call(arguments).join(' ');
|
|
||||||
postMessage({cmd: 'alert', text: text, threadId: selfThreadId});
|
|
||||||
}
|
|
||||||
out = threadPrint;
|
|
||||||
err = threadPrintErr;
|
|
||||||
this.alert = threadAlert;
|
|
||||||
|
|
||||||
// #if WASM
|
|
||||||
Module['instantiateWasm'] = function(info, receiveInstance) {
|
|
||||||
// Instantiate from the module posted from the main thread.
|
|
||||||
// We can just use sync instantiation in the worker.
|
|
||||||
instance = new WebAssembly.Instance(Module['wasmModule'], info);
|
|
||||||
// We don't need the module anymore; new threads will be spawned from the main thread.
|
|
||||||
delete Module['wasmModule'];
|
|
||||||
receiveInstance(instance);
|
|
||||||
return instance.exports;
|
|
||||||
}
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
this.onmessage = function(e) {
|
|
||||||
try {
|
|
||||||
if (e.data.cmd === 'load') { // Preload command that is called once per worker to parse and load the Emscripten code.
|
|
||||||
// Initialize the thread-local field(s):
|
|
||||||
tempDoublePtr = e.data.tempDoublePtr;
|
|
||||||
|
|
||||||
// Initialize the global "process"-wide fields:
|
|
||||||
Module['TOTAL_MEMORY'] = TOTAL_MEMORY = e.data.TOTAL_MEMORY;
|
|
||||||
STATICTOP = e.data.STATICTOP;
|
|
||||||
DYNAMIC_BASE = e.data.DYNAMIC_BASE;
|
|
||||||
DYNAMICTOP_PTR = e.data.DYNAMICTOP_PTR;
|
|
||||||
|
|
||||||
|
|
||||||
//#if WASM
|
|
||||||
if (e.data.wasmModule) {
|
|
||||||
// Module and memory were sent from main thread
|
|
||||||
Module['wasmModule'] = e.data.wasmModule;
|
|
||||||
Module['wasmMemory'] = e.data.wasmMemory;
|
|
||||||
buffer = Module['wasmMemory'].buffer;
|
|
||||||
} else {
|
|
||||||
//#else
|
|
||||||
buffer = e.data.buffer;
|
|
||||||
}
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
PthreadWorkerInit = e.data.PthreadWorkerInit;
|
|
||||||
if (typeof e.data.urlOrBlob === 'string') {
|
|
||||||
importScripts(e.data.urlOrBlob);
|
|
||||||
} else {
|
|
||||||
var objectUrl = URL.createObjectURL(e.data.urlOrBlob);
|
|
||||||
importScripts(objectUrl);
|
|
||||||
URL.revokeObjectURL(objectUrl);
|
|
||||||
}
|
|
||||||
//#if !ASMFS
|
|
||||||
if (typeof FS !== 'undefined' && typeof FS.createStandardStreams === 'function') FS.createStandardStreams();
|
|
||||||
//#endif
|
|
||||||
postMessage({ cmd: 'loaded' });
|
|
||||||
} else if (e.data.cmd === 'objectTransfer') {
|
|
||||||
PThread.receiveObjectTransfer(e.data);
|
|
||||||
} else if (e.data.cmd === 'run') { // This worker was idle, and now should start executing its pthread entry point.
|
|
||||||
__performance_now_clock_drift = performance.now() - e.data.time; // Sync up to the clock of the main thread.
|
|
||||||
threadInfoStruct = e.data.threadInfoStruct;
|
|
||||||
__register_pthread_ptr(threadInfoStruct, /*isMainBrowserThread=*/0, /*isMainRuntimeThread=*/0); // Pass the thread address inside the asm.js scope to store it for fast access that avoids the need for a FFI out.
|
|
||||||
assert(threadInfoStruct);
|
|
||||||
selfThreadId = e.data.selfThreadId;
|
|
||||||
parentThreadId = e.data.parentThreadId;
|
|
||||||
assert(selfThreadId);
|
|
||||||
assert(parentThreadId);
|
|
||||||
// TODO: Emscripten runtime has these variables twice(!), once outside the asm.js module, and a second time inside the asm.js module.
|
|
||||||
// Review why that is? Can those get out of sync?
|
|
||||||
STACK_BASE = STACKTOP = e.data.stackBase;
|
|
||||||
STACK_MAX = STACK_BASE + e.data.stackSize;
|
|
||||||
assert(STACK_BASE != 0);
|
|
||||||
assert(STACK_MAX > STACK_BASE);
|
|
||||||
Module['establishStackSpace'](e.data.stackBase, e.data.stackBase + e.data.stackSize);
|
|
||||||
var result = 0;
|
|
||||||
//#if STACK_OVERFLOW_CHECK
|
|
||||||
if (typeof writeStackCookie === 'function') writeStackCookie();
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
PThread.receiveObjectTransfer(e.data);
|
|
||||||
PThread.setThreadStatus(_pthread_self(), 1/*EM_THREAD_STATUS_RUNNING*/);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// pthread entry points are always of signature 'void *ThreadMain(void *arg)'
|
|
||||||
// Native codebases sometimes spawn threads with other thread entry point signatures,
|
|
||||||
// such as void ThreadMain(void *arg), void *ThreadMain(), or void ThreadMain().
|
|
||||||
// That is not acceptable per C/C++ specification, but x86 compiler ABI extensions
|
|
||||||
// enable that to work. If you find the following line to crash, either change the signature
|
|
||||||
// to "proper" void *ThreadMain(void *arg) form, or try linking with the Emscripten linker
|
|
||||||
// flag -s EMULATE_FUNCTION_POINTER_CASTS=1 to add in emulation for this x86 ABI extension.
|
|
||||||
result = Module['dynCall_ii'](e.data.start_routine, e.data.arg);
|
|
||||||
|
|
||||||
//#if STACK_OVERFLOW_CHECK
|
|
||||||
if (typeof checkStackCookie === 'function') checkStackCookie();
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
if (e === 'Canceled!') {
|
|
||||||
PThread.threadCancel();
|
|
||||||
return;
|
|
||||||
} else if (e === 'SimulateInfiniteLoop') {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
Atomics.store(HEAPU32, (threadInfoStruct + 4 /*{{{ C_STRUCTS.pthread.threadExitCode }}}*/ ) >> 2, (e instanceof ExitStatus) ? e.status : -2 /*A custom entry specific to Emscripten denoting that the thread crashed.*/);
|
|
||||||
Atomics.store(HEAPU32, (threadInfoStruct + 0 /*{{{ C_STRUCTS.pthread.threadStatus }}}*/ ) >> 2, 1); // Mark the thread as no longer running.
|
|
||||||
_emscripten_futex_wake(threadInfoStruct + 0 /*{{{ C_STRUCTS.pthread.threadStatus }}}*/, 0x7FFFFFFF/*INT_MAX*/); // Wake all threads waiting on this thread to finish.
|
|
||||||
if (!(e instanceof ExitStatus)) throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The thread might have finished without calling pthread_exit(). If so, then perform the exit operation ourselves.
|
|
||||||
// (This is a no-op if explicit pthread_exit() had been called prior.)
|
|
||||||
PThread.threadExit(result);
|
|
||||||
} else if (e.data.cmd === 'cancel') { // Main thread is asking for a pthread_cancel() on this thread.
|
|
||||||
if (threadInfoStruct && PThread.thisThreadCancelState == 0/*PTHREAD_CANCEL_ENABLE*/) {
|
|
||||||
PThread.threadCancel();
|
|
||||||
}
|
|
||||||
} else if (e.data.target === 'setimmediate') {
|
|
||||||
// no-op
|
|
||||||
} else if (e.data.cmd === 'processThreadQueue') {
|
|
||||||
if (threadInfoStruct) { // If this thread is actually running?
|
|
||||||
_emscripten_current_thread_process_queued_calls();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err('pthread-main.js received unknown command ' + e.data.cmd);
|
|
||||||
console.error(e.data);
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
console.error('pthread-main.js onmessage() captured an uncaught exception: ' + e);
|
|
||||||
console.error(e.stack);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -74,7 +74,7 @@ export default class App extends Component<Props, State> {
|
|||||||
render({}: Props, { file, Compress }: State) {
|
render({}: Props, { file, Compress }: State) {
|
||||||
return (
|
return (
|
||||||
<div id="app" class={style.app}>
|
<div id="app" class={style.app}>
|
||||||
<file-drop accept="image/*" onfiledrop={this.onFileDrop}>
|
<file-drop accept="image/*" onfiledrop={this.onFileDrop} class={style.drop}>
|
||||||
{(!file)
|
{(!file)
|
||||||
? <Intro onFile={this.onIntroPickFile} onError={this.showError} />
|
? <Intro onFile={this.onIntroPickFile} onError={this.showError} />
|
||||||
: (Compress)
|
: (Compress)
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ Note: These styles are temporary. They will be replaced before going live.
|
|||||||
contain: strict;
|
contain: strict;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global {
|
.drop {
|
||||||
file-drop {
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
height:100%;
|
height: 100%;
|
||||||
width:100%;
|
width: 100%;
|
||||||
|
|
||||||
&:after {
|
&:global {
|
||||||
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -28,28 +28,20 @@ Note: These styles are temporary. They will be replaced before going live.
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
border: 2px dashed #fff;
|
border: 2px dashed #fff;
|
||||||
|
background-color:rgba(88, 116, 88, 0.2);
|
||||||
|
border-color: rgba(65, 129, 65, 0.5);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
transition: opacity 300ms ease, transform 300ms cubic-bezier(.6,2,.6,1), background-color 300ms step-end, border-color 300ms step-end;
|
transition: all 200ms ease-in;
|
||||||
|
transition-property: transform, opacity;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.drop-valid:after,
|
&.drop-valid::after {
|
||||||
&.drop-invalid:after {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
transition: opacity 300ms ease, transform 300ms cubic-bezier(.6,2,.6,1);
|
transition-timing-function: ease-out;
|
||||||
}
|
|
||||||
|
|
||||||
&.drop-valid:after {
|
|
||||||
background-color:rgba(88, 116, 88, 0.2);
|
|
||||||
border-color: rgba(65, 129, 65, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.drop-invalid:after {
|
|
||||||
background-color:rgba(119, 85, 85, 0.2);
|
|
||||||
border-color:rgba(129, 63, 63, 0.5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export default class PinchZoom extends HTMLElement {
|
|||||||
const relativeToEl = (relativeTo === 'content' ? this._positioningEl : this);
|
const relativeToEl = (relativeTo === 'content' ? this._positioningEl : this);
|
||||||
|
|
||||||
// No content element? Fall back to just setting scale
|
// No content element? Fall back to just setting scale
|
||||||
if (!relativeToEl) {
|
if (!relativeToEl || !this._positioningEl) {
|
||||||
this.setTransform({ scale, allowChangeEvent });
|
this.setTransform({ scale, allowChangeEvent });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -157,6 +157,10 @@ export default class PinchZoom extends HTMLElement {
|
|||||||
if (relativeTo === 'content') {
|
if (relativeTo === 'content') {
|
||||||
originX += this.x;
|
originX += this.x;
|
||||||
originY += this.y;
|
originY += this.y;
|
||||||
|
} else {
|
||||||
|
const currentRect = this._positioningEl.getBoundingClientRect();
|
||||||
|
originX -= currentRect.left;
|
||||||
|
originY -= currentRect.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._applyChange({
|
this._applyChange({
|
||||||
|
|||||||
@@ -68,12 +68,14 @@ export default class TwoUp extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this._handle.innerHTML = `<div class="${styles.scrubber}">${
|
|
||||||
'<svg viewBox="0 0 20 10" fill="currentColor"><path d="M8 0v10L0 5zM12 0v10l8-5z"/></svg>'
|
|
||||||
}</div>`;
|
|
||||||
|
|
||||||
this._childrenChange();
|
this._childrenChange();
|
||||||
|
|
||||||
if (!this._everConnected) {
|
if (!this._everConnected) {
|
||||||
|
this._handle.innerHTML = `<div class="${styles.scrubber}">${
|
||||||
|
`<svg viewBox="0 0 27 20" fill="currentColor">${
|
||||||
|
'<path d="M17 19.2l9.5-9.6L16.9 0zM9.6 0L0 9.6l9.6 9.6z"/>'
|
||||||
|
}</svg>`
|
||||||
|
}</div>`;
|
||||||
this._resetPosition();
|
this._resetPosition();
|
||||||
this._everConnected = true;
|
this._everConnected = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,29 +6,41 @@ two-up {
|
|||||||
--track-color: var(--accent-color);
|
--track-color: var(--accent-color);
|
||||||
--thumb-background: #fff;
|
--thumb-background: #fff;
|
||||||
--thumb-color: var(--accent-color);
|
--thumb-color: var(--accent-color);
|
||||||
|
--thumb-size: 62px;
|
||||||
|
--bar-size: 6px;
|
||||||
|
--bar-touch-size: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
two-up > * {
|
two-up > * {
|
||||||
/* Overlay all children on top of each other, and let
|
/* Overlay all children on top of each other, and let two-up's layout contain all of them. */
|
||||||
two-up's layout contain all of them. */
|
|
||||||
grid-area: 1/1;
|
grid-area: 1/1;
|
||||||
}
|
}
|
||||||
|
|
||||||
two-up[legacy-clip-compat] > :not(.twoUpHandle) {
|
two-up[legacy-clip-compat] > :not(.two-up-handle) {
|
||||||
|
/* Legacy mode uses clip rather than clip-path (Edge doesn't support clip-path), but clip requires
|
||||||
|
elements to be positioned absolutely */
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.twoUpHandle {
|
.two-up-handle {
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 10px;
|
width: var(--bar-touch-size);
|
||||||
background: var(--track-color);
|
|
||||||
transform: translateX(var(--split-point)) translateX(-50%);
|
transform: translateX(var(--split-point)) translateX(-50%);
|
||||||
box-shadow: inset 4px 0 0 rgba(0,0,0,0.1), 0 1px 4px rgba(0,0,0,0.4);
|
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.two-up-handle::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: var(--bar-size);
|
||||||
|
margin: 0 auto;
|
||||||
|
box-shadow: inset calc(var(--bar-size) / 2) 0 0 rgba(0,0,0,0.1), 0 1px 4px rgba(0,0,0,0.4);
|
||||||
|
background: var(--track-color);
|
||||||
|
}
|
||||||
|
|
||||||
.scrubber {
|
.scrubber {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -36,51 +48,56 @@ two-up[legacy-clip-compat] > :not(.twoUpHandle) {
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform-origin: 50% 50%;
|
transform-origin: 50% 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 62px;
|
width: var(--thumb-size);
|
||||||
height: 56px;
|
height: calc(var(--thumb-size) * 0.9);
|
||||||
background: var(--thumb-background);
|
background: var(--thumb-background);
|
||||||
border: 1px solid rgba(0,0,0,0.2);
|
border: 1px solid rgba(0,0,0,0.2);
|
||||||
border-radius: 5px;
|
border-radius: calc(var(--thumb-size) * 0.08);
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
||||||
color: var(--thumb-color);
|
color: var(--thumb-color);
|
||||||
}
|
box-sizing: border-box;
|
||||||
.scrubber svg {
|
padding: 0 48%;
|
||||||
flex: 1;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
two-up[orientation='vertical'] .twoUpHandle {
|
.scrubber svg {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
two-up[orientation='vertical'] .two-up-handle {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 7px;
|
height: var(--bar-touch-size);
|
||||||
transform: translateY(var(--split-point)) translateY(-50%);
|
transform: translateY(var(--split-point)) translateY(-50%);
|
||||||
box-shadow: inset 0 3px 0 rgba(0,0,0,0.1), 0 1px 4px rgba(0,0,0,0.4);
|
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
two-up[orientation='vertical'] .two-up-handle::before {
|
||||||
|
width: auto;
|
||||||
|
height: var(--bar-size);
|
||||||
|
box-shadow: inset 0 calc(var(--bar-size) / 2) 0 rgba(0,0,0,0.1), 0 1px 4px rgba(0,0,0,0.4);
|
||||||
|
margin: calc((var(--bar-touch-size) - var(--bar-size)) / 2) 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
two-up[orientation='vertical'] .scrubber {
|
two-up[orientation='vertical'] .scrubber {
|
||||||
width: 46px;
|
|
||||||
height: 40px;
|
|
||||||
font-size: 18px;
|
|
||||||
box-shadow: 1px 0 4px rgba(0,0,0,0.1);
|
box-shadow: 1px 0 4px rgba(0,0,0,0.1);
|
||||||
transform: translate(-50%, -50%) rotate(-90deg);
|
transform: translate(-50%, -50%) rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
two-up > :nth-child(1):not(.twoUpHandle) {
|
two-up > :nth-child(1):not(.two-up-handle) {
|
||||||
-webkit-clip-path: inset(0 calc(100% - var(--split-point)) 0 0);
|
-webkit-clip-path: inset(0 calc(100% - var(--split-point)) 0 0);
|
||||||
clip-path: inset(0 calc(100% - var(--split-point)) 0 0);
|
clip-path: inset(0 calc(100% - var(--split-point)) 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
two-up > :nth-child(2):not(.twoUpHandle) {
|
two-up > :nth-child(2):not(.two-up-handle) {
|
||||||
-webkit-clip-path: inset(0 0 0 var(--split-point));
|
-webkit-clip-path: inset(0 0 0 var(--split-point));
|
||||||
clip-path: inset(0 0 0 var(--split-point));
|
clip-path: inset(0 0 0 var(--split-point));
|
||||||
}
|
}
|
||||||
|
|
||||||
two-up[orientation='vertical'] > :nth-child(1):not(.twoUpHandle) {
|
two-up[orientation='vertical'] > :nth-child(1):not(.two-up-handle) {
|
||||||
-webkit-clip-path: inset(0 0 calc(100% - var(--split-point)) 0);
|
-webkit-clip-path: inset(0 0 calc(100% - var(--split-point)) 0);
|
||||||
clip-path: inset(0 0 calc(100% - var(--split-point)) 0);
|
clip-path: inset(0 0 calc(100% - var(--split-point)) 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
two-up[orientation='vertical'] > :nth-child(2):not(.twoUpHandle) {
|
two-up[orientation='vertical'] > :nth-child(2):not(.two-up-handle) {
|
||||||
-webkit-clip-path: inset(var(--split-point) 0 0 0);
|
-webkit-clip-path: inset(var(--split-point) 0 0 0);
|
||||||
clip-path: inset(var(--split-point) 0 0 0);
|
clip-path: inset(var(--split-point) 0 0 0);
|
||||||
}
|
}
|
||||||
@@ -90,19 +107,19 @@ two-up[orientation='vertical'] > :nth-child(2):not(.twoUpHandle) {
|
|||||||
It performs way better in Safari.
|
It performs way better in Safari.
|
||||||
*/
|
*/
|
||||||
@supports not ((clip-path: inset(0 0 0 0)) or (-webkit-clip-path: inset(0 0 0 0))) {
|
@supports not ((clip-path: inset(0 0 0 0)) or (-webkit-clip-path: inset(0 0 0 0))) {
|
||||||
two-up[legacy-clip-compat] > :nth-child(1):not(.twoUpHandle) {
|
two-up[legacy-clip-compat] > :nth-child(1):not(.two-up-handle) {
|
||||||
clip: rect(auto var(--split-point) auto auto);
|
clip: rect(auto var(--split-point) auto auto);
|
||||||
}
|
}
|
||||||
|
|
||||||
two-up[legacy-clip-compat] > :nth-child(2):not(.twoUpHandle) {
|
two-up[legacy-clip-compat] > :nth-child(2):not(.two-up-handle) {
|
||||||
clip: rect(auto auto auto var(--split-point));
|
clip: rect(auto auto auto var(--split-point));
|
||||||
}
|
}
|
||||||
|
|
||||||
two-up[orientation='vertical'][legacy-clip-compat] > :nth-child(1):not(.twoUpHandle) {
|
two-up[orientation='vertical'][legacy-clip-compat] > :nth-child(1):not(.two-up-handle) {
|
||||||
clip: rect(auto auto var(--split-point) auto);
|
clip: rect(auto auto var(--split-point) auto);
|
||||||
}
|
}
|
||||||
|
|
||||||
two-up[orientation='vertical'][legacy-clip-compat] > :nth-child(2):not(.twoUpHandle) {
|
two-up[orientation='vertical'][legacy-clip-compat] > :nth-child(2):not(.two-up-handle) {
|
||||||
clip: rect(var(--split-point) auto auto auto);
|
clip: rect(var(--split-point) auto auto auto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,14 +113,19 @@ export default class Output extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
private editScale() {
|
private onScaleValueFocus() {
|
||||||
this.setState({ editingScale: true }, () => {
|
this.setState({ editingScale: true }, () => {
|
||||||
if (this.scaleInput) this.scaleInput.focus();
|
if (this.scaleInput) {
|
||||||
|
// Firefox unfocuses the input straight away unless I force a style calculation here. I have
|
||||||
|
// no idea why, but it's late and I'm quite tired.
|
||||||
|
getComputedStyle(this.scaleInput).transform;
|
||||||
|
this.scaleInput.focus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
private cancelEditScale() {
|
private onScaleInputBlur() {
|
||||||
this.setState({ editingScale: false });
|
this.setState({ editingScale: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +193,7 @@ export default class Output extends Component<Props, State> {
|
|||||||
<div class={`${style.output} ${altBackground ? style.altBackground : ''}`}>
|
<div class={`${style.output} ${altBackground ? style.altBackground : ''}`}>
|
||||||
<two-up
|
<two-up
|
||||||
legacy-clip-compat
|
legacy-clip-compat
|
||||||
|
class={style.twoUp}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
// Event redirecting. See onRetargetableEvent.
|
// Event redirecting. See onRetargetableEvent.
|
||||||
onTouchStartCapture={this.onRetargetableEvent}
|
onTouchStartCapture={this.onRetargetableEvent}
|
||||||
@@ -198,6 +204,7 @@ export default class Output extends Component<Props, State> {
|
|||||||
onWheelCapture={this.onRetargetableEvent}
|
onWheelCapture={this.onRetargetableEvent}
|
||||||
>
|
>
|
||||||
<pinch-zoom
|
<pinch-zoom
|
||||||
|
class={style.pinchZoom}
|
||||||
onChange={this.onPinchZoomLeftChange}
|
onChange={this.onPinchZoomLeftChange}
|
||||||
ref={linkRef(this, 'pinchZoomLeft')}
|
ref={linkRef(this, 'pinchZoomLeft')}
|
||||||
>
|
>
|
||||||
@@ -213,7 +220,7 @@ export default class Output extends Component<Props, State> {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</pinch-zoom>
|
</pinch-zoom>
|
||||||
<pinch-zoom ref={linkRef(this, 'pinchZoomRight')}>
|
<pinch-zoom class={style.pinchZoom} ref={linkRef(this, 'pinchZoomRight')}>
|
||||||
<canvas
|
<canvas
|
||||||
class={style.outputCanvas}
|
class={style.outputCanvas}
|
||||||
ref={linkRef(this, 'canvasRight')}
|
ref={linkRef(this, 'canvasRight')}
|
||||||
@@ -229,7 +236,7 @@ export default class Output extends Component<Props, State> {
|
|||||||
</two-up>
|
</two-up>
|
||||||
|
|
||||||
<div class={style.controls}>
|
<div class={style.controls}>
|
||||||
<div class={style.group}>
|
<div class={style.zoomControls}>
|
||||||
<button class={style.button} onClick={this.zoomOut}>
|
<button class={style.button} onClick={this.zoomOut}>
|
||||||
<RemoveIcon />
|
<RemoveIcon />
|
||||||
</button>
|
</button>
|
||||||
@@ -243,11 +250,11 @@ export default class Output extends Component<Props, State> {
|
|||||||
class={style.zoom}
|
class={style.zoom}
|
||||||
value={Math.round(scale * 100)}
|
value={Math.round(scale * 100)}
|
||||||
onInput={this.onScaleInputChanged}
|
onInput={this.onScaleInputChanged}
|
||||||
onBlur={this.cancelEditScale}
|
onBlur={this.onScaleInputBlur}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span class={style.zoom} tabIndex={0} onFocus={this.editScale}>
|
<span class={style.zoom} tabIndex={0} onFocus={this.onScaleValueFocus}>
|
||||||
<strong>{Math.round(scale * 100)}</strong>
|
<span class={style.zoomValue}>{Math.round(scale * 100)}</span>
|
||||||
%
|
%
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,76 +2,79 @@
|
|||||||
Note: These styles are temporary. They will be replaced before going live.
|
Note: These styles are temporary. They will be replaced before going live.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
%fill {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
contain: strict;
|
|
||||||
}
|
|
||||||
|
|
||||||
.output {
|
.output {
|
||||||
@extend %fill;
|
composes: abs-fill from '../../lib/util.scss';
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
@extend %fill;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
background: #000;
|
background: #000;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 500ms ease;
|
transition: opacity 500ms ease;
|
||||||
}
|
}
|
||||||
&.altBackground:before {
|
|
||||||
opacity: .6;
|
&.alt-background::before {
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> two-up {
|
.two-up {
|
||||||
@extend %fill;
|
composes: abs-fill from '../../lib/util.scss';
|
||||||
--accent-color: var(--button-fg);
|
--accent-color: var(--button-fg);
|
||||||
|
}
|
||||||
|
|
||||||
> pinch-zoom {
|
.pinch-zoom {
|
||||||
@extend %fill;
|
composes: abs-fill from '../../lib/util.scss';
|
||||||
outline: none;
|
outline: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
left: 220px;
|
top: 0;
|
||||||
right: 220px;
|
left: 0;
|
||||||
bottom: 0;
|
right: 0;
|
||||||
padding: 9px;
|
padding: 9px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
@media (max-width: 680px) {
|
@media (min-width: 680px) {
|
||||||
top: 0;
|
top: auto;
|
||||||
bottom: auto;
|
left: 220px;
|
||||||
left: 0;
|
right: 220px;
|
||||||
right: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> * {
|
.zoom-controls {
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
|
||||||
|
|
||||||
.button,
|
& :not(:first-child) {
|
||||||
.zoom {
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
& :not(:last-child) {
|
||||||
|
margin-right: 0;
|
||||||
|
border-right-width: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
.zoom {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
@@ -80,7 +83,6 @@ Note: These styles are temporary. They will be replaced before going live.
|
|||||||
border: 1px solid rgba(0,0,0,0.2);
|
border: 1px solid rgba(0,0,0,0.2);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: 110%;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@@ -88,53 +90,39 @@ Note: These styles are temporary. They will be replaced before going live.
|
|||||||
outline: none;
|
outline: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--button-fg);
|
color: var(--button-fg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-indent: 6px;
|
text-indent: 6px;
|
||||||
}
|
font-size: 110%;
|
||||||
|
|
||||||
.button:hover {
|
&:hover {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.zoom {
|
.zoom {
|
||||||
flex: 0 0 6em;
|
|
||||||
color: #625E80;
|
color: #625E80;
|
||||||
font: inherit;
|
|
||||||
cursor: text;
|
cursor: text;
|
||||||
width: 6em;
|
width: 6em;
|
||||||
|
font: inherit;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: inset 0 1px 4px rgba(0,0,0,0.2), 0 0 0 2px var(--button-fg);
|
box-shadow: inset 0 1px 4px rgba(0,0,0,0.2), 0 0 0 2px var(--button-fg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
strong {
|
.zoom-value {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
margin: 0 3px 0 0;
|
margin: 0 3px 0 0;
|
||||||
color: #888;
|
color: #888;
|
||||||
font-weight: normal;
|
|
||||||
border-bottom: 1px dashed #999;
|
border-bottom: 1px dashed #999;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.group > :not(:first-child) {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
.group > :not(:last-child) {
|
|
||||||
margin-right: 0;
|
|
||||||
border-right-width: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.output-canvas {
|
.output-canvas {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
.option-pair {
|
.option-pair {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@@ -15,5 +14,6 @@
|
|||||||
|
|
||||||
&.vertical {
|
&.vertical {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ if (process.env.NODE_ENV === 'development') {
|
|||||||
|
|
||||||
// When an update to any module is received, re-import the app and trigger a full re-render:
|
// When an update to any module is received, re-import the app and trigger a full re-render:
|
||||||
module.hot.accept('./components/App', () => {
|
module.hot.accept('./components/App', () => {
|
||||||
|
// The linter doesn't like the capital A in App. It is wrong.
|
||||||
// tslint:disable-next-line variable-name
|
// tslint:disable-next-line variable-name
|
||||||
import('./components/App').then(({ default: App }) => {
|
import('./components/App').then(({ default: App }) => {
|
||||||
root = render(<App />, document.body, root);
|
root = render(<App />, document.body, root);
|
||||||
|
|||||||
@@ -2,31 +2,29 @@ import { h } from 'preact';
|
|||||||
|
|
||||||
// tslint:disable:max-line-length variable-name
|
// tslint:disable:max-line-length variable-name
|
||||||
|
|
||||||
export interface IconProps extends JSX.HTMLAttributes {}
|
const Icon = (props: JSX.HTMLAttributes) => (
|
||||||
|
|
||||||
const Icon = (props: IconProps) => (
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" {...props} />
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export const DownloadIcon = (props: IconProps) => (
|
export const DownloadIcon = (props: JSX.HTMLAttributes) => (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z" />
|
<path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z" />
|
||||||
</Icon>
|
</Icon>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ToggleIcon = (props: IconProps) => (
|
export const ToggleIcon = (props: JSX.HTMLAttributes) => (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3c0 1.1.89 2 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9c-1.11 0-2 .9-2 2v10c0 1.1.89 2 2 2h10c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z" />
|
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3c0 1.1.89 2 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9c-1.11 0-2 .9-2 2v10c0 1.1.89 2 2 2h10c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z" />
|
||||||
</Icon>
|
</Icon>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AddIcon = (props: IconProps) => (
|
export const AddIcon = (props: JSX.HTMLAttributes) => (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||||
</Icon>
|
</Icon>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RemoveIcon = (props: IconProps) => (
|
export const RemoveIcon = (props: JSX.HTMLAttributes) => (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<path d="M19 13H5v-2h14v2z"/>
|
<path d="M19 13H5v-2h14v2z"/>
|
||||||
</Icon>
|
</Icon>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
contain: strict;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unbutton {
|
.unbutton {
|
||||||
|
|||||||
Reference in New Issue
Block a user