feat(webui): Surface view as Column in Browse
ci/woodpecker/push/woodpecker Pipeline failed
Details
ci/woodpecker/push/woodpecker Pipeline failed
Details
parent
044e19e9a7
commit
27aeca9f4f
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue