feat(webui): Surface view as Column in Browse
ci/woodpecker/push/woodpecker Pipeline failed Details

feat/selector-improvements
Tomáš Mládek 2023-11-15 17:40:38 +01:00
parent 044e19e9a7
commit 27aeca9f4f
8 changed files with 176 additions and 123 deletions

View File

@ -9,7 +9,6 @@
import DropPasteHandler from "./components/DropPasteHandler.svelte";
import AddModal from "./components/AddModal.svelte";
import Store from "./views/Store.svelte";
import Surface from "./views/Surface.svelte";
import Setup from "./views/Setup.svelte";
import "./styles/main.scss";
@ -32,10 +31,6 @@
<Search query={decodeURIComponent(params.query)} />
</Route>
<Route path="/surface">
<Surface />
</Route>
<Route path="/store">
<Store />
</Route>

View File

@ -500,14 +500,6 @@
{/if}
{#if $entityInfo?.t === "Attribute"}
<div class="buttons">
<div class="button">
<Link to="/surface?x={$entityInfo.c}">
{$i18n.t("Surface view")}
</Link>
</div>
</div>
<LabelBorder>
<span slot="header"
>{$i18n.t("Used")} ({attributesUsed.length})</span

View File

@ -1,25 +1,27 @@
<script lang="ts">
import UpObject from "../components/display/UpObject.svelte";
import UpObject from "./display/UpObject.svelte";
import api from "../lib/api";
import Selector from "../components/utils/Selector.svelte";
import { onMount } from "svelte";
import Selector from "./utils/Selector.svelte";
import { onMount, tick } from "svelte";
import type { ZoomTransform } from "d3";
import Spinner from "../components/utils/Spinner.svelte";
import UpObjectCard from "../components/display/UpObjectCard.svelte";
import BlobPreview from "../components/display/BlobPreview.svelte";
import SurfacePoint from "../components/display/SurfacePoint.svelte";
import Spinner from "./utils/Spinner.svelte";
import UpObjectCard from "./display/UpObjectCard.svelte";
import BlobPreview from "./display/BlobPreview.svelte";
import SurfacePoint from "./display/SurfacePoint.svelte";
import { i18n } from "../i18n";
import type { IValue } from "@upnd/upend/types";
import { useNavigate } from "svelte-navigator";
const navigate = useNavigate();
import debug from "debug";
const dbg = debug("kestrel:surface");
// import { useNavigate } from "svelte-navigator";
// const navigate = useNavigate();
const urlParams = new URLSearchParams(
window.location.href.substring(window.location.href.indexOf("?")),
);
// const urlParams = new URLSearchParams(
// window.location.href.substring(window.location.href.indexOf("?")),
// );
export let x: string = urlParams.get("x");
export let y: string = urlParams.get("y");
$: if (x && y) navigate(`/surface?x=${x}&y=${y}`, { replace: true });
export let x: string;
export let y: string | undefined = undefined; // TODO
// $: if (x && y) navigate(`/surface?x=${x}&y=${y}`, { replace: true });
let viewMode = "point";
@ -27,9 +29,9 @@
let currentY = NaN;
let loaded = false;
let viewEl: HTMLElement | undefined;
let viewHeight = 0;
let viewWidth = 0;
let rootEl: HTMLElement | undefined;
interface IPoint {
address: string;
@ -69,34 +71,54 @@
const d3 = await import("d3");
function init() {
const view = d3.select(".view");
viewWidth = viewEl.clientWidth;
viewHeight = viewEl.clientHeight;
const selector = Array.from(viewEl.classList)
.map((c) => `.${c}`)
.join(" ");
dbg(
"Initializing Surface view for %s = %dx%d",
selector,
viewWidth,
viewHeight,
);
const view = d3.select(selector);
const svg = view.select("svg");
if (svg.empty()) {
throw new Error(
"Failed initializing Surface - couldn't locate SVG element",
);
}
svg.selectAll("*").remove();
const xScale = d3
.scaleLinear()
.domain([0, viewWidth])
.domain([0, viewWidth / 10])
.range([0, viewWidth]);
const xAxis = d3
.axisBottom(xScale)
.ticks(15)
.tickSize(viewHeight)
.tickPadding(5 - viewHeight);
const yScale = d3
.scaleLinear()
.domain([0, viewHeight])
.domain([0, viewHeight / 10])
.range([viewHeight, 0]);
let xTicks = 10;
let yTicks = viewHeight / (viewWidth / xTicks);
const xAxis = d3
.axisBottom(xScale)
.ticks(xTicks)
.tickSize(viewHeight)
.tickPadding(5 - viewHeight);
const yAxis = d3
.axisRight(yScale)
.ticks(viewHeight / (viewWidth / 15))
.ticks(yTicks)
.tickSize(viewWidth)
.tickPadding(5 - viewWidth);
const gX = svg.append("g").attr("class", "axis axis--x").call(xAxis);
const gY = svg.append("g").attr("class", "axis axis--y").call(yAxis);
const gX = svg.append("g").call(xAxis);
const gY = svg.append("g").call(yAxis);
const zoom = d3
.zoom()
@ -108,12 +130,12 @@
.on("zoom", zoomed);
function zoomed({ transform }: { transform: ZoomTransform }) {
const points = d3.select(".content");
const points = view.select(".content");
points.style(
"transform",
`translate(${transform.x}px, ${transform.y}px) scale(${transform.k})`,
);
const allPoints = d3.selectAll(".point");
const allPoints = view.selectAll(".point");
allPoints.style("transform", `scale(${1 / transform.k})`);
gX.call(xAxis.scale(transform.rescaleX(xScale)));
@ -128,14 +150,14 @@
// not using offsetXY because `translate` transforms on .inner mess it up
const viewBBox = (view.node() as HTMLElement).getBoundingClientRect();
const [x, y] = d3
.zoomTransform(d3.select(".content").node() as HTMLElement)
.zoomTransform(view.select(".content").node() as HTMLElement)
.invert([ev.clientX - viewBBox.left, ev.clientY - viewBBox.top]);
currentX = xScale.invert(x);
currentY = yScale.invert(y);
});
d3.select(".view")
d3.select(selector)
.call(zoom)
.on("dblclick.zoom", (_ev: MouseEvent) => {
selectorCoords = [currentX, currentY];
@ -144,9 +166,9 @@
}
const resizeObserver = new ResizeObserver(() => {
init();
tick().then(() => init());
});
resizeObserver.observe(rootEl);
resizeObserver.observe(viewEl);
});
async function onSelectorInput(ev: CustomEvent<IValue>) {
@ -167,16 +189,34 @@
}
</script>
<div class="surface" bind:this={rootEl}>
<div class="header">
<div class="surface">
<div class="header ui">
<div class="axis-selector">
X: <Selector type="attribute" bind:attribute={x} />
<div class="label">X</div>
<Selector type="attribute" bind:attribute={x} />
<div class="value">
{(Math.round(currentX * 100) / 100).toLocaleString("en", {
useGrouping: false,
minimumFractionDigits: 2,
})}
</div>
</div>
<div class="axis-selector">
Y: <Selector type="attribute" bind:attribute={y} />
<div class="label">Y</div>
<Selector type="attribute" bind:attribute={y} />
<div class="value">
{(Math.round(currentY * 100) / 100).toLocaleString("en", {
useGrouping: false,
minimumFractionDigits: 2,
})}
</div>
</div>
<div class="view-mode-selector">
{$i18n.t("View as")}
</div>
<div class="view" class:loaded bind:this={viewEl}>
<div class="ui view-mode-selector">
<div class="label">
{$i18n.t("View as")}
</div>
<select bind:value={viewMode}>
<option value="point">{$i18n.t("Point")}</option>
<option value="link">{$i18n.t("Link")}</option>
@ -184,30 +224,6 @@
<!-- <option value="preview">{$i18n.t("Preview")}</option> -->
</select>
</div>
<div class="position">
{$i18n.t("Current position")}:
<div class="label">
<em>X:</em>
{x || "?"} = {(Math.round(currentX * 100) / 100).toLocaleString("en", {
useGrouping: false,
minimumFractionDigits: 2,
})}
</div>
<div class="label">
<em>Y:</em>
{y || "?"} = {(Math.round(currentY * 100) / 100).toLocaleString("en", {
useGrouping: false,
minimumFractionDigits: 2,
})}
</div>
</div>
</div>
<div
class="view"
class:loaded
bind:clientWidth={viewWidth}
bind:clientHeight={viewHeight}
>
{#if !loaded}
<div class="loading">
<Spinner centered="absolute" />
@ -263,23 +279,22 @@
.header {
display: flex;
gap: 2em;
padding: 1em;
border-bottom: 1px solid var(--foreground);
flex-wrap: wrap;
gap: 1em;
align-items: center;
justify-content: space-between;
margin: 0.5em 0;
.axis-selector {
display: flex;
gap: 1em;
align-items: center;
}
.position {
display: flex;
gap: 1em;
em {
font-weight: bold;
font-style: normal;
.label {
font-size: 1rem;
&::after {
content: ":";
}
}
}
}
@ -313,11 +328,37 @@
}
}
.view-mode-selector {
position: absolute;
top: 2rem;
right: 1.5em;
padding: 0.66em;
border-radius: 4px;
border: 1px solid var(--foreground-lighter);
background: var(--background);
opacity: 0.66;
transition: opacity 0.25s;
&:hover {
opacity: 1;
}
}
&:not(.loaded) {
pointer-events: none;
}
}
.view-mode-selector {
display: flex;
flex-direction: column;
gap: 0.5em;
align-items: center;
}
.ui {
font-size: 0.8rem;
}
.loading {
position: absolute;
top: 0;

View File

@ -1,32 +1,28 @@
<script lang="ts">
import type { Address } from "@upnd/upend/types";
import UpObject from "./UpObject.svelte";
import { useNavigate } from "svelte-navigator";
const navigate = useNavigate();
import UpLink from "./UpLink.svelte";
export let address: Address;
let popup = false;
function visit() {
navigate(`/browse/${address}`);
}
</script>
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="surface-point"
class:popup
on:mouseover={() => (popup = true)}
on:mouseleave={() => (popup = false)}
on:click={visit}
>
{#if popup}
<div class="popup-inner">
<UpObject {address} />
</div>
{/if}
</div>
<UpLink passthrough to={{ entity: address }}>
<div
class="surface-point"
class:popup
on:mouseover={() => (popup = true)}
on:mouseleave={() => (popup = false)}
>
{#if popup}
<div class="popup-inner">
<UpObject {address} />
</div>
{/if}
</div>
</UpLink>
<style lang="scss">
@use "../../styles/colors.scss";

View File

@ -9,9 +9,11 @@
const navigate = useNavigate();
export let passthrough = false;
export let title: string | undefined = undefined;
export let to: {
entity?: Address;
attribute?: string;
surfaceAttribute?: string;
value?: { t: VALUE_TYPE; c: string };
};
@ -24,6 +26,8 @@
api.getAddress({ attribute: to.attribute }).then((address) => {
targetHref = address;
});
} else if (to.surfaceAttribute) {
targetHref = `surface:${to.surfaceAttribute}`;
}
}
@ -58,6 +62,7 @@
class:unresolved={targetHref === NOOP}
href="/#/browse/{targetHref}"
on:click|preventDefault={onClick}
{title}
>
<slot />
</a>

View File

@ -182,26 +182,38 @@
{/if}
</div>
</div>
{#if banner && isFile}
<div class="icon">
<a
class="link-button"
href="{api.apiUrl}/raw/{address}"
download={inferredIds[0]}
title={$i18n.t("Download as file")}
>
<Icon name="download" />
</a>
</div>
{#if $vaultInfo?.desktop}
{#if banner}
{#if $entityInfo?.t === "Attribute"}
<div class="icon">
<IconButton
name="window-alt"
on:click={nativeOpen}
title={$i18n.t("Open in default application...")}
/>
<UpLink
to={{ surfaceAttribute: $entityInfo.c }}
title={$i18n.t("Open on surface")}
>
<Icon name="cross" />
</UpLink>
</div>
{/if}
{#if isFile}
<div class="icon">
<a
class="link-button"
href="{api.apiUrl}/raw/{address}"
download={inferredIds[0]}
title={$i18n.t("Download as file")}
>
<Icon name="download" />
</a>
</div>
{#if $vaultInfo?.desktop}
<div class="icon">
<IconButton
name="window-alt"
on:click={nativeOpen}
title={$i18n.t("Open in default application...")}
/>
</div>
{/if}
{/if}
{/if}
</div>
</div>

View File

@ -7,6 +7,7 @@
export let placeholder = "";
export let value = "";
export let disabled = false;
export let size: number | undefined = 7;
let focused = false;
$: dispatch("focusChange", focused);
@ -29,6 +30,7 @@
on:input={onInput}
on:focus={() => (focused = true)}
on:blur={() => (focused = false)}
size={Math.max(value.length, size)}
on:keydown
{disabled}
/>

View File

@ -8,6 +8,7 @@
import Inspect from "../components/Inspect.svelte";
import CombineColumn from "../components/CombineColumn.svelte";
import GroupColumn from "../components/GroupColumn.svelte";
import SurfaceColumn from "../components/SurfaceColumn.svelte";
const navigate = useNavigate();
const params = useParams();
@ -118,6 +119,15 @@
>
<GroupColumn />
</BrowseColumn>
{:else if address.startsWith("surface")}
<BrowseColumn
{index}
{only}
on:close={() => close(index)}
on:detail={(ev) => onDetailChanged(index, ev)}
>
<SurfaceColumn x={address.split(":")[1]} />
</BrowseColumn>
{:else}
<BrowseColumn
{address}