Initial page
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*package-lock.json
|
||||
32
README.md
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||

|
||||
|
||||
## Product not yet ready!
|
||||
This project was developed for the [Tech Challenge](https://academy.unternehmertum.de/programs/tech-challenge) and is not production ready in any way!
|
||||
|
||||
|
||||
This is a web-based digital assistant chatbot system built with Svelte and OpenAI. It is designed to empathize with victims of stalking.
|
||||
|
||||
## Features
|
||||
|
||||
- 24/7 personalized, empathic and intelligent assistant
|
||||
- Anonymous & secure
|
||||
- Zero obligations, offers buffet like possibilities, e.g. call/report to police, fill psychological symptoms list, contact oranizations such as [Weisser Ring](https://weisser-ring.de/english) / [Telefonseelsorge](https://www.telefonseelsorge.de/international-helplines/), start taking daily notes ("stalking diary") using [NO STALK](https://nostalk.de)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
Svelte: A modern JavaScript framework for building web applications
|
||||
OpenAI: A leading artificial intelligence research laboratory
|
||||
|
||||
## Deployment
|
||||
|
||||
### Getting Started
|
||||
Clone the repository: `git clone git@github.com:wasnertobias/StalkingAId.git`
|
||||
Install dependencies: `pnpm install`
|
||||
Run the development server: `pnpm run dev`
|
||||
|
||||
Build the production version of the app: `pnpm run build`
|
||||
Deploy the built app to your preferred hosting platform (e.g. GitHub Pages, AWS, etc.)
|
||||
|
||||
## Contribution
|
||||
If you would like to contribute to the development of this digital assistant, please fork the repository and submit a pull request. All contributions are welcome!
|
||||
1
backend/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
||||
85
backend/node.js
Normal file
@ -0,0 +1,85 @@
|
||||
const express = require('express');
|
||||
const https = require('https');
|
||||
const fs = require("fs");
|
||||
const cors = require("cors");
|
||||
bodyParser = require('body-parser');
|
||||
const app = express();
|
||||
|
||||
app.use(cors({
|
||||
origin: 'https://stalkingaid.org'
|
||||
}));
|
||||
|
||||
|
||||
// support parsing of application/json type post data
|
||||
app.use(bodyParser.json());
|
||||
|
||||
//support parsing of application/x-www-form-urlencoded post data
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
app.route("/chat").post((req, res) => {
|
||||
const {history, msg} = req.body;
|
||||
getResponse(history, msg, res);
|
||||
});
|
||||
|
||||
https
|
||||
.createServer({
|
||||
key: fs.readFileSync("/etc/letsencrypt/live/stalkingaid.org/privkey.pem", "utf8"),
|
||||
cert: fs.readFileSync("/etc/letsencrypt/live/stalkingaid.org/cert.pem", "utf8"),
|
||||
ca: fs.readFileSync("/etc/letsencrypt/live/stalkingaid.org/chain.pem", "utf8"),
|
||||
}, app)
|
||||
.listen(8543, ()=>{
|
||||
console.log('server is runing at port 8543')
|
||||
});
|
||||
|
||||
async function getResponse(history, msg, res) {
|
||||
var staticPrefix = "The following is a chat conversation with an AI assistant for victims of stalking crimes and relatives. The assistant is helpful and very empathic and should response with short messages."
|
||||
+"\nContact free, anonymous but only german speaking telephone helpline 'Telefonseelsorge' https://www.telefonseelsorge.de (telephone number 0800.1110111 or 0800.111.0222; international helplines could be found here: https://www.telefonseelsorge.de/international-helplines) if you need someone to talk."
|
||||
+"\nContact free helpline from WEISSER RING (116 006, available from 7 to 22 o'clock)"
|
||||
+"\nContact the police (telephone number 110) in urgent cases."
|
||||
+"\nRecommendations should be only made once, though! It does not urge the user to do anything, just offers them buffet like possibilities."
|
||||
+"\nIf screenshots can be taken of the commited crime let them know that taking them as proof can be valuable, e.g. the app NO STALK (see https://nostalk.de) from WEISSER RING: cloud-based safe space for evidence. Also it can recommend going through a psychological symptoms checklist and to search psychological help. The following information of WEISSER RING (website https://weisser-ring.de/english) might be helpful to keep in mind as well:"
|
||||
+"\nStalking is a crime in which a person is intentionally and repeatedly harassed and pursued. Most victims are women, and stalking can last for months or even years. It can involve phone calls, messages, lurking, following, SMS messages, emails, letters, gifts, threats, insults, property damage, and coercion. Cyberstalking refers to stalking in the digital world, such as through emails, messaging services, chats, and social media. The consequences of stalking can include psychological stress symptoms such as sleep problems, nightmares, gastrointestinal problems, concentration problems, fear, panic attacks, depression, cardiovascular disorders, and limitations on daily life. It is important for those affected to seek help in order to end the stalking and restore their safety."
|
||||
+"\nThe purpose of an anti-stalking app is to provide a way for victims of stalking to document and report the stalking incidents they experience. This documentation can be used as evidence to take to the police in order to successfully file a report. The app allows victims to quickly and easily document incidents, rather than waiting until later and potentially forgetting or downplaying the events. The app also involves the police in the process to verify the authenticity of the documentation and help ensure that someone is not falsely accused. Stalking can have significant impacts on social life, including the need to change jobs or residences, the end of relationships, and the loss of friends. The app is designed to be user-friendly and easy to use in order to make it more accessible to victims. It also includes resources for seeking help and support. NO STALK is a completely free service!"
|
||||
+"\nThe legal options for addressing stalking either include seeking a restraining order or pursuing criminal charges. A restraining order, also known as an injunction, can be obtained through the civil court system and prohibits the perpetrator from certain actions. It can be obtained quickly without the need for evidence, but having documentation such as a stalking diary can be helpful. A restraining order is effective in the short term and can help establish a safe distance between the victim and perpetrator. It can also be used to prevent the perpetrator from entering the victim's home or coming within a certain distance of the victim. Criminal charges can be pursued through the criminal justice system and may result in fines or imprisonment for the perpetrator. It is important for victims of stalking to seek help and support, including through hotlines, counseling, and support groups."
|
||||
+"\nIf you are being stalked, it can be stressful, exhausting, and make you sick. Here are some tips on how to protect yourself and push back against the stalker:"
|
||||
+"\n- Make it clear that you do not want any contact, now or in the future, and do so in front of witnesses."
|
||||
+"\n- Inform your loved ones and your community about the situation, so they can support you and help protect you."
|
||||
+"\n- Document everything, including all unwanted calls, messages, and letters. Consider using an app like NO STALK to keep a record of each incident and its impact on you."
|
||||
+"\n- Use technology to protect yourself, such as getting a private phone number or using an answering machine to screen calls. If you are being stalked online, create a new email address and social media accounts."
|
||||
+"\n- In serious cases: File a report with the police, and consider bringing a trusted person or legal representative with you."
|
||||
+"\n- Take advantage of counseling and support services, such as those offered by Weisser Ring and the police."
|
||||
+"\n- See a doctor or counselor to address any physical or emotional effects of the stalking."
|
||||
+"\n- Ultimatively: Consider obtaining a restraining order, which can help prevent further contact from the stalker."
|
||||
|
||||
|
||||
const { Configuration, OpenAIApi } = require("openai");
|
||||
|
||||
const configuration = new Configuration({
|
||||
apiKey: process.env.OPEN_AI_API_KEY,
|
||||
});
|
||||
const openai = new OpenAIApi(configuration);
|
||||
|
||||
history = history + '\nHuman: ' + msg;
|
||||
|
||||
const prompt = staticPrefix + '\n' + history;
|
||||
console.log("sending to openai: " + prompt);
|
||||
|
||||
try {
|
||||
const response = await openai.createCompletion({
|
||||
model: "text-davinci-003",
|
||||
prompt: prompt + "\n",
|
||||
temperature: 0.9,
|
||||
max_tokens: 500,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0.5,
|
||||
presence_penalty: 0.6,
|
||||
stop: [" Human:", " AI:"],
|
||||
});
|
||||
const stringResponse = JSON.stringify(response.data);
|
||||
console.log("got from openai: " + stringResponse);
|
||||
res.status(200).end(history + response.data.choices[0].text);
|
||||
} catch(error) {
|
||||
console.error(error.response.status, error.response.data);
|
||||
res.status(error.response.status).json(error.response.data);
|
||||
}
|
||||
}
|
||||
18
backend/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "nodejs",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend of StalkingAId",
|
||||
"main": "node.js",
|
||||
"scripts": {
|
||||
"dev": "npm run node.js"
|
||||
},
|
||||
"author": "Tobias Wasner",
|
||||
"license": "GNU General Public License v3.0",
|
||||
"repository": "https://github.com/wasnertobias/StalkingAId",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"openai": "^3.1.0"
|
||||
}
|
||||
}
|
||||
13
frontend/.eslintignore
Normal file
@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
20
frontend/.eslintrc.cjs
Normal file
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||
plugins: ['svelte3', '@typescript-eslint'],
|
||||
ignorePatterns: ['*.cjs'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
settings: {
|
||||
'svelte3/typescript': () => require('typescript')
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
};
|
||||
11
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
*.log
|
||||
1
frontend/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
engine-strict=true
|
||||
13
frontend/.prettierignore
Normal file
@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
9
frontend/.prettierrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
14
frontend/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"cssvar.files": [
|
||||
"./node_modules/open-props/open-props.min.css",
|
||||
// if you have an alternative path to where your styles are located
|
||||
// you can add it in this array of files
|
||||
"./src/lib/assets/style/global.scss"
|
||||
],
|
||||
|
||||
// Do not ignore node_modules css files, which is ignored by default
|
||||
"cssvar.ignore": [],
|
||||
|
||||
// add support for autocomplete in JS or JS like files
|
||||
"cssvar.extensions": ["css", "scss", "postcss", "jsx", "tsx", "svelte"]
|
||||
}
|
||||
40
frontend/package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "Frontend of StalkingAId",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^1.0.0",
|
||||
"@sveltejs/adapter-static": "^1.0.1",
|
||||
"@sveltejs/kit": "^1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-check": "^2.9.2",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@poppanator/sveltekit-svg": "^2.0.2",
|
||||
"@sveltejs/adapter-node": "^1.1.0",
|
||||
"open-props": "^1.5.3",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-jit-props": "^1.0.9",
|
||||
"sass": "^1.57.1"
|
||||
}
|
||||
}
|
||||
2070
frontend/pnpm-lock.yaml
Normal file
9
frontend/src/app.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
26
frontend/src/app.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
(function () {
|
||||
var itemKey = 'isDarkMode';
|
||||
if (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(itemKey) !== null) {
|
||||
var isDarkMode = sessionStorage.getItem(itemKey) === 'true';
|
||||
if (isDarkMode) {
|
||||
document.body.classList.add('dark-mode');
|
||||
} else {
|
||||
document.body.classList.add('light-mode');
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import AutocompleteItem from './AutocompleteItem.svelte';
|
||||
|
||||
export let options: string[] = ['test-1', 'test-2', 'test-3'];
|
||||
export let placeholder: string | undefined = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher<{ select: string | undefined }>();
|
||||
|
||||
/* FILTERING countres DATA BASED ON INPUT */
|
||||
let filteredCountries: string[] = [];
|
||||
// $: console.log(filteredCountries)
|
||||
|
||||
const filterNace = () => {
|
||||
let storageArr: string[] = [];
|
||||
if (inputValue) {
|
||||
options.forEach((option) => {
|
||||
const matchIndex = option.toLowerCase().indexOf(inputValue.toLowerCase());
|
||||
if (matchIndex !== -1) {
|
||||
storageArr = [...storageArr, makeMatchBold(option, matchIndex)];
|
||||
}
|
||||
});
|
||||
}
|
||||
filteredCountries = storageArr;
|
||||
};
|
||||
|
||||
/* HANDLING THE INPUT */
|
||||
let searchInput: HTMLInputElement; // use with bind:this to focus element
|
||||
let inputValue = '';
|
||||
|
||||
$: if (!inputValue) {
|
||||
filteredCountries = [];
|
||||
hiLiteIndex = null;
|
||||
}
|
||||
|
||||
const clearInput = () => {
|
||||
inputValue = '';
|
||||
searchInput.focus();
|
||||
};
|
||||
|
||||
const setInputVal = (categoryName: string) => {
|
||||
inputValue = removeBold(categoryName);
|
||||
filteredCountries = [];
|
||||
hiLiteIndex = null;
|
||||
document.querySelector('#nace-input').focus();
|
||||
|
||||
dispatch('select', inputValue);
|
||||
};
|
||||
|
||||
const makeMatchBold = (str: string, start: number) => {
|
||||
// replace part of (country name === inputValue) with strong tags
|
||||
let matched = str.substring(start, start + inputValue.length);
|
||||
let makeBold = `<strong>${matched}</strong>`;
|
||||
let boldedMatch = str.replace(matched, makeBold);
|
||||
return boldedMatch;
|
||||
};
|
||||
|
||||
const removeBold = (str: string) => {
|
||||
//replace < and > all characters between
|
||||
return str.replace(/<(.)*?>/g, '');
|
||||
// return str.replace(/<(strong)>/g, "").replace(/<\/(strong)>/g, "");
|
||||
};
|
||||
|
||||
/* NAVIGATING OVER THE LIST OF COUNTRIES W HIGHLIGHTING */
|
||||
let hiLiteIndex: null | number = null;
|
||||
//$: console.log(hiLiteIndex);
|
||||
$: hiLitedCountry = filteredCountries[hiLiteIndex];
|
||||
|
||||
const navigateList = (e) => {
|
||||
if (e.key === 'ArrowDown' && hiLiteIndex <= filteredCountries.length - 1) {
|
||||
hiLiteIndex === null ? (hiLiteIndex = 0) : (hiLiteIndex += 1);
|
||||
} else if (e.key === 'ArrowUp' && hiLiteIndex !== null) {
|
||||
hiLiteIndex === 0 ? (hiLiteIndex = filteredCountries.length - 1) : (hiLiteIndex -= 1);
|
||||
} else if (e.key === 'Enter') {
|
||||
setInputVal(filteredCountries[hiLiteIndex]);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={navigateList} />
|
||||
|
||||
<div class="autocomplete">
|
||||
<input
|
||||
id="nace-input"
|
||||
type="text"
|
||||
autocomplete=""
|
||||
{placeholder}
|
||||
bind:this={searchInput}
|
||||
bind:value={inputValue}
|
||||
on:input={filterNace}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- FILTERED LIST OF COUNTRIES -->
|
||||
{#if filteredCountries.length > 0}
|
||||
<ul id="autocomplete-items-list">
|
||||
{#each filteredCountries as country, i}
|
||||
<AutocompleteItem
|
||||
itemLabel={country}
|
||||
highlighted={i === hiLiteIndex}
|
||||
on:click={() => setInputVal(country)}
|
||||
/>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
div.autocomplete {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#autocomplete-items-list {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
top: -11px;
|
||||
border: 2px solid rgba(var(--color-foreground-light-raw), 0.2);
|
||||
background-color: var(--color-background);
|
||||
border-bottom-left-radius: var(--size-3);
|
||||
border-bottom-right-radius: var(--size-3);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-5);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,45 @@
|
||||
<script>
|
||||
export let itemLabel;
|
||||
export let highlighted;
|
||||
</script>
|
||||
|
||||
<li class="autocomplete-items" class:autocomplete-active={highlighted} on:click>
|
||||
{@html itemLabel}
|
||||
</li>
|
||||
|
||||
<style lang="scss">
|
||||
li.autocomplete-items {
|
||||
display: block;
|
||||
list-style: none;
|
||||
border-bottom: 1px solid rgba(var(--color-foreground-raw), 0.2);
|
||||
z-index: 99;
|
||||
/*position the autocomplete items to be the same width as the container:*/
|
||||
width: 100%;
|
||||
padding: var(--size-2) var(--size-4);
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
|
||||
max-inline-size: unset;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
li.autocomplete-items:hover {
|
||||
/*when hovering an item:*/
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
li.autocomplete-items:active {
|
||||
/*when navigating through the items using the arrow keys:*/
|
||||
background-color: var(--color-primary) !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
.autocomplete-active {
|
||||
/*when navigating through the items using the arrow keys:*/
|
||||
background-color: var(--color-primary) !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
83
frontend/src/lib/components/Button.svelte
Normal file
@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
let className: string | undefined = undefined;
|
||||
|
||||
export { className as class };
|
||||
export let small: boolean = false;
|
||||
export let fullWidth: boolean = false;
|
||||
export let secondary: boolean = false;
|
||||
export let disabled: boolean = false;
|
||||
</script>
|
||||
|
||||
<button class={className} {disabled} class:fullWidth class:secondary class:small on:click
|
||||
><slot /></button
|
||||
>
|
||||
|
||||
<style lang="scss">
|
||||
button {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
color: rgba(var(--color-background-raw), 0.8);
|
||||
outline: none;
|
||||
border: none;
|
||||
text-transform: uppercase;
|
||||
border-radius: var(--size-3);
|
||||
padding: var(--size-3) var(--size-5);
|
||||
background-color: var(--color-primary);
|
||||
transition: all 0.2s var(--easing-default);
|
||||
|
||||
text-shadow: none !important;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--color-primary-raw), 0.5);
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: calc(var(--size-2) * -1);
|
||||
top: calc(var(--size-2) * -1);
|
||||
right: calc(var(--size-2) * -1);
|
||||
bottom: calc(var(--size-2) * -1);
|
||||
background-color: rgba(var(--color-primary-raw), 0.5);
|
||||
border-radius: var(--size-4);
|
||||
z-index: -1;
|
||||
transition: transform 0.2s var(--easing-default);
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
&:active:before {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
&.small {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background-color: rgba(var(--color-foreground-raw), 0.8);
|
||||
}
|
||||
|
||||
&.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
:global(svg) {
|
||||
stroke: rgba(var(--color-background-raw), 0.8);
|
||||
stroke-width: 2px;
|
||||
margin-right: 0.5rem;
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
frontend/src/lib/components/ButtonGroup.svelte
Normal file
@ -0,0 +1,13 @@
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '$lib/style/app.scss';
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--size-3);
|
||||
}
|
||||
</style>
|
||||
29
frontend/src/lib/components/Card.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
export let background: boolean = true;
|
||||
let className: string | undefined = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={(className ?? '') + ' card'} class:background><slot /></div>
|
||||
|
||||
<style lang="scss">
|
||||
.card {
|
||||
padding: var(--size-fluid-3);
|
||||
|
||||
&.background {
|
||||
background-color: var(--color-background-light);
|
||||
border: 3px solid var(--color-background);
|
||||
border-radius: var(--radius-4);
|
||||
box-shadow: var(--shadow-5);
|
||||
transition: box-shadow 0.2s var(--easing-default);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-6);
|
||||
}
|
||||
}
|
||||
|
||||
@media (--motionOK) {
|
||||
animation: var(--animation-fade-in);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
60
frontend/src/lib/components/Container.svelte
Normal file
@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import Liquid from './Liquid.svelte';
|
||||
|
||||
let className: string | undefined = undefined;
|
||||
export { className as class };
|
||||
export let centered = false;
|
||||
export let loading = false;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class={className ?? '' + ' container'} class:centered>
|
||||
<slot />
|
||||
</div>
|
||||
{#if loading}
|
||||
<div class="loading-overlay">
|
||||
<Liquid />
|
||||
<h3>LOADING</h3>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss" global>
|
||||
@import '$lib/style/app.scss';
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--size-10);
|
||||
padding: 0 var(--size-5);
|
||||
|
||||
:global(> *) {
|
||||
width: 100%;
|
||||
max-width: $max-page-width;
|
||||
}
|
||||
|
||||
&.centered {
|
||||
@media (min-width: $breakpoint-small) {
|
||||
height: 80vh;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background-color: rgba(var(--color-background-raw), 0.8);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
</style>
|
||||
49
frontend/src/lib/components/Footer.svelte
Normal file
@ -0,0 +1,49 @@
|
||||
<div class="footer-container">
|
||||
<span class="legal">© StalkingAId - {new Date().getFullYear()}</span>
|
||||
<div class="menu">
|
||||
<a href="/imprint">Imprint</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import './src/lib/style/app.scss';
|
||||
|
||||
.footer-container {
|
||||
border-top: 1px solid var(--color-primary);
|
||||
width: 100%;
|
||||
max-width: $max-page-width;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem 0;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-top: var(--size-10);
|
||||
margin-top: var(--size-10);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.legal {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.menu {
|
||||
padding-bottom: 1.5rem;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
padding-left: 1.5rem;
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
156
frontend/src/lib/components/Header.svelte
Normal file
@ -0,0 +1,156 @@
|
||||
<script lang="ts">
|
||||
import IconGithub from '$lib/icons/github.svg';
|
||||
import IconSun from '$lib/icons/sun.svg';
|
||||
import IconHash from '$lib/icons/hash.svg';
|
||||
import Button from './Button.svelte';
|
||||
import HeaderIcon from '$lib/icons/logo.png';
|
||||
import Tag from './Tag.svelte';
|
||||
|
||||
const onToggleDarkMode = () => {
|
||||
const isDarkMode = document.body.classList.contains('dark-mode');
|
||||
if (isDarkMode) {
|
||||
document.body.classList.remove('dark-mode');
|
||||
document.body.classList.add('light-mode');
|
||||
} else {
|
||||
document.body.classList.remove('light-mode');
|
||||
document.body.classList.add('dark-mode');
|
||||
}
|
||||
sessionStorage.setItem('isDarkMode', !isDarkMode + '');
|
||||
};
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<div class="inner-head">
|
||||
<nav class="left">
|
||||
<a class="logo" href="/">
|
||||
<HeaderIcon width="180" />
|
||||
</a>
|
||||
</nav>
|
||||
<div />
|
||||
<nav class="right">
|
||||
<Tag>ALPHA v0.3</Tag>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style lang="scss">
|
||||
@import '$lib/style/app.scss';
|
||||
|
||||
header {
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 var(--size-5);
|
||||
|
||||
backdrop-filter: blur(12px);
|
||||
background-color: rgba(var(--color-background-raw), 0.85);
|
||||
border-bottom: 1px solid rgba(var(--color-foreground-raw), 0.1);
|
||||
}
|
||||
|
||||
.left {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
|
||||
.right {
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
.inner-head {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
justify-content: space-between;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
|
||||
width: 100%;
|
||||
max-width: $max-page-width;
|
||||
|
||||
> nav {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
gap: 0.25rem;
|
||||
@media (min-width: $breakpoint-small) {
|
||||
gap: var(--default-pd);
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
> nav > * {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-foreground);
|
||||
cursor: pointer;
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
:global(svg) {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
:global(svg) {
|
||||
transition: all 0.2s var(--easing-default);
|
||||
stroke-width: 2px;
|
||||
stroke: var(--color-primary);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--color-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 0.175rem;
|
||||
font-size: 1rem;
|
||||
|
||||
transition: all 0.2s var(--easing-default);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-foreground);
|
||||
border-radius: 3px;
|
||||
}
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only make labels transparent in DarkMode due to supperior contrast
|
||||
:global(.dark-mode .label) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
a.logo {
|
||||
:global(svg) {
|
||||
transition: fill 0.2 var(--easing-default);
|
||||
|
||||
height: 2.8rem;
|
||||
@media (min-width: $breakpoint-small) {
|
||||
height: 3.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
0
frontend/src/lib/components/Icon.svelte
Normal file
9
frontend/src/lib/components/Input.svelte
Normal file
@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
export let placeholder: string | undefined = undefined;
|
||||
export let type: string = 'text';
|
||||
</script>
|
||||
|
||||
<input {type} {placeholder} />
|
||||
|
||||
<style lang="scss" global>
|
||||
</style>
|
||||
128
frontend/src/lib/components/Liquid.svelte
Normal file
@ -0,0 +1,128 @@
|
||||
<!--
|
||||
|
||||
Follow me on
|
||||
Dribbble: https://dribbble.com/supahfunk
|
||||
Twitter: https://twitter.com/supahfunk
|
||||
Codepen: https://codepen.io/supah/
|
||||
|
||||
-->
|
||||
<div>
|
||||
<div class="wrapper">
|
||||
<div class="ball" />
|
||||
<div class="ball" />
|
||||
<div class="ball" />
|
||||
</div>
|
||||
|
||||
<svg>
|
||||
<defs>
|
||||
<filter id="filter">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="18" result="blur" />
|
||||
<feColorMatrix
|
||||
in="blur"
|
||||
mode="matrix"
|
||||
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 28 -10"
|
||||
result="filter"
|
||||
/>
|
||||
<feComposite in="SourceGraphic" in2="filter" operator="atop" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use 'sass:math';
|
||||
$radius: 80px;
|
||||
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
filter: url('#filter');
|
||||
}
|
||||
|
||||
.ball {
|
||||
width: $radius;
|
||||
height: $radius;
|
||||
position: absolute;
|
||||
top: calc(50% - math.div($radius, 2));
|
||||
left: calc(50% - math.div($radius, 2));
|
||||
|
||||
&:before {
|
||||
background: var(--color-primary);
|
||||
border-radius: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
transform: translate(-$radius * 1.1);
|
||||
animation: x-axis-lateral 2s infinite alternate ease-in-out;
|
||||
&:before {
|
||||
animation: y-axis-lateral 1s infinite 0.1s alternate ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation: x-axis 2s infinite alternate ease-in-out;
|
||||
&:before {
|
||||
animation: y-axis 1s infinite 0.5s alternate ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
transform: translate($radius * 1.1, $radius * 1.3);
|
||||
animation: x-axis-lateral 2s infinite alternate ease;
|
||||
&:before {
|
||||
animation: y-axis-lateral 1s infinite 0.4s alternate ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes x-axis {
|
||||
0% {
|
||||
transform: translate(-$radius * 1.6);
|
||||
}
|
||||
100% {
|
||||
transform: translate($radius * 1.6);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes y-axis {
|
||||
0% {
|
||||
transform: translateY($radius * 0.7);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-$radius * 1.1) scale(0.8);
|
||||
background: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes x-axis-lateral {
|
||||
0% {
|
||||
transform: translate(-$radius * 0.6);
|
||||
}
|
||||
100% {
|
||||
transform: translate($radius * 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes y-axis-lateral {
|
||||
0% {
|
||||
transform: translateY(math.div($radius, 5));
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-$radius);
|
||||
background: var(--color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
frontend/src/lib/components/Notification.svelte
Normal file
@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import Card from './Card.svelte';
|
||||
import IconWarn from '$lib/icons/alert-octagon.svg';
|
||||
|
||||
export let outline = false;
|
||||
</script>
|
||||
|
||||
<Card class={'notification-card' + (outline ? ' outline' : '')}>
|
||||
<div class="icon"><IconWarn width="60" height="60" /></div>
|
||||
<div class="text"><slot /></div>
|
||||
</Card>
|
||||
|
||||
<style lang="scss" global>
|
||||
@import '$lib/style/app.scss';
|
||||
|
||||
:global(.card.notification-card) {
|
||||
padding: var(--size-3) var(--size-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-5);
|
||||
background-color: transparent !important;
|
||||
border: 3px solid #e67d22 !important;
|
||||
color: #fff;
|
||||
flex-direction: column;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
:global(p) {
|
||||
color: var(--color-foreground);
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
:global(h2),
|
||||
:global(h3),
|
||||
:global(h4) {
|
||||
margin-bottom: 0;
|
||||
color: var(--color-foreground);
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
:global(svg) {
|
||||
stroke: #e67d22;
|
||||
stroke-width: 1.5px;
|
||||
fill: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
127
frontend/src/lib/components/RadioGroup/RadioGroup.svelte
Normal file
@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import Card from '../Card.svelte';
|
||||
import type { RadioOption } from './types';
|
||||
|
||||
// based on suggestions from:
|
||||
// Sami Keijonen https://webdesign.tutsplus.com/tutorials/how-to-make-custom-accessible-checkboxes-and-radio-buttons--cms-32074
|
||||
// and Inclusive Components by Heydon Pickering https://inclusive-components.design/toggle-button/
|
||||
|
||||
export let options: RadioOption[] = [];
|
||||
export let legend: string | undefined = undefined;
|
||||
export let userSelected: string | undefined = undefined;
|
||||
export let fontSize = 16;
|
||||
export let flexDirection = 'column';
|
||||
|
||||
const uniqueID = Math.floor(Math.random() * 100);
|
||||
|
||||
const slugify = (str = '') => str.toLowerCase().replace(/ /g, '-').replace(/\./g, '');
|
||||
</script>
|
||||
|
||||
<div
|
||||
role="radiogroup"
|
||||
class="group-container"
|
||||
aria-labelledby={`label-${uniqueID}`}
|
||||
id={`group-${uniqueID}`}
|
||||
>
|
||||
{#each options as { value, label, icon }}
|
||||
<label for={slugify(label)}>
|
||||
<Card class={'radio-wrap' + (value === userSelected ? ' selected' : '')}>
|
||||
<span class="icon">{icon}</span>
|
||||
<input class="sr-only" type="radio" id={slugify(label)} bind:group={userSelected} {value} />
|
||||
{label}
|
||||
</Card>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use 'sass:math';
|
||||
|
||||
@import '$lib/style/app.scss';
|
||||
|
||||
$dot-size: 1.5rem;
|
||||
|
||||
:global(.radio-wrap) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
border: 5px solid var(--color-background) !important;
|
||||
|
||||
.icon {
|
||||
font-size: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.card.selected) {
|
||||
border: 5px solid var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.group-container {
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
@media (min-width: $breakpoint-small) {
|
||||
flex-direction: row;
|
||||
}
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--size-4);
|
||||
}
|
||||
|
||||
.legend {
|
||||
font-weight: bold;
|
||||
}
|
||||
label {
|
||||
width: 100%;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
margin-bottom: var(--size-4);
|
||||
|
||||
:global(.card) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
position: relative;
|
||||
|
||||
&:checked {
|
||||
& + label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
& + label::before {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
& + label::after {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type='radio']:disabled + label {
|
||||
color: rgba(var(--color-foreground-raw), 0.2);
|
||||
}
|
||||
|
||||
input[type='radio']:disabled + label::before {
|
||||
background: rgba(var(--color-foreground-raw), 0.2);
|
||||
}
|
||||
</style>
|
||||
1
frontend/src/lib/components/RadioGroup/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type RadioOption = { value: string; label: string; icon: string };
|
||||
304
frontend/src/lib/components/Slider.svelte
Normal file
@ -0,0 +1,304 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fly, fade } from 'svelte/transition';
|
||||
|
||||
// Props
|
||||
export let min = 1;
|
||||
export let max = 10000;
|
||||
export let initialValue = 0;
|
||||
export let id = null;
|
||||
export let value = typeof initialValue === 'string' ? parseInt(initialValue) : initialValue;
|
||||
|
||||
// Node Bindings
|
||||
let container = null;
|
||||
let thumb = null;
|
||||
let progressBar = null;
|
||||
let element = null;
|
||||
|
||||
// Internal State
|
||||
let elementX = null;
|
||||
let currentThumb = null;
|
||||
let holding = false;
|
||||
let thumbHover = false;
|
||||
let keydownAcceleration = 0;
|
||||
let accelerationTimer = null;
|
||||
|
||||
// Dispatch 'change' events
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Mouse shield used onMouseDown to prevent any mouse events penetrating other elements,
|
||||
// ie. hover events on other elements while dragging. Especially for Safari
|
||||
const mouseEventShield = document.createElement('div');
|
||||
mouseEventShield.setAttribute('class', 'mouse-over-shield');
|
||||
mouseEventShield.addEventListener('mouseover', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
function resizeWindow() {
|
||||
elementX = element.getBoundingClientRect().left;
|
||||
}
|
||||
|
||||
// Allows both bind:value and on:change for parent value retrieval
|
||||
function setValue(val) {
|
||||
value = val;
|
||||
dispatch('change', { value });
|
||||
}
|
||||
|
||||
function onTrackEvent(e) {
|
||||
// Update value immediately before beginning drag
|
||||
updateValueOnEvent(e);
|
||||
onDragStart(e);
|
||||
}
|
||||
|
||||
function onHover(e) {
|
||||
thumbHover = thumbHover ? false : true;
|
||||
}
|
||||
|
||||
function onDragStart(e) {
|
||||
// If mouse event add a pointer events shield
|
||||
if (e.type === 'mousedown') document.body.append(mouseEventShield);
|
||||
currentThumb = thumb;
|
||||
}
|
||||
|
||||
function onDragEnd(e) {
|
||||
// If using mouse - remove pointer event shield
|
||||
if (e.type === 'mouseup') {
|
||||
if (document.body.contains(mouseEventShield)) document.body.removeChild(mouseEventShield);
|
||||
// Needed to check whether thumb and mouse overlap after shield removed
|
||||
if (isMouseInElement(e, thumb)) thumbHover = true;
|
||||
}
|
||||
currentThumb = null;
|
||||
}
|
||||
|
||||
// Check if mouse event cords overlay with an element's area
|
||||
function isMouseInElement(event, element) {
|
||||
let rect = element.getBoundingClientRect();
|
||||
let { clientX: x, clientY: y } = event;
|
||||
if (x < rect.left || x >= rect.right) return false;
|
||||
if (y < rect.top || y >= rect.bottom) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Accessible keypress handling
|
||||
function onKeyPress(e) {
|
||||
// Max out at +/- 10 to value per event (50 events / 5)
|
||||
// 100 below is to increase the amount of events required to reach max velocity
|
||||
if (keydownAcceleration < 50) keydownAcceleration++;
|
||||
let throttled = Math.ceil(keydownAcceleration / 5);
|
||||
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
|
||||
if (value + throttled > max || value >= max) {
|
||||
setValue(max);
|
||||
} else {
|
||||
setValue(value + throttled);
|
||||
}
|
||||
}
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
|
||||
if (value - throttled < min || value <= min) {
|
||||
setValue(min);
|
||||
} else {
|
||||
setValue(value - throttled);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset acceleration after 100ms of no events
|
||||
clearTimeout(accelerationTimer);
|
||||
accelerationTimer = setTimeout(() => (keydownAcceleration = 1), 100);
|
||||
}
|
||||
|
||||
function calculateNewValue(clientX) {
|
||||
// Find distance between cursor and element's left cord (20px / 2 = 10px) - Center of thumb
|
||||
let delta = clientX - (elementX + 10);
|
||||
|
||||
// Use width of the container minus (5px * 2 sides) offset for percent calc
|
||||
let percent = (delta * 100) / (container.clientWidth - 10);
|
||||
|
||||
// Limit percent 0 -> 100
|
||||
percent = percent < 0 ? 0 : percent > 100 ? 100 : percent;
|
||||
|
||||
// Limit value min -> max
|
||||
setValue(parseInt((percent * (max - min)) / 100) + min);
|
||||
}
|
||||
|
||||
// Handles both dragging of touch/mouse as well as simple one-off click/touches
|
||||
function updateValueOnEvent(e) {
|
||||
// touchstart && mousedown are one-off updates, otherwise expect a currentPointer node
|
||||
if (!currentThumb && e.type !== 'touchstart' && e.type !== 'mousedown') return false;
|
||||
|
||||
if (e.stopPropagation) e.stopPropagation();
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
|
||||
// Get client's x cord either touch or mouse
|
||||
const clientX =
|
||||
e.type === 'touchmove' || e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
|
||||
|
||||
calculateNewValue(clientX);
|
||||
}
|
||||
|
||||
// React to left position of element relative to window
|
||||
$: if (element) elementX = element.getBoundingClientRect().left;
|
||||
|
||||
// Set a class based on if dragging
|
||||
$: holding = Boolean(currentThumb);
|
||||
|
||||
// Update progressbar and thumb styles to represent value
|
||||
$: if (progressBar && thumb) {
|
||||
// Limit value min -> max
|
||||
value = value > min ? value : min;
|
||||
value = value < max ? value : max;
|
||||
|
||||
let percent = ((value - min) * 100) / (max - min);
|
||||
let offsetLeft = (container.clientWidth - 10) * (percent / 100) + 5;
|
||||
|
||||
// Update thumb position + active range track width
|
||||
thumb.style.left = `${offsetLeft}px`;
|
||||
progressBar.style.width = `${offsetLeft}px`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:touchmove|nonpassive={updateValueOnEvent}
|
||||
on:touchcancel={onDragEnd}
|
||||
on:touchend={onDragEnd}
|
||||
on:mousemove={updateValueOnEvent}
|
||||
on:mouseup={onDragEnd}
|
||||
on:resize={resizeWindow}
|
||||
/>
|
||||
<div class="range">
|
||||
<div
|
||||
class="range__wrapper"
|
||||
tabindex="0"
|
||||
on:keydown={onKeyPress}
|
||||
bind:this={element}
|
||||
role="slider"
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
aria-valuenow={value}
|
||||
{id}
|
||||
on:mousedown={onTrackEvent}
|
||||
on:touchstart={onTrackEvent}
|
||||
>
|
||||
<div class="range__track" bind:this={container}>
|
||||
<div class="range__track--highlighted" bind:this={progressBar} />
|
||||
<div
|
||||
class="range__thumb"
|
||||
class:range__thumb--holding={holding}
|
||||
bind:this={thumb}
|
||||
on:touchstart={onDragStart}
|
||||
on:mousedown={onDragStart}
|
||||
on:mouseover={() => (thumbHover = true)}
|
||||
on:mouseout={() => (thumbHover = false)}
|
||||
>
|
||||
{#if holding || thumbHover}
|
||||
<div class="range__tooltip" in:fly={{ y: 7, duration: 200 }} out:fade={{ duration: 100 }}>
|
||||
{value}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:head>
|
||||
<style>
|
||||
.mouse-over-shield {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(255, 0, 0, 0);
|
||||
z-index: 10000;
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
||||
</svelte:head>
|
||||
|
||||
<style>
|
||||
.range {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.range__wrapper {
|
||||
min-width: 100%;
|
||||
position: relative;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.range__wrapper:focus-visible > .range__track {
|
||||
box-shadow: 0 0 0 2px white, 0 0 0 3px var(--color-primary);
|
||||
}
|
||||
|
||||
.range__track {
|
||||
height: 6px;
|
||||
background-color: rgba(var(--color-foreground-raw), 0.1);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.range__track--highlighted {
|
||||
background-color: var(--color-primary);
|
||||
width: 0;
|
||||
height: 6px;
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.range__thumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
border-radius: 999px;
|
||||
margin-top: -8px;
|
||||
transition: box-shadow 100ms;
|
||||
user-select: none;
|
||||
box-shadow: var(
|
||||
--thumb-boxshadow,
|
||||
0 1px 1px 0 rgba(0, 0, 0, 0.14),
|
||||
0 0px 2px 1px rgba(0, 0, 0, 0.2)
|
||||
);
|
||||
}
|
||||
|
||||
.range__thumb--holding {
|
||||
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 2px 1px rgba(0, 0, 0, 0.2),
|
||||
0 0 0 6px var(--thumb-holding-outline, rgba(113, 119, 250, 0.3));
|
||||
}
|
||||
|
||||
.range__tooltip {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
color: var(--tooltip-text, white);
|
||||
width: 120px;
|
||||
padding: 4px 0;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
background-color: var(--color-primary);
|
||||
background: var(
|
||||
--tooltip-bg,
|
||||
linear-gradient(45deg, var(--color-primary), var(--color-primary))
|
||||
);
|
||||
}
|
||||
|
||||
.range__tooltip::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
background-color: var(--color-primary);
|
||||
bottom: -3px;
|
||||
left: calc(50% - 3px);
|
||||
clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
|
||||
transform: rotate(-45deg);
|
||||
border-radius: 0 0 0 3px;
|
||||
}
|
||||
</style>
|
||||
86
frontend/src/lib/components/Stepper.svelte
Normal file
@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
export let stepCount: number = 4;
|
||||
export let currentStep: number = 4;
|
||||
export let enabledStep: number = 4;
|
||||
|
||||
const onStepClick = (idx: number) => {
|
||||
if (idx >= enabledStep) {
|
||||
return;
|
||||
}
|
||||
currentStep = idx;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="stepper">
|
||||
{#each Array(stepCount) as _, i}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
on:click={() => onStepClick(i + 1)}
|
||||
class="step"
|
||||
class:completed={i < currentStep}
|
||||
class:active={currentStep === i + 1}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
{#if i < stepCount - 1}
|
||||
<div class="dash" class:completed={i < currentStep} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '$lib/style/app.scss';
|
||||
.stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
gap: var(--size-2);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
gap: var(--size-4);
|
||||
}
|
||||
}
|
||||
|
||||
.step {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid rgba(var(--color-foreground-raw), 0.2);
|
||||
color: rgba(var(--color-foreground-raw), 0.2);
|
||||
transition: all 0.2s var(--easing-default);
|
||||
|
||||
&.completed {
|
||||
border: 2px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.dash {
|
||||
display: block;
|
||||
width: 50px;
|
||||
height: 6px;
|
||||
content: '';
|
||||
border-radius: 3px;
|
||||
background-color: rgba(var(--color-foreground-raw), 0.1);
|
||||
|
||||
&.completed {
|
||||
background-color: rgba(var(--color-primary-raw), 0.4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
frontend/src/lib/components/Tag.svelte
Normal file
@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
let className: string | undefined = undefined;
|
||||
export { className as class };
|
||||
|
||||
type TagType = 'primary' | 'secondary' | 'disabled';
|
||||
|
||||
export let kind: TagType = 'primary';
|
||||
|
||||
const colorForCategory = (type: TagType) => {
|
||||
switch (type) {
|
||||
case 'primary':
|
||||
return 'var(--color-primary)';
|
||||
case 'secondary':
|
||||
return 'var(--color-secondary)';
|
||||
case 'disabled':
|
||||
return 'rgba(var(--color-primary-raw), 0.2)';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const fontColorForCategory = (category: string) => {
|
||||
switch (category) {
|
||||
case 'primary':
|
||||
return 'var(--color-background)';
|
||||
case 'secondary':
|
||||
return 'var(--color-secondary)';
|
||||
case 'disabled':
|
||||
return 'var(--color-foreground-light)';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={className}
|
||||
style={`background-color:${colorForCategory(kind)}; color: ${fontColorForCategory(kind)}`}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
white-space: nowrap;
|
||||
padding: 0.3rem 0.75rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-radius: var(--size-2);
|
||||
font-weight: bold;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15
frontend/src/lib/components/TagContainer.svelte
Normal file
@ -0,0 +1,15 @@
|
||||
<div class="tag-list">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.tag-container {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
gap: var(--size-1);
|
||||
}
|
||||
</style>
|
||||
62
frontend/src/lib/components/steps/NaceStep.svelte
Normal file
@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import AutocompleteInput from '../Autocomplete.svelte/AutocompleteInput.svelte';
|
||||
import type { NaceEntry } from './types';
|
||||
|
||||
export let selectedNace: NaceEntry | undefined = undefined;
|
||||
|
||||
let options: string[] = [];
|
||||
let naceOptions: NaceEntry[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
const naceEntries: NaceEntry[] = await fetch('/nace.json').then((r) => r.json());
|
||||
if (!naceEntries) {
|
||||
return;
|
||||
}
|
||||
naceOptions = naceEntries;
|
||||
|
||||
options = naceEntries.map((entry) => `${entry.code} - ${entry.name}`);
|
||||
});
|
||||
|
||||
const onNaceSelected = (evt: CustomEvent<string | undefined>) => {
|
||||
const nace = evt.detail;
|
||||
if (!nace) {
|
||||
selectedNace = undefined;
|
||||
}
|
||||
|
||||
let code = nace!.split(' ')[0];
|
||||
|
||||
// Find matching entry
|
||||
selectedNace = naceOptions.find((nace) => nace.code === code);
|
||||
};
|
||||
</script>
|
||||
|
||||
<form>
|
||||
<section>
|
||||
<h2>Company Classification</h2>
|
||||
<label>
|
||||
<span>Select NACE code</span>
|
||||
<AutocompleteInput on:select={onNaceSelected} {options} placeholder="NACE Code" />
|
||||
</label>
|
||||
<slot />
|
||||
<br /><br />
|
||||
<br />
|
||||
<div>
|
||||
<h3>What are NACE codes?</h3>
|
||||
<p>
|
||||
NACE codes are used to classify economic activities in the European Union (EU). They are
|
||||
based on the NACE (Nomenclature of Economic Activities) classification system, which was
|
||||
developed by the EU to provide a common framework for the collection and analysis of
|
||||
statistical data related to economic activities. NACE codes are used to identify the type of
|
||||
business or industry in which a company operates, and are typically used for statistical and
|
||||
analytical purposes.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
label > span {
|
||||
padding-left: var(--size-3);
|
||||
}
|
||||
</style>
|
||||
5
frontend/src/lib/components/steps/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export type NaceEntry = {
|
||||
section: string;
|
||||
code: string;
|
||||
name: string;
|
||||
};
|
||||
1
frontend/src/lib/icons/activity.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-activity"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 282 B |
1
frontend/src/lib/icons/airplay.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-airplay"><path d="M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"></path><polygon points="12 15 17 21 7 21 12 15"></polygon></svg>
|
||||
|
After Width: | Height: | Size: 362 B |
1
frontend/src/lib/icons/alert-circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
||||
|
After Width: | Height: | Size: 356 B |
1
frontend/src/lib/icons/alert-octagon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-octagon"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
||||
|
After Width: | Height: | Size: 416 B |
1
frontend/src/lib/icons/alert-triangle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||
|
After Width: | Height: | Size: 424 B |
1
frontend/src/lib/icons/align-center.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-center"><line x1="18" y1="10" x2="6" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="18" y1="18" x2="6" y2="18"></line></svg>
|
||||
|
After Width: | Height: | Size: 398 B |
1
frontend/src/lib/icons/align-justify.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-justify"><line x1="21" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="3" y2="18"></line></svg>
|
||||
|
After Width: | Height: | Size: 399 B |
1
frontend/src/lib/icons/align-left.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>
|
||||
|
After Width: | Height: | Size: 396 B |
1
frontend/src/lib/icons/align-right.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-right"><line x1="21" y1="10" x2="7" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="7" y2="18"></line></svg>
|
||||
|
After Width: | Height: | Size: 397 B |
1
frontend/src/lib/icons/anchor.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor"><circle cx="12" cy="5" r="3"></circle><line x1="12" y1="22" x2="12" y2="8"></line><path d="M5 12H2a10 10 0 0 0 20 0h-3"></path></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
1
frontend/src/lib/icons/aperture.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-aperture"><circle cx="12" cy="12" r="10"></circle><line x1="14.31" y1="8" x2="20.05" y2="17.94"></line><line x1="9.69" y1="8" x2="21.17" y2="8"></line><line x1="7.38" y1="12" x2="13.12" y2="2.06"></line><line x1="9.69" y1="16" x2="3.95" y2="6.06"></line><line x1="14.31" y1="16" x2="2.83" y2="16"></line><line x1="16.62" y1="12" x2="10.88" y2="21.94"></line></svg>
|
||||
|
After Width: | Height: | Size: 568 B |
1
frontend/src/lib/icons/archive.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-archive"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg>
|
||||
|
After Width: | Height: | Size: 361 B |
1
frontend/src/lib/icons/arrow-down-circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="8 12 12 16 16 12"></polyline><line x1="12" y1="8" x2="12" y2="16"></line></svg>
|
||||
|
After Width: | Height: | Size: 360 B |
1
frontend/src/lib/icons/arrow-down-left.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-left"><line x1="17" y1="7" x2="7" y2="17"></line><polyline points="17 17 7 17 7 7"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 315 B |
1
frontend/src/lib/icons/arrow-down-right.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-right"><line x1="7" y1="7" x2="17" y2="17"></line><polyline points="17 7 17 17 7 17"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 317 B |
1
frontend/src/lib/icons/arrow-down.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 313 B |
1
frontend/src/lib/icons/arrow-left-circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="12 8 8 12 12 16"></polyline><line x1="16" y1="12" x2="8" y2="12"></line></svg>
|
||||
|
After Width: | Height: | Size: 359 B |
1
frontend/src/lib/icons/arrow-left.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 312 B |
1
frontend/src/lib/icons/arrow-right-circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="12 16 16 12 12 8"></polyline><line x1="8" y1="12" x2="16" y2="12"></line></svg>
|
||||
|
After Width: | Height: | Size: 361 B |
1
frontend/src/lib/icons/arrow-right.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 314 B |
1
frontend/src/lib/icons/arrow-up-circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="16 12 12 8 8 12"></polyline><line x1="12" y1="16" x2="12" y2="8"></line></svg>
|
||||
|
After Width: | Height: | Size: 357 B |
1
frontend/src/lib/icons/arrow-up-left.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-left"><line x1="17" y1="17" x2="7" y2="7"></line><polyline points="7 17 7 7 17 7"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 312 B |
1
frontend/src/lib/icons/arrow-up-right.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-right"><line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 314 B |
1
frontend/src/lib/icons/arrow-up.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 310 B |
1
frontend/src/lib/icons/at-sign.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-at-sign"><circle cx="12" cy="12" r="4"></circle><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"></path></svg>
|
||||
|
After Width: | Height: | Size: 322 B |
1
frontend/src/lib/icons/award.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-award"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 325 B |
1
frontend/src/lib/icons/bar-chart-2.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart-2"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>
|
||||
|
After Width: | Height: | Size: 355 B |
1
frontend/src/lib/icons/bar-chart.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart"><line x1="12" y1="20" x2="12" y2="10"></line><line x1="18" y1="20" x2="18" y2="4"></line><line x1="6" y1="20" x2="6" y2="16"></line></svg>
|
||||
|
After Width: | Height: | Size: 353 B |
1
frontend/src/lib/icons/battery-charging.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-battery-charging"><path d="M5 18H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3.19M15 6h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3.19"></path><line x1="23" y1="13" x2="23" y2="11"></line><polyline points="11 6 7 12 13 12 9 18"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 427 B |
1
frontend/src/lib/icons/battery.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-battery"><rect x="1" y="6" width="18" height="12" rx="2" ry="2"></rect><line x1="23" y1="13" x2="23" y2="11"></line></svg>
|
||||
|
After Width: | Height: | Size: 326 B |
1
frontend/src/lib/icons/bell-off.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bell-off"><path d="M13.73 21a2 2 0 0 1-3.46 0"></path><path d="M18.63 13A17.89 17.89 0 0 1 18 8"></path><path d="M6.26 6.26A5.86 5.86 0 0 0 6 8c0 7-3 9-3 9h14"></path><path d="M18 8a6 6 0 0 0-9.33-5"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
|
||||
|
After Width: | Height: | Size: 460 B |
1
frontend/src/lib/icons/bell.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bell"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
|
||||
|
After Width: | Height: | Size: 321 B |
1
frontend/src/lib/icons/bluetooth.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bluetooth"><polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 298 B |
1
frontend/src/lib/icons/bold.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bold"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path></svg>
|
||||
|
After Width: | Height: | Size: 327 B |
1
frontend/src/lib/icons/book-open.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book-open"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>
|
||||
|
After Width: | Height: | Size: 339 B |
1
frontend/src/lib/icons/book.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
1
frontend/src/lib/icons/bookmark.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bookmark"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>
|
||||
|
After Width: | Height: | Size: 287 B |
1
frontend/src/lib/icons/box.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-box"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
|
||||
|
After Width: | Height: | Size: 462 B |
1
frontend/src/lib/icons/briefcase.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-briefcase"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>
|
||||
|
After Width: | Height: | Size: 343 B |
1
frontend/src/lib/icons/calendar.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-calendar"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
|
||||
|
After Width: | Height: | Size: 410 B |
1
frontend/src/lib/icons/camera-off.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-camera-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M21 21H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3m3-3h6l2 3h4a2 2 0 0 1 2 2v9.34m-7.72-2.06a4 4 0 1 1-5.56-5.56"></path></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
1
frontend/src/lib/icons/camera.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-camera"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
|
||||
|
After Width: | Height: | Size: 356 B |
1
frontend/src/lib/icons/cast.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cast"><path d="M2 16.1A5 5 0 0 1 5.9 20M2 12.05A9 9 0 0 1 9.95 20M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"></path><line x1="2" y1="20" x2="2.01" y2="20"></line></svg>
|
||||
|
After Width: | Height: | Size: 387 B |
1
frontend/src/lib/icons/check-circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-circle"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 328 B |
1
frontend/src/lib/icons/check-square.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-square"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
1
frontend/src/lib/icons/check.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 262 B |
1
frontend/src/lib/icons/chevron-down.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 269 B |
1
frontend/src/lib/icons/chevron-left.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-left"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 270 B |
1
frontend/src/lib/icons/chevron-right.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 270 B |
1
frontend/src/lib/icons/chevron-up.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 268 B |
1
frontend/src/lib/icons/chevrons-down.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-down"><polyline points="7 13 12 18 17 13"></polyline><polyline points="7 6 12 11 17 6"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 317 B |
1
frontend/src/lib/icons/chevrons-left.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-left"><polyline points="11 17 6 12 11 7"></polyline><polyline points="18 17 13 12 18 7"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 318 B |
1
frontend/src/lib/icons/chevrons-right.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-right"><polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 318 B |
1
frontend/src/lib/icons/chevrons-up.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-up"><polyline points="17 11 12 6 7 11"></polyline><polyline points="17 18 12 13 7 18"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 316 B |
1
frontend/src/lib/icons/chrome.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chrome"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="21.17" y1="8" x2="12" y2="8"></line><line x1="3.95" y1="6.06" x2="8.54" y2="14"></line><line x1="10.88" y1="21.94" x2="15.46" y2="14"></line></svg>
|
||||
|
After Width: | Height: | Size: 448 B |
1
frontend/src/lib/icons/circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-circle"><circle cx="12" cy="12" r="10"></circle></svg>
|
||||
|
After Width: | Height: | Size: 258 B |
1
frontend/src/lib/icons/clipboard.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clipboard"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>
|
||||
|
After Width: | Height: | Size: 371 B |
1
frontend/src/lib/icons/clock.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clock"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 304 B |
1
frontend/src/lib/icons/cloud-drizzle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-drizzle"><line x1="8" y1="19" x2="8" y2="21"></line><line x1="8" y1="13" x2="8" y2="15"></line><line x1="16" y1="19" x2="16" y2="21"></line><line x1="16" y1="13" x2="16" y2="15"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="12" y1="15" x2="12" y2="17"></line><path d="M20 16.58A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25"></path></svg>
|
||||
|
After Width: | Height: | Size: 557 B |
1
frontend/src/lib/icons/cloud-lightning.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-lightning"><path d="M19 16.9A5 5 0 0 0 18 7h-1.26a8 8 0 1 0-11.62 9"></path><polyline points="13 11 9 17 15 17 11 23"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
1
frontend/src/lib/icons/cloud-off.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-off"><path d="M22.61 16.95A5 5 0 0 0 18 10h-1.26a8 8 0 0 0-7.05-6M5 5a8 8 0 0 0 4 15h9a5 5 0 0 0 1.7-.3"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
|
||||
|
After Width: | Height: | Size: 371 B |
1
frontend/src/lib/icons/cloud-rain.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-rain"><line x1="16" y1="13" x2="16" y2="21"></line><line x1="8" y1="13" x2="8" y2="21"></line><line x1="12" y1="15" x2="12" y2="23"></line><path d="M20 16.58A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25"></path></svg>
|
||||
|
After Width: | Height: | Size: 421 B |
1
frontend/src/lib/icons/cloud-snow.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-snow"><path d="M20 17.58A5 5 0 0 0 18 8h-1.26A8 8 0 1 0 4 16.25"></path><line x1="8" y1="16" x2="8.01" y2="16"></line><line x1="8" y1="20" x2="8.01" y2="20"></line><line x1="12" y1="18" x2="12.01" y2="18"></line><line x1="12" y1="22" x2="12.01" y2="22"></line><line x1="16" y1="16" x2="16.01" y2="16"></line><line x1="16" y1="20" x2="16.01" y2="20"></line></svg>
|
||||
|
After Width: | Height: | Size: 572 B |
1
frontend/src/lib/icons/cloud.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></svg>
|
||||
|
After Width: | Height: | Size: 280 B |