refactor(webui): switch to SvelteKit | prettier everything

develop
Tomáš Mládek 2024-01-22 22:58:55 +01:00
parent e52560ae07
commit 8c1dc5388f
28 changed files with 855 additions and 879 deletions

View File

@ -28,15 +28,21 @@ module.exports = {
}
}
],
rules: {
"svelte/valid-compile": ["error", { "ignoreWarnings": true }],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["warn", {
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}],
"no-console": ["error", {
allow: ["debug", "warn", "error"]
}],
},
rules: {
'svelte/valid-compile': ['error', { ignoreWarnings: true }],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'no-console': [
'error',
{
allow: ['debug', 'warn', 'error']
}
]
}
};

View File

@ -1,6 +1,6 @@
*Psst — looking for a more complete solution? Check out [SvelteKit](https://kit.svelte.dev), the official framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.*
_Psst — looking for a more complete solution? Check out [SvelteKit](https://kit.svelte.dev), the official framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing._
*Looking for a shareable component template instead? You can [use SvelteKit for that as well](https://kit.svelte.dev/docs#packaging) or the older [sveltejs/component-template](https://github.com/sveltejs/component-template)*
_Looking for a shareable component template instead? You can [use SvelteKit for that as well](https://kit.svelte.dev/docs#packaging) or the older [sveltejs/component-template](https://github.com/sveltejs/component-template)_
---
@ -15,8 +15,7 @@ npx degit sveltejs/template svelte-app
cd svelte-app
```
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
_Note that you will need to have [Node.js](https://nodejs.org) installed._
## Get started
@ -49,12 +48,11 @@ npm run build
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
## Single-page app mode
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for _any_ path. You can make it so by editing the `"start"` command in package.json:
```js
"start": "sirv public --single"

View File

@ -1,68 +1,68 @@
{
"name": "upend-kestrel",
"version": "2.0.0",
"private": true,
"type": "module",
"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 --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "8.56.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
"dependencies": {
"@ibm/plex": "^6.3.0",
"@recogito/annotorious": "^2.7.11",
"@types/d3": "^7.4.3",
"@types/debug": "^4.1.12",
"@types/dompurify": "^3.0.5",
"@types/lodash": "^4.14",
"@types/marked": "^4.3.2",
"@types/node": "^18.19.8",
"@types/three": "^0.160.0",
"@types/wavesurfer.js": "^6.0.12",
"@upnd/upend": "file:../tools/upend_js",
"@upnd/wasm-web": "file:../tools/upend_wasm/pkg-web",
"boxicons": "^2.1.4",
"d3": "^7.8.5",
"date-fns": "^2.30.0",
"debug": "^4.3.4",
"dompurify": "^2.4.7",
"filesize": "^8.0.7",
"history": "^5.3.0",
"i18next": "^22.5.1",
"lodash": "^4.17.21",
"lru-cache": "^6.0.0",
"marked": "^4.3.0",
"match-sorter": "^6.3.1",
"mitt": "^3.0.1",
"normalize.css": "^8.0.1",
"sass": "^1.66.1",
"sirv-cli": "^2.0.2",
"sswr": "^1.11.0",
"svelte-i18next": "^1.2.2",
"three": "^0.147.0",
"vite-plugin-static-copy": "^0.13.1",
"wavesurfer.js": "^6.6.4"
}
"name": "upend-kestrel",
"version": "2.0.0",
"private": true,
"type": "module",
"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 --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "8.56.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
"dependencies": {
"@ibm/plex": "^6.3.0",
"@recogito/annotorious": "^2.7.11",
"@types/d3": "^7.4.3",
"@types/debug": "^4.1.12",
"@types/dompurify": "^3.0.5",
"@types/lodash": "^4.14",
"@types/marked": "^4.3.2",
"@types/node": "^18.19.8",
"@types/three": "^0.160.0",
"@types/wavesurfer.js": "^6.0.12",
"@upnd/upend": "file:../tools/upend_js",
"@upnd/wasm-web": "file:../tools/upend_wasm/pkg-web",
"boxicons": "^2.1.4",
"d3": "^7.8.5",
"date-fns": "^2.30.0",
"debug": "^4.3.4",
"dompurify": "^2.4.7",
"filesize": "^8.0.7",
"history": "^5.3.0",
"i18next": "^22.5.1",
"lodash": "^4.17.21",
"lru-cache": "^6.0.0",
"marked": "^4.3.0",
"match-sorter": "^6.3.1",
"mitt": "^3.0.1",
"normalize.css": "^8.0.1",
"sass": "^1.66.1",
"sirv-cli": "^2.0.2",
"sswr": "^1.11.0",
"svelte-i18next": "^1.2.2",
"three": "^0.147.0",
"vite-plugin-static-copy": "^0.13.1",
"wavesurfer.js": "^6.6.4"
}
}

View File

@ -1,6 +1,6 @@
import { UpEndApi } from "@upnd/upend";
import { UpEndWasmExtensionsWeb } from "@upnd/upend/wasm/web";
import wasmURL from "@upnd/wasm-web/upend_wasm_bg.wasm?url";
import { UpEndApi } from '@upnd/upend';
import { UpEndWasmExtensionsWeb } from '@upnd/upend/wasm/web';
import wasmURL from '@upnd/wasm-web/upend_wasm_bg.wasm?url';
const wasm = new UpEndWasmExtensionsWeb(wasmURL);
export default new UpEndApi({ instanceUrl: "/", wasmExtensions: wasm });
export default new UpEndApi({ instanceUrl: '/', wasmExtensions: wasm });

View File

@ -1,90 +1,90 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { createEventDispatcher } from 'svelte';
import Icon from "./utils/Icon.svelte";
import Selector from "./utils/Selector.svelte";
const dispatch = createEventDispatcher();
let selector: Selector;
import Icon from './utils/Icon.svelte';
import Selector from './utils/Selector.svelte';
const dispatch = createEventDispatcher();
let selector: Selector;
let editable = false;
$: {
if (editable) {
dispatch("editable");
setTimeout(() => dispatch("editable"), 500); // once animation has finished
}
}
$: editable && selector && selector.focus();
let editable = false;
$: {
if (editable) {
dispatch('editable');
setTimeout(() => dispatch('editable'), 500); // once animation has finished
}
}
$: editable && selector && selector.focus();
</script>
<div
class="view"
class:editable
on:click={() => (editable = true)}
on:keydown={(ev) => {
if (["Space", "Enter"].includes(ev.key)) editable = true;
}}
class="view"
class:editable
on:click={() => (editable = true)}
on:keydown={(ev) => {
if (['Space', 'Enter'].includes(ev.key)) editable = true;
}}
>
<div class="icon">
<Icon name="plus-circle" />
</div>
{#if editable}
<div class="controls">
<Selector
bind:this={selector}
types={["Address", "NewAddress", "Attribute"]}
on:input={(ev) => {
dispatch("input", ev.detail);
editable = false;
}}
on:focus={(ev) => {
if (!ev.detail) editable = false;
}}
/>
</div>
{/if}
<div class="icon">
<Icon name="plus-circle" />
</div>
{#if editable}
<div class="controls">
<Selector
bind:this={selector}
types={['Address', 'NewAddress', 'Attribute']}
on:input={(ev) => {
dispatch('input', ev.detail);
editable = false;
}}
on:focus={(ev) => {
if (!ev.detail) editable = false;
}}
/>
</div>
{/if}
</div>
<style lang="scss">
.view {
position: relative;
.view {
position: relative;
background: var(--background-lighter);
color: var(--foreground-lighter);
border: 1px solid var(--foreground-lightest);
border-radius: 0.5em;
padding: 1rem;
background: var(--background-lighter);
color: var(--foreground-lighter);
border: 1px solid var(--foreground-lightest);
border-radius: 0.5em;
padding: 1rem;
cursor: pointer;
cursor: pointer;
transition:
opacity 0.3s,
width 0.5s,
min-width 0.5s;
transition:
opacity 0.3s,
width 0.5s,
min-width 0.5s;
opacity: 0.4;
width: 48px;
min-width: 48px;
opacity: 0.4;
width: 48px;
min-width: 48px;
&:hover,
&.editable {
opacity: 1;
}
&:hover,
&.editable {
opacity: 1;
}
&.editable {
width: 18em;
min-width: 18em;
.icon {
opacity: 0.4;
}
}
}
&.editable {
width: 18em;
min-width: 18em;
.icon {
opacity: 0.4;
}
}
}
.icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 36px;
transition: opacity 0.3s;
}
.icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 36px;
transition: opacity 0.3s;
}
</style>

View File

@ -1,67 +1,58 @@
<script lang="ts">
import type { UpEntry } from "@upnd/upend";
import { attributeLabels } from "../../util/labels";
import UpObject from "./UpObject.svelte";
export let resolve = true;
import type { UpEntry } from '@upnd/upend';
import { attributeLabels } from '../../util/labels';
import UpObject from './UpObject.svelte';
export let resolve = true;
export let entry: UpEntry;
export let entry: UpEntry;
</script>
<div class="entry">
<div class="entity">
<UpObject
plain
link
address={entry.entity}
labels={resolve ? undefined : []}
/>
</div>
<div class="attribute" title={entry.attribute}>
{$attributeLabels[entry.attribute] || entry.attribute}
</div>
<div class="value value-{entry.value.t.toLowerCase()}">
{#if entry.value.t === "Address"}
<UpObject
link
address={entry.value.c}
labels={resolve ? undefined : []}
/>
{:else}
{entry.value.c}
{/if}
</div>
<div class="entity">
<UpObject plain link address={entry.entity} labels={resolve ? undefined : []} />
</div>
<div class="attribute" title={entry.attribute}>
{$attributeLabels[entry.attribute] || entry.attribute}
</div>
<div class="value value-{entry.value.t.toLowerCase()}">
{#if entry.value.t === 'Address'}
<UpObject link address={entry.value.c} labels={resolve ? undefined : []} />
{:else}
{entry.value.c}
{/if}
</div>
</div>
<style lang="scss">
.entry {
border: 1px solid var(--foreground);
background: var(--background-lighter);
border-radius: 4px;
padding: 0.1em 0.5em 0.1em 0.25em;
display: flex;
align-content: center;
align-items: center;
justify-content: space-between;
gap: 1em;
.entry {
border: 1px solid var(--foreground);
background: var(--background-lighter);
border-radius: 4px;
padding: 0.1em 0.5em 0.1em 0.25em;
display: flex;
align-content: center;
align-items: center;
justify-content: space-between;
gap: 1em;
& > * {
min-width: 0;
}
}
& > * {
min-width: 0;
}
}
.attribute {
flex-grow: 1;
text-align: center;
font-weight: 300;
&::before {
content: "→\00a0";
}
&::after {
content: "\00a0→";
}
}
.attribute {
flex-grow: 1;
text-align: center;
font-weight: 300;
&::before {
content: '→\00a0';
}
&::after {
content: '\00a0→';
}
}
:global(.value-value) {
font-family: var(--monospace-font);
}
:global(.value-value) {
font-family: var(--monospace-font);
}
</style>

View File

@ -1,59 +1,59 @@
<script lang="ts">
import HashBadge from "./HashBadge.svelte";
import UpLink from "./UpLink.svelte";
import BlobPreview from "./BlobPreview.svelte";
import UpObject from "./UpObject.svelte";
import HashBadge from './HashBadge.svelte';
import UpLink from './UpLink.svelte';
import BlobPreview from './BlobPreview.svelte';
import UpObject from './UpObject.svelte';
export let address: string;
export let labels: string[] | undefined = undefined;
export let thumbnail = true;
export let banner = true;
export let select = true;
export let address: string;
export let labels: string[] | undefined = undefined;
export let thumbnail = true;
export let banner = true;
export let select = true;
</script>
<div class="upobjectcard">
<UpLink to={{ entity: address }} passthrough>
<div class="inner">
{#if thumbnail}
<BlobPreview {address} />
{:else}
<div class="badge">
<HashBadge {address} />
</div>
{/if}
<div class="label">
<UpObject {address} {labels} {banner} {select} on:resolved />
</div>
</div>
</UpLink>
<UpLink to={{ entity: address }} passthrough>
<div class="inner">
{#if thumbnail}
<BlobPreview {address} />
{:else}
<div class="badge">
<HashBadge {address} />
</div>
{/if}
<div class="label">
<UpObject {address} {labels} {banner} {select} on:resolved />
</div>
</div>
</UpLink>
</div>
<style lang="scss">
.upobjectcard {
display: flex;
flex-direction: column;
justify-content: flex-end;
overflow: hidden;
.upobjectcard {
display: flex;
flex-direction: column;
justify-content: flex-end;
overflow: hidden;
.inner {
border: 1px solid var(--foreground-lighter);
border-radius: 4px;
padding: 0.25rem;
max-height: 420px;
min-height: 0;
.inner {
border: 1px solid var(--foreground-lighter);
border-radius: 4px;
padding: 0.25rem;
max-height: 420px;
min-height: 0;
display: flex;
flex-direction: column;
}
}
display: flex;
flex-direction: column;
}
}
.label {
margin-top: 0.25rem;
}
.label {
margin-top: 0.25rem;
}
.badge {
font-size: 3rem;
display: flex;
justify-content: center;
}
.badge {
font-size: 3rem;
display: flex;
justify-content: center;
}
</style>

View File

@ -1,48 +1,48 @@
<script lang="ts">
import Ellipsis from "../utils/Ellipsis.svelte";
import Ellipsis from '../utils/Ellipsis.svelte';
export let label: string;
export let backpath: string[] = [];
export let label: string;
export let backpath: string[] = [];
</script>
<div class="upobject-label">
<Ellipsis value={label}>
{#if backpath.length}
<span class="backpath">
{#each backpath as component}
<span class="component">
{component}
</span>
{/each}
</span>
{/if}
<span class="label">
{label}
</span>
</Ellipsis>
<Ellipsis value={label}>
{#if backpath.length}
<span class="backpath">
{#each backpath as component}
<span class="component">
{component}
</span>
{/each}
</span>
{/if}
<span class="label">
{label}
</span>
</Ellipsis>
</div>
<style lang="scss">
.upobject-label {
max-width: 100%;
}
.upobject-label {
max-width: 100%;
}
.backpath {
opacity: 0.66;
margin-right: 0.25em;
.backpath {
opacity: 0.66;
margin-right: 0.25em;
.component::after {
content: "∋";
margin-left: 0.2em;
margin-right: 0.4em;
font-size: 0.66em;
font-weight: bold;
position: relative;
top: -0.125em;
}
.component::after {
content: '∋';
margin-left: 0.2em;
margin-right: 0.4em;
font-size: 0.66em;
font-weight: bold;
position: relative;
top: -0.125em;
}
.component:last-child::after {
content: "";
}
}
.component:last-child::after {
content: '';
}
}
</style>

View File

@ -1,106 +1,106 @@
<script lang="ts">
import Icon from "../utils/Icon.svelte";
import Jobs from "./Jobs.svelte";
import Notifications from "./Notifications.svelte";
import { i18n } from "../../i18n";
import Icon from '../utils/Icon.svelte';
import Jobs from './Jobs.svelte';
import Notifications from './Notifications.svelte';
import { i18n } from '../../i18n';
let hidden = true;
let activeJobs: number;
$: togglable = activeJobs > 0 || !hidden;
let hidden = true;
let activeJobs: number;
$: togglable = activeJobs > 0 || !hidden;
</script>
<footer id="footer" class:hidden>
<div class="notifications">
<Notifications />
</div>
<div
class="status"
class:togglable
on:click={() => (hidden = !hidden)}
on:keydown={(ev) => {
if (["Space", "Enter"].includes(ev.key)) hidden = !hidden;
}}
>
<div class="info">
{#if activeJobs > 0}
{$i18n.t("Active jobs:")} {activeJobs}
{:else}
{$i18n.t("No active jobs.")}
{/if}
</div>
<div class="icons">
<Icon name="{hidden ? 'up' : 'down'}-arrow" />
</div>
</div>
<div class="jobs">
<Jobs bind:active={activeJobs} />
</div>
<div class="notifications">
<Notifications />
</div>
<div
class="status"
class:togglable
on:click={() => (hidden = !hidden)}
on:keydown={(ev) => {
if (['Space', 'Enter'].includes(ev.key)) hidden = !hidden;
}}
>
<div class="info">
{#if activeJobs > 0}
{$i18n.t('Active jobs:')} {activeJobs}
{:else}
{$i18n.t('No active jobs.')}
{/if}
</div>
<div class="icons">
<Icon name="{hidden ? 'up' : 'down'}-arrow" />
</div>
</div>
<div class="jobs">
<Jobs bind:active={activeJobs} />
</div>
</footer>
<style lang="scss">
footer {
position: fixed;
bottom: 0;
width: 100%;
footer {
position: fixed;
bottom: 0;
width: 100%;
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
& > * {
padding: 0 0.5rem;
}
& > * {
padding: 0 0.5rem;
}
background: var(--background);
border-top: 1px solid var(--foreground-lighter);
background: var(--background);
border-top: 1px solid var(--foreground-lighter);
transition: 0.7s bottom ease;
transition: 0.7s bottom ease;
--height: calc(100vh / 6);
}
--height: calc(100vh / 6);
}
footer.hidden {
bottom: calc(var(--height) * -1);
}
footer.hidden {
bottom: calc(var(--height) * -1);
}
.status {
height: 2rem;
width: 100%;
.status {
height: 2rem;
width: 100%;
display: flex;
align-items: center;
display: flex;
align-items: center;
cursor: pointer;
cursor: pointer;
&:not(.togglable) {
cursor: unset;
pointer-events: none;
opacity: 0.66;
}
&:not(.togglable) {
cursor: unset;
pointer-events: none;
opacity: 0.66;
}
transition: opacity 0.7s ease;
transition: opacity 0.7s ease;
.info {
flex-grow: 1;
}
}
.info {
flex-grow: 1;
}
}
.notifications,
.jobs {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.notifications,
.jobs {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
:global(.notifications > *:first-child) {
padding-top: 0.5rem;
}
:global(.notifications > *:first-child) {
padding-top: 0.5rem;
}
.jobs {
overflow-y: scroll;
height: var(--height);
.jobs {
overflow-y: scroll;
height: var(--height);
padding-top: 0.5rem;
padding-top: 0.5rem;
background: var(--background-lighter);
}
background: var(--background-lighter);
}
</style>

View File

@ -1,17 +1,17 @@
section.labelborder {
margin-top: 0.66rem;
margin-top: 0.66rem;
header {
display: flex;
align-items: end;
justify-content: space-between;
header {
display: flex;
align-items: end;
justify-content: space-between;
border-bottom: 1px solid var(--foreground);
padding-bottom: 0.33rem;
margin-bottom: 0.33rem;
border-bottom: 1px solid var(--foreground);
padding-bottom: 0.33rem;
margin-bottom: 0.33rem;
h3 {
margin: 0;
}
}
h3 {
margin: 0;
}
}
}

View File

@ -1,15 +1,15 @@
<script lang="ts">
export let value: string;
export let title: string | undefined = undefined;
export let value: string;
export let title: string | undefined = undefined;
</script>
<div class="ellipsis" title={title || value}><slot>{value}</slot></div>
<style lang="scss">
.ellipsis {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.ellipsis {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
</style>

View File

@ -1,88 +1,88 @@
<script lang="ts">
import Icon from "./Icon.svelte";
import Icon from './Icon.svelte';
export let name: string;
export let active = false;
export let disabled = false;
export let title: string | undefined = undefined;
export let outline = false;
export let subdued = false;
export let small = false;
export let plain = false;
export let color: string | undefined = "var(--active-color, var(--primary))";
export let name: string;
export let active = false;
export let disabled = false;
export let title: string | undefined = undefined;
export let outline = false;
export let subdued = false;
export let small = false;
export let plain = false;
export let color: string | undefined = 'var(--active-color, var(--primary))';
</script>
<button
on:click
class:active
class:outline
class:subdued
class:small
class:plain
{disabled}
{title}
style="--color: {color}"
on:click
class:active
class:outline
class:subdued
class:small
class:plain
{disabled}
{title}
style="--color: {color}"
>
<Icon {name} />
<div class="text">
<slot />
</div>
<Icon {name} />
<div class="text">
<slot />
</div>
</button>
<style lang="scss">
button {
display: flex;
flex-direction: column;
align-items: center;
button {
display: flex;
flex-direction: column;
align-items: center;
border: 0;
background: transparent;
cursor: pointer;
color: inherit;
opacity: 0.66;
border: 0;
background: transparent;
cursor: pointer;
color: inherit;
opacity: 0.66;
&.plain {
padding: 0;
}
&.plain {
padding: 0;
}
display: flex;
align-items: center;
display: flex;
align-items: center;
transition:
opacity 0.2s,
color 0.2s,
border-color 0.2s;
}
transition:
opacity 0.2s,
color 0.2s,
border-color 0.2s;
}
button.subdued {
opacity: 0.4;
}
button.subdued {
opacity: 0.4;
}
.outline {
border: 1px solid var(--foreground);
border-radius: 4px;
.outline {
border: 1px solid var(--foreground);
border-radius: 4px;
padding: 0.25em 1em;
&.small {
padding: 0.1em 0.8em;
}
}
padding: 0.25em 1em;
&.small {
padding: 0.1em 0.8em;
}
}
.active,
button:hover {
opacity: 1;
color: var(--color);
border-color: var(--color);
}
.active,
button:hover {
opacity: 1;
color: var(--color);
border-color: var(--color);
}
button:disabled {
color: gray;
pointer-events: none;
}
button:disabled {
color: gray;
pointer-events: none;
}
.text {
font-size: 0.5em;
text-align: center;
margin-top: 0.2em;
}
.text {
font-size: 0.5em;
text-align: center;
margin-top: 0.2em;
}
</style>

View File

@ -1,63 +1,63 @@
<script lang="ts">
export let hide = false;
let hidden = true;
export let hide = false;
let hidden = true;
</script>
<section class="labelborder" class:hide class:hidden>
<header
on:click={() => {
if (hide) {
hidden = !hidden;
}
}}
on:keydown={(ev) => {
if (["Space", "Enter"].includes(ev.key) && hide) hidden = !hidden;
}}
>
<slot name="header-full">
<h3><slot name="header" /></h3>
</slot>
</header>
<div class="content">
<slot />
</div>
<header
on:click={() => {
if (hide) {
hidden = !hidden;
}
}}
on:keydown={(ev) => {
if (['Space', 'Enter'].includes(ev.key) && hide) hidden = !hidden;
}}
>
<slot name="header-full">
<h3><slot name="header" /></h3>
</slot>
</header>
<div class="content">
<slot />
</div>
</section>
<style lang="scss">
section.labelborder {
margin-top: 0.66rem;
section.labelborder {
margin-top: 0.66rem;
header {
display: flex;
align-items: end;
justify-content: space-between;
header {
display: flex;
align-items: end;
justify-content: space-between;
border-bottom: 1px solid var(--foreground);
padding-bottom: 0.33rem;
margin-bottom: 0.33rem;
border-bottom: 1px solid var(--foreground);
padding-bottom: 0.33rem;
margin-bottom: 0.33rem;
h3 {
margin: 0;
}
}
h3 {
margin: 0;
}
}
&.hide {
header {
cursor: pointer;
}
&.hide {
header {
cursor: pointer;
}
transition: opacity 0.2s ease-in-out;
&.hidden {
opacity: 0.66;
transition: opacity 0.2s ease-in-out;
&.hidden {
opacity: 0.66;
header {
border-bottom-width: 0.5px;
}
header {
border-bottom-width: 0.5px;
}
.content {
display: none;
}
}
}
}
.content {
display: none;
}
}
}
}
</style>

View File

@ -1,71 +1,67 @@
<script lang="ts">
export let value: number | undefined = undefined;
export let value: number | undefined = undefined;
</script>
<div
class="progress-bar"
class:indeterminate={value == undefined}
style="--value: {value}%"
>
<div class="value" />
<div class="label">
<slot>
{value ? Math.round(value) : "?"}%
</slot>
</div>
<div class="progress-bar" class:indeterminate={value == undefined} style="--value: {value}%">
<div class="value" />
<div class="label">
<slot>
{value ? Math.round(value) : '?'}%
</slot>
</div>
</div>
<style lang="scss">
.progress-bar {
width: 100%;
height: 1em;
background: white;
position: relative;
}
.progress-bar {
width: 100%;
height: 1em;
background: white;
position: relative;
}
.value {
background: var(--primary);
height: 100%;
width: var(--value, 100%);
transition: width 0.2s ease;
}
.value {
background: var(--primary);
height: 100%;
width: var(--value, 100%);
transition: width 0.2s ease;
}
.progress-bar,
.value {
border-radius: 2px;
}
.progress-bar,
.value {
border-radius: 2px;
}
.label {
font-size: 0.8em;
color: white;
z-index: 9;
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
text-align: center;
font-weight: bold;
mix-blend-mode: difference;
}
.label {
font-size: 0.8em;
color: white;
z-index: 9;
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
text-align: center;
font-weight: bold;
mix-blend-mode: difference;
}
.progress-bar.indeterminate {
.value {
animation-name: indeterminate;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
}
.progress-bar.indeterminate {
.value {
animation-name: indeterminate;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
}
@keyframes indeterminate {
0% {
background-color: var(--primary);
}
50% {
background-color: var(--primary-lighter);
}
100% {
background-color: var(--primary);
}
}
@keyframes indeterminate {
0% {
background-color: var(--primary);
}
50% {
background-color: var(--primary-lighter);
}
100% {
background-color: var(--primary);
}
}
</style>

View File

@ -1,30 +1,30 @@
<script lang="ts">
export let centered: boolean | string = false;
export let centered: boolean | string = false;
</script>
<div
class="spinner lds-ripple"
class:centered={Boolean(centered)}
class:absolute-centered={centered == "absolute"}
class="spinner lds-ripple"
class:centered={Boolean(centered)}
class:absolute-centered={centered == 'absolute'}
>
<i class="bx bx-loader bx-spin" />
<i class="bx bx-loader bx-spin" />
</div>
<style>
.spinner {
height: 1em;
}
.spinner.centered {
position: relative;
left: 50%;
transform: translateX(-50%);
text-align: center;
}
.spinner {
height: 1em;
}
.spinner.centered {
position: relative;
left: 50%;
transform: translateX(-50%);
text-align: center;
}
.spinner.absolute-centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.spinner.absolute-centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

View File

@ -1,36 +1,34 @@
export const DEBUG = {
imageQueueHalt: Boolean(localStorage.getItem("DEBUG:IMAGEHALT")),
mockJobs: parseInt(localStorage.getItem("DEBUG:MOCK:JOBS") || "0"),
mockNotifications: parseInt(
localStorage.getItem("DEBUG:MOCK:NOTIFICATIONS") || "0",
),
imageQueueHalt: Boolean(localStorage.getItem('DEBUG:IMAGEHALT')),
mockJobs: parseInt(localStorage.getItem('DEBUG:MOCK:JOBS') || '0'),
mockNotifications: parseInt(localStorage.getItem('DEBUG:MOCK:NOTIFICATIONS') || '0')
};
export function lipsum(length: number): string {
const words = [
"lorem",
"ipsum",
"dolor",
"sit",
"amet",
"consectetur",
"adipiscing",
"elit",
"sed",
"do",
"eiusmod",
"tempor",
"incididunt",
"ut",
"labore",
"et",
"dolore",
"magna",
"aliqua",
];
const result = ["lorem", "ipsum"];
for (let i = 0; i < length; i++) {
result.push(words[Math.floor(Math.random() * words.length)]);
}
return result.join(" ");
const words = [
'lorem',
'ipsum',
'dolor',
'sit',
'amet',
'consectetur',
'adipiscing',
'elit',
'sed',
'do',
'eiusmod',
'tempor',
'incididunt',
'ut',
'labore',
'et',
'dolore',
'magna',
'aliqua'
];
const result = ['lorem', 'ipsum'];
for (let i = 0; i < length; i++) {
result.push(words[Math.floor(Math.random() * words.length)]);
}
return result.join(' ');
}

View File

@ -1,13 +1,13 @@
{
"attributes": {
"FILE_MIME": "MIME type",
"FILE_SIZE": "File size",
"ADDED": "Added at",
"LAST_VISITED": "Last visited at",
"NUM_VISITED": "Times visited",
"ATTR_LABEL": "Label",
"IS": "Type",
"TYPE": "Type ID",
"MEDIA_DURATION": "Duration"
}
"attributes": {
"FILE_MIME": "MIME type",
"FILE_SIZE": "File size",
"ADDED": "Added at",
"LAST_VISITED": "Last visited at",
"NUM_VISITED": "Times visited",
"ATTR_LABEL": "Label",
"IS": "Type",
"TYPE": "Type ID",
"MEDIA_DURATION": "Duration"
}
}

View File

@ -1,12 +1,12 @@
import i18next from "i18next";
import { createI18nStore } from "svelte-i18next";
import en from "./en.json";
import i18next from 'i18next';
import { createI18nStore } from 'svelte-i18next';
import en from './en.json';
i18next.init({
lng: "en",
resources: {
en,
},
lng: 'en',
resources: {
en
}
});
export const i18n = createI18nStore(i18next);

View File

@ -1,27 +1,27 @@
import mitt from "mitt";
import mitt from 'mitt';
type NotifyEvents = {
notification: UpNotification;
notification: UpNotification;
};
export type UpNotificationLevel = "info" | "warning" | "error";
export type UpNotificationLevel = 'info' | 'warning' | 'error';
export interface INotification {
id: string;
content: string;
level: UpNotificationLevel;
id: string;
content: string;
level: UpNotificationLevel;
}
export class UpNotification implements INotification {
id: string;
content: string;
level: UpNotificationLevel;
id: string;
content: string;
level: UpNotificationLevel;
constructor(content: string, level?: UpNotificationLevel) {
this.id = String(Math.random());
this.content = content;
this.level = level || "info";
}
constructor(content: string, level?: UpNotificationLevel) {
this.id = String(Math.random());
this.content = content;
this.level = level || 'info';
}
}
export const notify = mitt<NotifyEvents>();

View File

@ -1,92 +1,83 @@
@use "colors";
@use "sass:color";
@use 'colors';
@use 'sass:color';
@mixin rebase(
$rebase03,
$rebase02,
$rebase01,
$rebase00,
$rebase0,
$rebase1,
$rebase2,
$rebase3
) {
--foreground: #{$rebase0};
--foreground-lighter: #{$rebase1};
--foreground-lightest: #{$rebase2};
--background: #{$rebase03};
--background-lighter: #{$rebase02};
--background-lightest: #{color.adjust($rebase02, $lightness: 1.5%)};
--primary: #{colors.$blue};
--primary-lighter: #{color.adjust(colors.$blue, $lightness: 25%)};
@mixin rebase($rebase03, $rebase02, $rebase01, $rebase00, $rebase0, $rebase1, $rebase2, $rebase3) {
--foreground: #{$rebase0};
--foreground-lighter: #{$rebase1};
--foreground-lightest: #{$rebase2};
--background: #{$rebase03};
--background-lighter: #{$rebase02};
--background-lightest: #{color.adjust($rebase02, $lightness: 1.5%)};
--primary: #{colors.$blue};
--primary-lighter: #{color.adjust(colors.$blue, $lightness: 25%)};
background-color: $rebase03;
color: $rebase0;
background-color: $rebase03;
color: $rebase0;
}
@mixin accentize($accent) {
a,
a:active,
a:visited,
code.url {
color: $accent;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: $accent;
}
a,
a:active,
a:visited,
code.url {
color: $accent;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: $accent;
}
}
@media (prefers-color-scheme: light) {
html {
@include rebase(
colors.$base3,
colors.$base2,
colors.$base1,
colors.$base0,
colors.$base00,
colors.$base01,
colors.$base02,
colors.$base03
);
}
html {
@include rebase(
colors.$base3,
colors.$base2,
colors.$base1,
colors.$base0,
colors.$base00,
colors.$base01,
colors.$base02,
colors.$base03
);
}
}
html {
@include rebase(
colors.$base03,
colors.$base02,
colors.$base01,
colors.$base00,
colors.$base0,
colors.$base1,
colors.$base2,
colors.$base3
);
@include rebase(
colors.$base03,
colors.$base02,
colors.$base01,
colors.$base00,
colors.$base0,
colors.$base1,
colors.$base2,
colors.$base3
);
}
html,
body {
color: var(--foreground);
background: var(--background);
color: var(--foreground);
background: var(--background);
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--foreground-lighter);
border-color: var(--foreground);
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--foreground-lighter);
border-color: var(--foreground);
}
a,
a:active,
a:visited {
color: var(--foreground-lighter);
}
a,
a:active,
a:visited {
color: var(--foreground-lighter);
}
}

View File

@ -1,68 +1,68 @@
@use "sass:color";
@use "colors";
@use 'sass:color';
@use 'colors';
html {
box-sizing: border-box;
box-sizing: border-box;
}
* {
box-sizing: inherit;
box-sizing: inherit;
}
select {
background: var(--background-lighter);
color: var(--foreground);
font-family: var(--default-font);
background: var(--background-lighter);
color: var(--foreground);
font-family: var(--default-font);
border: 1px solid var(--foreground-lighter);
border-radius: 4px;
border: 1px solid var(--foreground-lighter);
border-radius: 4px;
}
.spinner {
font-size: 2em;
font-size: 2em;
}
.button {
border: 1px solid var(--foreground);
border-radius: 4px;
border: 1px solid var(--foreground);
border-radius: 4px;
background: var(--background-lighter);
color: var(--foreground);
background: var(--background-lighter);
color: var(--foreground);
padding: 0.25em 1em;
line-height: 1;
padding: 0.25em 1em;
line-height: 1;
display: block;
text-align: center;
display: block;
text-align: center;
cursor: pointer;
cursor: pointer;
input {
display: none;
}
input {
display: none;
}
&.disabled,
&:disabled {
pointer-events: none;
opacity: 0.7;
}
&.disabled,
&:disabled {
pointer-events: none;
opacity: 0.7;
}
@media screen and (max-width: 600px) {
padding: 0.5em;
}
@media screen and (max-width: 600px) {
padding: 0.5em;
}
}
.mark-entity::first-letter,
.mark-entity *::first-letter {
color: color.scale(color.mix(colors.$base1, colors.$red), $saturation: -33%);
color: color.scale(color.mix(colors.$base1, colors.$red), $saturation: -33%);
}
.mark-attribute::first-letter,
.mark-attribute *::first-letter {
color: color.mix(colors.$base1, colors.$green);
color: color.mix(colors.$base1, colors.$green);
}
.mark-value::first-letter,
.mark-value *::first-letter {
color: color.mix(colors.$base1, colors.$blue);
color: color.mix(colors.$base1, colors.$blue);
}

View File

@ -1,13 +1,13 @@
@import "node_modules/@ibm/plex/scss/ibm-plex.scss";
@import 'node_modules/@ibm/plex/scss/ibm-plex.scss';
html {
--default-font: "IBM Plex Sans", "Helvetica Neue", Arial, sans-serif;
--monospace-font: "IBM Plex Mono", "Menlo", "DejaVu Sans Mono", "Consolas",
"Inconsolata", "Hack", monospace;
font-size: 16px;
--default-font: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
--monospace-font: 'IBM Plex Mono', 'Menlo', 'DejaVu Sans Mono', 'Consolas', 'Inconsolata', 'Hack',
monospace;
font-size: 16px;
}
html,
body {
font-family: var(--default-font);
font-family: var(--default-font);
}

View File

@ -1,37 +1,37 @@
@use "normalize.css/normalize.css";
@use "colors-app";
@use "fonts";
@use "common";
@use 'normalize.css/normalize.css';
@use 'colors-app';
@use 'fonts';
@use 'common';
body {
height: calc(100vh - 2rem);
display: flex;
flex-direction: column;
height: calc(100vh - 2rem);
display: flex;
flex-direction: column;
}
main {
flex-grow: 1;
flex-grow: 1;
}
.image-queued,
.image-loading {
animation: gradient 1.5s ease infinite;
animation: gradient 1.5s ease infinite;
color: transparent; // Hide alt-text
color: transparent; // Hide alt-text
@keyframes gradient {
0% {
background-color: rgba(255, 255, 255, 0.2);
}
50% {
background-color: rgba(255, 255, 255, 0.4);
}
100% {
background-color: rgba(255, 255, 255, 0.2);
}
}
@keyframes gradient {
0% {
background-color: rgba(255, 255, 255, 0.2);
}
50% {
background-color: rgba(255, 255, 255, 0.4);
}
100% {
background-color: rgba(255, 255, 255, 0.2);
}
}
}
.image-queued {
opacity: 0.5;
opacity: 0.5;
}

View File

@ -1,35 +1,35 @@
import type { IValue } from "@upnd/upend/types";
import type { IValue } from '@upnd/upend/types';
export type WidgetChange =
| AttributeCreate
| AttributeUpdate
| AttributeDelete
| EntryInAdd
| EntryInDelete;
| AttributeCreate
| AttributeUpdate
| AttributeDelete
| EntryInAdd
| EntryInDelete;
export interface AttributeCreate {
type: "create";
attribute: string;
value: IValue;
type: 'create';
attribute: string;
value: IValue;
}
export interface AttributeUpdate {
type: "update";
attribute: string;
value: IValue;
type: 'update';
attribute: string;
value: IValue;
}
export interface AttributeDelete {
type: "delete";
address: string;
type: 'delete';
address: string;
}
export interface EntryInAdd {
type: "entry-add";
address: string;
type: 'entry-add';
address: string;
}
export interface EntryInDelete {
type: "entry-delete";
address: string;
type: 'entry-delete';
address: string;
}

View File

@ -1,6 +1,6 @@
import type { Readable } from "svelte/store";
import type { Readable } from 'svelte/store';
export interface BrowseContext {
index: Readable<number>;
addresses: Readable<string[]>;
index: Readable<number>;
addresses: Readable<string[]>;
}

View File

@ -1,41 +1,37 @@
import type { EntityInfo } from "@upnd/upend/types";
import type { UpObject } from "@upnd/upend";
import { ATTR_IN } from "@upnd/upend/constants";
import type { EntityInfo } from '@upnd/upend/types';
import type { UpObject } from '@upnd/upend';
import { ATTR_IN } from '@upnd/upend/constants';
export function getTypes(entity: UpObject, entityInfo: EntityInfo) {
const mimeType = String(entity?.get("FILE_MIME"));
const mimeType = String(entity?.get('FILE_MIME'));
const video =
["video", "application/x-matroska"].some((p) => mimeType.startsWith(p)) ||
entity?.identify().some((l) => l.endsWith(".avi"));
const audio =
(["audio", "application/x-riff"].some((p) => mimeType.startsWith(p)) &&
!video) ||
[".ogg", ".mp3", ".wav"].some(
(suffix) =>
entity?.identify().some((l) => l.toLowerCase().endsWith(suffix)),
);
const image = mimeType.startsWith("image");
const text = mimeType.startsWith("text");
const pdf = mimeType.startsWith("application/pdf");
const model =
mimeType?.startsWith("model") ||
entity?.identify().some((l) => l.endsWith(".stl"));
const web = entityInfo?.t == "Url";
const fragment = Boolean(entity?.get("ANNOTATES"));
const video =
['video', 'application/x-matroska'].some((p) => mimeType.startsWith(p)) ||
entity?.identify().some((l) => l.endsWith('.avi'));
const audio =
(['audio', 'application/x-riff'].some((p) => mimeType.startsWith(p)) && !video) ||
['.ogg', '.mp3', '.wav'].some((suffix) =>
entity?.identify().some((l) => l.toLowerCase().endsWith(suffix))
);
const image = mimeType.startsWith('image');
const text = mimeType.startsWith('text');
const pdf = mimeType.startsWith('application/pdf');
const model = mimeType?.startsWith('model') || entity?.identify().some((l) => l.endsWith('.stl'));
const web = entityInfo?.t == 'Url';
const fragment = Boolean(entity?.get('ANNOTATES'));
const group = entity?.backlinks.some((e) => e.attribute == ATTR_IN);
const group = entity?.backlinks.some((e) => e.attribute == ATTR_IN);
return {
mimeType,
audio,
video,
image,
text,
pdf,
model,
web,
fragment,
group,
};
return {
mimeType,
audio,
video,
image,
text,
pdf,
model,
web,
fragment,
group
};
}

View File

@ -1,10 +1,10 @@
export function updateTitle(route?: string, title?: string) {
let newTitle = "UpEnd";
if (route?.length) {
newTitle += ` | ${route}`;
}
if (title?.length) {
newTitle += ` - ${title}`;
}
document.title = newTitle;
let newTitle = 'UpEnd';
if (route?.length) {
newTitle += ` | ${route}`;
}
if (title?.length) {
newTitle += ` - ${title}`;
}
document.title = newTitle;
}

View File

@ -1,30 +1,30 @@
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
import { viteStaticCopy } from "vite-plugin-static-copy";
import * as path from "path";
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import * as path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
sveltekit(),
viteStaticCopy({
targets: [
{
src: path.join(__dirname, "node_modules/boxicons", "fonts"),
dest: path.resolve(__dirname, "dist/vendor/boxicons"),
},
{
src: path.join(__dirname, "node_modules/boxicons", "css"),
dest: path.resolve(__dirname, "dist/vendor/boxicons"),
},
],
}),
],
server: {
proxy: {
"/api": {
target: "http://localhost:8093/",
},
},
},
plugins: [
sveltekit(),
viteStaticCopy({
targets: [
{
src: path.join(__dirname, 'node_modules/boxicons', 'fonts'),
dest: path.resolve(__dirname, 'dist/vendor/boxicons')
},
{
src: path.join(__dirname, 'node_modules/boxicons', 'css'),
dest: path.resolve(__dirname, 'dist/vendor/boxicons')
}
]
})
],
server: {
proxy: {
'/api': {
target: 'http://localhost:8093/'
}
}
}
});