wip: LabelBorder component, modeless AudioViewer, ImageViewer
parent
0a5398b0a7
commit
a29d66d829
|
@ -21,6 +21,7 @@
|
|||
import IconButton from "./utils/IconButton.svelte";
|
||||
import { createEventDispatcher, type ComponentType } from "svelte";
|
||||
import UpObject from "./display/UpObject.svelte";
|
||||
import LabelBorder from "./utils/LabelBorder.svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let entries: UpEntry[];
|
||||
|
@ -72,9 +73,9 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<section class="entry-view labelborder" class:highlighted>
|
||||
<header>
|
||||
<h3>
|
||||
<LabelBorder>
|
||||
<svelte:fragment slot="header-full">
|
||||
<h3 class:highlighted>
|
||||
{#if group}
|
||||
{#if icon}
|
||||
<div class="icon">
|
||||
|
@ -107,35 +108,30 @@
|
|||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
<div class="content">
|
||||
{#each components as component}
|
||||
<svelte:component
|
||||
this={component.component}
|
||||
{...component.props || {}}
|
||||
on:change
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</svelte:fragment>
|
||||
{#each components as component}
|
||||
<svelte:component
|
||||
this={component.component}
|
||||
{...component.props || {}}
|
||||
on:change
|
||||
/>
|
||||
{/each}
|
||||
</LabelBorder>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "./util";
|
||||
.icon {
|
||||
display: inline-block;
|
||||
font-size: 1.25em;
|
||||
margin-top: -0.3em;
|
||||
position: relative;
|
||||
bottom: -2px;
|
||||
}
|
||||
|
||||
section.entry-view {
|
||||
.icon {
|
||||
display: inline-block;
|
||||
font-size: 1.25em;
|
||||
margin-top: -0.3em;
|
||||
position: relative;
|
||||
bottom: -2px;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
transition: text-shadow 0.2s;
|
||||
|
||||
h3 {
|
||||
transition: text-shadow 0.2s;
|
||||
}
|
||||
|
||||
&.highlighted h3 {
|
||||
&.highlighted {
|
||||
text-shadow: #cb4b16 0 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
import { ATTR_IN, ATTR_LABEL, ATTR_KEY, ATTR_OF } from "upend/constants";
|
||||
import InspectGroups from "./InspectGroups.svelte";
|
||||
import InspectTypeEditor from "./InspectTypeEditor.svelte";
|
||||
import LabelBorder from "./utils/LabelBorder.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const params = useParams();
|
||||
|
@ -412,19 +413,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<section class="labelborder">
|
||||
<header>
|
||||
<h3>{$i18n.t("Used")} ({attributesUsed.length})</h3>
|
||||
</header>
|
||||
<div class="content">
|
||||
<EntryList
|
||||
columns="entity,value"
|
||||
columnWidths={["auto", "33%"]}
|
||||
entries={attributesUsed}
|
||||
orderByValue
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<LabelBorder>
|
||||
<span slot="header"
|
||||
>{$i18n.t("Used")} ({attributesUsed.length})</span
|
||||
>
|
||||
<EntryList
|
||||
columns="entity,value"
|
||||
columnWidths={["auto", "33%"]}
|
||||
entries={attributesUsed}
|
||||
orderByValue
|
||||
/>
|
||||
</LabelBorder>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
|
@ -472,8 +471,6 @@
|
|||
</div>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "./util";
|
||||
|
||||
header h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { i18n } from "../i18n";
|
||||
import type { UpObject } from "upend";
|
||||
import type { Readable } from "svelte/store";
|
||||
import LabelBorder from "./utils/LabelBorder.svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let entity: Readable<UpObject>;
|
||||
|
@ -49,90 +50,84 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<section class="groups labelborder">
|
||||
<header><h3>{$i18n.t("Groups")}</h3></header>
|
||||
<div class="content">
|
||||
{#if adding}
|
||||
<div class="selector">
|
||||
<Selector
|
||||
bind:this={groupSelector}
|
||||
type="value"
|
||||
valueTypes={["Address"]}
|
||||
bind:value={groupToAdd}
|
||||
on:input={addGroup}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) adding = false;
|
||||
}}
|
||||
placeholder={$i18n.t("Choose an entity...")}
|
||||
<LabelBorder hide={groups.length === 0}>
|
||||
<span slot="header">{$i18n.t("Groups")}</span>
|
||||
|
||||
{#if adding}
|
||||
<div class="selector">
|
||||
<Selector
|
||||
bind:this={groupSelector}
|
||||
type="value"
|
||||
valueTypes={["Address"]}
|
||||
bind:value={groupToAdd}
|
||||
on:input={addGroup}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) adding = false;
|
||||
}}
|
||||
placeholder={$i18n.t("Choose an entity...")}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="body">
|
||||
<div class="group-list">
|
||||
{#each groups as [groupAddress, groupEntryAddress]}
|
||||
<div
|
||||
class="group"
|
||||
on:mouseenter={() => dispatch("highlighted", groupAddress)}
|
||||
on:mouseleave={() => dispatch("highlighted", undefined)}
|
||||
>
|
||||
<UpObjectDisplay address={groupAddress} link />
|
||||
<IconButton
|
||||
subdued
|
||||
name="x-circle"
|
||||
on:click={() => removeGroup(groupEntryAddress)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-groups">
|
||||
{$i18n.t("Object is not in any groups.")}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !adding}
|
||||
<div class="add-button">
|
||||
<IconButton
|
||||
outline
|
||||
small
|
||||
name="folder-plus"
|
||||
on:click={() => (adding = true)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="body">
|
||||
<div class="group-list">
|
||||
{#each groups as [groupAddress, groupEntryAddress]}
|
||||
<div
|
||||
class="group"
|
||||
on:mouseenter={() => dispatch("highlighted", groupAddress)}
|
||||
on:mouseleave={() => dispatch("highlighted", undefined)}
|
||||
>
|
||||
<UpObjectDisplay address={groupAddress} link />
|
||||
<IconButton
|
||||
subdued
|
||||
name="x-circle"
|
||||
on:click={() => removeGroup(groupEntryAddress)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-groups">
|
||||
{$i18n.t("Object is not in any groups.")}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !adding}
|
||||
<div class="add-button">
|
||||
<IconButton
|
||||
outline
|
||||
small
|
||||
name="folder-plus"
|
||||
on:click={() => (adding = true)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</LabelBorder>
|
||||
|
||||
<style lang="scss">
|
||||
@use "./util";
|
||||
.group-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem 0.2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
|
||||
.groups {
|
||||
margin: 0.25rem 0;
|
||||
.group-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem 0.2rem;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
|
||||
.group-list {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.selector {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.selector {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.no-groups {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import type { Readable } from "svelte/store";
|
||||
import { ATTR_OF } from "upend/constants";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import LabelBorder from "./utils/LabelBorder.svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let entity: Readable<UpObject>;
|
||||
|
@ -50,59 +51,52 @@
|
|||
</script>
|
||||
|
||||
{#if typeEntries.length || $entity?.attr["~IN"]?.length}
|
||||
<section class="labelborder">
|
||||
<header><h3>{$i18n.t("Type Attributes")}</h3></header>
|
||||
<div class="content">
|
||||
{#if adding}
|
||||
<div class="selector">
|
||||
<Selector
|
||||
bind:this={typeSelector}
|
||||
type="attribute"
|
||||
bind:attribute={attributeToAdd}
|
||||
on:input={add}
|
||||
placeholder={$i18n.t("Assign an attribute to this type...")}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) adding = false;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="body">
|
||||
<ul class="attributes">
|
||||
{#each typeEntries as typeEntry}
|
||||
<li class="attribute">
|
||||
<div class="label">
|
||||
<UpObjectDisplay address={typeEntry.entity} link />
|
||||
</div>
|
||||
<div class="controls">
|
||||
<IconButton
|
||||
name="x-circle"
|
||||
on:click={() => remove(typeEntry)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="no-attributes">
|
||||
{$i18n.t("No attributes assigned to this type.")}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="add-button">
|
||||
<IconButton
|
||||
outline
|
||||
small
|
||||
name="plus-circle"
|
||||
on:click={() => (adding = true)}
|
||||
/>
|
||||
</div>
|
||||
<LabelBorder hide={typeEntries.length === 0}>
|
||||
<span slot="header">{$i18n.t("Type Attributes")}</span>
|
||||
{#if adding}
|
||||
<div class="selector">
|
||||
<Selector
|
||||
bind:this={typeSelector}
|
||||
type="attribute"
|
||||
bind:attribute={attributeToAdd}
|
||||
on:input={add}
|
||||
placeholder={$i18n.t("Assign an attribute to this type...")}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) adding = false;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="body">
|
||||
<ul class="attributes">
|
||||
{#each typeEntries as typeEntry}
|
||||
<li class="attribute">
|
||||
<div class="label">
|
||||
<UpObjectDisplay address={typeEntry.entity} link />
|
||||
</div>
|
||||
<div class="controls">
|
||||
<IconButton name="x-circle" on:click={() => remove(typeEntry)} />
|
||||
</div>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="no-attributes">
|
||||
{$i18n.t("No attributes assigned to this type.")}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="add-button">
|
||||
<IconButton
|
||||
outline
|
||||
small
|
||||
name="plus-circle"
|
||||
on:click={() => (adding = true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</LabelBorder>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use "./util";
|
||||
|
||||
.attributes {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
import Selector from "../../utils/Selector.svelte";
|
||||
import UpObject from "../../display/UpObject.svelte";
|
||||
import Spinner from "../../utils/Spinner.svelte";
|
||||
import IconButton from "../../../components/utils/IconButton.svelte";
|
||||
import LabelBorder from "../../../components/utils/LabelBorder.svelte";
|
||||
import { i18n } from "../../../i18n";
|
||||
import { ATTR_LABEL } from "upend/constants";
|
||||
import debug from "debug";
|
||||
|
@ -18,7 +20,7 @@
|
|||
export let address: string;
|
||||
export let detail: boolean;
|
||||
|
||||
let editable = false; // TODO
|
||||
let editable = false;
|
||||
|
||||
let containerEl: HTMLDivElement;
|
||||
let timelineEl: HTMLDivElement;
|
||||
|
@ -27,7 +29,7 @@
|
|||
let wavesurfer: WaveSurfer;
|
||||
|
||||
// Zoom handling
|
||||
$: zoom = detail ? 1 : undefined;
|
||||
let zoom = 1;
|
||||
const setZoom = throttle((level: number) => {
|
||||
wavesurfer.zoom(level);
|
||||
}, 250);
|
||||
|
@ -276,13 +278,19 @@
|
|||
{/if}
|
||||
{#if loaded}
|
||||
<header>
|
||||
{#if zoom}
|
||||
<div class="zoom">
|
||||
<Icon name="zoom-out" />
|
||||
<input type="range" min="1" max="50" bind:value={zoom} />
|
||||
<Icon name="zoom-in" />
|
||||
</div>
|
||||
{/if}
|
||||
<IconButton
|
||||
name="edit"
|
||||
title={$i18n.t("Toggle Edit Mode")}
|
||||
on:click={() => (editable = !editable)}
|
||||
active={editable}
|
||||
>
|
||||
{$i18n.t("Annotate")}
|
||||
</IconButton>
|
||||
<div class="zoom">
|
||||
<Icon name="zoom-out" />
|
||||
<input type="range" min="1" max="50" bind:value={zoom} />
|
||||
<Icon name="zoom-in" />
|
||||
</div>
|
||||
</header>
|
||||
{/if}
|
||||
<div
|
||||
|
@ -292,10 +300,8 @@
|
|||
/>
|
||||
<div class="wavesurfer" bind:this={containerEl} />
|
||||
{#if currentAnnotation}
|
||||
<section class="annotationEditor labelborder">
|
||||
<header>
|
||||
<h3>{$i18n.t("Annotation")}</h3>
|
||||
</header>
|
||||
<LabelBorder>
|
||||
<span slot="header">{$i18n.t("Annotation")}</span>
|
||||
{#if currentAnnotation.attributes["upend-address"]}
|
||||
<UpObject
|
||||
link
|
||||
|
@ -367,13 +373,12 @@
|
|||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</section>
|
||||
</LabelBorder>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use "../../../styles/colors";
|
||||
@use "../../util";
|
||||
|
||||
.audio {
|
||||
width: 100%;
|
||||
|
@ -381,7 +386,7 @@
|
|||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
& > * {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
|
@ -395,31 +400,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
.annotationEditor {
|
||||
& > * {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.baseControls,
|
||||
.content {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.baseControls,
|
||||
.regionControls,
|
||||
.existControls {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.baseControls,
|
||||
.regionControls,
|
||||
.existControls {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.baseControls {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.baseControls {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.regionControls div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
.regionControls div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 6em;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
import Spinner from "../../utils/Spinner.svelte";
|
||||
import UpObject from "../UpObject.svelte";
|
||||
import { ATTR_LABEL } from "upend/constants";
|
||||
import { i18n } from "../../../i18n";
|
||||
|
||||
export let address: string;
|
||||
export let detail: boolean;
|
||||
|
||||
let editable = false; // TODO
|
||||
let editable = false;
|
||||
|
||||
const { entity } = useEntity(address);
|
||||
|
||||
|
@ -219,8 +220,21 @@
|
|||
{/if}
|
||||
{#if imageLoaded}
|
||||
<div class="toolbar">
|
||||
<IconButton name="brightness-half" on:click={cycleBrightness} />
|
||||
<IconButton name="tone" on:click={cycleContrast} />
|
||||
<IconButton
|
||||
name="edit"
|
||||
on:click={() => (editable = !editable)}
|
||||
active={editable}
|
||||
>
|
||||
{$i18n.t("Annotate")}
|
||||
</IconButton>
|
||||
<div class="image-controls">
|
||||
<IconButton name="brightness-half" on:click={cycleBrightness}>
|
||||
{$i18n.t("Brightness")}
|
||||
</IconButton>
|
||||
<IconButton name="tone" on:click={cycleContrast}>
|
||||
{$i18n.t("Contrast")}
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
|
@ -274,8 +288,12 @@
|
|||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
.image-controls {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.zoomable {
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<script lang="ts">
|
||||
export let hide = false;
|
||||
let hidden = true;
|
||||
</script>
|
||||
|
||||
<section class="labelborder" class:hide class:hidden>
|
||||
<header
|
||||
on:click={() => {
|
||||
if (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;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
|
||||
border-bottom: 1px solid var(--foreground);
|
||||
padding-bottom: 0.33rem;
|
||||
margin-bottom: 0.33rem;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.hide {
|
||||
header {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
&.hidden {
|
||||
opacity: 0.66;
|
||||
|
||||
header {
|
||||
border-bottom-width: 0.5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -4,12 +4,11 @@
|
|||
import { useEntity } from "../../lib/entity";
|
||||
import type { AttributeCreate, AttributeUpdate } from "../../types/base";
|
||||
import type { UpEntry } from "upend";
|
||||
import LabelBorder from "./LabelBorder.svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let address: string;
|
||||
|
||||
let editable = false;
|
||||
|
||||
$: ({ entity } = useEntity(address));
|
||||
|
||||
let noteEntry: UpEntry | undefined;
|
||||
|
@ -45,26 +44,15 @@
|
|||
}, 500);
|
||||
</script>
|
||||
|
||||
{#if notes || editable}
|
||||
<section class="notes labelborder">
|
||||
<header>
|
||||
<h3>Notes</h3>
|
||||
</header>
|
||||
<div
|
||||
class="content"
|
||||
contenteditable={editable ? "true" : "false"}
|
||||
on:input={update}
|
||||
bind:this={contentEl}
|
||||
>
|
||||
{notes ? notes : ""}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
<LabelBorder hide={!notes?.length}>
|
||||
<span slot="header">Notes</span>
|
||||
<div class="notes" contenteditable on:input={update} bind:this={contentEl}>
|
||||
{notes ? notes : ""}
|
||||
</div>
|
||||
</LabelBorder>
|
||||
|
||||
<style lang="scss">
|
||||
@use "../util";
|
||||
|
||||
.content {
|
||||
.notes {
|
||||
background: var(--background);
|
||||
border-radius: 4px;
|
||||
padding: 0.5em !important;
|
||||
|
|
Loading…
Reference in New Issue