Init commit Fivem Data

This commit is contained in:
jack
2026-06-20 13:18:14 +00:00
commit b8dd09ae66
137 changed files with 161716 additions and 0 deletions

View File

@@ -0,0 +1,467 @@
import { post } from './utils';
import CONFIG from './config';
import Vue from 'vue';
import Suggestions from './Suggestions.vue';
import MessageV from './Message.vue';
import { Suggestion } from './Suggestions';
export interface Message {
args: string[];
template: string;
params?: { [key: string]: string };
multiline?: boolean;
color?: [ number, number, number ];
templateId?: number;
mode?: string;
modeData?: Mode;
id?: string;
}
export interface ThemeData {
style: string;
styleSheet: string;
baseUrl: string;
script: string;
templates: { [id: string]: string }; // not supported rn
msgTemplates: { [id: string]: string };
}
export interface Mode {
name: string;
displayName: string;
color: string;
hidden?: boolean;
isChannel?: boolean;
isGlobal?: boolean;
}
enum ChatHideStates {
ShowWhenActive = 0,
AlwaysShow = 1,
AlwaysHide = 2,
}
const defaultMode: Mode = {
name: 'all',
displayName: 'All',
color: '#fff'
};
const globalMode: Mode = {
name: '_global',
displayName: 'All',
color: '#fff',
isGlobal: true,
hidden: true
};
export default Vue.extend({
template: "#app_template",
name: "app",
components: {
Suggestions,
MessageV
},
data() {
return {
style: CONFIG.style,
showInput: false,
showWindow: false,
showHideState: false,
hideState: ChatHideStates.ShowWhenActive,
backingSuggestions: [] as Suggestion[],
removedSuggestions: [] as string[],
templates: { ...CONFIG.templates } as { [ key: string ]: string },
message: "",
messages: [] as Message[],
oldMessages: [] as string[],
oldMessagesIndex: -1,
tplBackups: [] as unknown as [ HTMLElement, string ][],
msgTplBackups: [] as unknown as [ string, string ][],
focusTimer: 0,
showWindowTimer: 0,
showHideStateTimer: 0,
listener: (event: MessageEvent) => {},
modes: [defaultMode, globalMode] as Mode[],
modeIdx: 0,
};
},
destroyed() {
clearInterval(this.focusTimer);
window.removeEventListener("message", this.listener);
},
mounted() {
post("http://chat/loaded", JSON.stringify({}));
this.listener = (event: MessageEvent) => {
const item: any = event.data || (<any>event).detail; //'detail' is for debugging via browsers
if (!item || !item.type) {
return;
}
const typeRef = item.type as
'ON_OPEN' | 'ON_SCREEN_STATE_CHANGE' | 'ON_MESSAGE' | 'ON_CLEAR' | 'ON_SUGGESTION_ADD' |
'ON_SUGGESTION_REMOVE' | 'ON_TEMPLATE_ADD' | 'ON_UPDATE_THEMES' | 'ON_MODE_ADD' | 'ON_MODE_REMOVE';
if (this[typeRef]) {
this[typeRef](item);
}
};
window.addEventListener("message", this.listener);
},
watch: {
messages() {
if (this.hideState !== ChatHideStates.AlwaysHide) {
if (this.showWindowTimer) {
clearTimeout(this.showWindowTimer);
}
this.showWindow = true;
this.resetShowWindowTimer();
}
const messagesObj = this.$refs.messages as HTMLDivElement;
this.$nextTick(() => {
messagesObj.scrollTop = messagesObj.scrollHeight;
});
}
},
computed: {
filteredMessages(): Message[] {
return this.messages.filter(
// show messages that are
// - (if the current mode is a channel) global, or in the current mode
// - (if the message is a channel) in the current mode
el => (el.modeData?.isChannel || this.modes[this.modeIdx].isChannel) ?
(el.mode === this.modes[this.modeIdx].name || el.modeData?.isGlobal) :
true
);
},
suggestions(): Suggestion[] {
return this.backingSuggestions.filter(
el => this.removedSuggestions.indexOf(el.name) <= -1
);
},
hideAnimated(): boolean {
return this.hideState !== ChatHideStates.AlwaysHide;
},
modeIdxGet(): number {
return (this.modeIdx >= this.modes.length) ? (this.modes.length - 1) : this.modeIdx;
},
modePrefix(): string {
if (this.modes.length === 2) {
return ``;
}
return this.modes[this.modeIdxGet].displayName;
},
modeColor(): string {
return this.modes[this.modeIdxGet].color;
},
hideStateString(): string {
// TODO: localization
switch (this.hideState) {
case ChatHideStates.AlwaysShow:
return 'Visible';
case ChatHideStates.AlwaysHide:
return 'Hidden';
case ChatHideStates.ShowWhenActive:
return 'When active';
}
}
},
methods: {
ON_SCREEN_STATE_CHANGE({ hideState, fromUserInteraction }: { hideState: ChatHideStates, fromUserInteraction: boolean }) {
this.hideState = hideState;
if (this.hideState === ChatHideStates.AlwaysHide) {
if (!this.showInput) {
this.showWindow = false;
}
} else if (this.hideState === ChatHideStates.AlwaysShow) {
this.showWindow = true;
if (this.showWindowTimer) {
clearTimeout(this.showWindowTimer);
}
} else {
this.resetShowWindowTimer();
}
if (fromUserInteraction) {
this.showHideState = true;
if (this.showHideStateTimer) {
clearTimeout(this.showHideStateTimer);
}
this.showHideStateTimer = window.setTimeout(() => {
this.showHideState = false;
}, 1500);
}
},
ON_OPEN() {
this.showInput = true;
this.showWindow = true;
if (this.showWindowTimer) {
clearTimeout(this.showWindowTimer);
}
this.focusTimer = window.setInterval(() => {
if (this.$refs.input) {
(this.$refs.input as HTMLInputElement).focus();
} else {
clearInterval(this.focusTimer);
}
}, 100);
},
ON_MESSAGE({ message }: { message: Message }) {
message.id = `${new Date().getTime()}${Math.random()}`;
message.modeData = this.modes.find(mode => mode.name === message.mode);
this.messages.push(message);
},
ON_CLEAR() {
this.messages = [];
this.oldMessages = [];
this.oldMessagesIndex = -1;
},
ON_SUGGESTION_ADD({ suggestion }: { suggestion: Suggestion }) {
this.removedSuggestions = this.removedSuggestions.filter(a => a !== suggestion.name);
const duplicateSuggestion = this.backingSuggestions.find(
a => a.name == suggestion.name
);
if (duplicateSuggestion) {
if (suggestion.help || suggestion.params) {
duplicateSuggestion.help = suggestion.help || "";
duplicateSuggestion.params = suggestion.params || [];
}
return;
}
if (!suggestion.params) {
suggestion.params = []; //TODO Move somewhere else
}
this.backingSuggestions.push(suggestion);
},
ON_SUGGESTION_REMOVE({ name }: { name: string }) {
if (this.removedSuggestions.indexOf(name) <= -1) {
this.removedSuggestions.push(name);
}
},
ON_MODE_ADD({ mode }: { mode: Mode }) {
this.modes = [
...this.modes.filter(a => a.name !== mode.name),
mode
];
},
ON_MODE_REMOVE({ name }: { name: string }) {
this.modes = this.modes.filter(a => a.name !== name);
if (this.modes.length === 0) {
this.modes = [defaultMode];
}
},
ON_TEMPLATE_ADD({ template }: { template: { id: string, html: string }}) {
if (this.templates[template.id]) {
this.warn(`Tried to add duplicate template '${template.id}'`);
} else {
this.templates[template.id] = template.html;
}
},
ON_UPDATE_THEMES({ themes }: { themes: { [key: string]: ThemeData } }) {
this.removeThemes();
this.setThemes(themes);
},
removeThemes() {
for (let i = 0; i < document.styleSheets.length; i++) {
const styleSheet = document.styleSheets[i];
const node = styleSheet.ownerNode as Element;
if (node.getAttribute("data-theme")) {
node.parentNode?.removeChild(node);
}
}
this.tplBackups.reverse();
for (const [elem, oldData] of this.tplBackups) {
elem.innerText = oldData;
}
this.tplBackups = [];
this.msgTplBackups.reverse();
for (const [id, oldData] of this.msgTplBackups) {
this.templates[id] = oldData;
}
this.msgTplBackups = [];
},
setThemes(themes: { [key: string]: ThemeData }) {
for (const [id, data] of Object.entries(themes)) {
if (data.style) {
const style = document.createElement("style");
style.type = "text/css";
style.setAttribute("data-theme", id);
style.appendChild(document.createTextNode(data.style));
document.head.appendChild(style);
}
if (data.styleSheet) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = data.baseUrl + data.styleSheet;
link.setAttribute("data-theme", id);
document.head.appendChild(link);
}
if (data.templates) {
for (const [tplId, tpl] of Object.entries(data.templates)) {
const elem = document.getElementById(tplId);
if (elem) {
this.tplBackups.push([elem, elem.innerText]);
elem.innerText = tpl;
}
}
}
if (data.script) {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = data.baseUrl + data.script;
document.head.appendChild(script);
}
if (data.msgTemplates) {
for (const [tplId, tpl] of Object.entries(data.msgTemplates)) {
this.msgTplBackups.push([tplId, this.templates[tplId]]);
this.templates[tplId] = tpl;
}
}
}
},
warn(msg: string) {
this.messages.push({
args: [msg],
template: "^3<b>CHAT-WARN</b>: ^0{0}"
});
},
clearShowWindowTimer() {
clearTimeout(this.showWindowTimer);
},
resetShowWindowTimer() {
this.clearShowWindowTimer();
this.showWindowTimer = window.setTimeout(() => {
if (this.hideState !== ChatHideStates.AlwaysShow && !this.showInput) {
this.showWindow = false;
}
}, CONFIG.fadeTimeout);
},
keyUp() {
this.resize();
},
keyDown(e: KeyboardEvent) {
if (e.which === 38 || e.which === 40) {
e.preventDefault();
this.moveOldMessageIndex(e.which === 38);
} else if (e.which == 33) {
var buf = document.getElementsByClassName("chat-messages")[0];
buf.scrollTop = buf.scrollTop - 100;
} else if (e.which == 34) {
var buf = document.getElementsByClassName("chat-messages")[0];
buf.scrollTop = buf.scrollTop + 100;
} else if (e.which === 9) { // tab
if (e.shiftKey || e.altKey) {
do {
--this.modeIdx;
if (this.modeIdx < 0) {
this.modeIdx = this.modes.length - 1;
}
} while (this.modes[this.modeIdx].hidden);
} else {
do {
this.modeIdx = (this.modeIdx + 1) % this.modes.length;
} while (this.modes[this.modeIdx].hidden);
}
const buf = document.getElementsByClassName('chat-messages')[0];
setTimeout(() => buf.scrollTop = buf.scrollHeight, 0);
}
this.resize();
},
moveOldMessageIndex(up: boolean) {
if (up && this.oldMessages.length > this.oldMessagesIndex + 1) {
this.oldMessagesIndex += 1;
this.message = this.oldMessages[this.oldMessagesIndex];
} else if (!up && this.oldMessagesIndex - 1 >= 0) {
this.oldMessagesIndex -= 1;
this.message = this.oldMessages[this.oldMessagesIndex];
} else if (!up && this.oldMessagesIndex - 1 === -1) {
this.oldMessagesIndex = -1;
this.message = "";
}
},
resize() {
const input = this.$refs.input as HTMLInputElement;
// scrollHeight includes padding, but content-box excludes padding
// remove padding before setting height on the element
const style = getComputedStyle(input);
const paddingRemove = parseFloat(style.paddingBottom) + parseFloat(style.paddingTop);
input.style.height = "5px";
input.style.height = `${input.scrollHeight - paddingRemove}px`;
},
send() {
if (this.message !== "") {
post(
"http://chat/chatResult",
JSON.stringify({
message: this.message,
mode: this.modes[this.modeIdxGet].name
})
);
this.oldMessages.unshift(this.message);
this.oldMessagesIndex = -1;
this.hideInput();
} else {
this.hideInput(true);
}
},
hideInput(canceled = false) {
setTimeout(() => {
const input = this.$refs.input as HTMLInputElement;
delete input.style.height;
}, 50);
if (canceled) {
post("http://chat/chatResult", JSON.stringify({ canceled }));
}
this.message = "";
this.showInput = false;
clearInterval(this.focusTimer);
if (this.hideState !== ChatHideStates.AlwaysHide) {
this.resetShowWindowTimer();
} else {
this.showWindow = false;
}
}
}
});

View File

@@ -0,0 +1,44 @@
<template>
<div id="app">
<div class="chat-window" :style="this.style" :class="{
'animated': !showWindow && hideAnimated,
'hidden': !showWindow
}">
<div class="chat-messages" ref="messages">
<message v-for="msg in filteredMessages"
:templates="templates"
:multiline="msg.multiline"
:args="msg.args"
:params="msg.params"
:color="msg.color"
:template="msg.template"
:template-id="msg.templateId"
:key="msg.id">
</message>
</div>
</div>
<div class="chat-input">
<div v-show="showInput" class="input">
<span class="prefix" :class="{ any: modes.length > 1 }" :style="{ color: modeColor }">{{modePrefix}}</span>
<textarea v-model="message"
ref="input"
type="text"
autofocus
spellcheck="false"
rows="1"
@keyup.esc="hideInput"
@keyup="keyUp"
@keydown="keyDown"
@keypress.enter.prevent="send">
</textarea>
</div>
<suggestions :message="message" :suggestions="suggestions">
</suggestions>
<div class="chat-hide-state" v-show="showHideState">
{{hideStateString}}
</div>
</div>
</div>
</template>
<script lang="ts" src="./App.ts"></script>

View File

@@ -0,0 +1,102 @@
import CONFIG from './config';
import Vue, { PropType } from 'vue';
export default Vue.component('message', {
data() {
return {};
},
computed: {
textEscaped(): string {
let s = this.template ? this.template : this.templates[this.templateId];
//This hack is required to preserve backwards compatability
if (!this.template && this.templateId == CONFIG.defaultTemplateId
&& this.args.length == 1) {
s = this.templates[CONFIG.defaultAltTemplateId] //Swap out default template :/
}
s = s.replace(`@default`, this.templates[this.templateId]);
s = s.replace(/{(\d+)}/g, (match, number) => {
const argEscaped = this.args[number] != undefined ? this.escape(this.args[number]) : match;
if (number == 0 && this.color) {
//color is deprecated, use templates or ^1 etc.
return this.colorizeOld(argEscaped);
}
return argEscaped;
});
// format variant args
s = s.replace(/\{\{([a-zA-Z0-9_\-]+?)\}\}/g, (match, id) => {
const argEscaped = this.params[id] != undefined ? this.escape(this.params[id]) : match;
return argEscaped;
});
return this.colorize(s);
},
},
methods: {
colorizeOld(str: string): string {
return `<span style="color: rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})">${str}</span>`
},
colorize(str: string): string {
let s = "<span>" + colorTrans(str) + "</span>";
const styleDict: {[ key: string ]: string} = {
'*': 'font-weight: bold;',
'_': 'text-decoration: underline;',
'~': 'text-decoration: line-through;',
'=': 'text-decoration: underline line-through;',
'r': 'text-decoration: none;font-weight: normal;',
};
const styleRegex = /\^(\_|\*|\=|\~|\/|r)(.*?)(?=$|\^r|<\/em>)/;
while (s.match(styleRegex)) { //Any better solution would be appreciated :P
s = s.replace(styleRegex, (str, style, inner) => `<em style="${styleDict[style]}">${inner}</em>`)
}
return s.replace(/<span[^>]*><\/span[^>]*>/g, '');
function colorTrans(str: string) {
return str
.replace(/\^([0-9])/g, (str, color) => `</span><span class="color-${color}">`)
.replace(/\^#([0-9A-F]{3,6})/gi, (str, color) => `</span><span class="color" style="color: #${color}">`)
.replace(/~([a-z])~/g, (str, color) => `</span><span class="gameColor-${color}">`);
}
},
escape(unsafe: string): string {
return String(unsafe)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
},
},
props: {
templates: {
type: Object as PropType<{ [key: string]: string }>,
},
args: {
type: Array as PropType<string[]>,
},
params: {
type: Object as PropType<{ [ key: string]: string }>,
},
template: {
type: String,
default: null,
},
templateId: {
type: String,
default: CONFIG.defaultTemplateId,
},
multiline: {
type: Boolean,
default: false,
},
color: { //deprecated
type: Array as PropType<number[]>,
default: null,
},
},
});

View File

@@ -0,0 +1,7 @@
<template>
<div class="msg" :class="{ multiline }">
<span v-html="textEscaped"></span>
</div>
</template>
<script lang="ts" src="./Message.ts"></script>

View File

@@ -0,0 +1,63 @@
import CONFIG from './config';
import Vue, { PropType } from 'vue';
export interface Suggestion {
name: string;
help: string;
params: string[];
disabled: boolean;
}
export default Vue.component('suggestions', {
props: {
message: {
type: String
},
suggestions: {
type: Array as PropType<Suggestion[]>
}
},
data() {
return {};
},
computed: {
currentSuggestions(): Suggestion[] {
if (this.message === '') {
return [];
}
const currentSuggestions = this.suggestions.filter((s) => {
if (!s.name.startsWith(this.message)) {
const suggestionSplitted = s.name.split(' ');
const messageSplitted = this.message.split(' ');
for (let i = 0; i < messageSplitted.length; i += 1) {
if (i >= suggestionSplitted.length) {
return i < suggestionSplitted.length + s.params.length;
}
if (suggestionSplitted[i] !== messageSplitted[i]) {
return false;
}
}
}
return true;
}).slice(0, CONFIG.suggestionLimit);
currentSuggestions.forEach((s) => {
// eslint-disable-next-line no-param-reassign
s.disabled = !s.name.startsWith(this.message);
s.params.forEach((p, index) => {
const wType = (index === s.params.length - 1) ? '.' : '\\S';
const regex = new RegExp(`${s.name} (?:\\w+ ){${index}}(?:${wType}*)$`, 'g');
// eslint-disable-next-line no-param-reassign
// @ts-ignore
p.disabled = this.message.match(regex) == null;
});
});
return currentSuggestions;
},
},
methods: {},
});

View File

@@ -0,0 +1,29 @@
<template>
<div class="suggestions-wrap" v-show="currentSuggestions.length > 0">
<ul class="suggestions">
<li class="suggestion" v-for="s in currentSuggestions" :key="s.name">
<p>
<span :class="{ 'disabled': s.disabled }">
{{s.name}}
</span>
<span class="param"
v-for="p in s.params"
:class="{ 'disabled': p.disabled }"
:key="p.name">
[{{p.name}}]
</span>
</p>
<small class="help">
<template v-if="!s.disabled">
{{s.help}}
</template>
<template v-for="p in s.params" v-if="!p.disabled">
{{p.help}}
</template>
</small>
</li>
</ul>
</div>
</template>
<script lang="ts" src="./Suggestions.ts"></script>

View File

@@ -0,0 +1,17 @@
export default {
defaultTemplateId: 'default', //This is the default template for 2 args1
defaultAltTemplateId: 'defaultAlt', //This one for 1 arg
templates: { //You can add static templates here
'default': '<b>{0}</b>: {1}',
'defaultAlt': '{0}',
'print': '<pre>{0}</pre>',
'example:important': '<h1>^2{0}</h1>'
},
fadeTimeout: 7000,
suggestionLimit: 5,
style: {
background: 'rgba(52, 73, 94, 0.7)',
width: '38vw',
height: '22%',
}
};

View File

@@ -0,0 +1,160 @@
.color-0{color: #ffffff;}
.color-1{color: #ff4444;}
.color-2{color: #99cc00;}
.color-3{color: #ffbb33;}
.color-4{color: #0099cc;}
.color-5{color: #33b5e5;}
.color-6{color: #aa66cc;}
.color-8{color: #cc0000;}
.color-9{color: #cc0068;}
.gameColor-w{color: #ffffff;}
.gameColor-r{color: #ff4444;}
.gameColor-g{color: #99cc00;}
.gameColor-y{color: #ffbb33;}
.gameColor-b{color: #33b5e5;}
/* todo: more game colors */
* {
font-family: 'Lato', sans-serif;
margin: 0;
padding: 0;
}
.no-grow {
flex-grow: 0;
}
em {
font-style: normal;
}
#app {
font-family: 'Lato', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: white;
}
.chat-window {
position: absolute;
top: 1.5%;
left: 0.8%;
width: 38%;
height: 22%;
max-width: 1000px;
background-color: rgba(52, 73, 94, 0.7);
-webkit-animation-duration: 2s;
}
.chat-messages {
position: relative;
height: 95%;
font-size: 1.8vh;
margin: 1%;
overflow-x: hidden;
overflow-y: hidden;
}
.chat-input {
font-size: 1.65vh;
position: absolute;
top: 23.8%;
left: 0.8%;
width: 38%;
max-width: 1000px;
box-sizing: border-box;
}
.chat-input > div.input {
position: relative;
display: flex;
align-items: stretch;
width: 100%;
background-color: rgba(44, 62, 80, 1.0);
}
.chat-hide-state {
text-transform: uppercase;
margin-left: 0.05vw;
font-size: 1.65vh;
}
.prefix {
font-size: 1.8vh;
/*position: absolute;
top: 0%;*/
height: 100%;
vertical-align: middle;
line-height: calc(1vh + 1vh + 1.85vh);
padding-left: 0.5vh;
text-transform: uppercase;
font-weight: bold;
display: inline-block;
}
textarea {
font-size: 1.65vh;
line-height: 1.85vh;
display: block;
box-sizing: content-box;
padding: 1vh;
padding-left: 0.5vh;
color: white;
border-width: 0;
height: 3.15%;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
background-color: transparent;
}
textarea:focus, input:focus {
outline: none;
}
.msg {
margin-bottom: 0.28%;
}
.multiline {
margin-left: 4%;
text-indent: -1.2rem;
white-space: pre-line;
}
.suggestions {
list-style-type: none;
padding: 0.5%;
padding-left: 1.4%;
font-size: 1.65vh;
box-sizing: border-box;
color: white;
background-color: rgba(44, 62, 80, 1.0);
width: 100%;
}
.help {
color: #b0bbbd;
}
.disabled {
color: #b0bbbd;
}
.suggestion {
margin-bottom: 0.5%;
}
.hidden {
opacity: 0;
}
.hidden.animated {
transition: opacity 1s;
}

View File

@@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<link href="/html/vendor/latofonts.css" rel="stylesheet">
<link href="/html/vendor/flexboxgrid.6.3.1.min.css" rel="stylesheet"></link>
<link href="/html/vendor/animate.3.5.2.min.css" rel="stylesheet"></link>
<link href="index.css" rel="stylesheet"></link>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -0,0 +1,7 @@
import Vue from 'vue';
import App from './App.vue';
const instance = new Vue({
el: '#app',
render: h => h(App),
});

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"outDir": "./",
"module": "es6",
"strict": true,
"moduleResolution": "node",
"target": "es6",
"allowJs": true,
"lib": [
"es2017",
"dom"
]
},
"include": [
"./**/*"
],
"exclude": []
}

View File

@@ -0,0 +1,31 @@
export function post(url: string, data: any) {
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
request.send(data);
}
function emulate(type: string, detail = {}) {
const detailRef = {
type,
...detail
};
window.dispatchEvent(new CustomEvent('message', {
detail: detailRef
}));
}
(window as any)['emulate'] = emulate;
(window as any)['demo'] = () => {
emulate('ON_MESSAGE', {
message: {
args: [ 'me', 'hello!' ]
}
})
emulate('ON_SCREEN_STATE_CHANGE', {
shouldHide: false
});
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,48 @@
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 300;
src: local('Lato Light'), local('Lato-Light'), url(fonts/LatoLight.woff2);
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 300;
src: local('Lato Light'), local('Lato-Light'), url(fonts/LatoLight2.woff2);
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: local('Lato Regular'), local('Lato-Regular'), url(fonts/LatoRegular.woff2);
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: local('Lato Regular'), local('Lato-Regular'), url(fonts/LatoRegular2.woff2);
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url(fonts/LatoBold.woff2);
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url(fonts/LatoBold2.woff2);
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}