├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── lib ├── image-uploader.coffee ├── insert-image-view.coffee ├── main.coffee └── utils.coffee ├── package.json └── styles └── markdown-assistant.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | * First Release 3 | * support upload image to qiniu 4 | 5 | ## 0.0.2 6 | * Fix uuid not found 7 | 8 | ## 0.0.3 9 | * Typo 10 | 11 | ## 0.0.4 12 | * Add desc and keywords in package.json 13 | 14 | ## 0.0.5 15 | * Add keywords 16 | 17 | ## 0.0.6 18 | * Config friendly: Domain now default begin with `http://` 19 | 20 | ## 0.0.7 21 | * Fix Redundant `http://` 22 | * Only insert image when edit markdown file 23 | 24 | ## 0.0.8 25 | * make uploader module as an external plugin 26 | * add setting: `disableImageUploaderIfNotMarkdown` 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 knightli 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown Assistant 2 | An assistant for markdown writers. 3 | 4 | For now there is only one feature: 5 | - Upload images from the clipboard automatically. 6 | 7 | ## Upload images from the clipboard 8 | 9 | ![upload image](http://7xkrm0.com1.z0.glb.clouddn.com/72b078601683bd35ad459172977a620f.png) 10 | 11 | 12 | ### Prepare an uploader 13 | You need prepare an `uploader` to upload image. 14 | 15 | How? 16 | 17 | 1. Find an uploader plugin and install it! 18 | > you can find some of them [here](https://github.com/knightli/markdown-assistant/wiki/plugins#uploader) 19 | > or search in atom with keywords `uploader` + `assistant` 20 | 21 | 2. set uploader package name as `uploader` in settings. 22 | ![settings outter](http://7xkrm0.com1.z0.glb.clouddn.com/46304a9b336ebb2cdde5c7ccc6f70d29.png) 23 | 24 | 3. config your uploader package in settings for upload ( [example](https://github.com/knightli/qiniu-uploader) ) 25 | 26 | ### Usage 27 | 1. Take a screenshot or copy any image to the clipboard. 28 | 2. Paste it into Atom by `cmd` + `v` (or `ctrl` + `v`). 29 | 3. It's uploading now. Wait for secs. 30 | 4. See preview for the uploaded image and maybe add a title for it. 31 | 5. Press `enter` to insert the image. 32 | -------------------------------------------------------------------------------- /lib/image-uploader.coffee: -------------------------------------------------------------------------------- 1 | qiniu = require "qiniu" 2 | crypto = require "crypto" 3 | 4 | module.exports = class imageUploader 5 | 6 | constructor: (uploadInfo) -> 7 | qiniu.conf.ACCESS_KEY = uploadInfo.ak 8 | qiniu.conf.SECRET_KEY = uploadInfo.sk 9 | @ak = uploadInfo.ak 10 | @sk = uploadInfo.sk 11 | @domain = uploadInfo.domain 12 | @bucket = uploadInfo.bucket; 13 | @token = @getToken() 14 | #console.log('tonken='+@token) 15 | 16 | getToken: () -> 17 | putPolicy = new qiniu.rs.PutPolicy(@bucket) 18 | return putPolicy.token() 19 | 20 | getKey: (imgbuffer) -> 21 | fsHash = crypto.createHash('md5') 22 | fsHash.update(imgbuffer) 23 | return fsHash.digest('hex') 24 | 25 | upload: (img, callback) -> 26 | imgbuffer = img.toPng() 27 | imgkey = @getKey(imgbuffer) 28 | 29 | qiniu.io.put @token, "#{imgkey}.png" , imgbuffer, null, (err, ret) => 30 | if !err 31 | #console.log(ret.key, ret.hash) 32 | callback(null, {ret: ret, url:"#{@domain}/#{ret.key}"}) 33 | else 34 | #console.log(err) 35 | callback(err) 36 | -------------------------------------------------------------------------------- /lib/insert-image-view.coffee: -------------------------------------------------------------------------------- 1 | {$, View, TextEditorView} = require "atom-space-pen-views" 2 | utils = require "./utils" 3 | 4 | module.exports = 5 | class InsertImageView extends View 6 | 7 | previouslyFocusedElement: null 8 | 9 | @content: -> 10 | @div class: "markdown-assistant-dialog", => 11 | @label "Insert Image", class: "icon icon-device-camera" 12 | @div class: "loading-layer", outlet: "loadingLayer", => 13 | @span class: "loading loading-spinner-tiny inline-block" 14 | @label "uploading...", class: "message" 15 | @div outlet: "imageInfoLayer", => 16 | @div => 17 | @label "Image Path (src)", class: "message" 18 | @subview "imageEditor", new TextEditorView(mini: true) 19 | @div outlet: "outputMessage", class: "text-info" 20 | @label "Title (alt)", class: "message" 21 | @subview "titleEditor", new TextEditorView(mini: true) 22 | @div class: "image-container", => 23 | @img outlet: 'imagePreview' 24 | 25 | initialize: -> 26 | @imageEditor.on "blur", => @displayImagePreview(@imageEditor.getText().trim()) 27 | @imageEditor.on "keyup", => @displayImagePreview(@imageEditor.getText().trim()) 28 | 29 | atom.commands.add @element, 30 | "core:confirm": => @onConfirm() 31 | "core:cancel": => @detach() 32 | 33 | onConfirm: -> 34 | imgUrl = @imageEditor.getText().trim() 35 | return unless imgUrl 36 | 37 | @insertImage(); 38 | @detach() 39 | 40 | insertImage: -> 41 | imgurl = @imageEditor.getText().trim() 42 | title = @titleEditor.getText().trim() 43 | text = "![#{title}](#{imgurl})" 44 | @editor.setTextInBufferRange(@range, text) 45 | 46 | detach: -> 47 | return unless @panel.isVisible() 48 | @panel.hide() 49 | @previouslyFocusedElement?.focus() 50 | super 51 | 52 | display: (uploadFn) -> 53 | @panel ?= atom.workspace.addModalPanel(item: this, visible: false) 54 | @previouslyFocusedElement = $(document.activeElement) 55 | @editor = atom.workspace.getActiveTextEditor() 56 | @setFieldsFromSelection() 57 | 58 | if uploadFn 59 | @imageInfoLayer.css({"display":"none"}) 60 | @loadingLayer.css({"display":"block"}) 61 | 62 | setTimeout => 63 | uploadFn (err, data)=> 64 | @imageInfoLayer.css({"display":"block"}) 65 | @loadingLayer.css({"display":"none"}) 66 | 67 | if not err 68 | imgSrc = data.url 69 | @imageEditor.setText(imgSrc) 70 | @displayImagePreview(imgSrc) 71 | @titleEditor.focus() 72 | else 73 | @showMessage("upload error(#{err.code}): #{err.error}", "error") 74 | 75 | ,200 76 | 77 | else 78 | @imageInfoLayer.css({"display":"block"}) 79 | @loadingLayer.css({"display":"none"}) 80 | 81 | @panel.show() 82 | @imageEditor.focus() 83 | 84 | showMessage: (msg, type='info') -> 85 | @outputMessage.text(msg) 86 | @outputMessage.attr('class', 'text-'+type) 87 | 88 | displayImagePreview: (imgSrc) -> 89 | return if @imageOnPreview == imgSrc 90 | 91 | @showMessage("Opening Image Preview ...") 92 | @imagePreview.attr("src", imgSrc) 93 | @imagePreview.load => 94 | @showMessage("Image is ready! now you can set title then press enter!", 'success') 95 | @imagePreview.error => 96 | @showMessage("Error: Failed to Load Image.", 'error') 97 | @imagePreview.attr("src", "") 98 | 99 | @imageOnPreview = imgSrc # cache preview image src 100 | 101 | setFieldsFromSelection: -> 102 | @range = utils.getTextBufferRange(@editor, "link") 103 | selection = @editor.getTextInRange(@range) 104 | -------------------------------------------------------------------------------- /lib/main.coffee: -------------------------------------------------------------------------------- 1 | insertImageViewModule = require "./insert-image-view" 2 | 3 | module.exports = 4 | config: 5 | uploader: 6 | title: "uploader" 7 | type: 'string' 8 | description: "uploader plugin for upload file" 9 | default: "qiniu-uploader" 10 | disableImageUploaderIfNotMarkdown: 11 | title: "disable image uploader if not markdown" 12 | type: "boolean" 13 | default: false 14 | 15 | activate: (state) -> 16 | @attachEvent() 17 | 18 | attachEvent: -> 19 | workspaceElement = atom.views.getView(atom.workspace) 20 | workspaceElement.addEventListener 'keydown', (e) => 21 | editor = atom.workspace.getActiveTextEditor() 22 | 23 | if atom.config.get('markdown-assistant.disableImageUploaderIfNotMarkdown') 24 | editor?.observeGrammar (grammar) => 25 | return unless grammar 26 | return unless grammar.scopeName is 'source.gfm' 27 | @eventHandler e 28 | else 29 | @eventHandler e 30 | 31 | eventHandler: (e) -> 32 | if (e.metaKey && e.keyCode == 86 || e.ctrlKey && e.keyCode == 86) 33 | clipboard = require('clipboard') 34 | img = clipboard.readImage() 35 | return if img.isEmpty() 36 | 37 | # insert loading text 38 | uploaderName = atom.config.get('markdown-assistant.uploader') 39 | uploaderPkg = atom.packages.getLoadedPackage(uploaderName) 40 | 41 | if not uploaderPkg 42 | atom.notifications.addWarning('markdown-assistant: uploader not found',{ 43 | detail: "package \"#{uploaderName}\" not found!" + 44 | "\nHow to Fix:" + 45 | "\ninstall this package OR change uploader in markdown-assistant's settings" 46 | }) 47 | return 48 | 49 | uploader = uploaderPkg?.mainModule 50 | if not uploader 51 | uploader = require(uploaderPkg.path) 52 | 53 | try 54 | uploaderIns = uploader.instance() 55 | 56 | uploadFn = (callback)-> 57 | uploaderIns.upload(img.toPng(), 'png', callback) 58 | 59 | insertImageViewInstance = new insertImageViewModule() 60 | insertImageViewInstance.display(uploadFn) 61 | catch e 62 | # add uploadName for trace uploader package error in feedback 63 | e.message += " [uploaderName=#{uploaderName}]" 64 | throw new Error(e) 65 | -------------------------------------------------------------------------------- /lib/utils.coffee: -------------------------------------------------------------------------------- 1 | {$} = require "atom-space-pen-views" 2 | os = require "os" 3 | path = require "path" 4 | 5 | # ================================================== 6 | # General Utils 7 | # 8 | 9 | getJSON = (uri, succeed, error) -> 10 | return error() if uri.length == 0 11 | $.getJSON(uri).done(succeed).fail(error) 12 | 13 | regexpEscape = (str) -> 14 | str && str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') 15 | 16 | dasherize = (str) -> 17 | str.trim().toLowerCase().replace(/[^-\w\s]|_/g, "").replace(/\s+/g,"-") 18 | 19 | getPackagePath = (segments...) -> 20 | segments.unshift(atom.packages.resolvePackagePath("markdown-writer")) 21 | path.join.apply(null, segments) 22 | 23 | # ================================================== 24 | # Template 25 | # 26 | 27 | dirTemplate = (directory, date) -> 28 | template(directory, getDate(date)) 29 | 30 | template = (text, data, matcher = /[<{]([\w-]+?)[>}]/g) -> 31 | text.replace matcher, (match, attr) -> 32 | if data[attr]? then data[attr] else match 33 | 34 | # ================================================== 35 | # Date and Time 36 | # 37 | 38 | DATE_REGEX = /// ^ 39 | (\d{4})[-\/] # year 40 | (\d{1,2})[-\/] # month 41 | (\d{1,2}) # day 42 | $ ///g 43 | 44 | parseDateStr = (str) -> 45 | date = new Date() 46 | matches = DATE_REGEX.exec(str) 47 | if matches 48 | date.setYear(parseInt(matches[1], 10)) 49 | date.setMonth(parseInt(matches[2], 10) - 1) 50 | date.setDate(parseInt(matches[3], 10)) 51 | return getDate(date) 52 | 53 | getDateStr = (date)-> 54 | date = getDate(date) 55 | return "#{date.year}-#{date.month}-#{date.day}" 56 | 57 | getTimeStr = (date) -> 58 | date = getDate(date) 59 | return "#{date.hour}:#{date.minute}" 60 | 61 | getDate = (date = new Date()) -> 62 | year: "" + date.getFullYear() 63 | i_month: "" + (date.getMonth() + 1) 64 | month: ("0" + (date.getMonth() + 1)).slice(-2) 65 | i_day: "" + date.getDate() 66 | day: ("0" + date.getDate()).slice(-2) 67 | hour: ("0" + date.getHours()).slice(-2) 68 | minute: ("0" + date.getMinutes()).slice(-2) 69 | seconds: ("0" + date.getSeconds()).slice(-2) 70 | 71 | # ================================================== 72 | # Title and Slug 73 | # 74 | 75 | SLUG_REGEX = /// 76 | ^ 77 | (\d{1,4}-\d{1,2}-\d{1,4}-) 78 | (.+) 79 | $ 80 | /// 81 | 82 | getTitleSlug = (str) -> 83 | str = path.basename(str, path.extname(str)) 84 | 85 | if matches = SLUG_REGEX.exec(str) 86 | matches[2] 87 | else 88 | str 89 | 90 | # ================================================== 91 | # Image HTML Tag 92 | # 93 | 94 | IMG_TAG_REGEX = /// ///i 95 | IMG_TAG_ATTRIBUTE = /// ([a-z]+?)=('|")(.*?)\2 ///ig 96 | 97 | # Detect it is a HTML image tag 98 | isImageTag = (input) -> IMG_TAG_REGEX.test(input) 99 | parseImageTag = (input) -> 100 | img = {} 101 | attributes = IMG_TAG_REGEX.exec(input)[1].match(IMG_TAG_ATTRIBUTE) 102 | pattern = /// #{IMG_TAG_ATTRIBUTE.source} ///i 103 | attributes.forEach (attr) -> 104 | elem = pattern.exec(attr) 105 | img[elem[1]] = elem[3] if elem 106 | return img 107 | 108 | # ================================================== 109 | # Image 110 | # 111 | 112 | IMG_REGEX = /// 113 | !\[(.+?)\] # ![text] 114 | \( # open ( 115 | ([^\)\s]+)\s? # a image path 116 | [\"\']?([^)]*?)[\"\']? # any description 117 | \) # close ) 118 | /// 119 | 120 | isImage = (input) -> IMG_REGEX.test(input) 121 | parseImage = (input) -> 122 | image = IMG_REGEX.exec(input) 123 | 124 | if image && image.length >= 3 125 | return alt: image[1], src: image[2], title: image[3] 126 | else 127 | return alt: input, src: "", title: "" 128 | 129 | # ================================================== 130 | # Inline link 131 | # 132 | 133 | INLINE_LINK_REGEX = /// 134 | \[(.+?)\] # [text] 135 | \( # open ( 136 | ([^\)\s]+)\s? # a url 137 | [\"\']?([^)]*?)[\"\']? # any title 138 | \) # close ) 139 | /// 140 | 141 | isInlineLink = (input) -> INLINE_LINK_REGEX.test(input) and !isImage(input) 142 | parseInlineLink = (input) -> 143 | link = INLINE_LINK_REGEX.exec(input) 144 | 145 | if link && link.length >= 2 146 | text: link[1], url: link[2], title: link[3] || "" 147 | else 148 | text: input, url: "", title: "" 149 | 150 | # ================================================== 151 | # Reference link 152 | # 153 | 154 | REFERENCE_LINK_REGEX_OF = (id, opts = {}) -> 155 | id = regexpEscape(id) unless opts.noEscape 156 | /// 157 | \[(#{id})\]\ ?\[\] # [text][] 158 | | # or 159 | \[([^\[\]]+?)\]\ ?\[(#{id})\] # [text][id] 160 | /// 161 | 162 | # REFERENCE_LINK_REGEX.exec("[text][id]") 163 | # => ["[text][id]", undefined, "text", "id"] 164 | # 165 | # REFERENCE_LINK_REGEX.exec("[text][]") 166 | # => ["[text][]", "text", undefined, undefined] 167 | REFERENCE_LINK_REGEX = REFERENCE_LINK_REGEX_OF(".+?", noEscape: true) 168 | 169 | REFERENCE_DEF_REGEX_OF = (id, opts = {}) -> 170 | id = regexpEscape(id) unless opts.noEscape 171 | /// ^\ * # start of line with any spaces 172 | \[(#{id})\]:\ + # [id]: followed by spaces 173 | (\S*?) # link 174 | (?:\ +['"\(]?(.+?)['"\)]?)? # any "link title" 175 | $ ///m 176 | 177 | REFERENCE_DEF_REGEX = REFERENCE_DEF_REGEX_OF(".+?", noEscape: true) 178 | 179 | isReferenceLink = (input) -> REFERENCE_LINK_REGEX.test(input) 180 | parseReferenceLink = (input, editor) -> 181 | link = REFERENCE_LINK_REGEX.exec(input) 182 | text = link[2] || link[1] 183 | id = link[3] || link[1] 184 | def = undefined 185 | editor.buffer.scan REFERENCE_DEF_REGEX_OF(id), (match) -> def = match 186 | 187 | if def 188 | id: id, text: text, url: def.match[2], title: def.match[3] || "", 189 | definitionRange: def.computedRange 190 | else 191 | id: id, text: text, url: "", title: "", definitionRange: null 192 | 193 | isReferenceDefinition = (input) -> REFERENCE_DEF_REGEX.test(input) 194 | parseReferenceDefinition = (input, editor) -> 195 | def = REFERENCE_DEF_REGEX.exec(input) 196 | id = def[1] 197 | link = undefined 198 | editor.buffer.scan REFERENCE_LINK_REGEX_OF(id), (match) -> link = match 199 | 200 | if link 201 | id: id, text: link.match[2] || link.match[1], url: def[2], 202 | title: def[3] || "", linkRange: link.computedRange 203 | else 204 | id: id, text: "", url: def[2], title: def[3] || "", linkRange: null 205 | 206 | # ================================================== 207 | # URL 208 | # 209 | 210 | URL_REGEX = /// 211 | ^(?:\w+:)?\/\/ 212 | ([^\s\.]+\.\S{2}|localhost[\:?\d]*) 213 | \S*$ 214 | ///i 215 | 216 | isUrl = (url) -> URL_REGEX.test(url) 217 | 218 | # ================================================== 219 | # Atom TextEditor 220 | # 221 | 222 | # Return scopeSelector if there is an exact match, 223 | # else return any scope descriptor contains scopeSelector 224 | getScopeDescriptor = (cursor, scopeSelector) -> 225 | scopes = cursor.getScopeDescriptor() 226 | .getScopesArray() 227 | .filter((scope) -> scope.indexOf(scopeSelector) >= 0) 228 | 229 | if scopes.indexOf(scopeSelector) >= 0 230 | return scopeSelector 231 | else if scopes.length > 0 232 | return scopes[0] 233 | 234 | # Atom has a bug returning the correct buffer range when cursor is 235 | # at the end of scope, refer https://github.com/atom/atom/issues/7961 236 | # 237 | # This provides a temporary fix for the bug. 238 | getBufferRangeForScope = (editor, cursor, scopeSelector) -> 239 | pos = cursor.getBufferPosition() 240 | 241 | range = editor.displayBuffer.bufferRangeForScopeAtPosition(scopeSelector, pos) 242 | return range if range 243 | 244 | # HACK if range is undefined, move the cursor position one char forward, and 245 | # try to get the buffer range for scope again 246 | pos = [pos.row, Math.max(0, pos.column - 1)] 247 | editor.displayBuffer.bufferRangeForScopeAtPosition(scopeSelector, pos) 248 | 249 | # Get the text buffer range if selection is not empty, or get the 250 | # buffer range if it is inside a scope selector, or the current word. 251 | # 252 | # selection is optional, when not provided, use the last selection 253 | getTextBufferRange = (editor, scopeSelector, selection) -> 254 | selection ?= editor.getLastSelection() 255 | cursor = selection.cursor 256 | 257 | if selection.getText() 258 | selection.getBufferRange() 259 | else if (scope = getScopeDescriptor(cursor, scopeSelector)) 260 | getBufferRangeForScope(editor, cursor, scope) 261 | else 262 | wordRegex = cursor.wordRegExp(includeNonWordCharacters: false) 263 | cursor.getCurrentWordBufferRange(wordRegex: wordRegex) 264 | 265 | # ================================================== 266 | # Exports 267 | # 268 | 269 | module.exports = 270 | getJSON: getJSON 271 | regexpEscape: regexpEscape 272 | dasherize: dasherize 273 | getPackagePath: getPackagePath 274 | 275 | dirTemplate: dirTemplate 276 | template: template 277 | 278 | getDate: getDate 279 | parseDateStr: parseDateStr 280 | getDateStr: getDateStr 281 | getTimeStr: getTimeStr 282 | 283 | getTitleSlug: getTitleSlug 284 | 285 | isImageTag: isImageTag 286 | parseImageTag: parseImageTag 287 | isImage: isImage 288 | parseImage: parseImage 289 | 290 | isInlineLink: isInlineLink 291 | parseInlineLink: parseInlineLink 292 | isReferenceLink: isReferenceLink 293 | parseReferenceLink: parseReferenceLink 294 | isReferenceDefinition: isReferenceDefinition 295 | parseReferenceDefinition: parseReferenceDefinition 296 | 297 | isUrl: isUrl 298 | 299 | getTextBufferRange: getTextBufferRange 300 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-assistant", 3 | "main": "./lib/main", 4 | "version": "0.2.0", 5 | "description": "assistant for markdown writer which can upload image from clipboard", 6 | "keywords": [ 7 | "markdown", 8 | "image", 9 | "upload", 10 | "qiniu", 11 | "assistant", 12 | "helper" 13 | ], 14 | "author": { 15 | "name": "knightli", 16 | "email": "knightli@foxmail.com", 17 | "url": "https://github.com/knightli" 18 | }, 19 | "repository": "https://github.com/knightli/markdown-assistant", 20 | "license": "MIT", 21 | "engines": { 22 | "atom": ">=1.0.0 <2.0.0" 23 | }, 24 | "dependencies": { 25 | "atom-space-pen-views": "^2.0.3", 26 | "qiniu": "^6.1.8" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /styles/markdown-assistant.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .markdown-assistant { 8 | } 9 | 10 | .markdown-assistant-dialog { 11 | 12 | .loading-layer { 13 | text-align: center; 14 | } 15 | 16 | .dialog-row { 17 | margin-bottom: 10px; 18 | } 19 | 20 | .side-label { 21 | margin-left: 10px; 22 | } 23 | 24 | .image-container { 25 | max-height: 320px; 26 | overflow: scroll; 27 | 28 | img { 29 | max-width: 100%; 30 | margin: 0 auto; 31 | } 32 | } 33 | 34 | .col-1 { 35 | display: inline-block; 36 | width: 31%; 37 | margin-right: 2%; 38 | } 39 | 40 | .col-2 { 41 | display: inline-block; 42 | width: 34%; 43 | } 44 | } 45 | --------------------------------------------------------------------------------