first commit
This commit is contained in:
2176
services/web/frontend/extracted-translations.json
Normal file
2176
services/web/frontend/extracted-translations.json
Normal file
File diff suppressed because it is too large
Load Diff
13
services/web/frontend/fonts/README.md
Normal file
13
services/web/frontend/fonts/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
This directory contains fonts used by the Overleaf web application.
|
||||
|
||||
* [DM Mono](https://github.com/googlefonts/dm-mono)
|
||||
* [Font Awesome](https://fontawesome.com/v4/)
|
||||
* [Lato](https://www.latofonts.com/)
|
||||
* [Material Symbols](https://github.com/google/material-design-icons)
|
||||
* [Merriweather](https://github.com/SorkinType/Merriweather)
|
||||
* [Noto Sans](https://notofonts.github.io/#latin-greek-cyrillic)
|
||||
* [Noto Serif](https://notofonts.github.io/#latin-greek-cyrillic)
|
||||
* [OpenDyslexic Mono](https://github.com/antijingoist/opendyslexic/tree/master)
|
||||
* [Open Sans](https://github.com/googlefonts/opensans)
|
||||
* [Source Code Pro](https://github.com/adobe-fonts/source-code-pro)
|
||||
* [STIX Two Math](https://github.com/stipub/stixfonts/)
|
92
services/web/frontend/fonts/STIXTwoMath/OFL.txt
Normal file
92
services/web/frontend/fonts/STIXTwoMath/OFL.txt
Normal file
@@ -0,0 +1,92 @@
|
||||
Copyright 2001-2021 The STIX Fonts Project Authors (https://github.com/stipub/stixfonts), with Reserved Font Name "TM Math". STIX Fonts™ is a trademark of The Institute of Electrical and Electronics Engineers, Inc.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
@font-face {
|
||||
font-family: 'Stix Two Math';
|
||||
src: url('STIXTwoMath-Regular.woff2') format('woff2');
|
||||
}
|
194
services/web/frontend/fonts/build-fonts.mjs
Normal file
194
services/web/frontend/fonts/build-fonts.mjs
Normal file
@@ -0,0 +1,194 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import { createWriteStream } from 'node:fs'
|
||||
import { basename, join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { execSync } from 'node:child_process'
|
||||
import yauzl from 'yauzl'
|
||||
|
||||
// brew install woff2
|
||||
|
||||
const families = [
|
||||
{
|
||||
folder: 'dm-mono',
|
||||
url: 'https://github.com/googlefonts/dm-mono',
|
||||
fonts: [
|
||||
'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-Italic.ttf',
|
||||
'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-Medium.ttf',
|
||||
'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-MediumItalic.ttf',
|
||||
'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-Regular.ttf',
|
||||
],
|
||||
},
|
||||
{
|
||||
folder: 'font-awesome',
|
||||
url: 'https://fontawesome.com/v4/',
|
||||
archive: 'https://fontawesome.com/v4/assets/font-awesome-4.7.0.zip',
|
||||
fonts: ['font-awesome-4.7.0/fonts/fontawesome-webfont.woff2'],
|
||||
},
|
||||
{
|
||||
folder: 'lato',
|
||||
url: 'https://www.latofonts.com/',
|
||||
archive: 'https://www.latofonts.com/download/lato2oflweb-zip/',
|
||||
fonts: [
|
||||
'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-Bold.woff2',
|
||||
'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-BoldItalic.woff2',
|
||||
'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-Italic.woff2',
|
||||
'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-Regular.woff2',
|
||||
],
|
||||
},
|
||||
{
|
||||
folder: 'merriweather',
|
||||
url: 'https://github.com/SorkinType/Merriweather',
|
||||
fonts: [
|
||||
'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-Bold.woff2',
|
||||
'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-BoldItalic.woff2',
|
||||
'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-Italic.woff2',
|
||||
'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-Regular.woff2',
|
||||
],
|
||||
license:
|
||||
'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/OFL.txt',
|
||||
},
|
||||
{
|
||||
folder: 'noto-sans',
|
||||
url: 'https://notofonts.github.io/#latin-greek-cyrillic',
|
||||
fonts: [
|
||||
'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-Italic.ttf',
|
||||
'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-Medium.ttf',
|
||||
'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-MediumItalic.ttf',
|
||||
'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-Regular.ttf',
|
||||
'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-SemiBold.ttf',
|
||||
'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-SemiBoldItalic.ttf',
|
||||
],
|
||||
license:
|
||||
'https://raw.githubusercontent.com/notofonts/latin-greek-cyrillic/refs/heads/main/OFL.txt',
|
||||
},
|
||||
{
|
||||
folder: 'noto-serif',
|
||||
url: 'https://notofonts.github.io/#latin-greek-cyrillic',
|
||||
fonts: [
|
||||
'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSerif/unhinted/slim-variable-ttf/NotoSerif%5Bwght%5D.ttf',
|
||||
'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSerif/unhinted/slim-variable-ttf/NotoSerif-Italic%5Bwght%5D.ttf',
|
||||
],
|
||||
license:
|
||||
'https://raw.githubusercontent.com/notofonts/latin-greek-cyrillic/refs/heads/main/OFL.txt',
|
||||
},
|
||||
{
|
||||
folder: 'open-dyslexic-mono',
|
||||
url: 'https://github.com/antijingoist/opendyslexic',
|
||||
fonts: [
|
||||
'https://github.com/antijingoist/open-dyslexic/blob/master/otf/OpenDyslexicMono-Regular.otf',
|
||||
],
|
||||
license: 'https://github.com/antijingoist/opendyslexic/blob/master/OFL.txt',
|
||||
},
|
||||
{
|
||||
folder: 'open-sans',
|
||||
url: 'https://github.com/googlefonts/opensans',
|
||||
fonts: [
|
||||
'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-Bold.ttf',
|
||||
'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-Light.ttf',
|
||||
'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-Regular.ttf',
|
||||
'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-SemiBold.ttf',
|
||||
],
|
||||
license:
|
||||
'https://raw.githubusercontent.com/googlefonts/opensans/refs/heads/main/OFL.txt',
|
||||
},
|
||||
{
|
||||
folder: 'source-code-pro',
|
||||
url: 'https://github.com/adobe-fonts/source-code-pro',
|
||||
fonts: [
|
||||
'https://github.com/adobe-fonts/source-code-pro/raw/refs/heads/release/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2',
|
||||
],
|
||||
license:
|
||||
'https://raw.githubusercontent.com/adobe-fonts/source-code-pro/refs/heads/release/LICENSE.md',
|
||||
},
|
||||
{
|
||||
folder: 'STIXTwoMath',
|
||||
url: 'https://github.com/stipub/stixfonts',
|
||||
fonts: [
|
||||
'https://github.com/stipub/stixfonts/raw/refs/heads/master/fonts/static_otf_woff2/STIXTwoMath-Regular.woff2',
|
||||
],
|
||||
license:
|
||||
'https://raw.githubusercontent.com/stipub/stixfonts/refs/heads/master/OFL.txt',
|
||||
},
|
||||
]
|
||||
|
||||
const fetchFile = async (url, path) => {
|
||||
console.log(`${url}\n${path}`)
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText)
|
||||
}
|
||||
await fs.writeFile(path, response.body)
|
||||
}
|
||||
|
||||
for (const family of families) {
|
||||
if (!family.folder) {
|
||||
throw new Error('Missing family information')
|
||||
}
|
||||
|
||||
await fs.mkdir(family.folder, { recursive: true })
|
||||
|
||||
const fonts = new Set(family.fonts)
|
||||
|
||||
if (family.archive) {
|
||||
const dir = await fs.mkdtemp(join(tmpdir(), 'fonts-'))
|
||||
const filename = decodeURIComponent(basename(family.archive))
|
||||
const path = `${dir}/${filename}`
|
||||
await fetchFile(family.archive, path)
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
yauzl.open(path, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
zipfile.on('entry', entry => {
|
||||
if (fonts.has(entry.fileName)) {
|
||||
console.log(entry.fileName)
|
||||
zipfile.openReadStream(entry, (err, readStream) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
const path = `${family.folder}/${basename(entry.fileName)}`
|
||||
const output = createWriteStream(path)
|
||||
readStream.on('end', async () => {
|
||||
output.close()
|
||||
if (path.endsWith('.ttf')) {
|
||||
execSync(`woff2_compress "${path}"`)
|
||||
await fs.unlink(path)
|
||||
}
|
||||
zipfile.readEntry()
|
||||
})
|
||||
readStream.pipe(output)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
zipfile.readEntry()
|
||||
}
|
||||
})
|
||||
zipfile.on('error', reject)
|
||||
zipfile.on('end', resolve)
|
||||
zipfile.readEntry()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await fs.unlink(path)
|
||||
} else {
|
||||
for (const url of fonts) {
|
||||
const filename = decodeURIComponent(basename(url))
|
||||
const path = `${family.folder}/${filename}`
|
||||
await fetchFile(url, path)
|
||||
|
||||
if (path.endsWith('.ttf')) {
|
||||
execSync(`woff2_compress "${path}"`)
|
||||
await fs.unlink(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (family.license) {
|
||||
const url = family.license
|
||||
const filename = decodeURIComponent(basename(url))
|
||||
const path = `${family.folder}/${filename}`
|
||||
await fetchFile(url, path)
|
||||
}
|
||||
}
|
BIN
services/web/frontend/fonts/dm-mono/DMMono-Italic.woff2
Normal file
BIN
services/web/frontend/fonts/dm-mono/DMMono-Italic.woff2
Normal file
Binary file not shown.
BIN
services/web/frontend/fonts/dm-mono/DMMono-Medium.woff2
Normal file
BIN
services/web/frontend/fonts/dm-mono/DMMono-Medium.woff2
Normal file
Binary file not shown.
BIN
services/web/frontend/fonts/dm-mono/DMMono-MediumItalic.woff2
Normal file
BIN
services/web/frontend/fonts/dm-mono/DMMono-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
services/web/frontend/fonts/dm-mono/DMMono-Regular.woff2
Normal file
BIN
services/web/frontend/fonts/dm-mono/DMMono-Regular.woff2
Normal file
Binary file not shown.
93
services/web/frontend/fonts/dm-mono/OFL.txt
Normal file
93
services/web/frontend/fonts/dm-mono/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The DM Mono Project Authors (https://www.github.com/googlefonts/dm-mono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
31
services/web/frontend/fonts/dm-mono/dm-mono.css
Normal file
31
services/web/frontend/fonts/dm-mono/dm-mono.css
Normal file
@@ -0,0 +1,31 @@
|
||||
@font-face {
|
||||
font-family: 'DM Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url('DMMono-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DM Mono';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url('DMMono-Italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DM Mono';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: fallback;
|
||||
src: url('DMMono-Medium.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DM Mono';
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: fallback;
|
||||
src: url('DMMono-MediumItalic.woff2') format('woff2');
|
||||
}
|
165
services/web/frontend/fonts/font-awesome/LICENSE.txt
Normal file
165
services/web/frontend/fonts/font-awesome/LICENSE.txt
Normal file
@@ -0,0 +1,165 @@
|
||||
Fonticons, Inc. (https://fontawesome.com)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Font Awesome Free License
|
||||
|
||||
Font Awesome Free is free, open source, and GPL friendly. You can use it for
|
||||
commercial projects, open source projects, or really almost whatever you want.
|
||||
Full Font Awesome Free license: https://fontawesome.com/license/free.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
The Font Awesome Free download is licensed under a Creative Commons
|
||||
Attribution 4.0 International License and applies to all icons packaged
|
||||
as SVG and JS file types.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Fonts: SIL OFL 1.1 License
|
||||
|
||||
In the Font Awesome Free download, the SIL OFL license applies to all icons
|
||||
packaged as web and desktop font files.
|
||||
|
||||
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
|
||||
with Reserved Font Name: "Font Awesome".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
SIL OPEN FONT LICENSE
|
||||
Version 1.1 - 26 February 2007
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting — in part or in whole — any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Code: MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
In the Font Awesome Free download, the MIT license applies to all non-font and
|
||||
non-icon files.
|
||||
|
||||
Copyright 2024 Fonticons, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without limitation the rights to use, copy,
|
||||
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Attribution
|
||||
|
||||
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
|
||||
Awesome Free files already contain embedded comments with sufficient
|
||||
attribution, so you shouldn't need to do anything additional when using these
|
||||
files normally.
|
||||
|
||||
We've kept attribution comments terse, so we ask that you do not actively work
|
||||
to remove them from files, especially code. They're a great way for folks to
|
||||
learn about Font Awesome.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Brand Icons
|
||||
|
||||
All brand icons are trademarks of their respective owners. The use of these
|
||||
trademarks does not indicate endorsement of the trademark holder by Font
|
||||
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
|
||||
to represent the company, product, or service to which they refer.**
|
2334
services/web/frontend/fonts/font-awesome/font-awesome.css
vendored
Normal file
2334
services/web/frontend/fonts/font-awesome/font-awesome.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
BIN
services/web/frontend/fonts/lato/LatoLatin-Bold.woff2
Normal file
BIN
services/web/frontend/fonts/lato/LatoLatin-Bold.woff2
Normal file
Binary file not shown.
BIN
services/web/frontend/fonts/lato/LatoLatin-BoldItalic.woff2
Normal file
BIN
services/web/frontend/fonts/lato/LatoLatin-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
services/web/frontend/fonts/lato/LatoLatin-Italic.woff2
Normal file
BIN
services/web/frontend/fonts/lato/LatoLatin-Italic.woff2
Normal file
Binary file not shown.
BIN
services/web/frontend/fonts/lato/LatoLatin-Regular.woff2
Normal file
BIN
services/web/frontend/fonts/lato/LatoLatin-Regular.woff2
Normal file
Binary file not shown.
94
services/web/frontend/fonts/lato/OFL.txt
Executable file
94
services/web/frontend/fonts/lato/OFL.txt
Executable file
@@ -0,0 +1,94 @@
|
||||
Copyright (c) 2010-2015, Łukasz Dziedzic (dziedzic@typoland.com),
|
||||
with Reserved Font Name Lato.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
27
services/web/frontend/fonts/lato/lato.css
Normal file
27
services/web/frontend/fonts/lato/lato.css
Normal file
@@ -0,0 +1,27 @@
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('LatoLatin-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url('LatoLatin-Italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('LatoLatin-Bold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url('LatoLatin-BoldItalic.woff2') format('woff2');
|
||||
}
|
202
services/web/frontend/fonts/material-symbols/LICENSE.txt
Normal file
202
services/web/frontend/fonts/material-symbols/LICENSE.txt
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,48 @@
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs/promises'
|
||||
import icons from './unfilled-symbols.mjs'
|
||||
|
||||
const iconList = [...new Set(icons)].sort().map(encodeURIComponent).join(',')
|
||||
|
||||
const url = `https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20,400,0,0&icon_names=${iconList}&display=block`
|
||||
console.log(`Fetching font configuration from ${url}`)
|
||||
|
||||
const cssFile = await (
|
||||
await fetch(url, {
|
||||
headers: {
|
||||
// Specify a user agent to get a woff2 file
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||
},
|
||||
})
|
||||
).text()
|
||||
|
||||
const woff2UrlText = cssFile.match(/url\(([^)]+)\) format\('woff2'\)/)?.[1]
|
||||
|
||||
if (!woff2UrlText) {
|
||||
throw new Error(
|
||||
'Could not find woff2 URL in CSS file, try accessing the font configuration URL to check whether an error is reported'
|
||||
)
|
||||
}
|
||||
|
||||
const woff2Url = new URL(woff2UrlText)
|
||||
if (woff2Url.protocol !== 'https:') {
|
||||
throw new Error(`Expected HTTPS URL, got ${woff2Url.protocol}`)
|
||||
}
|
||||
if (woff2Url.hostname !== 'fonts.gstatic.com') {
|
||||
throw new Error(
|
||||
`Expected to download font from fonts.gstatic.com, got ${woff2Url.hostname}`
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`Fetching woff2 file: ${woff2Url}`)
|
||||
|
||||
const outputPath = path.join(
|
||||
import.meta.dirname,
|
||||
'MaterialSymbolsRoundedUnfilledPartialSlice.woff2'
|
||||
)
|
||||
|
||||
const res = await fetch(woff2Url)
|
||||
|
||||
console.log(`Saving font file to ${outputPath}`)
|
||||
await fs.writeFile(outputPath, res.body)
|
@@ -0,0 +1,60 @@
|
||||
@font-face {
|
||||
font-family: 'Material Symbols Rounded';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
/*
|
||||
Generated from MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2 with specific values:
|
||||
'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 20
|
||||
*/
|
||||
src: url('MaterialSymbolsRoundedSlice.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Symbols Rounded Unfilled Partial';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
/*
|
||||
Generated by frontend/fonts/material-symbols/build-unfilled.mjs
|
||||
*/
|
||||
src: url('MaterialSymbolsRoundedUnfilledPartialSlice.woff2') format('woff2');
|
||||
}
|
||||
|
||||
.material-symbols {
|
||||
font-family: 'Material Symbols Rounded';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 20px;
|
||||
font-variation-settings:
|
||||
'FILL' 1,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 20;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&.size-2x {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
&.rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
&.unfilled {
|
||||
font-family: 'Material Symbols Rounded Unfilled Partial';
|
||||
font-variation-settings:
|
||||
'FILL' 0,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 20;
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
// @ts-check
|
||||
// Make sure to run the build-unfilled.mjs script after updating this list
|
||||
// to update the font file with the latest icons.
|
||||
// You may need to hard reload your browser window to see the changes.
|
||||
|
||||
export default /** @type {const} */ ([
|
||||
'book_5',
|
||||
'brush',
|
||||
'code',
|
||||
'create_new_folder',
|
||||
'description',
|
||||
'experiment',
|
||||
'forum',
|
||||
'help',
|
||||
'image',
|
||||
'info',
|
||||
'integration_instructions',
|
||||
'note_add',
|
||||
'picture_as_pdf',
|
||||
'rate_review',
|
||||
'report',
|
||||
'settings',
|
||||
'space_dashboard',
|
||||
'table_chart',
|
||||
'upload_file',
|
||||
'web_asset',
|
||||
'error',
|
||||
])
|
BIN
services/web/frontend/fonts/merriweather/Merriweather-Bold.woff2
Normal file
BIN
services/web/frontend/fonts/merriweather/Merriweather-Bold.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
93
services/web/frontend/fonts/merriweather/OFL.txt
Normal file
93
services/web/frontend/fonts/merriweather/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Merriweather Project Authors (https://github.com/EbenSorkin/Merriweather), with Reserved Font Name "Merriweather".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
24
services/web/frontend/fonts/merriweather/merriweather.css
Normal file
24
services/web/frontend/fonts/merriweather/merriweather.css
Normal file
@@ -0,0 +1,24 @@
|
||||
@font-face {
|
||||
font-family: 'Merriweather';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('Merriweather-Regular.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Merriweather';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url('Merriweather-Italic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Merriweather';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('Merriweather-Bold.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Merriweather';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url('Merriweather-BoldItalic.woff2') format('woff2');
|
||||
}
|
BIN
services/web/frontend/fonts/noto-sans/NotoSans-Italic.woff2
Normal file
BIN
services/web/frontend/fonts/noto-sans/NotoSans-Italic.woff2
Normal file
Binary file not shown.
BIN
services/web/frontend/fonts/noto-sans/NotoSans-Medium.woff2
Normal file
BIN
services/web/frontend/fonts/noto-sans/NotoSans-Medium.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
services/web/frontend/fonts/noto-sans/NotoSans-Regular.woff2
Normal file
BIN
services/web/frontend/fonts/noto-sans/NotoSans-Regular.woff2
Normal file
Binary file not shown.
BIN
services/web/frontend/fonts/noto-sans/NotoSans-SemiBold.woff2
Normal file
BIN
services/web/frontend/fonts/noto-sans/NotoSans-SemiBold.woff2
Normal file
Binary file not shown.
Binary file not shown.
93
services/web/frontend/fonts/noto-sans/OFL.txt
Normal file
93
services/web/frontend/fonts/noto-sans/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
50
services/web/frontend/fonts/noto-sans/noto-sans.css
Normal file
50
services/web/frontend/fonts/noto-sans/noto-sans.css
Normal file
@@ -0,0 +1,50 @@
|
||||
/* Noto Sans - Regular */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url('NotoSans-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url('NotoSans-Italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* Noto Sans - Medium */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: fallback;
|
||||
src: url('NotoSans-Medium.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: fallback;
|
||||
src: url('NotoSans-MediumItalic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* Noto Sans - SemiBold */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: fallback;
|
||||
src: url('NotoSans-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: fallback;
|
||||
src: url('NotoSans-SemiBoldItalic.woff2') format('woff2');
|
||||
}
|
Binary file not shown.
BIN
services/web/frontend/fonts/noto-serif/NotoSerif[wght].woff2
Normal file
BIN
services/web/frontend/fonts/noto-serif/NotoSerif[wght].woff2
Normal file
Binary file not shown.
93
services/web/frontend/fonts/noto-serif/OFL.txt
Normal file
93
services/web/frontend/fonts/noto-serif/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
15
services/web/frontend/fonts/noto-serif/noto-serif.css
Normal file
15
services/web/frontend/fonts/noto-serif/noto-serif.css
Normal file
@@ -0,0 +1,15 @@
|
||||
@font-face {
|
||||
font-family: 'Noto Serif';
|
||||
font-style: normal;
|
||||
font-weight: 200 700;
|
||||
font-stretch: 62.5% 100%;
|
||||
src: url('NotoSerif[wght].woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Serif';
|
||||
font-style: italic;
|
||||
font-weight: 200 700;
|
||||
font-stretch: 62.5% 100%;
|
||||
src: url('NotoSerif-Italic[wght].woff2') format('woff2');
|
||||
}
|
94
services/web/frontend/fonts/open-dyslexic-mono/OFL.txt
Normal file
94
services/web/frontend/fonts/open-dyslexic-mono/OFL.txt
Normal file
@@ -0,0 +1,94 @@
|
||||
Copyright (c) 2019-07-29, Abbie Gonzalez (https://abbiecod.es|support@abbiecod.es),
|
||||
with Reserved Font Name OpenDyslexic.
|
||||
Copyright (c) 12/2012 - 2019
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
@font-face {
|
||||
font-family: 'OpenDyslexic Mono';
|
||||
src: url('./OpenDyslexicMono-Regular.woff2') format('woff2');
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
ascent-override: 100%;
|
||||
}
|
88
services/web/frontend/fonts/open-sans/OFL.txt
Normal file
88
services/web/frontend/fonts/open-sans/OFL.txt
Normal file
@@ -0,0 +1,88 @@
|
||||
Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
services/web/frontend/fonts/open-sans/OpenSans-Bold.woff2
Normal file
BIN
services/web/frontend/fonts/open-sans/OpenSans-Bold.woff2
Normal file
Binary file not shown.
BIN
services/web/frontend/fonts/open-sans/OpenSans-Light.woff2
Normal file
BIN
services/web/frontend/fonts/open-sans/OpenSans-Light.woff2
Normal file
Binary file not shown.
BIN
services/web/frontend/fonts/open-sans/OpenSans-Regular.woff2
Normal file
BIN
services/web/frontend/fonts/open-sans/OpenSans-Regular.woff2
Normal file
Binary file not shown.
BIN
services/web/frontend/fonts/open-sans/OpenSans-SemiBold.woff2
Normal file
BIN
services/web/frontend/fonts/open-sans/OpenSans-SemiBold.woff2
Normal file
Binary file not shown.
24
services/web/frontend/fonts/open-sans/open-sans.css
Normal file
24
services/web/frontend/fonts/open-sans/open-sans.css
Normal file
@@ -0,0 +1,24 @@
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url('OpenSans-Light.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('OpenSans-Regular.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('OpenSans-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('OpenSans-Bold.woff2') format('woff2');
|
||||
}
|
93
services/web/frontend/fonts/source-code-pro/LICENSE.md
Normal file
93
services/web/frontend/fonts/source-code-pro/LICENSE.md
Normal file
@@ -0,0 +1,93 @@
|
||||
© 2023 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'Source Code Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('SourceCodePro-Regular.ttf.woff2') format('woff2');
|
||||
}
|
4
services/web/frontend/js/bootstrap-3.ts
Normal file
4
services/web/frontend/js/bootstrap-3.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import 'bootstrap'
|
||||
import './features/contact-form'
|
||||
import './features/bookmarkable-tab/index'
|
||||
import './features/tooltip/index-bs3'
|
3
services/web/frontend/js/bootstrap-5.ts
Normal file
3
services/web/frontend/js/bootstrap-5.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import './features/bookmarkable-tab/index-bs5'
|
||||
import './features/tooltip/index-bs5'
|
||||
import 'bootstrap-5'
|
5
services/web/frontend/js/dev-toolbar.ts
Normal file
5
services/web/frontend/js/dev-toolbar.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import importOverleafModules from '../macros/import-overleaf-module.macro'
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
importOverleafModules('devToolbar')
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
import _ from 'lodash'
|
||||
import AlgoliaSearch from 'algoliasearch'
|
||||
import getMeta from '../../utils/meta'
|
||||
|
||||
let wikiIdx
|
||||
export async function searchWiki(...args) {
|
||||
if (!wikiIdx) {
|
||||
const algoliaConfig = getMeta('ol-algolia')
|
||||
const wikiIndex = _.get(algoliaConfig, 'indexes.wiki')
|
||||
if (wikiIndex) {
|
||||
const client = AlgoliaSearch(algoliaConfig.appId, algoliaConfig.apiKey)
|
||||
wikiIdx = client.initIndex(wikiIndex)
|
||||
}
|
||||
}
|
||||
if (!wikiIdx) {
|
||||
return { hits: [], nbHits: 0, nbPages: 0 }
|
||||
}
|
||||
return wikiIdx.search(...args)
|
||||
}
|
||||
|
||||
export function formatWikiHit(hit) {
|
||||
const pageUnderscored = hit.pageName.replace(/\s/g, '_')
|
||||
const pageSlug = encodeURIComponent(pageUnderscored)
|
||||
const pagePath = hit.kb ? 'how-to' : 'latex'
|
||||
|
||||
let pageAnchor = ''
|
||||
const rawPageName = hit._highlightResult.pageName.value
|
||||
const sectionName = hit.sectionName
|
||||
let pageName = rawPageName
|
||||
if (sectionName) {
|
||||
pageAnchor = `#${sectionName.replace(/\s/g, '_')}`
|
||||
pageName += ' - ' + sectionName
|
||||
}
|
||||
|
||||
const body = hit._highlightResult.content.value
|
||||
const content = body
|
||||
.split('\n')
|
||||
.filter(line => line.includes('<em>') && !line.includes('[edit]'))
|
||||
.join('\n...\n')
|
||||
|
||||
const url = `/learn/${pagePath}/${pageSlug}${pageAnchor}`
|
||||
return { url, pageName, rawPageName, sectionName, content }
|
||||
}
|
56
services/web/frontend/js/features/autoplay-video/index.js
Normal file
56
services/web/frontend/js/features/autoplay-video/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
function setup(videoEl) {
|
||||
const reducedMotionReduce = window.matchMedia(
|
||||
'(prefers-reduced-motion: reduce)'
|
||||
)
|
||||
|
||||
if (reducedMotionReduce.matches) {
|
||||
// TODO: on firefox, if user enters this mode, video can throw error
|
||||
// in console, if user seeks the control seek bar relatively fast
|
||||
// AbortError: The fetching process for the media resource was aborted by the user agent at the user's request.
|
||||
// this is only a problem in firefox (tested in macOS), chrome and safari is fine
|
||||
videoEl.setAttribute('controls', '')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const DELAY_BEFORE_REPLAY = 15 * 1000
|
||||
// 0.7 will enable the autoplay on the desktop main homepage video for all users
|
||||
const INTERSECTION_THRESHOLD = 0.7
|
||||
|
||||
let videoIsVisible
|
||||
|
||||
videoEl.addEventListener('ended', () => {
|
||||
setTimeout(() => {
|
||||
videoEl.currentTime = 0
|
||||
if (videoIsVisible) {
|
||||
videoEl.play()
|
||||
}
|
||||
}, DELAY_BEFORE_REPLAY)
|
||||
})
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
function onIntersecting(changes) {
|
||||
for (const change of changes) {
|
||||
if (change.isIntersecting) {
|
||||
videoIsVisible = true
|
||||
if (videoEl.readyState >= videoEl.HAVE_FUTURE_DATA) {
|
||||
if (!videoEl.ended) {
|
||||
videoEl.play()
|
||||
}
|
||||
} else {
|
||||
videoEl.play()
|
||||
}
|
||||
} else {
|
||||
videoIsVisible = false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
threshold: INTERSECTION_THRESHOLD,
|
||||
}
|
||||
)
|
||||
|
||||
observer.observe(videoEl)
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-ol-autoplay-video]').forEach(setup)
|
@@ -0,0 +1,30 @@
|
||||
import { Tab } from 'bootstrap-5'
|
||||
|
||||
function bookmarkableTab(tabEl: HTMLElement) {
|
||||
tabEl.addEventListener('click', () => {
|
||||
window.location.hash = tabEl.getAttribute('href') as string
|
||||
})
|
||||
}
|
||||
|
||||
function handleHashChange() {
|
||||
const hash = window.location.hash
|
||||
if (!hash) return
|
||||
|
||||
// Find the bookmarkable tab that links to the hash
|
||||
const tabEl = document.querySelector(
|
||||
`[data-ol-bookmarkable-tab][href="${hash}"]`
|
||||
)
|
||||
|
||||
if (!tabEl) return
|
||||
|
||||
// Select the tab via Bootstrap 5
|
||||
const tab = new Tab(tabEl)
|
||||
tab.show()
|
||||
}
|
||||
|
||||
document
|
||||
.querySelectorAll('[data-ol-bookmarkable-tab]')
|
||||
.forEach(tabEl => bookmarkableTab(tabEl as HTMLElement))
|
||||
|
||||
window.addEventListener('hashchange', handleHashChange)
|
||||
handleHashChange()
|
21
services/web/frontend/js/features/bookmarkable-tab/index.js
Normal file
21
services/web/frontend/js/features/bookmarkable-tab/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
function bookmarkableTab(tabEl) {
|
||||
tabEl.addEventListener('click', () => {
|
||||
window.location.hash = tabEl.getAttribute('href')
|
||||
})
|
||||
}
|
||||
|
||||
function handleHashChange() {
|
||||
const hash = window.location.hash
|
||||
if (!hash) return
|
||||
|
||||
// Find the bookmarkable tab that links to the hash
|
||||
const $tabEl = $(`[data-ol-bookmarkable-tab][href="${hash}"]`)
|
||||
if (!$tabEl) return
|
||||
|
||||
// Select the tab via Bootstrap
|
||||
$tabEl.tab('show')
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-ol-bookmarkable-tab]').forEach(bookmarkableTab)
|
||||
window.addEventListener('hashchange', handleHashChange)
|
||||
handleHashChange()
|
@@ -0,0 +1,28 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
interface ChatFallbackErrorProps {
|
||||
reconnect?: () => void
|
||||
}
|
||||
|
||||
function ChatFallbackError({ reconnect }: ChatFallbackErrorProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<aside className="chat">
|
||||
<div className="chat-error">
|
||||
<OLNotification type="error" content={t('chat_error')} />
|
||||
{reconnect && (
|
||||
<p className="text-center">
|
||||
<OLButton variant="secondary" onClick={reconnect}>
|
||||
{t('reconnect')}
|
||||
</OLButton>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatFallbackError
|
117
services/web/frontend/js/features/chat/components/chat-pane.tsx
Normal file
117
services/web/frontend/js/features/chat/components/chat-pane.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import React, { lazy, Suspense, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import MessageInput from './message-input'
|
||||
import InfiniteScroll from './infinite-scroll'
|
||||
import ChatFallbackError from './chat-fallback-error'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
import { FetchError } from '../../../infrastructure/fetch-json'
|
||||
import { useChatContext } from '../context/chat-context'
|
||||
import { FullSizeLoadingSpinner } from '../../../shared/components/loading-spinner'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
const MessageList = lazy(() => import('./message-list'))
|
||||
|
||||
const Loading = () => <FullSizeLoadingSpinner delay={500} className="pt-4" />
|
||||
|
||||
const ChatPane = React.memo(function ChatPane() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { chatIsOpen } = useLayoutContext()
|
||||
const user = useUserContext()
|
||||
|
||||
const {
|
||||
status,
|
||||
messages,
|
||||
initialMessagesLoaded,
|
||||
atEnd,
|
||||
loadInitialMessages,
|
||||
loadMoreMessages,
|
||||
reset,
|
||||
sendMessage,
|
||||
markMessagesAsRead,
|
||||
error,
|
||||
} = useChatContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (chatIsOpen && !initialMessagesLoaded) {
|
||||
loadInitialMessages()
|
||||
}
|
||||
}, [chatIsOpen, loadInitialMessages, initialMessagesLoaded])
|
||||
|
||||
const shouldDisplayPlaceholder = status !== 'pending' && messages.length === 0
|
||||
|
||||
const messageContentCount = messages.reduce(
|
||||
(acc, { contents }) => acc + contents.length,
|
||||
0
|
||||
)
|
||||
|
||||
// Keep the chat pane in the DOM to avoid resetting the form input and re-rendering MathJax content.
|
||||
const [chatOpenedOnce, setChatOpenedOnce] = useState(chatIsOpen)
|
||||
useEffect(() => {
|
||||
if (chatIsOpen) {
|
||||
setChatOpenedOnce(true)
|
||||
}
|
||||
}, [chatIsOpen])
|
||||
|
||||
if (error) {
|
||||
// let user try recover from fetch errors
|
||||
if (error instanceof FetchError) {
|
||||
return <ChatFallbackError reconnect={reset} />
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
if (!chatOpenedOnce) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="chat">
|
||||
<InfiniteScroll
|
||||
atEnd={atEnd}
|
||||
className="messages"
|
||||
fetchData={loadMoreMessages}
|
||||
isLoading={status === 'pending'}
|
||||
itemCount={messageContentCount}
|
||||
>
|
||||
<div>
|
||||
<h2 className="visually-hidden">{t('chat')}</h2>
|
||||
<Suspense fallback={<Loading />}>
|
||||
{status === 'pending' && <Loading />}
|
||||
{shouldDisplayPlaceholder && <Placeholder />}
|
||||
<MessageList
|
||||
messages={messages}
|
||||
resetUnreadMessages={markMessagesAsRead}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
<MessageInput
|
||||
resetUnreadMessages={markMessagesAsRead}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
</aside>
|
||||
)
|
||||
})
|
||||
|
||||
function Placeholder() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<div className="no-messages text-center small">{t('no_messages')}</div>
|
||||
<div className="first-message text-center">
|
||||
{t('send_first_message')}
|
||||
<br />
|
||||
<MaterialIcon type="arrow_downward" />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withErrorBoundary(ChatPane, ChatFallbackError)
|
@@ -0,0 +1,95 @@
|
||||
import { useRef, useEffect, useLayoutEffect } from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
const SCROLL_END_OFFSET = 30
|
||||
|
||||
interface InfiniteScrollProps {
|
||||
atEnd?: boolean
|
||||
children: React.ReactElement
|
||||
className?: string
|
||||
fetchData(): void
|
||||
itemCount: number
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
function InfiniteScroll({
|
||||
atEnd,
|
||||
children,
|
||||
className = '',
|
||||
fetchData,
|
||||
itemCount,
|
||||
isLoading,
|
||||
}: InfiniteScrollProps) {
|
||||
const root = useRef<HTMLDivElement>(null)
|
||||
|
||||
// we keep the value in a Ref instead of state so it can be safely used in effects
|
||||
const scrollBottomRef = useRef(0)
|
||||
function setScrollBottom(value: number) {
|
||||
scrollBottomRef.current = value
|
||||
}
|
||||
|
||||
function updateScrollPosition() {
|
||||
if (root.current) {
|
||||
root.current.scrollTop =
|
||||
root.current.scrollHeight -
|
||||
root.current.clientHeight -
|
||||
scrollBottomRef.current
|
||||
}
|
||||
}
|
||||
|
||||
// Repositions the scroll after new items are loaded
|
||||
useLayoutEffect(updateScrollPosition, [itemCount])
|
||||
|
||||
// Repositions the scroll after a window resize
|
||||
useEffect(() => {
|
||||
const handleResize = _.debounce(updateScrollPosition, 400)
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}, [])
|
||||
|
||||
function onScrollHandler(event: React.UIEvent<HTMLDivElement>) {
|
||||
if (root.current) {
|
||||
setScrollBottom(
|
||||
root.current.scrollHeight -
|
||||
root.current.scrollTop -
|
||||
root.current.clientHeight
|
||||
)
|
||||
|
||||
if (event.target !== event.currentTarget) {
|
||||
// Ignore scroll events on nested divs
|
||||
// (this check won't be necessary in React 17: https://github.com/facebook/react/issues/15723
|
||||
return
|
||||
}
|
||||
if (shouldFetchData()) {
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shouldFetchData() {
|
||||
if (!root.current) {
|
||||
return false
|
||||
}
|
||||
const containerIsLargerThanContent =
|
||||
root.current.children[0].clientHeight < root.current.clientHeight
|
||||
if (atEnd || isLoading || containerIsLargerThanContent) {
|
||||
return false
|
||||
} else {
|
||||
return root.current.scrollTop < SCROLL_END_OFFSET
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={root}
|
||||
onScroll={onScrollHandler}
|
||||
className={`infinite-scroll ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfiniteScroll
|
@@ -0,0 +1,43 @@
|
||||
import { useRef, useEffect, type FC } from 'react'
|
||||
import Linkify from 'react-linkify'
|
||||
import useIsMounted from '../../../shared/hooks/use-is-mounted'
|
||||
import { loadMathJax } from '../../mathjax/load-mathjax'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
const MessageContent: FC<{ content: string }> = ({ content }) => {
|
||||
const root = useRef<HTMLDivElement | null>(null)
|
||||
const mounted = useIsMounted()
|
||||
|
||||
useEffect(() => {
|
||||
if (root.current) {
|
||||
// adds attributes to all the links generated by <Linkify/>, required due to https://github.com/tasti/react-linkify/issues/99
|
||||
for (const a of root.current.getElementsByTagName('a')) {
|
||||
a.setAttribute('target', '_blank')
|
||||
a.setAttribute('rel', 'noreferrer noopener')
|
||||
}
|
||||
|
||||
// MathJax v3 typesetting
|
||||
loadMathJax()
|
||||
.then(async MathJax => {
|
||||
if (mounted.current) {
|
||||
const element = root.current
|
||||
try {
|
||||
await MathJax.typesetPromise([element])
|
||||
MathJax.typesetClear([element])
|
||||
} catch (error) {
|
||||
debugConsole.error(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(debugConsole.error)
|
||||
}
|
||||
}, [content, mounted])
|
||||
|
||||
return (
|
||||
<p ref={root}>
|
||||
<Linkify>{content}</Linkify>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessageContent
|
@@ -0,0 +1,42 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type MessageInputProps = {
|
||||
resetUnreadMessages: () => void
|
||||
sendMessage: (message: string) => void
|
||||
}
|
||||
|
||||
function MessageInput({ resetUnreadMessages, sendMessage }: MessageInputProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent) {
|
||||
const selectingCharacter = event.nativeEvent.isComposing
|
||||
if (event.key === 'Enter' && !selectingCharacter) {
|
||||
event.preventDefault()
|
||||
const target = event.target as HTMLInputElement
|
||||
sendMessage(target.value)
|
||||
// wrap the form reset in setTimeout so input sources have time to finish
|
||||
// https://github.com/overleaf/internal/pull/9206
|
||||
window.setTimeout(() => {
|
||||
target.blur()
|
||||
target.closest('form')?.reset()
|
||||
target.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="new-message">
|
||||
<label htmlFor="chat-input" className="visually-hidden">
|
||||
{t('your_message_to_collaborators')}
|
||||
</label>
|
||||
<textarea
|
||||
id="chat-input"
|
||||
placeholder={`${t('your_message_to_collaborators')}…`}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={resetUnreadMessages}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessageInput
|
@@ -0,0 +1,77 @@
|
||||
import moment from 'moment'
|
||||
import Message from './message'
|
||||
import type { Message as MessageType } from '@/features/chat/context/chat-context'
|
||||
import MessageRedesign from '@/features/ide-redesign/components/chat/message'
|
||||
import { useUserContext } from '@/shared/context/user-context'
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000
|
||||
|
||||
function formatTimestamp(date: moment.MomentInput) {
|
||||
if (!date) {
|
||||
return 'N/A'
|
||||
} else {
|
||||
return `${moment(date).format('h:mm a')} ${moment(date).calendar()}`
|
||||
}
|
||||
}
|
||||
|
||||
interface MessageListProps {
|
||||
messages: MessageType[]
|
||||
resetUnreadMessages(...args: unknown[]): unknown
|
||||
newDesign?: boolean
|
||||
}
|
||||
|
||||
function MessageList({
|
||||
messages,
|
||||
resetUnreadMessages,
|
||||
newDesign,
|
||||
}: MessageListProps) {
|
||||
const user = useUserContext()
|
||||
const MessageComponent = newDesign ? MessageRedesign : Message
|
||||
function shouldRenderDate(messageIndex: number) {
|
||||
if (messageIndex === 0) {
|
||||
return true
|
||||
} else {
|
||||
const message = messages[messageIndex]
|
||||
const previousMessage = messages[messageIndex - 1]
|
||||
return (
|
||||
message.timestamp &&
|
||||
previousMessage.timestamp &&
|
||||
message.timestamp - previousMessage.timestamp > FIVE_MINUTES
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
onClick={resetUnreadMessages}
|
||||
onKeyDown={resetUnreadMessages}
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
// new messages are added to the beginning of the list, so we use a reversed index
|
||||
<li key={message.id} className="message">
|
||||
{shouldRenderDate(index) && (
|
||||
<div className="date">
|
||||
<time
|
||||
dateTime={
|
||||
message.timestamp
|
||||
? moment(message.timestamp).format()
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{formatTimestamp(message.timestamp)}
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
<MessageComponent
|
||||
message={message}
|
||||
fromSelf={message.user ? message.user.id === user.id : false}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessageList
|
@@ -0,0 +1,50 @@
|
||||
import { getHueForUserId } from '@/shared/utils/colors'
|
||||
import MessageContent from './message-content'
|
||||
import type { Message as MessageType } from '@/features/chat/context/chat-context'
|
||||
import { User } from '../../../../../types/user'
|
||||
|
||||
export interface MessageProps {
|
||||
message: MessageType
|
||||
fromSelf: boolean
|
||||
}
|
||||
|
||||
function hue(user?: User) {
|
||||
return user ? getHueForUserId(user.id) : 0
|
||||
}
|
||||
|
||||
function getMessageStyle(user?: User) {
|
||||
return {
|
||||
borderColor: `hsl(${hue(user)}, 85%, 40%)`,
|
||||
backgroundColor: `hsl(${hue(user)}, 85%, 40%`,
|
||||
}
|
||||
}
|
||||
|
||||
function getArrowStyle(user?: User) {
|
||||
return {
|
||||
borderColor: `hsl(${hue(user)}, 85%, 40%)`,
|
||||
}
|
||||
}
|
||||
|
||||
function Message({ message, fromSelf }: MessageProps) {
|
||||
return (
|
||||
<div className="message-wrapper">
|
||||
{!fromSelf && (
|
||||
<div className="name">
|
||||
<span>{message.user.first_name || message.user.email}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="message" style={getMessageStyle(message.user)}>
|
||||
{!fromSelf && (
|
||||
<div className="arrow" style={getArrowStyle(message.user)} />
|
||||
)}
|
||||
<div className="message-content">
|
||||
{message.contents.map((content, index) => (
|
||||
<MessageContent key={index} content={content} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Message
|
399
services/web/frontend/js/features/chat/context/chat-context.tsx
Normal file
399
services/web/frontend/js/features/chat/context/chat-context.tsx
Normal file
@@ -0,0 +1,399 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useReducer,
|
||||
useMemo,
|
||||
useRef,
|
||||
FC,
|
||||
} from 'react'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import { getJSON, postJSON } from '../../../infrastructure/fetch-json'
|
||||
import { appendMessage, prependMessages } from '../utils/message-list-appender'
|
||||
import useBrowserWindow from '../../../shared/hooks/use-browser-window'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import { useIdeContext } from '@/shared/context/ide-context'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { User } from '../../../../../types/user'
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export type Message = {
|
||||
id: string
|
||||
timestamp: number
|
||||
contents: string[]
|
||||
user: User
|
||||
}
|
||||
|
||||
type State = {
|
||||
status: 'idle' | 'pending' | 'error'
|
||||
messages: Message[]
|
||||
initialMessagesLoaded: boolean
|
||||
lastTimestamp: number | null
|
||||
atEnd: boolean
|
||||
unreadMessageCount: number
|
||||
error?: Error | null
|
||||
uniqueMessageIds: string[]
|
||||
}
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: 'INITIAL_FETCH_MESSAGES'
|
||||
}
|
||||
| {
|
||||
type: 'FETCH_MESSAGES'
|
||||
}
|
||||
| {
|
||||
type: 'FETCH_MESSAGES_SUCCESS'
|
||||
messages: Message[]
|
||||
}
|
||||
| {
|
||||
type: 'SEND_MESSAGE'
|
||||
user: any
|
||||
content: any
|
||||
}
|
||||
| {
|
||||
type: 'RECEIVE_MESSAGE'
|
||||
message: any
|
||||
}
|
||||
| {
|
||||
type: 'MARK_MESSAGES_AS_READ'
|
||||
}
|
||||
| {
|
||||
type: 'CLEAR'
|
||||
}
|
||||
| {
|
||||
type: 'ERROR'
|
||||
error: any
|
||||
}
|
||||
|
||||
// Wrap uuid in an object method so that it can be stubbed
|
||||
export const chatClientIdGenerator = {
|
||||
generate: () => uuid(),
|
||||
}
|
||||
|
||||
let nextChatMessageId = 1
|
||||
|
||||
function generateChatMessageId() {
|
||||
return '' + nextChatMessageId++
|
||||
}
|
||||
|
||||
function chatReducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'INITIAL_FETCH_MESSAGES':
|
||||
return {
|
||||
...state,
|
||||
status: 'pending',
|
||||
initialMessagesLoaded: true,
|
||||
}
|
||||
|
||||
case 'FETCH_MESSAGES':
|
||||
return {
|
||||
...state,
|
||||
status: 'pending',
|
||||
}
|
||||
|
||||
case 'FETCH_MESSAGES_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
status: 'idle',
|
||||
...prependMessages(
|
||||
state.messages,
|
||||
action.messages,
|
||||
state.uniqueMessageIds
|
||||
),
|
||||
lastTimestamp: action.messages[0] ? action.messages[0].timestamp : null,
|
||||
atEnd: action.messages.length < PAGE_SIZE,
|
||||
}
|
||||
|
||||
case 'SEND_MESSAGE':
|
||||
return {
|
||||
...state,
|
||||
...appendMessage(
|
||||
state.messages,
|
||||
{
|
||||
// Messages are sent optimistically, so don't have an id (used for
|
||||
// React keys). The id is valid for this session, and ensures all
|
||||
// messages have an id. It will be overwritten by the actual ids on
|
||||
// refresh
|
||||
id: generateChatMessageId(),
|
||||
user: action.user,
|
||||
content: action.content,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
state.uniqueMessageIds
|
||||
),
|
||||
}
|
||||
|
||||
case 'RECEIVE_MESSAGE':
|
||||
return {
|
||||
...state,
|
||||
...appendMessage(
|
||||
state.messages,
|
||||
action.message,
|
||||
state.uniqueMessageIds
|
||||
),
|
||||
unreadMessageCount: state.unreadMessageCount + 1,
|
||||
}
|
||||
|
||||
case 'MARK_MESSAGES_AS_READ':
|
||||
return {
|
||||
...state,
|
||||
unreadMessageCount: 0,
|
||||
}
|
||||
|
||||
case 'CLEAR':
|
||||
return { ...initialState }
|
||||
|
||||
case 'ERROR':
|
||||
return {
|
||||
...state,
|
||||
status: 'error',
|
||||
error: action.error,
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error('Unknown action')
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
status: 'idle',
|
||||
messages: [],
|
||||
initialMessagesLoaded: false,
|
||||
lastTimestamp: null,
|
||||
atEnd: false,
|
||||
unreadMessageCount: 0,
|
||||
error: null,
|
||||
uniqueMessageIds: [],
|
||||
}
|
||||
|
||||
export const ChatContext = createContext<
|
||||
| {
|
||||
status: 'idle' | 'pending' | 'error'
|
||||
messages: Message[]
|
||||
initialMessagesLoaded: boolean
|
||||
atEnd: boolean
|
||||
unreadMessageCount: number
|
||||
loadInitialMessages: () => void
|
||||
loadMoreMessages: () => void
|
||||
sendMessage: (message: any) => void
|
||||
markMessagesAsRead: () => void
|
||||
reset: () => void
|
||||
error?: Error | null
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
export const ChatProvider: FC = ({ children }) => {
|
||||
const chatEnabled = getMeta('ol-chatEnabled')
|
||||
|
||||
const clientId = useRef<string>()
|
||||
if (clientId.current === undefined) {
|
||||
clientId.current = chatClientIdGenerator.generate()
|
||||
}
|
||||
const user = useUserContext()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
const { chatIsOpen } = useLayoutContext()
|
||||
|
||||
const {
|
||||
hasFocus: windowHasFocus,
|
||||
flashTitle,
|
||||
stopFlashingTitle,
|
||||
} = useBrowserWindow()
|
||||
|
||||
const [state, dispatch] = useReducer(chatReducer, initialState)
|
||||
|
||||
const { loadInitialMessages, loadMoreMessages, reset } = useMemo(() => {
|
||||
function fetchMessages() {
|
||||
if (state.atEnd) return
|
||||
|
||||
const query: Record<string, string> = {
|
||||
limit: String(PAGE_SIZE),
|
||||
}
|
||||
|
||||
if (state.lastTimestamp) {
|
||||
query.before = String(state.lastTimestamp)
|
||||
}
|
||||
|
||||
const queryString = new URLSearchParams(query)
|
||||
const url = `/project/${projectId}/messages?${queryString.toString()}`
|
||||
|
||||
getJSON(url)
|
||||
.then((messages = []) => {
|
||||
dispatch({
|
||||
type: 'FETCH_MESSAGES_SUCCESS',
|
||||
messages: messages.reverse(),
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch({
|
||||
type: 'ERROR',
|
||||
error,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function loadInitialMessages() {
|
||||
if (!chatEnabled) {
|
||||
debugConsole.warn(`chat is disabled, won't load initial messages`)
|
||||
return
|
||||
}
|
||||
if (state.initialMessagesLoaded) return
|
||||
|
||||
dispatch({ type: 'INITIAL_FETCH_MESSAGES' })
|
||||
fetchMessages()
|
||||
}
|
||||
|
||||
function loadMoreMessages() {
|
||||
if (!chatEnabled) {
|
||||
debugConsole.warn(`chat is disabled, won't load messages`)
|
||||
return
|
||||
}
|
||||
dispatch({ type: 'FETCH_MESSAGES' })
|
||||
fetchMessages()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (!chatEnabled) {
|
||||
debugConsole.warn(`chat is disabled, won't reset chat`)
|
||||
return
|
||||
}
|
||||
dispatch({ type: 'CLEAR' })
|
||||
fetchMessages()
|
||||
}
|
||||
|
||||
return {
|
||||
loadInitialMessages,
|
||||
loadMoreMessages,
|
||||
reset,
|
||||
}
|
||||
}, [
|
||||
chatEnabled,
|
||||
projectId,
|
||||
state.atEnd,
|
||||
state.initialMessagesLoaded,
|
||||
state.lastTimestamp,
|
||||
])
|
||||
|
||||
const sendMessage = useCallback(
|
||||
content => {
|
||||
if (!chatEnabled) {
|
||||
debugConsole.warn(`chat is disabled, won't send message`)
|
||||
return
|
||||
}
|
||||
if (!content) return
|
||||
|
||||
dispatch({
|
||||
type: 'SEND_MESSAGE',
|
||||
user,
|
||||
content,
|
||||
})
|
||||
|
||||
const url = `/project/${projectId}/messages`
|
||||
postJSON(url, {
|
||||
body: { content, client_id: clientId.current },
|
||||
}).catch(error => {
|
||||
dispatch({
|
||||
type: 'ERROR',
|
||||
error,
|
||||
})
|
||||
})
|
||||
},
|
||||
[chatEnabled, projectId, user]
|
||||
)
|
||||
|
||||
const markMessagesAsRead = useCallback(() => {
|
||||
if (!chatEnabled) {
|
||||
debugConsole.warn(`chat is disabled, won't mark messages as read`)
|
||||
return
|
||||
}
|
||||
dispatch({ type: 'MARK_MESSAGES_AS_READ' })
|
||||
}, [chatEnabled])
|
||||
|
||||
// Handling receiving messages over the socket
|
||||
const { socket } = useIdeContext()
|
||||
useEffect(() => {
|
||||
if (!chatEnabled || !socket) return
|
||||
|
||||
function receivedMessage(message: any) {
|
||||
// If the message is from the current client id, then we are receiving the sent message back from the socket.
|
||||
// Ignore it to prevent double message.
|
||||
if (message.clientId === clientId.current) return
|
||||
|
||||
dispatch({ type: 'RECEIVE_MESSAGE', message })
|
||||
}
|
||||
|
||||
socket.on('new-chat-message', receivedMessage)
|
||||
return () => {
|
||||
if (!socket) return
|
||||
|
||||
socket.removeListener('new-chat-message', receivedMessage)
|
||||
}
|
||||
}, [chatEnabled, socket])
|
||||
|
||||
// Handle unread messages
|
||||
useEffect(() => {
|
||||
if (windowHasFocus) {
|
||||
stopFlashingTitle()
|
||||
if (chatIsOpen) {
|
||||
markMessagesAsRead()
|
||||
}
|
||||
}
|
||||
if (!windowHasFocus && state.unreadMessageCount > 0) {
|
||||
flashTitle('New Message')
|
||||
}
|
||||
}, [
|
||||
windowHasFocus,
|
||||
chatIsOpen,
|
||||
state.unreadMessageCount,
|
||||
flashTitle,
|
||||
stopFlashingTitle,
|
||||
markMessagesAsRead,
|
||||
])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
status: state.status,
|
||||
messages: state.messages,
|
||||
initialMessagesLoaded: state.initialMessagesLoaded,
|
||||
atEnd: state.atEnd,
|
||||
unreadMessageCount: state.unreadMessageCount,
|
||||
loadInitialMessages,
|
||||
loadMoreMessages,
|
||||
reset,
|
||||
sendMessage,
|
||||
markMessagesAsRead,
|
||||
error: state.error,
|
||||
}),
|
||||
[
|
||||
loadInitialMessages,
|
||||
loadMoreMessages,
|
||||
markMessagesAsRead,
|
||||
reset,
|
||||
sendMessage,
|
||||
state.atEnd,
|
||||
state.error,
|
||||
state.initialMessagesLoaded,
|
||||
state.messages,
|
||||
state.status,
|
||||
state.unreadMessageCount,
|
||||
]
|
||||
)
|
||||
|
||||
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>
|
||||
}
|
||||
|
||||
export function useChatContext() {
|
||||
const context = useContext(ChatContext)
|
||||
if (!context) {
|
||||
throw new Error('useChatContext is only available inside ChatProvider')
|
||||
}
|
||||
return context
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
const TIMESTAMP_GROUP_SIZE = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
export function appendMessage(messageList, message, uniqueMessageIds) {
|
||||
if (uniqueMessageIds.includes(message.id)) {
|
||||
return { messages: messageList, uniqueMessageIds }
|
||||
}
|
||||
|
||||
uniqueMessageIds = uniqueMessageIds.slice(0)
|
||||
|
||||
uniqueMessageIds.push(message.id)
|
||||
|
||||
const lastMessage = messageList[messageList.length - 1]
|
||||
|
||||
const shouldGroup =
|
||||
lastMessage &&
|
||||
message &&
|
||||
message.user &&
|
||||
message.user.id &&
|
||||
message.user.id === lastMessage.user.id &&
|
||||
message.timestamp - lastMessage.timestamp < TIMESTAMP_GROUP_SIZE
|
||||
|
||||
if (shouldGroup) {
|
||||
messageList = messageList.slice(0, messageList.length - 1).concat({
|
||||
...lastMessage,
|
||||
// the `id` is updated to the latest received content when a new
|
||||
// message is appended or prepended
|
||||
id: message.id,
|
||||
timestamp: message.timestamp,
|
||||
contents: lastMessage.contents.concat(message.content),
|
||||
})
|
||||
} else {
|
||||
messageList = messageList.slice(0).concat({
|
||||
id: message.id,
|
||||
user: message.user,
|
||||
timestamp: message.timestamp,
|
||||
contents: [message.content],
|
||||
})
|
||||
}
|
||||
|
||||
return { messages: messageList, uniqueMessageIds }
|
||||
}
|
||||
|
||||
export function prependMessages(messageList, messages, uniqueMessageIds) {
|
||||
const listCopy = messageList.slice(0)
|
||||
|
||||
uniqueMessageIds = uniqueMessageIds.slice(0)
|
||||
|
||||
messages
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.forEach(message => {
|
||||
if (uniqueMessageIds.includes(message.id)) {
|
||||
return
|
||||
}
|
||||
uniqueMessageIds.push(message.id)
|
||||
const firstMessage = listCopy[0]
|
||||
const shouldGroup =
|
||||
firstMessage &&
|
||||
message &&
|
||||
message.user &&
|
||||
message.user.id === firstMessage.user.id &&
|
||||
firstMessage.timestamp - message.timestamp < TIMESTAMP_GROUP_SIZE
|
||||
|
||||
if (shouldGroup) {
|
||||
firstMessage.id = message.id
|
||||
firstMessage.timestamp = message.timestamp
|
||||
firstMessage.contents = [message.content].concat(firstMessage.contents)
|
||||
} else {
|
||||
listCopy.unshift({
|
||||
id: message.id,
|
||||
user: message.user,
|
||||
timestamp: message.timestamp,
|
||||
contents: [message.content],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return { messages: listCopy, uniqueMessageIds }
|
||||
}
|
@@ -0,0 +1,159 @@
|
||||
/* eslint-disable jsx-a11y/no-autofocus */
|
||||
import PropTypes from 'prop-types'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { postJSON } from '../../../infrastructure/fetch-json'
|
||||
import { CloneProjectTag } from './clone-project-tag'
|
||||
import {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import Notification from '@/shared/components/notification'
|
||||
import OLForm from '@/features/ui/components/ol/ol-form'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
|
||||
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
export default function CloneProjectModalContent({
|
||||
handleHide,
|
||||
inFlight,
|
||||
setInFlight,
|
||||
handleAfterCloned,
|
||||
projectId,
|
||||
projectName,
|
||||
projectTags,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [error, setError] = useState()
|
||||
const [clonedProjectName, setClonedProjectName] = useState(
|
||||
`${projectName} (Copy)`
|
||||
)
|
||||
|
||||
const [clonedProjectTags, setClonedProjectTags] = useState(projectTags)
|
||||
|
||||
// valid if the cloned project has a name
|
||||
const valid = useMemo(
|
||||
() => clonedProjectName.trim().length > 0,
|
||||
[clonedProjectName]
|
||||
)
|
||||
|
||||
// form submission: clone the project if the name is valid
|
||||
const handleSubmit = event => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
setError(false)
|
||||
setInFlight(true)
|
||||
|
||||
// clone the project
|
||||
postJSON(`/project/${projectId}/clone`, {
|
||||
body: {
|
||||
projectName: clonedProjectName,
|
||||
tags: clonedProjectTags.map(tag => ({ id: tag._id })),
|
||||
},
|
||||
})
|
||||
.then(data => {
|
||||
// open the cloned project
|
||||
handleAfterCloned(data, clonedProjectTags)
|
||||
})
|
||||
.catch(({ response, data }) => {
|
||||
if (response?.status === 400) {
|
||||
setError(data.message)
|
||||
} else {
|
||||
setError(true)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setInFlight(false)
|
||||
})
|
||||
}
|
||||
|
||||
const removeTag = useCallback(tag => {
|
||||
setClonedProjectTags(value => value.filter(item => item._id !== tag._id))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>{t('copy_project')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<OLModalBody>
|
||||
<OLForm id="clone-project-form" onSubmit={handleSubmit}>
|
||||
<OLFormGroup controlId="clone-project-form-name">
|
||||
<OLFormLabel>{t('new_name')}</OLFormLabel>
|
||||
<OLFormControl
|
||||
type="text"
|
||||
placeholder="New Project Name"
|
||||
required
|
||||
value={clonedProjectName}
|
||||
onChange={event => setClonedProjectName(event.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</OLFormGroup>
|
||||
|
||||
{clonedProjectTags.length > 0 && (
|
||||
<OLFormGroup
|
||||
controlId="clone-project-tags-list"
|
||||
className="clone-project-tag"
|
||||
>
|
||||
<OLFormLabel>{t('tags')}: </OLFormLabel>
|
||||
<div role="listbox" id="clone-project-tags-list">
|
||||
{clonedProjectTags.map(tag => (
|
||||
<CloneProjectTag
|
||||
key={tag._id}
|
||||
tag={tag}
|
||||
removeTag={removeTag}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</OLFormGroup>
|
||||
)}
|
||||
</OLForm>
|
||||
|
||||
{error && (
|
||||
<Notification
|
||||
content={error.length ? error : t('generic_something_went_wrong')}
|
||||
type="error"
|
||||
/>
|
||||
)}
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" disabled={inFlight} onClick={handleHide}>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
<OLButton
|
||||
variant="primary"
|
||||
disabled={inFlight || !valid}
|
||||
form="clone-project-form"
|
||||
type="submit"
|
||||
>
|
||||
{inFlight ? <>{t('copying')}…</> : t('copy')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
CloneProjectModalContent.propTypes = {
|
||||
handleHide: PropTypes.func.isRequired,
|
||||
inFlight: PropTypes.bool,
|
||||
setInFlight: PropTypes.func.isRequired,
|
||||
handleAfterCloned: PropTypes.func.isRequired,
|
||||
projectId: PropTypes.string,
|
||||
projectName: PropTypes.string,
|
||||
projectTags: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
color: PropTypes.string,
|
||||
})
|
||||
),
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
import React, { memo, useCallback, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import CloneProjectModalContent from './clone-project-modal-content'
|
||||
import OLModal from '@/features/ui/components/ol/ol-modal'
|
||||
|
||||
function CloneProjectModal({
|
||||
show,
|
||||
handleHide,
|
||||
handleAfterCloned,
|
||||
projectId,
|
||||
projectName,
|
||||
projectTags,
|
||||
}) {
|
||||
const [inFlight, setInFlight] = useState(false)
|
||||
|
||||
const onHide = useCallback(() => {
|
||||
if (!inFlight) {
|
||||
handleHide()
|
||||
}
|
||||
}, [handleHide, inFlight])
|
||||
|
||||
return (
|
||||
<OLModal
|
||||
animation
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
id="clone-project-modal"
|
||||
// backdrop="static" will disable closing the modal by clicking
|
||||
// outside of the modal element
|
||||
backdrop={inFlight ? 'static' : undefined}
|
||||
>
|
||||
<CloneProjectModalContent
|
||||
handleHide={onHide}
|
||||
inFlight={inFlight}
|
||||
setInFlight={setInFlight}
|
||||
handleAfterCloned={handleAfterCloned}
|
||||
projectId={projectId}
|
||||
projectName={projectName}
|
||||
projectTags={projectTags}
|
||||
/>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
CloneProjectModal.propTypes = {
|
||||
handleHide: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
handleAfterCloned: PropTypes.func.isRequired,
|
||||
projectId: PropTypes.string,
|
||||
projectName: PropTypes.string,
|
||||
projectTags: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
color: PropTypes.string,
|
||||
})
|
||||
),
|
||||
}
|
||||
|
||||
export default memo(CloneProjectModal)
|
@@ -0,0 +1,26 @@
|
||||
import { FC } from 'react'
|
||||
import { Tag as TagType } from '../../../../../app/src/Features/Tags/types'
|
||||
import { getTagColor } from '@/features/project-list/util/tag'
|
||||
import Tag from '@/features/ui/components/bootstrap-5/tag'
|
||||
|
||||
export const CloneProjectTag: FC<{
|
||||
tag: TagType
|
||||
removeTag: (tag: TagType) => void
|
||||
}> = ({ tag, removeTag }) => {
|
||||
return (
|
||||
<Tag
|
||||
prepend={
|
||||
<i
|
||||
className="badge-tag-circle"
|
||||
style={{ backgroundColor: getTagColor(tag) }}
|
||||
/>
|
||||
}
|
||||
closeBtnProps={{
|
||||
onClick: () => removeTag(tag),
|
||||
}}
|
||||
className="ms-2 mb-2"
|
||||
>
|
||||
{tag.name}
|
||||
</Tag>
|
||||
)
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
import CloneProjectModal from './clone-project-modal'
|
||||
|
||||
const EditorCloneProjectModalWrapper = React.memo(
|
||||
function EditorCloneProjectModalWrapper({ show, handleHide, openProject }) {
|
||||
const {
|
||||
_id: projectId,
|
||||
name: projectName,
|
||||
tags: projectTags,
|
||||
} = useProjectContext()
|
||||
|
||||
if (!projectName) {
|
||||
// wait for useProjectContext
|
||||
return null
|
||||
} else {
|
||||
return (
|
||||
<CloneProjectModal
|
||||
handleHide={handleHide}
|
||||
show={show}
|
||||
handleAfterCloned={openProject}
|
||||
projectId={projectId}
|
||||
projectName={projectName}
|
||||
projectTags={projectTags}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
EditorCloneProjectModalWrapper.propTypes = {
|
||||
handleHide: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
openProject: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default withErrorBoundary(EditorCloneProjectModalWrapper)
|
@@ -0,0 +1,39 @@
|
||||
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Interstitial } from '@/shared/components/interstitial'
|
||||
|
||||
export function CompromisedPasswordCard() {
|
||||
const { t } = useTranslation()
|
||||
const { isReady } = useWaitForI18n()
|
||||
|
||||
if (!isReady) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Interstitial
|
||||
contentClassName="compromised-password-content"
|
||||
showLogo={false}
|
||||
title={t('compromised_password')}
|
||||
>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="your_password_was_detected"
|
||||
components={[
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
<a
|
||||
href="https://haveibeenpwned.com/passwords"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<OLButton className="btn-primary" href="/user/settings">
|
||||
{t('change_password_in_account_settings')}
|
||||
</OLButton>
|
||||
</Interstitial>
|
||||
)
|
||||
}
|
35
services/web/frontend/js/features/contact-form/index.js
Normal file
35
services/web/frontend/js/features/contact-form/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { setupSearch } from './search'
|
||||
|
||||
document
|
||||
.querySelectorAll('[data-ol-contact-form-with-search]')
|
||||
.forEach(setupSearch)
|
||||
|
||||
document
|
||||
.querySelectorAll('[data-ol-open-contact-form-modal="contact-us"]')
|
||||
.forEach(el => {
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault()
|
||||
$('[data-ol-contact-form-modal="contact-us"]').modal()
|
||||
})
|
||||
})
|
||||
|
||||
document
|
||||
.querySelectorAll('[data-ol-open-contact-form-modal="general"]')
|
||||
.forEach(el => {
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault()
|
||||
$('[data-ol-contact-form-modal="general"]').modal()
|
||||
})
|
||||
})
|
||||
|
||||
document.querySelectorAll('[data-ol-contact-form]').forEach(el => {
|
||||
el.addEventListener('submit', function (e) {
|
||||
const emailValue = document.querySelector(
|
||||
'[data-ol-contact-form-email-input]'
|
||||
).value
|
||||
const thankYouEmailEl = document.querySelector(
|
||||
'[data-ol-contact-form-thank-you-email]'
|
||||
)
|
||||
thankYouEmailEl.textContent = emailValue
|
||||
})
|
||||
})
|
70
services/web/frontend/js/features/contact-form/search.js
Normal file
70
services/web/frontend/js/features/contact-form/search.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import _ from 'lodash'
|
||||
import { formatWikiHit, searchWiki } from '../algolia-search/search-wiki'
|
||||
import { sendMB } from '../../infrastructure/event-tracking'
|
||||
|
||||
export function setupSearch(formEl) {
|
||||
const inputEl = formEl.querySelector('[name="subject"]')
|
||||
const resultsEl = formEl.querySelector('[data-ol-search-results]')
|
||||
const wrapperEl = formEl.querySelector('[data-ol-search-results-wrapper]')
|
||||
|
||||
let lastValue = ''
|
||||
function hideResults() {
|
||||
wrapperEl.setAttribute('hidden', '')
|
||||
}
|
||||
function showResults() {
|
||||
wrapperEl.removeAttribute('hidden')
|
||||
}
|
||||
|
||||
async function handleChange() {
|
||||
const value = inputEl.value
|
||||
if (value === lastValue) return
|
||||
lastValue = value
|
||||
if (value.length < 3) {
|
||||
hideResults()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { hits, nbHits } = await searchWiki(value, {
|
||||
hitsPerPage: 3,
|
||||
typoTolerance: 'strict',
|
||||
})
|
||||
resultsEl.innerText = ''
|
||||
|
||||
for (const hit of hits) {
|
||||
const { url, pageName } = formatWikiHit(hit)
|
||||
const liEl = document.createElement('li')
|
||||
|
||||
const linkEl = document.createElement('a')
|
||||
linkEl.className = 'contact-suggestion-list-item'
|
||||
linkEl.href = url
|
||||
linkEl.target = '_blank'
|
||||
liEl.append(linkEl)
|
||||
|
||||
const contentEl = document.createElement('span')
|
||||
contentEl.innerHTML = pageName
|
||||
linkEl.append(contentEl)
|
||||
|
||||
const iconEl = document.createElement('i')
|
||||
iconEl.className = 'fa fa-angle-right'
|
||||
iconEl.setAttribute('aria-hidden', 'true')
|
||||
linkEl.append(iconEl)
|
||||
|
||||
resultsEl.append(liEl)
|
||||
}
|
||||
if (nbHits > 0) {
|
||||
showResults()
|
||||
sendMB('contact-form-suggestions-shown')
|
||||
} else {
|
||||
hideResults()
|
||||
}
|
||||
} catch (e) {
|
||||
hideResults()
|
||||
}
|
||||
}
|
||||
|
||||
inputEl.addEventListener('input', _.debounce(handleChange, 350))
|
||||
|
||||
// display initial results
|
||||
handleChange()
|
||||
}
|
51
services/web/frontend/js/features/cookie-banner/index.js
Normal file
51
services/web/frontend/js/features/cookie-banner/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
function loadGA() {
|
||||
if (window.olLoadGA) {
|
||||
window.olLoadGA()
|
||||
}
|
||||
}
|
||||
|
||||
function setConsent(value) {
|
||||
document.querySelector('.cookie-banner').classList.add('hidden')
|
||||
const cookieDomain = getMeta('ol-ExposedSettings').cookieDomain
|
||||
const oneYearInSeconds = 60 * 60 * 24 * 365
|
||||
const cookieAttributes =
|
||||
'; path=/' +
|
||||
'; domain=' +
|
||||
cookieDomain +
|
||||
'; max-age=' +
|
||||
oneYearInSeconds +
|
||||
'; SameSite=Lax; Secure'
|
||||
if (value === 'all') {
|
||||
document.cookie = 'oa=1' + cookieAttributes
|
||||
loadGA()
|
||||
window.dispatchEvent(new CustomEvent('cookie-consent', { detail: true }))
|
||||
} else {
|
||||
document.cookie = 'oa=0' + cookieAttributes
|
||||
window.dispatchEvent(new CustomEvent('cookie-consent', { detail: false }))
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
getMeta('ol-ExposedSettings').gaToken ||
|
||||
getMeta('ol-ExposedSettings').gaTokenV4
|
||||
) {
|
||||
document
|
||||
.querySelectorAll('[data-ol-cookie-banner-set-consent]')
|
||||
.forEach(el => {
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault()
|
||||
const consentType = el.getAttribute('data-ol-cookie-banner-set-consent')
|
||||
setConsent(consentType)
|
||||
})
|
||||
})
|
||||
|
||||
const oaCookie = document.cookie.split('; ').find(c => c.startsWith('oa='))
|
||||
if (!oaCookie) {
|
||||
const cookieBannerEl = document.querySelector('.cookie-banner')
|
||||
if (cookieBannerEl) {
|
||||
cookieBannerEl.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useAsync from '../../../shared/hooks/use-async'
|
||||
import { postJSON } from '../../../infrastructure/fetch-json'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
|
||||
import { learnedWords as initialLearnedWords } from '@/features/source-editor/extensions/spelling/learned-words'
|
||||
|
||||
type DictionaryModalContentProps = {
|
||||
handleHide: () => void
|
||||
}
|
||||
|
||||
const wordsSortFunction = (a: string, b: string) => a.localeCompare(b)
|
||||
|
||||
export default function DictionaryModalContent({
|
||||
handleHide,
|
||||
}: DictionaryModalContentProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [learnedWords, setLearnedWords] = useState<Set<string>>(
|
||||
initialLearnedWords.global
|
||||
)
|
||||
|
||||
const { isError, runAsync } = useAsync()
|
||||
|
||||
const handleRemove = useCallback(
|
||||
word => {
|
||||
runAsync(postJSON('/spelling/unlearn', { body: { word } }))
|
||||
.then(() => {
|
||||
setLearnedWords(prevLearnedWords => {
|
||||
const learnedWords = new Set(prevLearnedWords)
|
||||
learnedWords.delete(word)
|
||||
return learnedWords
|
||||
})
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('editor:remove-learned-word', { detail: word })
|
||||
)
|
||||
})
|
||||
.catch(debugConsole.error)
|
||||
},
|
||||
[runAsync]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>{t('edit_dictionary')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<OLModalBody>
|
||||
{isError ? (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={t('generic_something_went_wrong')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{learnedWords.size > 0 ? (
|
||||
<ul className="list-unstyled dictionary-entries-list">
|
||||
{[...learnedWords].sort(wordsSortFunction).map(learnedWord => (
|
||||
<li key={learnedWord} className="dictionary-entry">
|
||||
<span className="dictionary-entry-name">{learnedWord}</span>
|
||||
<OLTooltip
|
||||
id={`tooltip-remove-learned-word-${learnedWord}`}
|
||||
description={t('edit_dictionary_remove')}
|
||||
overlayProps={{ delay: 0 }}
|
||||
>
|
||||
<OLIconButton
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => handleRemove(learnedWord)}
|
||||
icon="delete"
|
||||
accessibilityLabel={t('edit_dictionary_remove')}
|
||||
/>
|
||||
</OLTooltip>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="dictionary-empty-body text-center">
|
||||
<i>{t('edit_dictionary_empty')}</i>
|
||||
</p>
|
||||
)}
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={handleHide}>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import DictionaryModalContent from './dictionary-modal-content'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
import OLModal from '@/features/ui/components/ol/ol-modal'
|
||||
|
||||
type DictionaryModalProps = {
|
||||
show?: boolean
|
||||
handleHide: () => void
|
||||
}
|
||||
|
||||
function DictionaryModal({ show, handleHide }: DictionaryModalProps) {
|
||||
return (
|
||||
<OLModal
|
||||
animation
|
||||
show={show}
|
||||
onHide={handleHide}
|
||||
id="dictionary-modal"
|
||||
data-testid="dictionary-modal"
|
||||
size="sm"
|
||||
>
|
||||
<DictionaryModalContent handleHide={handleHide} />
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withErrorBoundary(DictionaryModal)
|
@@ -0,0 +1,22 @@
|
||||
export const globalIgnoredWords = new Set([
|
||||
'Overleaf',
|
||||
'overleaf',
|
||||
'ShareLaTeX',
|
||||
'sharelatex',
|
||||
'LaTeX',
|
||||
'TeX',
|
||||
'BibTeX',
|
||||
'BibLaTeX',
|
||||
'XeTeX',
|
||||
'XeLaTeX',
|
||||
'LuaTeX',
|
||||
'LuaLaTeX',
|
||||
'http',
|
||||
'https',
|
||||
'www',
|
||||
'COVID',
|
||||
'Lockdown',
|
||||
'lockdown',
|
||||
'Coronavirus',
|
||||
'coronavirus',
|
||||
])
|
@@ -0,0 +1,41 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import EditorCloneProjectModalWrapper from '../../clone-project-modal/components/editor-clone-project-modal-wrapper'
|
||||
import LeftMenuButton from './left-menu-button'
|
||||
import { useLocation } from '../../../shared/hooks/use-location'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
|
||||
type ProjectCopyResponse = {
|
||||
project_id: string
|
||||
}
|
||||
|
||||
export default function ActionsCopyProject() {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const location = useLocation()
|
||||
|
||||
const openProject = useCallback(
|
||||
({ project_id: projectId }: ProjectCopyResponse) => {
|
||||
location.assign(`/project/${projectId}`)
|
||||
},
|
||||
[location]
|
||||
)
|
||||
|
||||
const handleShowModal = useCallback(() => {
|
||||
eventTracking.sendMB('left-menu-copy')
|
||||
setShowModal(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftMenuButton onClick={handleShowModal} icon="file_copy">
|
||||
{t('copy_project')}
|
||||
</LeftMenuButton>
|
||||
<EditorCloneProjectModalWrapper
|
||||
show={showModal}
|
||||
handleHide={() => setShowModal(false)}
|
||||
openProject={openProject}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
import { ElementType } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
|
||||
import ActionsCopyProject from './actions-copy-project'
|
||||
import ActionsWordCount from './actions-word-count'
|
||||
|
||||
const components = importOverleafModules('editorLeftMenuManageTemplate') as {
|
||||
import: { default: ElementType }
|
||||
path: string
|
||||
}[]
|
||||
|
||||
export default function ActionsMenu() {
|
||||
const { t } = useTranslation()
|
||||
const anonymous = getMeta('ol-anonymous')
|
||||
|
||||
if (anonymous) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>{t('actions')}</h4>
|
||||
<ul className="list-unstyled nav">
|
||||
<li>
|
||||
<ActionsCopyProject />
|
||||
</li>
|
||||
{components.map(({ import: { default: Component }, path }) => (
|
||||
<li key={path}>
|
||||
<Component />
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<ActionsWordCount />
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import WordCountModal from '../../word-count-modal/components/word-count-modal'
|
||||
import LeftMenuButton from './left-menu-button'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
|
||||
export default function ActionsWordCount() {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const { pdfUrl } = useCompileContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleShowModal = useCallback(() => {
|
||||
eventTracking.sendMB('left-menu-count')
|
||||
setShowModal(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{pdfUrl ? (
|
||||
<LeftMenuButton onClick={handleShowModal} icon="match_case">
|
||||
{t('word_count')}
|
||||
</LeftMenuButton>
|
||||
) : (
|
||||
<OLTooltip
|
||||
id="disabled-word-count"
|
||||
description={t('please_compile_pdf_before_word_count')}
|
||||
overlayProps={{
|
||||
placement: 'top',
|
||||
}}
|
||||
>
|
||||
{/* OverlayTrigger won't fire unless the child is a non-react html element (e.g div, span) */}
|
||||
<div>
|
||||
<LeftMenuButton
|
||||
icon="match_case"
|
||||
disabled
|
||||
disabledAccesibilityText={t(
|
||||
'please_compile_pdf_before_word_count'
|
||||
)}
|
||||
>
|
||||
{t('word_count')}
|
||||
</LeftMenuButton>
|
||||
</div>
|
||||
</OLTooltip>
|
||||
)}
|
||||
<WordCountModal show={showModal} handleHide={() => setShowModal(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DownloadPDF from './download-pdf'
|
||||
import DownloadSource from './download-source'
|
||||
|
||||
export default function DownloadMenu() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="mt-0">{t('download')}</h4>
|
||||
<ul className="list-unstyled nav nav-downloads text-center">
|
||||
<li>
|
||||
<DownloadSource />
|
||||
</li>
|
||||
<li>
|
||||
<DownloadPDF />
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import { isSmallDevice } from '../../../infrastructure/event-tracking'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
|
||||
export default function DownloadPDF() {
|
||||
const { t } = useTranslation()
|
||||
const { pdfDownloadUrl, pdfUrl } = useCompileContext()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
function sendDownloadEvent() {
|
||||
eventTracking.sendMB('download-pdf-button-click', {
|
||||
projectId,
|
||||
location: 'left-menu',
|
||||
isSmallDevice,
|
||||
})
|
||||
}
|
||||
|
||||
if (pdfUrl) {
|
||||
return (
|
||||
<a
|
||||
href={pdfDownloadUrl || pdfUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={sendDownloadEvent}
|
||||
>
|
||||
<MaterialIcon type="picture_as_pdf" size="2x" />
|
||||
<br />
|
||||
PDF
|
||||
</a>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<OLTooltip
|
||||
id="disabled-pdf-download"
|
||||
description={t('please_compile_pdf_before_download')}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
<div className="link-disabled">
|
||||
<MaterialIcon type="picture_as_pdf" size="2x" />
|
||||
<br />
|
||||
PDF
|
||||
</div>
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import { isSmallDevice } from '../../../infrastructure/event-tracking'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
export default function DownloadSource() {
|
||||
const { t } = useTranslation()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
function sendDownloadEvent() {
|
||||
eventTracking.sendMB('download-zip-button-click', {
|
||||
projectId,
|
||||
location: 'left-menu',
|
||||
isSmallDevice,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/project/${projectId}/download/zip`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={sendDownloadEvent}
|
||||
>
|
||||
<MaterialIcon type="folder_zip" size="2x" />
|
||||
<br />
|
||||
{t('source')}
|
||||
</a>
|
||||
)
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
import DownloadMenu from './download-menu'
|
||||
import ActionsMenu from './actions-menu'
|
||||
import HelpMenu from './help-menu'
|
||||
import SyncMenu from './sync-menu'
|
||||
import SettingsMenu from './settings-menu'
|
||||
|
||||
export default function EditorLeftMenuBody() {
|
||||
return (
|
||||
<>
|
||||
<DownloadMenu />
|
||||
<ActionsMenu />
|
||||
<SyncMenu />
|
||||
<SettingsMenu />
|
||||
<HelpMenu />
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
import { createContext, FC, useCallback, useContext, useState } from 'react'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
|
||||
type EditorLeftMenuState = {
|
||||
settingToFocus?: string
|
||||
}
|
||||
|
||||
export const EditorLeftMenuContext = createContext<
|
||||
EditorLeftMenuState | undefined
|
||||
>(undefined)
|
||||
|
||||
export const EditorLeftMenuProvider: FC = ({ children }) => {
|
||||
const [value, setValue] = useState<EditorLeftMenuState>(() => ({
|
||||
settingToFocus: undefined,
|
||||
}))
|
||||
|
||||
useEventListener(
|
||||
'ui.focus-setting',
|
||||
useCallback(event => {
|
||||
setValue(value => ({
|
||||
...value,
|
||||
settingToFocus: (event as CustomEvent<string>).detail,
|
||||
}))
|
||||
}, [])
|
||||
)
|
||||
|
||||
return (
|
||||
<EditorLeftMenuContext.Provider value={value}>
|
||||
{children}
|
||||
</EditorLeftMenuContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useEditorLeftMenuContext = () => {
|
||||
const value = useContext(EditorLeftMenuContext)
|
||||
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
`useEditorLeftMenuContext is only available inside EditorLeftMenuProvider`
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import LeftMenuMask from './left-menu-mask'
|
||||
import classNames from 'classnames'
|
||||
import { lazy, memo, Suspense } from 'react'
|
||||
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
|
||||
import { Offcanvas } from 'react-bootstrap-5'
|
||||
import { EditorLeftMenuProvider } from './editor-left-menu-context'
|
||||
import withErrorBoundary from '@/infrastructure/error-boundary'
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const EditorLeftMenuBody = lazy(() => import('./editor-left-menu-body'))
|
||||
|
||||
const LazyEditorLeftMenuWithErrorBoundary = withErrorBoundary(
|
||||
() => (
|
||||
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
||||
<EditorLeftMenuBody />
|
||||
</Suspense>
|
||||
),
|
||||
() => {
|
||||
const { t } = useTranslation()
|
||||
return <OLNotification type="error" content={t('something_went_wrong')} />
|
||||
}
|
||||
)
|
||||
|
||||
function EditorLeftMenu() {
|
||||
const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
|
||||
|
||||
const closeLeftMenu = () => {
|
||||
setLeftMenuShown(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<EditorLeftMenuProvider>
|
||||
<Offcanvas
|
||||
show={leftMenuShown}
|
||||
onHide={closeLeftMenu}
|
||||
backdropClassName="left-menu-modal-backdrop"
|
||||
id="left-menu-offcanvas"
|
||||
>
|
||||
<Offcanvas.Body
|
||||
className={classNames('full-size', 'left-menu', {
|
||||
shown: leftMenuShown,
|
||||
})}
|
||||
id="left-menu"
|
||||
>
|
||||
<LazyEditorLeftMenuWithErrorBoundary />
|
||||
</Offcanvas.Body>
|
||||
</Offcanvas>
|
||||
{leftMenuShown && <LeftMenuMask />}
|
||||
</EditorLeftMenuProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(EditorLeftMenu)
|
@@ -0,0 +1,24 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback } from 'react'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import { useContactUsModal } from '../../../shared/hooks/use-contact-us-modal'
|
||||
import LeftMenuButton from './left-menu-button'
|
||||
|
||||
export default function HelpContactUs() {
|
||||
const { modal, showModal } = useContactUsModal()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const showModalWithAnalytics = useCallback(() => {
|
||||
eventTracking.sendMB('left-menu-contact')
|
||||
showModal()
|
||||
}, [showModal])
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftMenuButton onClick={showModalWithAnalytics} icon="contact_support">
|
||||
{t('contact_us')}
|
||||
</LeftMenuButton>
|
||||
{modal}
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import LeftMenuButton from './left-menu-button'
|
||||
|
||||
export default function HelpDocumentation() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftMenuButton type="link" href="/learn" icon="book_4">
|
||||
{t('documentation')}
|
||||
</LeftMenuButton>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import HelpContactUs from './help-contact-us'
|
||||
import HelpDocumentation from './help-documentation'
|
||||
import HelpShowHotkeys from './help-show-hotkeys'
|
||||
|
||||
export default function HelpMenu() {
|
||||
const { t } = useTranslation()
|
||||
const showSupport = getMeta('ol-showSupport')
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>{t('help')}</h4>
|
||||
<ul className="list-unstyled nav">
|
||||
<li>
|
||||
<HelpShowHotkeys />
|
||||
</li>
|
||||
{showSupport ? (
|
||||
<>
|
||||
<li>
|
||||
<HelpDocumentation />
|
||||
</li>
|
||||
<li>
|
||||
<HelpContactUs />
|
||||
</li>
|
||||
</>
|
||||
) : null}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import HotkeysModal from '../../hotkeys-modal/components/hotkeys-modal'
|
||||
import LeftMenuButton from './left-menu-button'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
|
||||
export default function HelpShowHotkeys() {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const { features } = useProjectContext()
|
||||
|
||||
const showModalWithAnalytics = useCallback(() => {
|
||||
eventTracking.sendMB('left-menu-hotkeys')
|
||||
setShowModal(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftMenuButton onClick={showModalWithAnalytics} icon="keyboard">
|
||||
{t('show_hotkeys')}
|
||||
</LeftMenuButton>
|
||||
<HotkeysModal
|
||||
show={showModal}
|
||||
handleHide={() => setShowModal(false)}
|
||||
isMac={isMac}
|
||||
trackChangesVisible={features?.trackChangesVisible}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
type Props = {
|
||||
onClick?: () => void
|
||||
icon?: string
|
||||
svgIcon?: React.ReactElement | null
|
||||
disabled?: boolean
|
||||
disabledAccesibilityText?: string
|
||||
type?: 'button' | 'link'
|
||||
href?: string
|
||||
}
|
||||
|
||||
function LeftMenuButtonIcon({
|
||||
svgIcon,
|
||||
icon,
|
||||
}: {
|
||||
svgIcon?: React.ReactElement | null
|
||||
icon?: string
|
||||
}) {
|
||||
if (svgIcon) {
|
||||
return <div className="material-symbols">{svgIcon}</div>
|
||||
} else if (icon) {
|
||||
return <MaterialIcon type={icon} />
|
||||
} else return null
|
||||
}
|
||||
|
||||
export default function LeftMenuButton({
|
||||
children,
|
||||
svgIcon,
|
||||
onClick,
|
||||
icon,
|
||||
disabled = false,
|
||||
disabledAccesibilityText,
|
||||
type = 'button',
|
||||
href,
|
||||
}: PropsWithChildren<Props>) {
|
||||
if (disabled) {
|
||||
return (
|
||||
<div className="left-menu-button link-disabled">
|
||||
<LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
|
||||
<span>{children}</span>
|
||||
{disabledAccesibilityText ? (
|
||||
<span className="sr-only">{disabledAccesibilityText}</span>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'button') {
|
||||
return (
|
||||
<button onClick={onClick} className="left-menu-button">
|
||||
<LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
|
||||
<span>{children}</span>
|
||||
</button>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="left-menu-button"
|
||||
>
|
||||
<LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
|
||||
<span>{children}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||
|
||||
export default memo(function LeftMenuMask() {
|
||||
const { setLeftMenuShown } = useLayoutContext()
|
||||
const { userSettings } = useUserSettingsContext()
|
||||
const { editorTheme, overallTheme } = userSettings
|
||||
const [original] = useState({ editorTheme, overallTheme })
|
||||
const maskRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (maskRef.current) {
|
||||
if (
|
||||
editorTheme !== original.editorTheme ||
|
||||
overallTheme !== original.overallTheme
|
||||
) {
|
||||
maskRef.current.style.opacity = '0'
|
||||
}
|
||||
}
|
||||
}, [editorTheme, overallTheme, original])
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
id="left-menu-mask"
|
||||
ref={maskRef}
|
||||
onClick={() => setLeftMenuShown(false)}
|
||||
/>
|
||||
)
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user