diff --git a/public/image_process.py b/public/image_process.py index 62b432e..f3f6782 100644 --- a/public/image_process.py +++ b/public/image_process.py @@ -2,7 +2,7 @@ import io import os -from js import Uint8Array, imageUploadList, imageUpload +from js import Uint8Array, imageUploadList from PIL import Image @@ -29,6 +29,11 @@ async def upload_single_image(origin_image, file_name): async def upload_file(): + from js import imageUpload + + # Since we will update `imageUpload` when calling this function, + # need to re-import it to force update to new value + # or we will always generate first uploaded image basename, ext = os.path.splitext(imageUpload.name) if ext.lower() not in [".psd", ".jpg", ".jpeg", ".png"]: return diff --git a/public/images/preview-gray.svg b/public/images/preview-gray.svg new file mode 100644 index 0000000..673dfa4 --- /dev/null +++ b/public/images/preview-gray.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/preview-white.svg b/public/images/preview-white.svg new file mode 100644 index 0000000..b857f64 --- /dev/null +++ b/public/images/preview-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/spinner-blue-2.png b/public/images/spinner-blue-2.png new file mode 100644 index 0000000..0ee2e96 Binary files /dev/null and b/public/images/spinner-blue-2.png differ diff --git a/public/scripts/models/image-upload.js b/public/scripts/models/image-upload.js index 24ea536..a8d85ed 100644 --- a/public/scripts/models/image-upload.js +++ b/public/scripts/models/image-upload.js @@ -2,13 +2,15 @@ Require: mobx, psd.js */ -const ReadState = { +const ImageUploadState = { ReadyForRead: "ReadyForRead", Reading: "Reading", + GeneratingPreview: "GeneratingPreviewImage", ReadSuccess: "ReadSuccess", ErrUnsupportedFileType: "ErrUnsupportedFileType", ErrExceedMaxFileSize: "ErrExceedMaxFileSize", ErrRead: "ErrRead", + ErrPreview: "ErrPreview", }; class ImageUpload { @@ -18,7 +20,7 @@ class ImageUpload { height = null; uuid = null; signedData = null; - readState = ReadState.ReadyForRead; + state = ImageUploadState.ReadyForRead; message = null; ulid = null; previewUrl = null; @@ -32,7 +34,7 @@ class ImageUpload { height: mobx.observable, uuid: mobx.observable, signedData: mobx.observable, - readState: mobx.observable, + state: mobx.observable, message: mobx.observable, isProcessingState: mobx.computed, isSuccessState: mobx.computed, @@ -44,21 +46,29 @@ class ImageUpload { } async read() { - this.readState = ReadState.Reading; + this.state = ImageUploadState.Reading; if (!(await this._verifyFileType())) { - this.readState = ReadState.ErrUnsupportedFileType; + this.state = ImageUploadState.ErrUnsupportedFileType; return; } if (!this._verifyFileSize()) { - this.readState = ReadState.ErrExceedMaxFileSize; + this.state = ImageUploadState.ErrExceedMaxFileSize; return; } const loadDimensionResult = await this._loadDimension(); if (loadDimensionResult.type === "failed") { - this.readState = loadDimensionResult.reason; + this.state = loadDimensionResult.reason; return; } - this.readState = ReadState.ReadSuccess; + this.state = ImageUploadState.ReadSuccess; + } + + updateState(state) { + this.state = state; + } + + updatePreviewUrl(previewUrl) { + this.previewUrl = previewUrl; } // Cache file type from header such that no need to parse again @@ -134,11 +144,11 @@ class ImageUpload { }; fileReader.onabort = () => { console.warn("onabort"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.onerror = () => { console.warn("onerror"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.readAsDataURL(this.file); }); @@ -159,39 +169,45 @@ class ImageUpload { }; img.onerror = () => { console.warn("onerror"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; img.onabort = () => { console.warn("onabort"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; img.src = fileReader.result; }; fileReader.onabort = () => { - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.onerror = () => { console.log("onerror"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.readAsDataURL(this.file); }); } + get isGeneratingPreviewState() { + return this.state === ImageUploadState.GeneratingPreview; + } + get isProcessingState() { return !this.isProcessedState; } get isSuccessState() { - return this.readState === ReadState.ReadSuccess; + return this.state === ImageUploadState.ReadSuccess; } get isProcessedState() { - return this.isErrorState || this.isSuccessState; + return ( + this.isErrorState || this.isSuccessState || this.isGeneratingPreviewState + ); } get isErrorState() { - return this.readState.startsWith("Err"); + return this.state.startsWith("Err"); } get imageFile() { diff --git a/public/scripts/preview_worker.js b/public/scripts/preview_worker.js index 37b9c9f..9c9cc77 100644 --- a/public/scripts/preview_worker.js +++ b/public/scripts/preview_worker.js @@ -59,9 +59,9 @@ async function main() { // TODO: Handle preview loading state in widget let results = await runPreviewMockup(pyodideObject); console.log("preview results", results); - self.postMessage(results); + self.postMessage({ ulid: event.data.ulid, results: results }); } catch (error) { - self.postMessage({ error: error.message }); + self.postMessage({ ulid: event.data.ulid, error: error.message }); } }; } diff --git a/public/scripts/upload.js b/public/scripts/upload.js index 4006909..28e47a5 100644 --- a/public/scripts/upload.js +++ b/public/scripts/upload.js @@ -34,10 +34,21 @@ async function runWorker(worker) { window.localforage .setItem("pictureArray", e.data) .then(function (pictureArray) { + if (e.data["error"] !== undefined) { + console.log("Get error while generating mockup", e.data["error"]); + window.viewModel.cancelMockup(); + + // Alert after `cancelMockup` finish + setTimeout(() => { + alert( + "Oops, something went wrong. Please try a different image/device.\nIf it persists, we'd appreciate if you report it on our GitHub 🙏 https://github.com/oursky/mockuphone.com/issues.", + ); + }, 100); + return; + } window.location.href = "/download/?deviceId=" + window.workerDeviceId; }) .catch(function (err) { - // TODO: Handle preview error in widget console.error("Get error while storing images to localforage:", err); }); }, @@ -46,41 +57,62 @@ async function runWorker(worker) { } function runPreviewWorker(worker, imageUpload) { + if (imageUpload.isErrorState) { + return; + } + window.viewModel.fileList.updateImageUploadStateByULID( + imageUpload.ulid, + ImageUploadState.GeneratingPreview, + ); const imageUploadFile = imageUpload.file; worker.postMessage({ imageUpload: imageUploadFile, location: window.location.toString(), deviceId: window.workerDeviceId, deviceInfo: window.deviceInfo, + ulid: imageUpload.ulid, }); worker.addEventListener( "message", function (e) { - window.localforage - .setItem(`previewImage-${imageUpload.ulid}`, e.data[1]) - .then(function () { - const imageContainer = document.querySelector( - ".upload__device-image-rect", - ); + if (e.data["error"] !== undefined) { + console.log( + "Get error while generating preview image", + e.data["error"], + ); + window.viewModel.fileList.updateImageUploadStateByULID( + e.data["ulid"], + ImageUploadState.ErrPreview, + ); + return; + } - /* Put first generated mockup to preview area */ - if (!imageContainer.style.backgroundImage) { - imageContainer.style.backgroundImage = `url(${e.data[1]})`; - imageContainer.style.backgroundSize = "cover"; - imageContainer.style.backgroundPosition = "center"; - - const imageUploadHints = document.querySelectorAll( - ".upload__device-hint", - ); - imageUploadHints.forEach((imageUploadHint) => { - imageUploadHint.innerHTML = ""; - imageUploadHint.style.background = "transparent"; - }); - } - }) - .catch(function (err) { - console.error("Get error while storing images to localforage:", err); + const ulid = e.data["ulid"]; + const [_, previewUrl] = e.data["results"]; + + const imageContainer = document.querySelector( + ".upload__device-image-rect", + ); + + /* Put first generated mockup to preview area */ + if (!imageContainer.style.backgroundImage) { + imageContainer.style.backgroundImage = `url(${previewUrl})`; + + const imageUploadHints = document.querySelectorAll( + ".upload__device-hint", + ); + imageUploadHints.forEach((imageUploadHint) => { + imageUploadHint.style.display = "none"; }); + } + window.viewModel.fileList.updateImageUploadPreviewUrlByULID( + ulid, + previewUrl, + ); + window.viewModel.fileList.updateImageUploadStateByULID( + ulid, + ImageUploadState.ReadSuccess, + ); }, false, ); @@ -127,20 +159,48 @@ class FileListViewModel { files = [file]; } - for (const file of files) { - const imageUpload = new ImageUpload(file, MAX_FILE_SIZE_BYTE); + for (let i = 0; i < files.length; i += 1) { + const imageUpload = new ImageUpload(files[i], MAX_FILE_SIZE_BYTE); await imageUpload.read(); imageUpload.ulid = ULID.ulid(); - this._imageUploads.push(imageUpload); - window.viewModel.generatePreviewMockup(imageUpload); + + // Avoiding read same image file + setTimeout(() => { + this._imageUploads.push(imageUpload); + window.viewModel.generatePreviewMockup(imageUpload); + }, i * 10); } + + window.viewModel.selectedPreviewImageULID = + window.viewModel.defaultImageUploadULID; } - async remove(filename, index) { - this._imageUploads = this._imageUploads.filter((upload, i) => { + async remove(filename, fileUlid) { + this._imageUploads = this._imageUploads.filter((upload) => { const isSameFilename = upload.file.name === filename; - const isSameIndex = i === index; - return !(isSameFilename && isSameIndex); + const isSameULID = fileUlid === upload.ulid; + return !(isSameFilename && isSameULID); + }); + + window.viewModel.selectedPreviewImageULID = + window.viewModel.defaultImageUploadULID; + } + + updateImageUploadStateByULID(ulid, state) { + this._imageUploads = this._imageUploads.map((imageUpload) => { + if (imageUpload.ulid == ulid) { + imageUpload.state = state; + } + return imageUpload; + }); + } + + updateImageUploadPreviewUrlByULID(ulid, previewUrl) { + this._imageUploads = this._imageUploads.map((imageUpload) => { + if (imageUpload.ulid == ulid) { + imageUpload.previewUrl = previewUrl; + } + return imageUpload; }); } } @@ -155,6 +215,7 @@ class RootViewModel { worker = new Worker("/scripts/web_worker.js"); previewWorker = new Worker("/scripts/preview_worker.js"); selectedColorId = null; + selectedPreviewImageULID = null; constructor(maxMockupWaitSec, fileListViewModel, selectedColorId) { mobx.makeObservable(this, { @@ -164,6 +225,7 @@ class RootViewModel { isGeneratingMockup: mobx.computed, generateMockup: mobx.action, cancelMockup: mobx.action, + selectedPreviewImageULID: mobx.observable, }); this.selectedColorId = selectedColorId; this.maxMockupWaitSec = maxMockupWaitSec; @@ -218,6 +280,12 @@ class RootViewModel { get previewUrl() { return "/download/?deviceId=" + window.workerDeviceId; } + + get defaultImageUploadULID() { + return this.fileList.imageUploads.length > 0 + ? this.fileList.imageUploads[0].ulid + : null; + } } function preventDefault(node, events) { @@ -249,23 +317,36 @@ function dismissUploading() { uploading?.classList.add("d-none"); } -function findFileListItem(fileIndex) { +function findFileListItem(fileUlid) { const fileListNode = document.querySelector(".file-list"); const itemNodes = fileListNode.querySelectorAll(".file-list-item"); for (const itemNode of itemNodes) { - if (itemNode.dataset.fileIndex === String(fileIndex)) { + if (itemNode.dataset.fileUlid === String(fileUlid)) { return itemNode; } } return null; } -function appendInitialFileListItem(fileIndex, filename) { +function appendInitialFileListItem(fileUlid, filename) { const fileListNode = document.querySelector(".file-list"); const itemNode = document.createElement("li"); + + const fileInfoNode = document.createElement("div"); + const previewStateNode = document.createElement("div"); + previewStateNode.addEventListener("click", () => { + window.viewModel.selectedPreviewImageULID = fileUlid; + }); + + previewStateNode.classList.add("file-list-item__preview-state"); + itemNode.appendChild(previewStateNode); + + fileInfoNode.classList.add("file-list-item__file-info"); + itemNode.appendChild(fileInfoNode); + itemNode.classList.add("file-list-item"); - itemNode.dataset.fileIndex = fileIndex; + itemNode.dataset.fileUlid = fileUlid; const headerNode = document.createElement("div"); headerNode.classList.add("file-list-item__filename"); @@ -277,24 +358,16 @@ function appendInitialFileListItem(fileIndex, filename) { const crossNode = document.createElement("button"); crossNode.classList.add("file-list-item__cross"); crossNode.onclick = async () => { - await window.viewModel.fileList.remove(filename, fileIndex); + await window.viewModel.fileList.remove(filename, fileUlid); }; headerNode.appendChild(crossNode); - itemNode.appendChild(headerNode); - - itemNode.insertAdjacentHTML( + fileInfoNode.appendChild(headerNode); + fileInfoNode.insertAdjacentHTML( "beforeend", `

`, ); - itemNode.insertAdjacentHTML( - "beforeend", - `
-
-
`, - ); - return fileListNode.appendChild(itemNode); } @@ -305,23 +378,20 @@ function removeAllFileListItems() { function updateFileListItem(itemNode, imageUpload) { const hintNode = itemNode.querySelector(".file-list-item__hint"); - const progressFillNode = itemNode.querySelector( - ".file-list-item__progress-bar-fill", - ); + const previewNode = itemNode.querySelector(".file-list-item__preview-state"); // clear previous state itemNode.classList.remove( "file-list-item--done", "file-list-item--error", "file-list-item--warning", + "file-list-item__previewable", // NOTE: do not remove progress state immediately so the progress bar can proceed to 100% before being removed // "file-list-item--progress" ); - progressFillNode.classList.remove( - "file-list-item__progress-bar-fill--30", - "file-list-item__progress-bar-fill--60", - "file-list-item__progress-bar-fill--90", - "file-list-item__progress-bar-fill--100", + previewNode.classList.remove( + "file-list-item__preview_selected", + "file-list-item__preview_non_selected", ); /* Expected UI for each state @@ -352,33 +422,50 @@ function updateFileListItem(itemNode, imageUpload) { isSameAspectRatio(imageDim, recommendDim) || isSameAspectRatio(imageDimRotate, recommendDim); const shouldShowAspectRatioWarning = - imageUpload.readState !== ReadState.Reading && !isCorrectDim; + imageUpload.state !== ImageUploadState.Reading && !isCorrectDim; // Update status icon // error status has higher precedence over warning if (imageUpload.isErrorState) { - itemNode.classList.remove("file-list-item--progress"); + itemNode.classList.remove( + "file-list-item--progress", + "file-list-item--loading", + ); itemNode.classList.add("file-list-item--error"); } else if (shouldShowAspectRatioWarning) { itemNode.classList.add("file-list-item--warning"); } + + if (imageUpload.isGeneratingPreviewState) { + itemNode.classList.remove("file-list-item--done"); + itemNode.classList.add("file-list-item--loading"); + } + if (imageUpload.isSuccessState) { - setTimeout(() => { - itemNode.classList.remove("file-list-item--progress"); - }, 10); - itemNode.classList.add("file-list-item--done"); + itemNode.classList.remove( + "file-list-item--loading", + "file-list-item--progress", + ); + itemNode.classList.add( + "file-list-item--done", + "file-list-item__previewable", + ); } else if (imageUpload.isProcessingState) { itemNode.classList.add("file-list-item--progress"); } // update hint text if (imageUpload.isErrorState) { - switch (imageUpload.readState) { - case ReadState.ErrUnsupportedFileType: + switch (imageUpload.state) { + case ImageUploadState.ErrUnsupportedFileType: hintNode.innerText = "Supported file extensions: JPG, PNG or PSD."; break; - case ReadState.ErrExceedMaxFileSize: + case ImageUploadState.ErrExceedMaxFileSize: hintNode.innerText = `File size should be less than ${MAX_FILE_SIZE_READABLE}.`; break; - case ReadState.ErrRead: + case ImageUploadState.ErrPreview: + hintNode.innerText = + "Preview failed. Please upload the image again to retry."; + break; + case ImageUploadState.ErrRead: default: hintNode.innerText = "Something went wrong. Please try upload again or refresh the page."; @@ -387,23 +474,13 @@ function updateFileListItem(itemNode, imageUpload) { } else if (shouldShowAspectRatioWarning) { hintNode.innerText = `Uploaded file dimension (${imageDim.width} × ${imageDim.height} pixels) differs from ideal (${recommendDim.width} × ${recommendDim.height} pixels).`; } - // update progress bar - if (imageUpload.isProcessingState || imageUpload.isSuccessState) { - progressFillNode.classList.add("file-list-item__progress-bar-fill--30"); - switch (imageUpload.readState) { - case ReadState.ReadyForRead: - progressFillNode.classList.add("file-list-item__progress-bar-fill--60"); - break; - case ReadState.Reading: - progressFillNode.classList.add("file-list-item__progress-bar-fill--90"); - break; - case ReadState.ReadSuccess: - progressFillNode.classList.add( - "file-list-item__progress-bar-fill--100", - ); - break; - default: - break; + + // update preview button + if (imageUpload.isSuccessState) { + if (window.viewModel.selectedPreviewImageULID == imageUpload.ulid) { + previewNode.classList.add("file-list-item__preview_selected"); + } else { + previewNode.classList.add("file-list-item__preview_non_selected"); } } @@ -616,18 +693,49 @@ function main() { } }); - // observe fileListViewModel: imageUploads[].readState + // observe fileListViewModel: imageUploads[].state + mobx.reaction( + () => + viewModel.fileList.imageUploads.map((imageUpload) => imageUpload.state), + async () => { + const imageUploads = viewModel.fileList.imageUploads; + for (let i = 0; i < imageUploads.length; ++i) { + let itemNode = findFileListItem(imageUploads[i].ulid); + if (itemNode == null) { + itemNode = appendInitialFileListItem( + imageUploads[i].ulid, + imageUploads[i].file.name, + ); + } + updateFileListItem(itemNode, imageUploads[i]); + } + + // scroll to upload element on mobile devices + if (window.innerWidth <= 992) { + const HEADER_HEIGHT = 80; + scrollToElementTop(uploadSection, HEADER_HEIGHT); + } + }, + { + equals: mobx.comparer.shallow, + }, + ); + + // observe fileListViewModel: imageUploads[].previewState mobx.reaction( () => viewModel.fileList.imageUploads.map( - (imageUpload) => imageUpload.readState, + (imageUpload) => imageUpload.previewState, ), async () => { const imageUploads = viewModel.fileList.imageUploads; for (let i = 0; i < imageUploads.length; ++i) { - let itemNode = findFileListItem(i); + let itemNode = findFileListItem(imageUploads[i].ulid); if (itemNode == null) { - itemNode = appendInitialFileListItem(i, imageUploads[i].file.name); + itemNode = appendInitialFileListItem( + imageUploads[i].ulid, + imageUploads[i].file.name, + ); } updateFileListItem(itemNode, imageUploads[i]); } @@ -650,9 +758,12 @@ function main() { removeAllFileListItems(); // remove then re-render const imageUploads = viewModel.fileList.imageUploads; for (let i = 0; i < imageUploads.length; ++i) { - let itemNode = findFileListItem(i); + let itemNode = findFileListItem(imageUploads[i].ulid); if (itemNode == null) { - itemNode = appendInitialFileListItem(i, imageUploads[i].file.name); + itemNode = appendInitialFileListItem( + imageUploads[i].ulid, + imageUploads[i].file.name, + ); } updateFileListItem(itemNode, imageUploads[i]); } @@ -662,15 +773,53 @@ function main() { }, ); + // observe viewModel: selectedPreviewImageULID + mobx.reaction( + () => viewModel.selectedPreviewImageULID, + () => { + // update preview area + const imageContainer = document.querySelector( + ".upload__device-image-rect", + ); + if (viewModel.selectedPreviewImageULID === null) { + imageContainer.style.backgroundImage = ""; + const imageUploadHints = document.querySelectorAll( + ".upload__device-hint", + ); + imageUploadHints.forEach((imageUploadHint) => { + imageUploadHint.style.display = "flex"; + }); + } + + removeAllFileListItems(); // remove then re-render + const imageUploads = viewModel.fileList.imageUploads; + for (let i = 0; i < imageUploads.length; ++i) { + let itemNode = findFileListItem(imageUploads[i].ulid); + if (itemNode == null) { + itemNode = appendInitialFileListItem( + imageUploads[i].ulid, + imageUploads[i].file.name, + ); + } + updateFileListItem(itemNode, imageUploads[i]); + + if ( + imageUploads[i].isSuccessState && + imageUploads[i].ulid == window.viewModel.selectedPreviewImageULID + ) { + imageContainer.style.backgroundImage = `url(${imageUploads[i].previewUrl})`; + } + } + }, + ); + if (isDebug) { - // observe fileListViewModel: imageUploads, imageUploads[].readState + // observe fileListViewModel: imageUploads, imageUploads[].state mobx.autorun(() => { console.log("file list:", mobx.toJS(viewModel.fileList.imageUploads)); console.log( "read states:", - viewModel.fileList.imageUploads.map( - (imageUpload) => imageUpload.readState, - ), + viewModel.fileList.imageUploads.map((imageUpload) => imageUpload.state), ); }); } diff --git a/src/styles/upload.css b/src/styles/upload.css index 71be1f2..a4a0bcd 100644 --- a/src/styles/upload.css +++ b/src/styles/upload.css @@ -153,6 +153,11 @@ main { z-index: 1; } +.upload__device-image-rect { + background-size: cover; + background-position: center; +} + .upload__device-image-rect-wrapper { position: absolute; top: 0; @@ -341,6 +346,48 @@ main { } .file-list-item { + display: flex; + + &.file-list-item__previewable { + gap: 4px; + + .file-list-item__file-info { + max-width: calc(100% - 28px); + } + + .file-list-item__preview-state { + cursor: pointer; + } + } +} + +.file-list-item_left { + width: 0; +} + +.file-list-item__preview_selected, +.file-list-item__preview_non_selected { + margin: 8px 0 0 0; + padding-inline: 6px; + width: 24px; + display: flex; + border-radius: 5px; + background-size: 12px 8px; + background-repeat: no-repeat; + background-position: center; +} + +.file-list-item__preview_selected { + background-image: url("/images/preview-white.svg"); + background-color: rgba(0, 67, 224, 1); +} + +.file-list-item__preview_non_selected { + background-image: url("/images/preview-gray.svg"); + background-color: white; +} + +.file-list-item__file-info { margin: 8px 0 0 0; border: 1px solid var(--gray-5); border-radius: 10px; @@ -349,6 +396,7 @@ main { display: flex; flex-direction: column; justify-content: center; + flex: 1; } .file-list-item__filename { @@ -409,44 +457,24 @@ main { background-image: url("/images/upload-error.svg"); } +.file-list-item--loading .file-list-item__filename::before { + content: ""; + display: block; + width: 20px; + height: 20px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + background-image: url("/images/spinner-blue-2.png"); + animation: spin 2s linear infinite; +} + .file-list-item__hint { margin: 5px 0 0 0; font-size: 12px; color: var(--gray-3); } -.file-list-item__progress-bar-border { - display: none; - margin: 5px 0 0 0; - width: 100%; - height: 6px; - background: rgba(0, 0, 0, 0.1); - border-radius: 30px; - overflow: hidden; -} -.file-list-item--progress .file-list-item__progress-bar-border { - display: block; -} - -.file-list-item__progress-bar-fill { - width: 0; - height: 100%; - background: var(--green-3); - transition: width 1s ease-in-out; -} -.file-list-item__progress-bar-fill--30 { - width: 30%; -} -.file-list-item__progress-bar-fill--60 { - width: 60%; -} -.file-list-item__progress-bar-fill--90 { - width: 90%; -} -.file-list-item__progress-bar-fill--100 { - width: 100%; -} - .color-section { margin: 20px 0 0 0; }