mirror of
https://github.com/gurucomputing/headscale-ui.git
synced 2026-01-23 02:34:43 +00:00
initial commit
This commit is contained in:
commit
3408c3172f
22 changed files with 5670 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
||||
13
.prettierignore
Normal file
13
.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
|
||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 400
|
||||
}
|
||||
11
Caddyfile
Normal file
11
Caddyfile
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
skip_install_trust
|
||||
}
|
||||
:443 {
|
||||
tls internal {
|
||||
on_demand
|
||||
}
|
||||
file_server {
|
||||
root ./build
|
||||
}
|
||||
}
|
||||
6
README.md
Normal file
6
README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Dependencies
|
||||
Dependencies are kept to a minimum and targets large, actively maintained repositories
|
||||
|
||||
* SvelteKit - The HTML/JS Framework and Toolkit
|
||||
* Tailwind CSS - The CSS Framework
|
||||
*
|
||||
13
jsconfig.json
Normal file
13
jsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
5171
package-lock.json
generated
Normal file
5171
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
package.json
Normal file
32
package.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "headscale-ui",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "svelte-kit dev --https --port 443 --host 0.0.0.0",
|
||||
"build": "svelte-kit build",
|
||||
"package": "svelte-kit package",
|
||||
"preview": "svelte-kit preview --https --port 443 --host 0.0.0.0",
|
||||
"stage": "/usr/bin/caddy run --adapter caddyfile --config ./Caddyfile",
|
||||
"prepare": "svelte-kit sync",
|
||||
"check": "svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"lint": "prettier --check --plugin-search-dir=. .",
|
||||
"format": "prettier --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "next",
|
||||
"@sveltejs/adapter-static": "^1.0.0-next.34",
|
||||
"@sveltejs/kit": "next",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"postcss": "^8.4.12",
|
||||
"postcss-load-config": "^3.1.4",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier-plugin-svelte": "^2.7.0",
|
||||
"svelte": "^3.44.0",
|
||||
"svelte-check": "^2.7.1",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"typescript": "^4.7.2"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
13
postcss.config.cjs
Normal file
13
postcss.config.cjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const tailwindcss = require('tailwindcss');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
|
||||
const config = {
|
||||
plugins: [
|
||||
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||
tailwindcss(),
|
||||
//But others, like autoprefixer, need to run after,
|
||||
autoprefixer
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
24
src/app.css
Normal file
24
src/app.css
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/* Write your global styles here, in PostCSS syntax */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.hyperlink {
|
||||
@apply underline text-blue-600 hover:text-blue-800 visited:text-purple-600 font-bold;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@apply invisible absolute text-sm opacity-0;
|
||||
}
|
||||
|
||||
.has-tooltip:hover .tooltip {
|
||||
@apply visible z-50 transition-opacity duration-300 opacity-100;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@apply shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 text-sm mb-3 leading-tight focus:outline-none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply mb-2 text-white text-sm font-bold py-1 px-3 rounded focus:outline-2 disabled:opacity-50;
|
||||
}
|
||||
10
src/app.d.ts
vendored
Normal file
10
src/app.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare namespace App {
|
||||
// interface Locals {}
|
||||
// interface Platform {}
|
||||
// interface Session {}
|
||||
// interface Stuff {}
|
||||
}
|
||||
12
src/app.html
Normal file
12
src/app.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!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, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body>
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
22
src/lib/common-functions.svelte
Normal file
22
src/lib/common-functions.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script context="module" lang="ts">
|
||||
export async function TestAPI(): Promise<boolean> {
|
||||
let headscaleURL = localStorage.getItem('headscaleURL') || '';
|
||||
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
|
||||
let apiStatus = false;
|
||||
// using /api/v1/machine until headscale provides an endpoint to test a key against
|
||||
await fetch(headscaleURL + '/api/v1/machine', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${headscaleAPIKey}`
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
apiStatus = response.ok;
|
||||
})
|
||||
.catch((error) => {
|
||||
apiStatus = false;
|
||||
});
|
||||
return apiStatus;
|
||||
}
|
||||
</script>
|
||||
76
src/lib/nav.svelte
Normal file
76
src/lib/nav.svelte
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<script lang='ts'>
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
// navigation bar variables
|
||||
let navExpanded = writable('');
|
||||
let componentLoaded = false;
|
||||
|
||||
onMount(async () => {
|
||||
// get the navbar state from the local store
|
||||
navExpanded = writable(localStorage.getItem('navExpanded') || '');
|
||||
|
||||
// subscribe to the navbar state and update the local storage where needed
|
||||
navExpanded.subscribe((val) => localStorage.setItem('navExpanded', val));
|
||||
|
||||
// if there is no initial stored navbar state, try to determine a state based on screen size
|
||||
if ($navExpanded == '') {
|
||||
// assuming a mobile unless larger than 640px
|
||||
if (window.outerWidth >= 640) {
|
||||
$navExpanded = 'expanded';
|
||||
} else {
|
||||
$navExpanded = 'collapsed';
|
||||
}
|
||||
}
|
||||
componentLoaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- let the page initialize before showing the nav bar -->
|
||||
{#if componentLoaded}
|
||||
<aside class="bg-gray-100 shadow-xl w-14" class:navCollapsed={$navExpanded == 'collapsed'} class:navExpanded={$navExpanded == 'expanded'} transition:fade>
|
||||
<!-- sidebar menu items -->
|
||||
<p class="nav-item" on:click={() => ($navExpanded == 'collapsed' ? ($navExpanded = 'expanded') : ($navExpanded = 'collapsed'))}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
|
||||
</svg>
|
||||
<span class="indent-4 text-emerald-900 font-extrabold">Headscale</span>
|
||||
</p>
|
||||
|
||||
<div />
|
||||
<a href="/" class="nav-item">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span class="indent-4">User View</span>
|
||||
</a>
|
||||
<a href="/settings.html" class="nav-item">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span class="indent-4">Settings</span>
|
||||
</a>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.navExpanded {
|
||||
transition: ease-out 200ms;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.navCollapsed {
|
||||
transition: ease-out 200ms;
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@apply flex items-center text-sm py-4 px-4 h-12 overflow-hidden text-gray-700 text-ellipsis cursor-default whitespace-nowrap rounded hover:text-gray-900 hover:bg-gray-400 transition duration-300 ease-in-out;
|
||||
}
|
||||
</style>
|
||||
14
src/lib/user-view/user-card.svelte
Normal file
14
src/lib/user-view/user-card.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
export let user = { id: '', name: '', createdAt: '' };
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 divide-y p-2 max-w-screen-lg border-2 mx-4 border-gray-100 rounded-md text-sm text-gray-600 shadow">
|
||||
<div in:fade class="flex justify-between">
|
||||
<span class="font-bold">{user.id}: {user.name}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="pt-2 pl-2"><p><span class="font-bold">User Created: </span><span class="font-normal">{new Date(user.createdAt)}</span></p></div>
|
||||
</div>
|
||||
21
src/routes/__layout.svelte
Normal file
21
src/routes/__layout.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import '../app.css';
|
||||
import Nav from '$lib/nav.svelte';
|
||||
</script>
|
||||
|
||||
<main class="flex flex-col">
|
||||
<div class="flex min-h-screen overflow-hidden">
|
||||
<!-- sidebar -->
|
||||
<Nav />
|
||||
<!-- main window -->
|
||||
<div class="flex flex-1 flex-col">
|
||||
<!-- header -->
|
||||
<!-- <div class="flex bg-gray-100 h-12 p-4">Header</div> -->
|
||||
<!-- content -->
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="flex">Footer</div> -->
|
||||
</main>
|
||||
73
src/routes/index.svelte
Normal file
73
src/routes/index.svelte
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<!-- typescript -->
|
||||
<script lang="ts">
|
||||
import UserCard from '$lib/user-view/user-card.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
//
|
||||
// Component Variables
|
||||
//
|
||||
|
||||
// let's the page know if it's ready to load
|
||||
let componentLoaded = false;
|
||||
|
||||
// page state variables
|
||||
let headscaleAPITest = 'untested';
|
||||
|
||||
// endpoint url for getting users
|
||||
let endpointURL = '/api/v1/namespace';
|
||||
|
||||
// note that headscale API refers to users as namespaces still. This variable will hold our user's array
|
||||
let headscaleUsers = [{ id: '', name: '', createdAt: '' }];
|
||||
|
||||
// We define the meat of our script in onMount as doing so forces client side rendering.
|
||||
// Doing so also does not perform any actions until components are initialized
|
||||
onMount(async () => {
|
||||
// attempt to pull list of users
|
||||
let headscaleURL = localStorage.getItem('headscaleURL') || '';
|
||||
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
|
||||
fetch(headscaleURL + endpointURL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${headscaleAPIKey}`
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
headscaleAPITest = 'succeeded';
|
||||
// return the api data
|
||||
response.json().then((data) => {
|
||||
headscaleUsers = data.namespaces;
|
||||
console.log(headscaleUsers);
|
||||
});
|
||||
} else {
|
||||
headscaleAPITest = 'failed';
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
headscaleAPITest = 'failed';
|
||||
});
|
||||
|
||||
// load the page
|
||||
componentLoaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- html -->
|
||||
{#if componentLoaded}
|
||||
<div in:fade class="px-4 py-4">
|
||||
<h1 class="text-2xl bold green-400 mb-4">User View</h1>
|
||||
</div>
|
||||
{#if headscaleAPITest === 'succeeded'}
|
||||
<!-- instantiate user based components -->
|
||||
{#each headscaleUsers as user}
|
||||
<UserCard user={user} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#if headscaleAPITest === 'failed'}
|
||||
<div in:fade class="max-w-lg mx-auto p-4 border-4 text-sm text-gray-600 shadow-lg text-center">
|
||||
<p>API test did not succeed.<br />Headscale might be down or API settings may need to be set<br />change server settings in the <a href="/settings" class="hyperlink">settings</a> page</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
109
src/routes/settings.html.svelte
Normal file
109
src/routes/settings.html.svelte
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<script lang="ts">
|
||||
//
|
||||
// Imports
|
||||
//
|
||||
import { onMount } from 'svelte';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import { TestAPI } from '$lib/common-functions.svelte';
|
||||
|
||||
//
|
||||
// Variable Declarations
|
||||
//
|
||||
|
||||
// Set to true once component is initialized
|
||||
let componentLoaded = false;
|
||||
|
||||
// Server Settings
|
||||
let headscaleURLStore: Writable<string>;
|
||||
let headscaleURL: string;
|
||||
let headscaleAPIKeyStore: Writable<string>;
|
||||
let headscaleAPIKey: string;
|
||||
let headscaleAPITest = 'untested';
|
||||
let serverSettingsForm: HTMLFormElement;
|
||||
|
||||
//
|
||||
// Functions
|
||||
//
|
||||
|
||||
// Server Settings
|
||||
function SaveServerSettings(): void {
|
||||
if (serverSettingsForm.reportValidity()) {
|
||||
$headscaleURLStore = headscaleURL;
|
||||
$headscaleAPIKeyStore = headscaleAPIKey;
|
||||
}
|
||||
}
|
||||
|
||||
function ClearServerSettings(): void {
|
||||
headscaleURL = '';
|
||||
headscaleAPIKey = '';
|
||||
$headscaleURLStore = headscaleURL;
|
||||
$headscaleAPIKeyStore = headscaleAPIKey;
|
||||
}
|
||||
|
||||
function TestServerSettings(): void {
|
||||
TestAPI().then((succeeded: boolean) => {
|
||||
if (succeeded === true) {
|
||||
headscaleAPITest = 'succeeded';
|
||||
} else {
|
||||
headscaleAPITest = 'failed';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Component Initialization
|
||||
//
|
||||
onMount(async () => {
|
||||
// get the current URL and APIKey state from the local store
|
||||
headscaleURL = localStorage.getItem('headscaleURL') || '';
|
||||
headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
|
||||
|
||||
// set the current subscription values to the initial value
|
||||
headscaleURLStore = writable(headscaleURL);
|
||||
headscaleAPIKeyStore = writable(headscaleAPIKey);
|
||||
|
||||
// subscribe to the URL and APIKey state and update the local storage where needed
|
||||
headscaleURLStore.subscribe((val) => localStorage.setItem('headscaleURL', val));
|
||||
headscaleAPIKeyStore.subscribe((val) => localStorage.setItem('headscaleAPIKey', val));
|
||||
|
||||
// Display component frontend
|
||||
componentLoaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- html -->
|
||||
<body>
|
||||
{#if componentLoaded}
|
||||
<div in:fade class="px-4 py-4 w-4/5">
|
||||
<!-- Server Settings -->
|
||||
<form bind:this={serverSettingsForm}>
|
||||
<h1 class="text-2xl bold green-400 mb-4">Server Settings</h1>
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="url"> Headscale URL </label>
|
||||
<input bind:value={headscaleURL} class="form-input" type="url" required pattern={String.raw`https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)`} placeholder="https://hs.yourdomain.com.au" />
|
||||
<p class="text-xs text-gray-400 text-italics mb-8">URL for your headscale server instance</p>
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="password"> Headscale API Key </label>
|
||||
<input bind:value={headscaleAPIKey} minlength="54" maxlength="54" class="form-input" type="password" required placeholder="******************" />
|
||||
<p class="text-xs text-gray-400 text-italics mb-8">Generate an API key for your headscale instance and place it here.</p>
|
||||
<span class="has-tooltip">
|
||||
<span class="tooltip rounded shadow-lg p-1 bg-gray-200 -mt-16">Note: API Key and URL currently save to localStorage (IE: Your Browser)<br />Make sure you are using a trusted computer</span>
|
||||
<!-- disable the SaveServerSettings button if nothing has changed from stored values, or the dependent inputs do not validate -->
|
||||
<button disabled={headscaleAPIKey === $headscaleAPIKeyStore && headscaleURL === $headscaleURLStore} on:click={() => SaveServerSettings()} class="btn-primary bg-green-700 hover:bg-green-900" type="button">Save Server Settings</button>
|
||||
</span>
|
||||
<button on:click={() => ClearServerSettings()} class="btn-primary bg-teal-700 hover:bg-teal-900 " type="button">Clear Server Settings</button>
|
||||
<button on:click={() => TestServerSettings()} class="btn-primary bg-sky-700 hover:bg-sky-900 " type="button">Test Server Settings</button>
|
||||
{#if headscaleAPITest === 'succeeded'}
|
||||
<svg in:fly={{ x: 10, duration: 600 }} xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline" fill="none" viewBox="0 0 24 24" stroke="green" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{/if}
|
||||
{#if headscaleAPITest === 'failed'}
|
||||
<svg in:fly={{ x: 10, duration: 600 }} xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline" fill="none" viewBox="0 0 24 24" stroke="red" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
{/if}
|
||||
</form>
|
||||
<!-- Server Settings End -->
|
||||
</div>
|
||||
{/if}
|
||||
</body>
|
||||
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
23
svelte.config.js
Normal file
23
svelte.config.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
import preprocess from 'svelte-preprocess';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
prerender: {
|
||||
default: true
|
||||
},
|
||||
adapter: adapter({
|
||||
fallback: 'index.html',
|
||||
precompress: false
|
||||
})
|
||||
},
|
||||
|
||||
preprocess: [
|
||||
preprocess({
|
||||
postcss: true
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
||||
11
tailwind.config.cjs
Normal file
11
tailwind.config.cjs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const config = {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
|
||||
plugins: []
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
Loading…
Add table
Add a link
Reference in a new issue