Graphical overhaul.
Remove all legacy pixeldrain pages, handlers and assets. Remove old theming engine. Make logo responsive to the accent colour. Lots of other stuff.
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Chart from "util/Chart.svelte";
|
||||
import type Chart from "util/Chart.svelte";
|
||||
import { formatDataVolume, formatDate, formatThousands } from "util/Formatting";
|
||||
import Modal from "util/Modal.svelte";
|
||||
import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "lib/FilesystemAPI.svelte";
|
||||
import { color_by_name } from "util/Util";
|
||||
import { tick } from "svelte";
|
||||
import CopyButton from "layout/CopyButton.svelte";
|
||||
import type { FSNavigator } from "./FSNavigator";
|
||||
|
||||
@@ -43,12 +42,9 @@ let total_downloads = $state(0)
|
||||
let total_egress = $state(0)
|
||||
|
||||
let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
||||
if (chart === undefined) {
|
||||
// Wait for the chart element to render, if it's not rendered already
|
||||
await tick()
|
||||
}
|
||||
|
||||
if (!visible || base.type !== "file" || chart === undefined) {
|
||||
// The chart is not there until it's downloaded. The $effect above reads it
|
||||
// through this function, so it calls this function again when it's ready
|
||||
if (!visible || base.type !== "file" || !chart) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -116,15 +112,15 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
||||
label: "Downloads",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("highlight_color"),
|
||||
backgroundColor: color_by_name("highlight_color"),
|
||||
borderColor: color_by_name("accent_color"),
|
||||
backgroundColor: color_by_name("accent_color"),
|
||||
data: resp.downloads.amounts,
|
||||
}, {
|
||||
label: "Egress",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("danger_color"),
|
||||
backgroundColor: color_by_name("danger_color"),
|
||||
borderColor: color_by_name("red_color"),
|
||||
backgroundColor: color_by_name("red_color"),
|
||||
yAxisID: "y1",
|
||||
data: resp.egress.amounts,
|
||||
},
|
||||
@@ -244,7 +240,10 @@ let direct_share_url = $derived(fs_share_hotlink_url($nav.path))
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Chart bind:this={chart} />
|
||||
<!-- Chart.js is big, so it's only downloaded when it's needed -->
|
||||
{#await import("util/Chart.svelte") then c}
|
||||
<c.default bind:this={chart} />
|
||||
{/await}
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { loading_finish, loading_start } from "lib/Loading";
|
||||
import { fs_get_node, fs_encode_path, fs_split_path } from "lib/FilesystemAPI.svelte";
|
||||
import { fs_get_node, fs_encode_path, fs_split_path, fs_path_from_json } from "lib/FilesystemAPI.svelte";
|
||||
import type { FSNode, FSPath, FSPermissions, FSContext } from "lib/FilesystemAPI.svelte";
|
||||
|
||||
export class FSNavigator {
|
||||
@@ -24,16 +24,6 @@ export class FSNavigator {
|
||||
this.history_enabled = history_enabled
|
||||
}
|
||||
|
||||
// The popstate event can be used to listen for navigation events. Register
|
||||
// this event listener on the <svelte:window> in the parent element. When
|
||||
// the user presses the back or forward buttons in the browser we'll catch
|
||||
// the event and navigate to the proper directory
|
||||
popstate = (e: PopStateEvent) => {
|
||||
// Get the part of the URL after the fs root and navigate to it
|
||||
const path = window.location.pathname.replace(/^\/d/, "")
|
||||
this.navigate(decodeURI(path), false)
|
||||
}
|
||||
|
||||
// The FSNavigator acts as a svelte store. This allows for DOM reactivity.
|
||||
// This works by implementing the store contract:
|
||||
// https://svelte.dev/docs/svelte-components#script-4-prefix-stores-with-$-to-access-their-values
|
||||
@@ -70,7 +60,13 @@ export class FSNavigator {
|
||||
|
||||
try {
|
||||
loading_start()
|
||||
const resp = await fs_get_node(path)
|
||||
|
||||
// The server embeds the node which the page was opened on. It's only
|
||||
// valid for the first navigation, which is to that same page
|
||||
const resp = window.initial_node !== undefined
|
||||
? fs_path_from_json(window.initial_node)
|
||||
: await fs_get_node(path)
|
||||
window.initial_node = undefined
|
||||
this.open_node(resp, push_history)
|
||||
} catch (err: any) {
|
||||
if (err.value !== undefined && err.value === "path_not_found") {
|
||||
|
||||
@@ -8,6 +8,7 @@ import FilePreview from "./viewers/FilePreview.svelte";
|
||||
import FSUploadWidget from "./upload_widget/FSUploadWidget.svelte";
|
||||
import { global_navigator } from "./FSNavigator"
|
||||
import { css_from_path } from "filesystem/edit_window/Branding";
|
||||
import { update_favicon } from "util/NovaLogo.svelte";
|
||||
import AffiliatePrompt from "user_home/AffiliatePrompt.svelte";
|
||||
import { current_page_store } from "wrap/RouterStore";
|
||||
import SearchBar from "./SearchBar.svelte";
|
||||
@@ -46,11 +47,13 @@ onMount(() => {
|
||||
// styles take precedence since they're lower level, and we can clean it
|
||||
// up afterwards without overwriting global style
|
||||
document.body.style = css_from_path(nav.path)
|
||||
update_favicon()
|
||||
})
|
||||
return () => {
|
||||
page_sub()
|
||||
nav_sub()
|
||||
document.body.style = ""
|
||||
update_favicon()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<script lang="ts">
|
||||
import NovaLogo from "util/NovaLogo.svelte";
|
||||
import Button from "layout/Button.svelte";
|
||||
import Euro from "util/Euro.svelte";
|
||||
import { formatDataVolume } from "util/Formatting";
|
||||
import { user } from "lib/UserStore";
|
||||
import Dialog from "layout/Dialog.svelte";
|
||||
|
||||
let button: HTMLButtonElement = $state()
|
||||
let dialog: Dialog = $state()
|
||||
|
||||
let {
|
||||
no_login_label = "Nova",
|
||||
hide_name = true,
|
||||
hide_logo = false,
|
||||
style = "",
|
||||
embedded = false
|
||||
}: {
|
||||
no_login_label?: string;
|
||||
// Hide the label if the screen is smaller than 800px
|
||||
hide_name?: boolean;
|
||||
hide_logo?: boolean;
|
||||
style?: string;
|
||||
embedded?: boolean;
|
||||
} = $props();
|
||||
|
||||
let target = $derived(embedded ? "_blank" : "_self")
|
||||
|
||||
const open = () => dialog.open(button.getBoundingClientRect())
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<button bind:this={button} onclick={open} class="button round" title="Menu" style={style}>
|
||||
{#if !hide_logo}
|
||||
<NovaLogo style="height: 1.6em; width: 1.6em;"/>
|
||||
{/if}
|
||||
<span class="button_username" class:hide_name>
|
||||
{#if $user !== null}
|
||||
{$user.username === "" ? no_login_label : $user.username}
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Dialog bind:this={dialog}>
|
||||
<div class="menu">
|
||||
{#if $user !== null}
|
||||
<Button link_href="/user" link_target={target} icon="dashboard" label="Dashboard" />
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="stats_table">
|
||||
<div>Subscription</div>
|
||||
<div>{$user.subscription.name}</div>
|
||||
|
||||
{#if $user.subscription.type === "prepaid"}
|
||||
<div>Credit</div>
|
||||
<div><Euro amount={$user.balance_micro_eur}/></div>
|
||||
{/if}
|
||||
|
||||
<div>Storage used</div>
|
||||
<div>{formatDataVolume($user.filesystem_storage_used, 3)}</div>
|
||||
<div>Transfer used</div>
|
||||
<div>{formatDataVolume($user.monthly_transfer_used, 3)}</div>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
|
||||
<Button link_href="/d/me" link_target={target} icon="folder" label="My Filesystem"/>
|
||||
<Button link_href="/filesystem" link_target={target} icon="description" label="Filesystem Guide"/>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<Button link_href="/user/filemanager#files" link_target={target} icon="image" label="My Files"/>
|
||||
<Button link_href="/user/filemanager#lists" link_target={target} icon="photo_library" label="My Albums"/>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<Button link_href="/user/settings" link_target={target} icon="settings" label="Account Settings"/>
|
||||
<Button link_href="/user/subscription" link_target={target} icon="shopping_cart" label="Subscription"/>
|
||||
<Button link_href="/user/subscription/transactions" link_target={target} icon="receipt" label="Transactions"/>
|
||||
|
||||
{#if $user.is_admin}
|
||||
<div class="separator"></div>
|
||||
<Button link_href="/admin" link_target={target} icon="admin_panel_settings" label="Admin Panel"/>
|
||||
{/if}
|
||||
{:else}
|
||||
<Button link_href="/" link_target={target} icon="home" label="Home"/>
|
||||
<Button link_href="/#pro" link_target={target} icon="star" label="Get Premium"/>
|
||||
<Button link_href="/login" link_target={target} icon="person" label="Log in"/>
|
||||
<Button link_href="/register" link_target={target} icon="person" label="Register"/>
|
||||
{/if}
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
flex: 0 0 content;
|
||||
}
|
||||
.button_username {
|
||||
margin: 0 4px;
|
||||
}
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 15em;
|
||||
}
|
||||
.separator {
|
||||
height: 1px;
|
||||
margin: 2px 0;
|
||||
width: 100%;
|
||||
background-color: var(--separator);
|
||||
}
|
||||
.stats_table {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 0.2em 1em;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
/* Hide username on small screen */
|
||||
@media(max-width: 800px) {
|
||||
.hide_name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -287,12 +287,12 @@ const window_keydown = (e: KeyboardEvent) => {
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
color: var(--body_text_color);
|
||||
padding: 2px;
|
||||
}
|
||||
.node_selected {
|
||||
background: var(--highlight_background);
|
||||
color: var(--highlight_text_color);
|
||||
background: var(--accent_background);
|
||||
color: var(--accent_text_color);
|
||||
}
|
||||
.node:hover:not(.node_selected) {
|
||||
background: var(--input_background);
|
||||
|
||||
@@ -42,7 +42,8 @@ const add_password = (e: SubmitEvent) => {
|
||||
}
|
||||
const generate_password = () => {
|
||||
// 32 hexadecimal characters, which any S3 client accepts as secret key
|
||||
new_password = crypto.randomUUID().replaceAll("-", "")
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(16))
|
||||
new_password = Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("")
|
||||
}
|
||||
// Passwords stay hidden until their reveal button is pressed
|
||||
let revealed: {[password: string]: boolean} = $state({})
|
||||
|
||||
@@ -11,12 +11,11 @@ type Style = {
|
||||
input_background?: string,
|
||||
input_hover_background?: string,
|
||||
input_text?: string,
|
||||
highlight_color?: string,
|
||||
highlight_background?: string,
|
||||
highlight_text_color?: string,
|
||||
accent_color?: string,
|
||||
accent_shade?: string,
|
||||
accent_background?: string,
|
||||
accent_text_color?: string,
|
||||
link_color?: string,
|
||||
danger_color?: string,
|
||||
danger_text_color?: string,
|
||||
background_color?: string,
|
||||
background?: string,
|
||||
background_text_color?: string,
|
||||
@@ -32,8 +31,45 @@ type Style = {
|
||||
background_image_size?: string,
|
||||
background_image_position?: string,
|
||||
background_image_repeat?: string,
|
||||
color_scheme?: string,
|
||||
green_color?: string,
|
||||
red_color?: string,
|
||||
yellow_color?: string,
|
||||
blue_color?: string,
|
||||
green_tint?: string,
|
||||
red_tint?: string,
|
||||
yellow_tint?: string,
|
||||
blue_tint?: string,
|
||||
logo_color_1?: string,
|
||||
logo_color_2?: string,
|
||||
logo_color_3?: string,
|
||||
logo_color_4?: string,
|
||||
}
|
||||
|
||||
// The hues of the accents for success, errors, warnings and notices, and the
|
||||
// accents for light themes. The dark defaults in layout.css are derived from
|
||||
// the default accent colour, the light defaults are these light accents.
|
||||
// Branding can also set each accent by itself
|
||||
const accent_hues = { green: 140, red: 8, yellow: 45, blue: 215 }
|
||||
const light_accents = { green: "#15793a", red: "#ba2c37", yellow: "#995c00", blue: "#2164ca" }
|
||||
const default_accent = "#49dfa0"
|
||||
|
||||
// The colourways of the logo, as designed. The hue the colourway is closest to,
|
||||
// the colours of the bars and the colours of the diagonal
|
||||
const logo_colourways: [number, string, string, string, string][] = [
|
||||
[1, "#f62e27", "#bb1d1c", "#bb1c1c", "#ee2c26"], // Red
|
||||
[20, "#f78146", "#bb5420", "#ee7a40", "#c05823"], // Orange
|
||||
[54, "#ffe500", "#8c8015", "#ead204", "#6d6310"], // Yellow
|
||||
[71, "#dde741", "#74a511", "#8bb925", "#eaed38"], // Lime
|
||||
[120, "#15c615", "#0c750c", "#0c730c", "#0c730c"], // Green
|
||||
[141, "#45ed52", "#11a574", "#24b880", "#39ef3d"], // Mint
|
||||
[190, "#48eef1", "#1378a8", "#258eba", "#38e8ec"], // Light blue
|
||||
[230, "#4b82f5", "#1111a5", "#2354b5", "#393bee"], // Dark blue
|
||||
[253, "#974af2", "#212eae", "#2929b8", "#8e39ee"], // Purple blue
|
||||
[270, "#8447f0", "#7514a9", "#5924b8", "#b039ef"], // Purple
|
||||
[330, "#f74bbb", "#ad174e", "#b72484", "#f13a77"], // Pink
|
||||
]
|
||||
|
||||
// Generate a branding style from a file's properties map
|
||||
export const branding_from_path = (path: Array<FSNode>) => {
|
||||
const style = {}
|
||||
@@ -57,7 +93,7 @@ export const css_from_path = (path: Array<FSNode>) => {
|
||||
}
|
||||
|
||||
const gen_css = (style: Style) => {
|
||||
return Object.entries(style).map(([key, value]) => `--${key}:${value}`).join(';');
|
||||
return Object.entries(with_accents(style)).map(([key, value]) => `--${key}:${value}`).join(';');
|
||||
}
|
||||
|
||||
// add_styles adds the styles configured in the properties struct to the
|
||||
@@ -74,20 +110,33 @@ const add_styles = (style: Style, properties: FSNodeProperties) => {
|
||||
style.input_hover_background = properties.brand_input_color
|
||||
style.input_text = add_contrast(properties.brand_input_color, text_contrast)
|
||||
}
|
||||
if (properties.brand_highlight_color) {
|
||||
style.highlight_color = properties.brand_highlight_color
|
||||
style.highlight_background = properties.brand_highlight_color
|
||||
style.highlight_text_color = add_contrast(properties.brand_highlight_color, text_contrast)
|
||||
if (properties.brand_accent_color) {
|
||||
style.accent_color = properties.brand_accent_color
|
||||
style.accent_background = properties.brand_accent_color
|
||||
style.accent_text_color = add_contrast(properties.brand_accent_color, text_contrast)
|
||||
|
||||
// If we have a body colour to compare it to we use the highlight colour
|
||||
// If we have a body colour to compare it to we use the accent colour
|
||||
// to generate the link colour
|
||||
if (properties.brand_body_color) {
|
||||
style.link_color = generate_link_color(properties.brand_highlight_color, properties.brand_body_color)
|
||||
style.link_color = generate_link_color(properties.brand_accent_color, properties.brand_body_color)
|
||||
}
|
||||
|
||||
// The logo gets the colourway closest to the accent colour. Greys
|
||||
// have no meaningful hue, they keep the default
|
||||
const [hue, saturation] = rgb2hsl(parse(properties.brand_accent_color))
|
||||
if (saturation > 15) {
|
||||
const distance = (h: number) => Math.min(Math.abs(h - hue), 360 - Math.abs(h - hue))
|
||||
const way = logo_colourways.reduce((a, b) => distance(b[0]) < distance(a[0]) ? b : a)
|
||||
style.logo_color_1 = way[1]
|
||||
style.logo_color_2 = way[2]
|
||||
style.logo_color_3 = way[3]
|
||||
style.logo_color_4 = way[4]
|
||||
}
|
||||
}
|
||||
if (properties.brand_danger_color) {
|
||||
style.danger_color = properties.brand_danger_color
|
||||
style.danger_text_color = add_contrast(properties.brand_danger_color, text_contrast)
|
||||
for (const name of Object.keys(accent_hues)) {
|
||||
if (properties["brand_" + name + "_color"]) {
|
||||
style[name + "_color"] = properties["brand_" + name + "_color"]
|
||||
}
|
||||
}
|
||||
if (properties.brand_background_color) {
|
||||
style.background_color = properties.brand_background_color
|
||||
@@ -102,6 +151,10 @@ const add_styles = (style: Style, properties: FSNodeProperties) => {
|
||||
style.shaded_background = set_alpha(properties.brand_body_color, body_alpha)
|
||||
style.separator = add_contrast(properties.brand_body_color, 15)
|
||||
style.shadow_color = darken(properties.brand_body_color, 0.8)
|
||||
|
||||
// Makes the browser draw its own controls, like scrollbars and date
|
||||
// pickers, in matching colours
|
||||
style.color_scheme = rgb2hsl(parse(properties.brand_body_color))[2] < 50 ? "dark" : "light"
|
||||
}
|
||||
if (properties.brand_card_color) {
|
||||
style.card_color = properties.brand_card_color
|
||||
@@ -114,6 +167,45 @@ const add_styles = (style: Style, properties: FSNodeProperties) => {
|
||||
}
|
||||
}
|
||||
|
||||
// with_accents adds the accents for success, errors, warnings and notices, and
|
||||
// their tints. Accents which the branding doesn't set get the saturation and
|
||||
// lightness of the accent colour, so they match it. A light theme's
|
||||
// accent colour is too light for text, those get fixed accents. A branded
|
||||
// page always gets all accents, else a shared directory would inherit the
|
||||
// accents of the visitor's own theme
|
||||
const with_accents = (style: Style): Style => {
|
||||
if (Object.keys(style).length === 0) {
|
||||
return style
|
||||
}
|
||||
|
||||
// Greys have no hue to match, those use the default accent colour
|
||||
let [, saturation, lightness] = rgb2hsl(parse(style.accent_color ?? default_accent))
|
||||
if (saturation <= 15) {
|
||||
[, saturation, lightness] = rgb2hsl(parse(default_accent))
|
||||
}
|
||||
|
||||
// Without its own background colours the branding is shown on the default
|
||||
// theme, which follows the browser's preference
|
||||
const light = (style.color_scheme ?? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark")) === "light"
|
||||
|
||||
const full = { ...style }
|
||||
|
||||
// The background of selected buttons. A subtle dark shade of the accent on
|
||||
// dark themes and a light one on light themes, so the accent is readable on it
|
||||
const [accent_hue, accent_saturation] = rgb2hsl(parse(style.accent_color ?? default_accent))
|
||||
full.accent_shade = rgb2hex(hsl2rgb([accent_hue, Math.min(accent_saturation, 25), light ? 91 : 19]))
|
||||
|
||||
for (const [name, hue] of Object.entries(accent_hues)) {
|
||||
let colour = style[name + "_color"]
|
||||
if (!colour) {
|
||||
colour = light ? light_accents[name] : rgb2hex(hsl2rgb([hue, saturation, lightness]))
|
||||
}
|
||||
full[name + "_color"] = colour
|
||||
full[name + "_tint"] = set_alpha(colour, 0.12)
|
||||
}
|
||||
return full
|
||||
}
|
||||
|
||||
const add_contrast = (color: string, amt: number) => {
|
||||
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
|
||||
// If the lightness is less than 40 it is considered a dark colour. This
|
||||
|
||||
@@ -67,7 +67,7 @@ const handle_picker = async (nodes: FSNode[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
let highlight_info = $state(false)
|
||||
let accent_info = $state(false)
|
||||
</script>
|
||||
|
||||
<fieldset>
|
||||
@@ -93,14 +93,14 @@ let highlight_info = $state(false)
|
||||
<fieldset class="grid" disabled={!enabled}>
|
||||
<legend>Custom colours</legend>
|
||||
<div>
|
||||
<div style="display: inline-block">Highlight</div>
|
||||
<HelpButton bind:toggle={highlight_info}/>
|
||||
<div style="display: inline-block">Accent</div>
|
||||
<HelpButton bind:toggle={accent_info}/>
|
||||
</div>
|
||||
<input type="color" value={options.brand_highlight_color} onchange={e => options.brand_highlight_color = (e.target as HTMLInputElement).value}/>
|
||||
<input type="text" bind:value={options.brand_highlight_color}/>
|
||||
{#if highlight_info}
|
||||
<input type="color" value={options.brand_accent_color} onchange={e => options.brand_accent_color = (e.target as HTMLInputElement).value}/>
|
||||
<input type="text" bind:value={options.brand_accent_color}/>
|
||||
{#if accent_info}
|
||||
<p class="span3">
|
||||
The highlight colour is used for highlighting selected buttons and
|
||||
The accent colour is used for highlighting selected buttons and
|
||||
other elements. It's also used as the page's theme colour, this
|
||||
affects things like the embed colour in Discord and the colour of
|
||||
the address bar in some web browsers.
|
||||
@@ -109,9 +109,6 @@ let highlight_info = $state(false)
|
||||
<div>Button and input</div>
|
||||
<input type="color" value={options.brand_input_color} onchange={e => options.brand_input_color = (e.target as HTMLInputElement).value}/>
|
||||
<input type="text" bind:value={options.brand_input_color}/>
|
||||
<div>Delete button</div>
|
||||
<input type="color" value={options.brand_danger_color} onchange={e => options.brand_danger_color = (e.target as HTMLInputElement).value}/>
|
||||
<input type="text" bind:value={options.brand_danger_color}/>
|
||||
<div>Background</div>
|
||||
<input type="color" value={options.brand_background_color} onchange={e => options.brand_background_color = (e.target as HTMLInputElement).value}/>
|
||||
<input type="text" bind:value={options.brand_background_color}/>
|
||||
@@ -123,6 +120,26 @@ let highlight_info = $state(false)
|
||||
<input type="text" bind:value={options.brand_card_color}/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="grid" disabled={!enabled}>
|
||||
<legend>Accent colours</legend>
|
||||
<p class="span3">
|
||||
Colours for success messages, errors, warnings and notices. Colours you
|
||||
leave empty will be derived from the accent colour, so they match it.
|
||||
</p>
|
||||
<div>Success</div>
|
||||
<input type="color" value={options.brand_green_color} onchange={e => options.brand_green_color = (e.target as HTMLInputElement).value}/>
|
||||
<input type="text" bind:value={options.brand_green_color}/>
|
||||
<div>Error</div>
|
||||
<input type="color" value={options.brand_red_color} onchange={e => options.brand_red_color = (e.target as HTMLInputElement).value}/>
|
||||
<input type="text" bind:value={options.brand_red_color}/>
|
||||
<div>Warning</div>
|
||||
<input type="color" value={options.brand_yellow_color} onchange={e => options.brand_yellow_color = (e.target as HTMLInputElement).value}/>
|
||||
<input type="text" bind:value={options.brand_yellow_color}/>
|
||||
<div>Notice</div>
|
||||
<input type="color" value={options.brand_blue_color} onchange={e => options.brand_blue_color = (e.target as HTMLInputElement).value}/>
|
||||
<input type="text" bind:value={options.brand_blue_color}/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="grid" disabled={!enabled}>
|
||||
<legend>Image customization</legend>
|
||||
<p class="span3">
|
||||
@@ -174,6 +191,13 @@ let highlight_info = $state(false)
|
||||
<input type="text" value="A text field"/>
|
||||
</div>
|
||||
|
||||
<div class="example_button_row" style="gap: 4px;">
|
||||
<div class="highlight_green">Success</div>
|
||||
<div class="highlight_red">Error</div>
|
||||
<div class="highlight_yellow">Warning</div>
|
||||
<div class="highlight_blue">Notice</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div class="example example_card">
|
||||
|
||||
@@ -12,6 +12,13 @@ let current_theme = $state(-1)
|
||||
const set_theme = (index: number) => {
|
||||
current_theme = index
|
||||
|
||||
// Presets without accents derive them from the accent colour, the
|
||||
// accents of an earlier preset shouldn't stick
|
||||
properties.brand_green_color = ""
|
||||
properties.brand_red_color = ""
|
||||
properties.brand_yellow_color = ""
|
||||
properties.brand_blue_color = ""
|
||||
|
||||
// Copy all the properties of the theme to the file, except for the theme
|
||||
// name
|
||||
Object.keys(themes[index]).forEach(key => {
|
||||
@@ -25,99 +32,91 @@ const themes = [
|
||||
{
|
||||
name: "PD classic",
|
||||
brand_input_color: "#2d2d2d",
|
||||
brand_highlight_color: "#75b72d",
|
||||
brand_danger_color: "#821b3f",
|
||||
brand_accent_color: "#75b72d",
|
||||
brand_background_color: "#141414",
|
||||
brand_body_color: "#1e1e1e",
|
||||
brand_card_color: "#282828",
|
||||
}, {
|
||||
name: "Nord (dark)",
|
||||
brand_input_color: "#4f596d",
|
||||
brand_highlight_color: "#a4be8c",
|
||||
brand_danger_color: "#bd5f69",
|
||||
brand_accent_color: "#a4be8c",
|
||||
brand_background_color: "#2f3541",
|
||||
brand_body_color: "#3b4252",
|
||||
brand_card_color: "#434c5f",
|
||||
}, {
|
||||
name: "Nord (light)",
|
||||
brand_input_color: "#cad2e1",
|
||||
brand_highlight_color: "#a4be8c",
|
||||
brand_danger_color: "#bd5f69",
|
||||
brand_accent_color: "#a4be8c",
|
||||
brand_background_color: "#d7dde8",
|
||||
brand_body_color: "#ebeef3",
|
||||
brand_card_color: "#e5e9f0",
|
||||
}, {
|
||||
name: "Black",
|
||||
brand_input_color: "#202020",
|
||||
brand_highlight_color: "#57e389",
|
||||
brand_danger_color: "#ed333b",
|
||||
brand_accent_color: "#57e389",
|
||||
brand_background_color: "#000000",
|
||||
brand_body_color: "#080808",
|
||||
brand_card_color: "#101010",
|
||||
}, {
|
||||
name: "White",
|
||||
brand_input_color: "#f4f4f4",
|
||||
brand_highlight_color: "#2ec27e",
|
||||
brand_danger_color: "#c01c28",
|
||||
brand_accent_color: "#2ec27e",
|
||||
brand_background_color: "#f8f8f8",
|
||||
brand_body_color: "#ffffff",
|
||||
brand_card_color: "#f0f0f0",
|
||||
}, {
|
||||
name: "Solarized (dark)",
|
||||
brand_input_color: "#084453",
|
||||
brand_highlight_color: "#849900",
|
||||
brand_danger_color: "#db302d",
|
||||
brand_accent_color: "#849900",
|
||||
brand_background_color: "#002c38",
|
||||
brand_body_color: "#063540",
|
||||
brand_card_color: "#073c49",
|
||||
}, {
|
||||
name: "Solarized (light)",
|
||||
brand_input_color: "#e7dfc5",
|
||||
brand_highlight_color: "#849900",
|
||||
brand_danger_color: "#db302d",
|
||||
brand_accent_color: "#849900",
|
||||
brand_background_color: "#ede7d3",
|
||||
brand_body_color: "#fdf5e2",
|
||||
brand_card_color: "#fcf2d8",
|
||||
}, {
|
||||
name: "Mocha",
|
||||
brand_input_color: "#313244",
|
||||
brand_highlight_color: "#a6e3a1",
|
||||
brand_danger_color: "#f38ba8",
|
||||
brand_accent_color: "#a6e3a1",
|
||||
brand_background_color: "#1e1e2e",
|
||||
brand_body_color: "#181825",
|
||||
brand_card_color: "#313244",
|
||||
}, {
|
||||
name: "Dracula",
|
||||
brand_input_color: "#424450",
|
||||
brand_highlight_color: "#50FA7B",
|
||||
brand_danger_color: "#FF5555",
|
||||
brand_accent_color: "#50FA7B",
|
||||
brand_green_color: "#50FA7B",
|
||||
brand_red_color: "#FF5555",
|
||||
brand_yellow_color: "#F1FA8C",
|
||||
brand_blue_color: "#8BE9FD",
|
||||
brand_background_color: "#191A21",
|
||||
brand_body_color: "#21222C",
|
||||
brand_card_color: "#343746",
|
||||
}, {
|
||||
name: "Nova",
|
||||
brand_input_color: "#28033f",
|
||||
brand_highlight_color: "#57e389",
|
||||
brand_danger_color: "#ed333b",
|
||||
brand_background_color: "#0d0015",
|
||||
brand_body_color: "#130020",
|
||||
brand_card_color: "#1a002b",
|
||||
name: "Deep purple",
|
||||
brand_input_color: "#342848",
|
||||
brand_accent_color: "#c49efa",
|
||||
brand_background_color: "#160f1f",
|
||||
brand_body_color: "#1d1429",
|
||||
brand_card_color: "#281e38",
|
||||
}, {
|
||||
name: "Green Phosphor",
|
||||
brand_input_color: "#001e09",
|
||||
brand_highlight_color: "#57e389",
|
||||
brand_danger_color: "#ed333b",
|
||||
brand_background_color: "#000502",
|
||||
brand_body_color: "#000903",
|
||||
brand_card_color: "#001707",
|
||||
brand_input_color: "#263129",
|
||||
brand_accent_color: "#69d38c",
|
||||
brand_background_color: "#0d120e",
|
||||
brand_body_color: "#141a16",
|
||||
brand_card_color: "#1d261f",
|
||||
}, {
|
||||
name: "Amber",
|
||||
brand_input_color: "#3c2b00",
|
||||
brand_highlight_color: "#f5c211",
|
||||
brand_danger_color: "#c01c28",
|
||||
brand_background_color: "#171100",
|
||||
brand_body_color: "#201700",
|
||||
brand_card_color: "#281d00",
|
||||
brand_input_color: "#363026",
|
||||
brand_accent_color: "#e2b05a",
|
||||
brand_background_color: "#16130e",
|
||||
brand_body_color: "#1f1b14",
|
||||
brand_card_color: "#2a251d",
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -62,7 +62,7 @@ let {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-decoration: none;
|
||||
color: var(--body_text-color);
|
||||
color: var(--body_text_color);
|
||||
padding: 2px;
|
||||
align-items: center;
|
||||
background: var(--body_background);
|
||||
@@ -78,8 +78,8 @@ let {
|
||||
text-decoration: none;
|
||||
}
|
||||
.node.node_selected {
|
||||
background-color: var(--highlight_color) !important;
|
||||
color: var(--highlight_text_color);
|
||||
background-color: var(--accent_color) !important;
|
||||
color: var(--accent_text_color);
|
||||
}
|
||||
.node>* {
|
||||
flex: 0 0 content;
|
||||
|
||||
@@ -290,7 +290,7 @@ const move_here = async () => {
|
||||
class="container"
|
||||
use:drop_target={{
|
||||
upload: (files) => upload_widget.upload_files(files),
|
||||
shadow: "var(--highlight_color) 0 0 10px 2px inset",
|
||||
shadow: "var(--accent_color) 0 0 10px 2px inset",
|
||||
}}
|
||||
>
|
||||
<div class="width_container bubble">
|
||||
@@ -443,10 +443,7 @@ const move_here = async () => {
|
||||
text-align: center;
|
||||
}
|
||||
.toolbar_edit {
|
||||
background-color: rgba(0, 255, 0, 0.05);
|
||||
}
|
||||
.icon.button_highlight {
|
||||
border-radius: 4px;
|
||||
background-color: var(--green_tint);
|
||||
}
|
||||
|
||||
/* Large icon mode only supported on wide screens. Hide the button on small screen */
|
||||
|
||||
@@ -237,7 +237,4 @@ onMount(() => {
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.icon.button_highlight {
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -95,8 +95,8 @@ let {
|
||||
background: var(--input_hover_background);
|
||||
}
|
||||
.file.selected {
|
||||
background: var(--highlight_background);
|
||||
color: var(--highlight_text_color);
|
||||
background: var(--accent_background);
|
||||
color: var(--accent_text_color);
|
||||
}
|
||||
.node_icon {
|
||||
flex: 1 1 0;
|
||||
|
||||
@@ -202,7 +202,7 @@ td {
|
||||
}
|
||||
.node {
|
||||
text-decoration: none;
|
||||
color: var(--body_text-color);
|
||||
color: var(--body_text_color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.node:not(:last-child) {
|
||||
@@ -214,8 +214,8 @@ td {
|
||||
text-decoration: none;
|
||||
}
|
||||
.node.node_selected {
|
||||
background-color: var(--highlight_color) !important;
|
||||
color: var(--highlight_text_color);
|
||||
background-color: var(--accent_color) !important;
|
||||
color: var(--accent_text_color);
|
||||
}
|
||||
td {
|
||||
vertical-align: middle;
|
||||
|
||||
@@ -178,7 +178,7 @@ onMount(() => {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.error {
|
||||
color: var(--danger_color, #c0392b);
|
||||
color: var(--red_color);
|
||||
}
|
||||
.trash_table {
|
||||
width: 100%;
|
||||
|
||||
@@ -99,7 +99,7 @@ const cancel = () => {
|
||||
align-items: center; /* Stop stretching the button */
|
||||
}
|
||||
.error {
|
||||
background: var(--danger_color);
|
||||
color: var(--highlight_text_color);
|
||||
background: var(--red_tint);
|
||||
color: var(--red_color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -186,6 +186,6 @@ onMount(() => {
|
||||
margin: 4px;
|
||||
}
|
||||
.playing {
|
||||
color: var(--highlight_color);
|
||||
color: var(--accent_color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -206,7 +206,7 @@ const video_keydown = (e: KeyboardEvent) => {
|
||||
.compatibility_warning {
|
||||
background-color: var(--shaded_background);
|
||||
backdrop-filter: blur(4px);
|
||||
border-bottom: 2px solid #6666FF;
|
||||
border-bottom: 2px solid var(--blue_color);
|
||||
padding: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user