From da9b87620f683ff356efa71fa322a302a8fb0fa1 Mon Sep 17 00:00:00 2001 From: Benoit Schweblin Date: Thu, 20 Sep 2018 10:00:07 +0100 Subject: [PATCH] GitLab provider (part 2) --- src/components/Modal.vue | 2 +- src/components/menus/HistoryMenu.vue | 6 +- src/components/menus/WorkspacesMenu.vue | 12 +-- .../modals/providers/GitlabAccountModal.vue | 10 ++- .../modals/providers/GitlabOpenModal.vue | 2 +- src/data/constants.js | 2 +- src/services/providers/common/Provider.js | 19 +++-- .../providers/couchdbWorkspaceProvider.js | 12 +-- .../providers/githubWorkspaceProvider.js | 20 +++-- src/services/providers/gitlabProvider.js | 27 +++---- .../providers/gitlabWorkspaceProvider.js | 76 +++++++++++-------- .../providers/googleDriveAppDataProvider.js | 8 +- .../providers/googleDriveWorkspaceProvider.js | 8 +- .../providers/helpers/gitlabHelper.js | 6 +- src/services/userSvc.js | 4 +- src/services/utils.js | 9 ++- src/services/workspaceSvc.js | 6 +- src/styles/app.scss | 2 +- src/styles/base.scss | 4 +- src/styles/variables.scss | 2 +- 20 files changed, 129 insertions(+), 108 deletions(-) diff --git a/src/components/Modal.vue b/src/components/Modal.vue index dc5d9d37..eb20789f 100644 --- a/src/components/Modal.vue +++ b/src/components/Modal.vue @@ -219,7 +219,7 @@ export default { z-index: 1; width: 100%; color: darken($error-color, 10%); - background-color: transparentize(lighten($error-color, 33%), 0.1); + background-color: transparentize(lighten($error-color, 33%), 0.075); font-size: 0.9em; line-height: 1.33; text-align: center; diff --git a/src/components/menus/HistoryMenu.vue b/src/components/menus/HistoryMenu.vue index 58bc46f3..37d239af 100644 --- a/src/components/menus/HistoryMenu.vue +++ b/src/components/menus/HistoryMenu.vue @@ -64,7 +64,7 @@ let cachedHistoryContextHash; let revisionsPromise; let revisionContentPromises; const pageSize = 30; -const spacerThreshold = 60 * 60 * 1000; // 1h +const spacerThreshold = 6 * 60 * 60 * 1000; // 6h export default { components: { @@ -129,8 +129,8 @@ export default { if (fileSyncData && contentSyncData) { return { ...historyContext, - fileSyncData, - contentSyncData, + fileSyncDataId: fileSyncData.id, + contentSyncDataId: contentSyncData.id, }; } } diff --git a/src/components/menus/WorkspacesMenu.vue b/src/components/menus/WorkspacesMenu.vue index 9ce7c570..006e0d62 100644 --- a/src/components/menus/WorkspacesMenu.vue +++ b/src/components/menus/WorkspacesMenu.vue @@ -9,23 +9,19 @@
-
CouchDB workspace
- Add a workspace synced with a CouchDB database. + Add a CouchDB backed workspace
-
GitHub workspace
- Add a workspace synced with a GitHub repository. + Add a GitHub backed workspace
-
GitLab workspace
- Add a workspace synced with a GitLab project. + Add a GitLab backed workspace
-
Google Drive workspace
- Add a workspace synced with a Google Drive folder. + Add a Google Drive backed workspace
diff --git a/src/components/modals/providers/GitlabAccountModal.vue b/src/components/modals/providers/GitlabAccountModal.vue index cbcca682..ee339b42 100644 --- a/src/components/modals/providers/GitlabAccountModal.vue +++ b/src/components/modals/providers/GitlabAccountModal.vue @@ -6,7 +6,8 @@

Link your GitLab account to StackEdit.

- + + @@ -42,14 +43,15 @@ export default modalTemplate({ }, methods: { resolve() { - if (!this.serverUrl) { + const serverUrl = this.config.forceServerUrl || this.serverUrl; + if (!serverUrl) { this.setError('serverUrl'); } if (!this.applicationId) { this.setError('applicationId'); } - if (this.serverUrl && this.applicationId) { - const parsedUrl = this.serverUrl.match(/^(https:\/\/[^/]+)/); + if (serverUrl && this.applicationId) { + const parsedUrl = serverUrl.match(/^(https:\/\/[^/]+)/); if (!parsedUrl) { this.setError('serverUrl'); } else { diff --git a/src/components/modals/providers/GitlabOpenModal.vue b/src/components/modals/providers/GitlabOpenModal.vue index 8098fc58..facfcd0a 100644 --- a/src/components/modals/providers/GitlabOpenModal.vue +++ b/src/components/modals/providers/GitlabOpenModal.vue @@ -8,7 +8,7 @@ diff --git a/src/data/constants.js b/src/data/constants.js index 90c4c138..00f35800 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -1,7 +1,7 @@ const origin = `${window.location.protocol}//${window.location.host}`; export default { - cleanTrashAfter: 0 * 24 * 60 * 60 * 1000, // 7 days + cleanTrashAfter: 7 * 24 * 60 * 60 * 1000, // 7 days origin, oauth2RedirectUri: `${origin}/oauth2/callback`, types: [ diff --git a/src/services/providers/common/Provider.js b/src/services/providers/common/Provider.js index 0e13dc4b..1a74aae1 100644 --- a/src/services/providers/common/Provider.js +++ b/src/services/providers/common/Provider.js @@ -44,15 +44,18 @@ export default class Provider { * Parse content serialized with serializeContent() */ static parseContent(serializedContent, id) { - const result = utils.deepCopy(store.state.content.itemsById[id]) || emptyContent(id); - result.text = utils.sanitizeText(serializedContent); - result.history = []; + let text = serializedContent; const extractedData = dataExtractor.exec(serializedContent); - if (extractedData) { + let result; + if (!extractedData) { + // In case stackedit's data has been manually removed, try to restore them + result = utils.deepCopy(store.state.content.itemsById[id]) || emptyContent(id); + } else { + result = emptyContent(id); try { const serializedData = extractedData[1].replace(/\s/g, ''); const parsedData = JSON.parse(utils.decodeBase64(serializedData)); - result.text = utils.sanitizeText(serializedContent.slice(0, extractedData.index)); + text = text.slice(0, extractedData.index); if (parsedData.properties) { result.properties = utils.sanitizeText(parsedData.properties); } @@ -62,11 +65,15 @@ export default class Provider { if (parsedData.comments) { result.comments = parsedData.comments; } - result.history = parsedData.history || []; + result.history = parsedData.history; } catch (e) { // Ignore } } + result.text = utils.sanitizeText(text); + if (!result.history) { + result.history = []; + } return utils.addItemHash(result); } diff --git a/src/services/providers/couchdbWorkspaceProvider.js b/src/services/providers/couchdbWorkspaceProvider.js index a4c58882..0a656d3a 100644 --- a/src/services/providers/couchdbWorkspaceProvider.js +++ b/src/services/providers/couchdbWorkspaceProvider.js @@ -194,8 +194,8 @@ export default new Provider({ }, }; }, - async listFileRevisions({ token, contentSyncData }) { - const body = await couchdbHelper.retrieveDocumentWithRevisions(token, contentSyncData.id); + async listFileRevisions({ token, contentSyncDataId }) { + const body = await couchdbHelper.retrieveDocumentWithRevisions(token, contentSyncDataId); const revisions = []; body._revs_info.forEach((revInfo, idx) => { // eslint-disable-line no-underscore-dangle if (revInfo.status === 'available') { @@ -209,19 +209,19 @@ export default new Provider({ }); return revisions; }, - async loadFileRevision({ token, contentSyncData, revision }) { + async loadFileRevision({ token, contentSyncDataId, revision }) { if (revision.loaded) { return false; } - const body = await couchdbHelper.retrieveDocument(token, contentSyncData.id, revision.id); + const body = await couchdbHelper.retrieveDocument(token, contentSyncDataId, revision.id); revision.sub = body.sub; revision.created = body.time; revision.loaded = true; return true; }, - async getFileRevisionContent({ token, contentSyncData, revisionId }) { + async getFileRevisionContent({ token, contentSyncDataId, revisionId }) { const body = await couchdbHelper - .retrieveDocumentWithAttachments(token, contentSyncData.id, revisionId); + .retrieveDocumentWithAttachments(token, contentSyncDataId, revisionId); return Provider.parseContent(body.attachments.data, body.item.id); }, }); diff --git a/src/services/providers/githubWorkspaceProvider.js b/src/services/providers/githubWorkspaceProvider.js index 749160f7..1f1020d5 100644 --- a/src/services/providers/githubWorkspaceProvider.js +++ b/src/services/providers/githubWorkspaceProvider.js @@ -46,12 +46,19 @@ export default new Provider({ async initWorkspace() { const { owner, repo, branch } = utils.queryParams; const workspaceParams = this.getWorkspaceParams({ owner, repo, branch }); + if (!branch) { + workspaceParams.branch = 'master'; + } + + // Extract path param const path = (utils.queryParams.path || '') + .trim() .replace(/^\/*/, '') // Remove leading `/` .replace(/\/*$/, '/'); // Add trailing `/` if (path !== '/') { workspaceParams.path = path; } + const workspaceId = utils.makeWorkspaceId(workspaceParams); const workspace = store.getters['workspace/workspacesById'][workspaceId]; @@ -217,14 +224,14 @@ export default new Provider({ }, }; }, - async listFileRevisions({ token, fileSyncData }) { + async listFileRevisions({ token, fileSyncDataId }) { const { owner, repo, branch } = store.getters['workspace/currentWorkspace']; const entries = await githubHelper.getCommits({ token, owner, repo, sha: branch, - path: getAbsolutePath(fileSyncData), + path: getAbsolutePath({ id: fileSyncDataId }), }); return entries.map(({ @@ -242,11 +249,12 @@ export default new Provider({ const sub = `${githubHelper.subPrefix}:${user.id}`; userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url }); const date = (commit.author && commit.author.date) - || (commit.committer && commit.committer.date); + || (commit.committer && commit.committer.date) + || 1; return { id: sha, sub, - created: date ? new Date(date).getTime() : 1, + created: new Date(date).getTime(), }; }); }, @@ -257,14 +265,14 @@ export default new Provider({ async getFileRevisionContent({ token, contentId, - fileSyncData, + fileSyncDataId, revisionId, }) { const { data } = await githubHelper.downloadFile({ ...store.getters['workspace/currentWorkspace'], token, branch: revisionId, - path: getAbsolutePath(fileSyncData), + path: getAbsolutePath({ id: fileSyncDataId }), }); return Provider.parseContent(data, contentId); }, diff --git a/src/services/providers/gitlabProvider.js b/src/services/providers/gitlabProvider.js index b0805cfa..b48d256c 100644 --- a/src/services/providers/gitlabProvider.js +++ b/src/services/providers/gitlabProvider.js @@ -135,24 +135,17 @@ export default new Provider({ token, }); - return entries.map(({ - author, - committer, - commit, - sha, - }) => { - let user; - if (author && author.login) { - user = author; - } else if (committer && committer.login) { - user = committer; - } - const sub = `${gitlabHelper.subPrefix}:${user.id}`; - userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url }); - const date = (commit.author && commit.author.date) - || (commit.committer && commit.committer.date); + return entries.map((entry) => { + const email = entry.author_email || entry.committer_email; + const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`; + userSvc.addInfo({ + id: sub, + name: entry.author_name || entry.committer_name, + imageUrl: '', + }); + const date = entry.authored_date || entry.committed_date || 1; return { - id: sha, + id: entry.id, sub, created: date ? new Date(date).getTime() : 1, }; diff --git a/src/services/providers/gitlabWorkspaceProvider.js b/src/services/providers/gitlabWorkspaceProvider.js index 8439cca8..a0a95443 100644 --- a/src/services/providers/gitlabWorkspaceProvider.js +++ b/src/services/providers/gitlabWorkspaceProvider.js @@ -45,29 +45,44 @@ export default new Provider({ return getAbsolutePath({ id }); }, async initWorkspace() { - const { projectPath, branch } = utils.queryParams; - const workspaceParams = this.getWorkspaceParams({ projectPath, branch }); + const { serverUrl, branch } = utils.queryParams; + const workspaceParams = this.getWorkspaceParams({ serverUrl, branch }); + if (!branch) { + workspaceParams.branch = 'master'; + } + + // Extract project path param + const projectPath = (utils.queryParams.projectPath || '') + .trim() + .replace(/^\/*/, '') // Remove leading `/` + .replace(/\/*$/, ''); // Remove trailing `/` + workspaceParams.projectPath = projectPath; + + // Extract path param const path = (utils.queryParams.path || '') + .trim() .replace(/^\/*/, '') // Remove leading `/` .replace(/\/*$/, '/'); // Add trailing `/` if (path !== '/') { workspaceParams.path = path; } + const workspaceId = utils.makeWorkspaceId(workspaceParams); const workspace = store.getters['workspace/workspacesById'][workspaceId]; // See if we already have a token - let token; - if (workspace) { - // Token sub is in the workspace - token = store.getters['data/gitlabTokensBySub'][workspace.sub]; - } + const sub = workspace ? workspace.sub : utils.queryParams.sub; + let token = store.getters['data/gitlabTokensBySub'][sub]; if (!token) { - const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' }); - token = await gitlabHelper.addAccount(serverUrl, applicationId); + const { applicationId } = await store.dispatch('modal/open', { + type: 'gitlabAccount', + forceServerUrl: serverUrl, + }); + token = await gitlabHelper.addAccount(serverUrl, applicationId, sub); } if (!workspace) { + const projectId = await gitlabHelper.getProjectId(token, workspaceParams); const pathEntries = (path || '').split('/'); const projectPathEntries = (projectPath || '').split('/'); const name = pathEntries[pathEntries.length - 2] // path ends with `/` @@ -75,6 +90,7 @@ export default new Provider({ store.dispatch('workspace/patchWorkspacesById', { [workspaceId]: { ...workspaceParams, + projectId, id: workspaceId, sub: token.sub, name, @@ -178,12 +194,13 @@ export default new Provider({ async uploadWorkspaceContent({ token, content, file }) { const path = store.getters.gitPathsByItemId[file.id]; const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`; - const res = await gitlabHelper.uploadFile({ + const sha = gitWorkspaceSvc.shaByPath[path]; + await gitlabHelper.uploadFile({ ...store.getters['workspace/currentWorkspace'], token, path: absolutePath, content: Provider.serializeContent(content), - sha: gitWorkspaceSvc.shaByPath[path], + sha, }); // Return new sync data @@ -192,7 +209,7 @@ export default new Provider({ id: store.getters.gitPathsByItemId[content.id], type: content.type, hash: content.hash, - sha: res.content.sha, + sha, }, fileSyncData: { id: path, @@ -223,33 +240,26 @@ export default new Provider({ }, }; }, - async listFileRevisions({ token, fileSyncData }) { + async listFileRevisions({ token, fileSyncDataId }) { const { projectId, branch } = store.getters['workspace/currentWorkspace']; const entries = await gitlabHelper.getCommits({ token, projectId, sha: branch, - path: getAbsolutePath(fileSyncData), + path: getAbsolutePath({ id: fileSyncDataId }), }); - return entries.map(({ - author, - committer, - commit, - sha, - }) => { - let user; - if (author && author.login) { - user = author; - } else if (committer && committer.login) { - user = committer; - } - const sub = `${gitlabHelper.subPrefix}:${user.id}`; - userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url }); - const date = (commit.author && commit.author.date) - || (commit.committer && commit.committer.date); + return entries.map((entry) => { + const email = entry.author_email || entry.committer_email; + const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`; + userSvc.addInfo({ + id: sub, + name: entry.author_name || entry.committer_name, + imageUrl: '', // No way to get user's avatar url... + }); + const date = entry.authored_date || entry.committed_date || 1; return { - id: sha, + id: entry.id, sub, created: date ? new Date(date).getTime() : 1, }; @@ -262,14 +272,14 @@ export default new Provider({ async getFileRevisionContent({ token, contentId, - fileSyncData, + fileSyncDataId, revisionId, }) { const { data } = await gitlabHelper.downloadFile({ ...store.getters['workspace/currentWorkspace'], token, branch: revisionId, - path: getAbsolutePath(fileSyncData), + path: getAbsolutePath({ id: fileSyncDataId }), }); return Provider.parseContent(data, contentId); }, diff --git a/src/services/providers/googleDriveAppDataProvider.js b/src/services/providers/googleDriveAppDataProvider.js index 7aeceee7..196f6354 100644 --- a/src/services/providers/googleDriveAppDataProvider.js +++ b/src/services/providers/googleDriveAppDataProvider.js @@ -167,8 +167,8 @@ export default new Provider({ }, }; }, - async listFileRevisions({ token, contentSyncData }) { - const revisions = await googleHelper.getAppDataFileRevisions(token, contentSyncData.id); + async listFileRevisions({ token, contentSyncDataId }) { + const revisions = await googleHelper.getAppDataFileRevisions(token, contentSyncDataId); return revisions.map(revision => ({ id: revision.id, sub: `${googleHelper.subPrefix}:${revision.lastModifyingUser.permissionId}`, @@ -179,9 +179,9 @@ export default new Provider({ // Revisions are already loaded return false; }, - async getFileRevisionContent({ token, contentSyncData, revisionId }) { + async getFileRevisionContent({ token, contentSyncDataId, revisionId }) { const content = await googleHelper - .downloadAppDataFileRevision(token, contentSyncData.id, revisionId); + .downloadAppDataFileRevision(token, contentSyncDataId, revisionId); return JSON.parse(content); }, }); diff --git a/src/services/providers/googleDriveWorkspaceProvider.js b/src/services/providers/googleDriveWorkspaceProvider.js index d4d8ac8d..cc7134f9 100644 --- a/src/services/providers/googleDriveWorkspaceProvider.js +++ b/src/services/providers/googleDriveWorkspaceProvider.js @@ -508,8 +508,8 @@ export default new Provider({ }, }; }, - async listFileRevisions({ token, fileSyncData }) { - const revisions = await googleHelper.getFileRevisions(token, fileSyncData.id); + async listFileRevisions({ token, fileSyncDataId }) { + const revisions = await googleHelper.getFileRevisions(token, fileSyncDataId); return revisions.map(revision => ({ id: revision.id, sub: `${googleHelper.subPrefix}:${revision.lastModifyingUser.permissionId}`, @@ -523,10 +523,10 @@ export default new Provider({ async getFileRevisionContent({ token, contentId, - fileSyncData, + fileSyncDataId, revisionId, }) { - const content = await googleHelper.downloadFileRevision(token, fileSyncData.id, revisionId); + const content = await googleHelper.downloadFileRevision(token, fileSyncDataId, revisionId); return Provider.parseContent(content, contentId); }, }); diff --git a/src/services/providers/helpers/gitlabHelper.js b/src/services/providers/helpers/gitlabHelper.js index 5ebd14ea..90fdfbb7 100644 --- a/src/services/providers/helpers/gitlabHelper.js +++ b/src/services/providers/helpers/gitlabHelper.js @@ -88,8 +88,8 @@ export default { store.dispatch('data/addGitlabToken', token); return token; }, - addAccount(serverUrl, applicationId) { - return this.startOauth2(serverUrl, applicationId); + addAccount(serverUrl, applicationId, sub = null) { + return this.startOauth2(serverUrl, applicationId, sub); }, /** @@ -133,7 +133,7 @@ export default { path, }) { return request(token, { - url: `projects/${encodeURIComponent(projectId)}/repository/tree`, + url: `projects/${encodeURIComponent(projectId)}/repository/commits`, params: { ref_name: branch, path, diff --git a/src/services/userSvc.js b/src/services/userSvc.js index fb18e8b2..8f6898db 100644 --- a/src/services/userSvc.js +++ b/src/services/userSvc.js @@ -23,7 +23,7 @@ export default { if (!loginToken) { return null; } - const loginType = store.getters['workspace/loginToken']; + const loginType = store.getters['workspace/loginType']; const prefix = subPrefixesByType[loginType]; return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub; }, @@ -44,7 +44,7 @@ export default { const [type, sub] = parseUserId(userId); // Try to find a token with this sub to resolve name as soon as possible - const token = store.getters[`data/${type}TokensBySub`][sub]; + const token = store.getters['data/tokensByType'][type][sub]; if (token) { store.commit('userInfo/addItem', { id: userId, diff --git a/src/services/utils.js b/src/services/utils.js index 5f01264c..ce7722d7 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -94,11 +94,15 @@ export default { }, sanitizeName(name) { return `${name || ''}` - // Replace `/`, control characters and other kind of spaces with a space - .replace(/[/\x00-\x1F\x7f-\xa0\s]+/g, ' ').trim() // eslint-disable-line no-control-regex // Keep only 250 characters .slice(0, 250) || constants.defaultName; }, + sanitizeFilename(name) { + return this.sanitizeName(`${name || ''}` + // Replace `/`, control characters and other kind of spaces with a space + .replace(/[/\x00-\x1F\x7f-\xa0\s]+/g, ' ') // eslint-disable-line no-control-regex + .trim()) || constants.defaultName; + }, deepCopy, serializeObject(obj) { return obj === undefined ? obj : JSON.stringify(obj, (key, value) => { @@ -128,6 +132,7 @@ export default { return array.cl_map(value => alphabet[value % radix]).join(''); }, hash(str) { + // https://stackoverflow.com/a/7616484/1333165 let hash = 0; if (!str) return hash; for (let i = 0; i < str.length; i += 1) { diff --git a/src/services/workspaceSvc.js b/src/services/workspaceSvc.js index 4aeaf138..e653a5d8 100644 --- a/src/services/workspaceSvc.js +++ b/src/services/workspaceSvc.js @@ -20,7 +20,7 @@ export default { const id = utils.uid(); const item = { id, - name: utils.sanitizeName(name), + name: utils.sanitizeFilename(name), parentId: parentId || null, }; const content = { @@ -72,7 +72,7 @@ export default { */ async storeItem(item) { const id = item.id || utils.uid(); - const sanitizedName = utils.sanitizeName(item.name); + const sanitizedName = utils.sanitizeFilename(item.name); if (item.type === 'folder' && forbiddenFolderNameMatcher.exec(sanitizedName)) { await store.dispatch('modal/open', { @@ -125,7 +125,7 @@ export default { item.parentId = patch.parentId || null; } if (patch.name) { - const sanitizedName = utils.sanitizeName(patch.name); + const sanitizedName = utils.sanitizeFilename(patch.name); if (item.type !== 'folder' || !forbiddenFolderNameMatcher.exec(sanitizedName)) { item.name = sanitizedName; } diff --git a/src/styles/app.scss b/src/styles/app.scss index 22c05148..b0ac8f23 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -186,7 +186,7 @@ textarea { &[disabled] { cursor: not-allowed; - background-color: #f2f2f2; + background-color: #f0f0f0; color: #999; } } diff --git a/src/styles/base.scss b/src/styles/base.scss index 9cbca137..18e55a2f 100644 --- a/src/styles/base.scss +++ b/src/styles/base.scss @@ -85,12 +85,12 @@ samp { blockquote { color: rgba(0, 0, 0, 0.5); padding-left: 1.5em; - border-left: 5px solid rgba(0, 0, 0, 0.075); + border-left: 5px solid rgba(0, 0, 0, 0.1); .app--dark .layout__panel--editor &, .app--dark .layout__panel--preview & { color: rgba(255, 255, 255, 0.4); - border-left-color: rgba(255, 255, 255, 0.075); + border-left-color: rgba(255, 255, 255, 0.1); } } diff --git a/src/styles/variables.scss b/src/styles/variables.scss index f34ed491..cc17493c 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -13,7 +13,7 @@ $code-border-radius: 3px; $link-color: #0c93e4; $error-color: #f31; $border-radius-base: 3px; -$hr-color: rgba(128, 128, 128, 0.2); +$hr-color: rgba(128, 128, 128, 0.33); $navbar-bg: #2c2c2c; $navbar-color: mix($navbar-bg, #fff, 33%); $navbar-hover-color: #fff;