0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-06 14:22:52 +00:00

Merge branch 'master' into localSnippetEditor

This commit is contained in:
David Bolack
2024-12-10 21:17:51 -06:00
11 changed files with 146 additions and 135 deletions

View File

@@ -1,26 +1,29 @@
<!-- > [!TIP]
Before submitting a Pull Request, please consider the following to speed up reviews: > Before submitting a Pull Request, please consider the following to speed up reviews:
- 👷‍♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs. > - 👷‍♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
- 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first. > - 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
- 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests? > - 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
- 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding. > - 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
-->
## Description ## Description
_Describe what your PR accomplishes. Consider walking through the main changes to aid reviewers in following your code, especially if it covers multiple files._
## Related Issues or Discussions ## Related Issues or Discussions
> [!CAUTION]
> If no issue exists yet, create it, and get agreement on the approach (or paste in a previous agreement from chat, etc.) before moving forward. (Experimental PRs are OK without prior discussion, but do not expect to get merged.)
- Closes # - Closes #
## QA Instructions, Screenshots, Recordings ## QA Instructions, Screenshots, Recordings
_Please replace this line with instructions on how to test or view your changes, as well as any before/after _Replace this line with instructions on how to test or view your changes, as well as any before/after
images for UI changes._ screenshots or recordings for UI changes._
### Reviewer Checklist ### Reviewer Checklist
_Please replace the list below with specific features you want reviewers to look at._ _Replace the list below with specific features you want reviewers to look at._
*Reviewers, refer to this list when testing features, or suggest new items * *Reviewers, refer to this list when testing features, or suggest new items *
- [ ] Verify new features are functional - [ ] Verify new features are functional
@@ -32,5 +35,3 @@ _Please replace the list below with specific features you want reviewers to look
- [ ] Feature A handles negative numbers - [ ] Feature A handles negative numbers
- [ ] Identify opportunities for simplification and refactoring - [ ] Identify opportunities for simplification and refactoring
- [ ] Check for code legibility and appropriate comments - [ ] Check for code legibility and appropriate comments
<details><summary>Copy this list</summary>

View File

@@ -115,10 +115,10 @@
color : #D3D3D3; color : #D3D3D3;
accent-color : #D3D3D3; accent-color : #D3D3D3;
&::-webkit-slider-thumb, &::-moz-slider-thumb { &::-webkit-slider-thumb, &::-moz-range-thumb {
width : 5px; width : 5px;
height : 5px; height : 5px;
cursor : pointer; cursor : ew-resize;
outline : none; outline : none;
} }

View File

@@ -217,19 +217,11 @@ const Snippetbar = createClass({
renderEditorButtons : function(){ renderEditorButtons : function(){
if(!this.props.showEditButtons) return; if(!this.props.showEditButtons) return;
const foldButtons = <>
<div className={`editorTool foldAll ${this.props.view !== 'meta' && this.props.foldCode ? 'active' : ''}`}
onClick={this.props.foldCode} >
<i className='fas fa-compress-alt' />
</div>
<div className={`editorTool unfoldAll ${this.props.view !== 'meta' && this.props.unfoldCode ? 'active' : ''}`}
onClick={this.props.unfoldCode} >
<i className='fas fa-expand-alt' />
</div>
</>;
return <div className='editors'> return (
<div className='historyTools'> <div className='editors'>
{this.props.view !== 'meta' && <><div className='historyTools'>
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`} <div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
onClick={this.toggleHistoryMenu} > onClick={this.toggleHistoryMenu} >
<i className='fas fa-clock-rotate-left' /> <i className='fas fa-clock-rotate-left' />
@@ -245,14 +237,21 @@ const Snippetbar = createClass({
</div> </div>
</div> </div>
<div className='codeTools'> <div className='codeTools'>
{foldButtons} <div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`} onClick={this.props.foldCode} >
<i className='fas fa-compress-alt' />
</div>
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
onClick={this.props.unfoldCode} >
<i className='fas fa-expand-alt' />
</div>
<div className={`editorTheme ${this.state.themeSelector ? 'active' : ''}`}
onClick={this.toggleThemeSelector} > onClick={this.toggleThemeSelector} >
<i className='fas fa-palette' /> <i className='fas fa-palette' />
{this.state.themeSelector && this.renderThemeSelector()} {this.state.themeSelector && this.renderThemeSelector()}
</div> </div>
</div> </div></>}
<div className='tabs'> <div className='tabs'>
<div className={cx('text', { selected: this.props.view === 'text' })} <div className={cx('text', { selected: this.props.view === 'text' })}
@@ -273,7 +272,8 @@ const Snippetbar = createClass({
</div> </div>
</div> </div>
</div>; </div>
)
}, },
render : function(){ render : function(){

View File

@@ -22,7 +22,7 @@
justify-content : flex-end; justify-content : flex-end;
min-width : 225px; min-width : 225px;
&:only-child { margin-left : auto; } &:only-child { margin-left : auto;min-width:unset;}
>div { >div {
display : flex; display : flex;
@@ -38,6 +38,11 @@
line-height : @menuHeight; line-height : @menuHeight;
text-align : center; text-align : center;
cursor : pointer; cursor : pointer;
&.editorTool:not(.active) {
cursor:not-allowed;
}
&:hover,&.selected { background-color : #999999; } &:hover,&.selected { background-color : #999999; }
&.text { &.text {
.tooltipLeft('Brew Editor'); .tooltipLeft('Brew Editor');

47
package-lock.json generated
View File

@@ -13,7 +13,7 @@
"@babel/core": "^7.26.0", "@babel/core": "^7.26.0",
"@babel/plugin-transform-runtime": "^7.25.9", "@babel/plugin-transform-runtime": "^7.25.9",
"@babel/preset-env": "^7.26.0", "@babel/preset-env": "^7.26.0",
"@babel/preset-react": "^7.25.9", "@babel/preset-react": "^7.26.3",
"@googleapis/drive": "^8.14.0", "@googleapis/drive": "^8.14.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
@@ -21,9 +21,9 @@
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"dompurify": "^3.2.2", "dompurify": "^3.2.3",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.21.1", "express": "^4.21.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.2.0", "express-static-gzip": "2.2.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
@@ -39,7 +39,7 @@
"marked-smartypants-lite": "^1.0.2", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.8.3", "mongoose": "^8.8.4",
"nanoid": "5.0.9", "nanoid": "5.0.9",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.3.1", "react": "^18.3.1",
@@ -1660,9 +1660,10 @@
} }
}, },
"node_modules/@babel/preset-react": { "node_modules/@babel/preset-react": {
"version": "7.25.9", "version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.26.3.tgz",
"integrity": "sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw==", "integrity": "sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9",
"@babel/helper-validator-option": "^7.25.9", "@babel/helper-validator-option": "^7.25.9",
@@ -5475,9 +5476,9 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "3.2.2", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.2.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
"integrity": "sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==", "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
"license": "(MPL-2.0 OR Apache-2.0)", "license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": { "optionalDependencies": {
"@types/trusted-types": "^2.0.7" "@types/trusted-types": "^2.0.7"
@@ -6269,9 +6270,10 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.1", "version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
@@ -6292,7 +6294,7 @@
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.10", "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.13.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
@@ -6307,6 +6309,10 @@
}, },
"engines": { "engines": {
"node": ">= 0.10.0" "node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-async-handler": { "node_modules/express-async-handler": {
@@ -10902,9 +10908,9 @@
} }
}, },
"node_modules/mongoose": { "node_modules/mongoose": {
"version": "8.8.3", "version": "8.8.4",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.3.tgz", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.4.tgz",
"integrity": "sha512-/I4n/DcXqXyIiLRfAmUIiTjj3vXfeISke8dt4U4Y8Wfm074Wa6sXnQrXN49NFOFf2mM1kUdOXryoBvkuCnr+Qw==", "integrity": "sha512-yJbn695qCsqDO+xyPII29x2R7flzXhxCDv09mMZPSGllf0sm4jKw3E9s9uvQ9hjO6bL2xjU8KKowYqcY9eSTMQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bson": "^6.7.0", "bson": "^6.7.0",
@@ -11772,9 +11778,10 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.10", "version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
}, },
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",

View File

@@ -85,7 +85,7 @@
"@babel/core": "^7.26.0", "@babel/core": "^7.26.0",
"@babel/plugin-transform-runtime": "^7.25.9", "@babel/plugin-transform-runtime": "^7.25.9",
"@babel/preset-env": "^7.26.0", "@babel/preset-env": "^7.26.0",
"@babel/preset-react": "^7.25.9", "@babel/preset-react": "^7.26.3",
"@googleapis/drive": "^8.14.0", "@googleapis/drive": "^8.14.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
@@ -93,9 +93,9 @@
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"dompurify": "^3.2.2", "dompurify": "^3.2.3",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.21.1", "express": "^4.21.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.2.0", "express-static-gzip": "2.2.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
@@ -111,7 +111,7 @@
"marked-smartypants-lite": "^1.0.2", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.8.3", "mongoose": "^8.8.4",
"nanoid": "5.0.9", "nanoid": "5.0.9",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.3.1", "react": "^18.3.1",

View File

@@ -241,8 +241,8 @@ const GoogleActions = {
return obj.data.id; return obj.data.id;
}, },
getGoogleBrew : async (id, accessId, accessType)=>{ getGoogleBrew : async (auth = defaultAuth, id, accessId, accessType)=>{
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth }); const drive = googleDrive.drive({ version: 'v3', auth: auth });
const obj = await drive.files.get({ const obj = await drive.files.get({
fileId : id, fileId : id,

View File

@@ -89,76 +89,68 @@ const api = {
// Create middleware with the accessType passed in as part of the scope // Create middleware with the accessType passed in as part of the scope
return async (req, res, next)=>{ return async (req, res, next)=>{
// Get relevant IDs for the brew // Get relevant IDs for the brew
const { id, googleId } = api.getId(req); let { id, googleId } = api.getId(req);
const accessMap = { const accessMap = {
edit : { editId: id }, edit : { editId: id },
share : { shareId: id }, share : { shareId: id },
admin : { admin : { $or : [{ editId: id }, { shareId: id }] }
$or : [
{ editId: id },
{ shareId: id },
] }
}; };
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine. // Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
let stub = await HomebrewModel.get(accessMap[accessType]) let stub = await HomebrewModel.get(accessMap[accessType])
.catch((err)=>{ .catch((err)=>{
if(googleId) { if(googleId)
console.warn(`Unable to find document stub for ${accessType}Id ${id}`); console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
} else { else
console.warn(err); console.warn(err);
}
}); });
stub = stub?.toObject(); stub = stub?.toObject();
googleId ??= stub?.googleId;
const isOwner = stub?.authors?.length === 0 || stub?.authors?.[0] === req.account?.username;
const isAuthor = stub?.authors?.includes(req.account?.username);
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
if(accessType === 'edit' && !(isOwner || isAuthor || isInvited)) {
const accessError = { name: 'Access Error', status: 401, authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
if(req.account)
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03' };
else
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04' };
}
if(stub?.lock?.locked && accessType != 'edit') { if(stub?.lock?.locked && accessType != 'edit') {
throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title }; throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
} }
// If there is a google id, try to find the google brew // If there is a google id, try to find the google brew
if(!stubOnly && (googleId || stub?.googleId)) { if(!stubOnly && googleId) {
let googleError; const oAuth2Client = isOwner? GoogleActions.authCheck(req.account, res) : undefined;
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
.catch((err)=>{ const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
googleError = err; .catch((googleError)=>{
const reason = googleError.errors?.[0].reason;
if(reason == 'notFound')
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
else
throw { ...googleError, HBErrorCode: '01' };
}); });
// Throw any error caught while attempting to retrieve Google brew.
if(googleError) {
const reason = googleError.errors?.[0].reason;
if(reason == 'notFound') {
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
} else {
throw { ...googleError, HBErrorCode: '01' };
}
}
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew // Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew; stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
} }
const authorsExist = stub?.authors?.length > 0;
const isAuthor = stub?.authors?.includes(req.account?.username);
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
const accessError = { name: 'Access Error', status: 401 };
if(req.account){
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
}
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
}
// If after all of that we still don't have a brew, throw an exception // If after all of that we still don't have a brew, throw an exception
if(!stub && !stubOnly) { if(!stub)
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id }; throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
}
// Clean up brew: fill in missing fields with defaults / fix old invalid values // Clean up brew: fill in missing fields with defaults / fix old invalid values
if(stub) { stub.tags = stub.tags || undefined; // Clear empty strings
stub.tags = stub.tags || undefined; // Clear empty strings stub.renderer = stub.renderer || undefined; // Clear empty strings
stub.renderer = stub.renderer || undefined; // Clear empty strings stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
}
req.brew = stub ?? {}; req.brew = stub;
next(); next();
}; };
}, },

View File

@@ -298,7 +298,7 @@ describe('Tests for api', ()=>{
expect(next).toHaveBeenCalled(); expect(next).toHaveBeenCalled();
expect(api.getId).toHaveBeenCalledWith(req); expect(api.getId).toHaveBeenCalledWith(req);
expect(model.get).toHaveBeenCalledWith({ shareId: '1' }); expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share'); expect(google.getGoogleBrew).toHaveBeenCalledWith(undefined, '2', '1', 'share');
}); });
it('access is denied to a locked brew', async()=>{ it('access is denied to a locked brew', async()=>{

View File

@@ -11,49 +11,54 @@
@import (less) './themes/fonts/iconFonts/fontAwesome.less'; @import (less) './themes/fonts/iconFonts/fontAwesome.less';
@keyframes sourceMoveAnimation { @keyframes sourceMoveAnimation {
50% {background-color: red; color: white;} 50% { color : white;background-color : red;}
100% {background-color: unset; color: unset;} 100% { color : unset;background-color : unset;}
} }
.codeEditor{ .codeEditor {
@media screen and (pointer : coarse) { @media screen and (pointer : coarse) {
font-size : 16px; font-size : 16px;
} }
.CodeMirror-foldmarker { .CodeMirror-foldmarker {
font-family: inherit; font-family : inherit;
text-shadow: none; font-weight : 600;
font-weight: 600; color : grey;
color: grey; text-shadow : none;
} }
.sourceMoveFlash .CodeMirror-line{ .CodeMirror-foldgutter {
animation-name: sourceMoveAnimation; cursor : pointer;
animation-duration: 0.4s; border-left : 1px solid #EEEEEE;
} transition : background 0.1s;
&:hover { background : #DDDDDD; }
}
.CodeMirror-vscrollbar { .sourceMoveFlash .CodeMirror-line {
&::-webkit-scrollbar { animation-name : sourceMoveAnimation;
width: 20px; animation-duration : 0.4s;
} }
&::-webkit-scrollbar-thumb {
width: 20px; .CodeMirror-vscrollbar {
background: linear-gradient(90deg, #858585 15px, #808080 15px); &::-webkit-scrollbar { width : 20px; }
} &::-webkit-scrollbar-thumb {
} width : 20px;
background : linear-gradient(90deg, #858585 15px, #808080 15px);
}
}
//.cm-tab { //.cm-tab {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right; // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
//} //}
//.cm-trailingspace { //.cm-trailingspace {
// .cm-space { // .cm-space {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right; // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
// } // }
//} //}
} }
.emojiPreview { .emojiPreview {
font-size: 1.5em; font-size : 1.5em;
line-height: 1.2em; line-height : 1.2em;
} }

View File

@@ -43,5 +43,6 @@ html,body, #reactRoot{
} }
&:disabled{ &:disabled{
background-color : @silver !important; background-color : @silver !important;
cursor:not-allowed;
} }
} }