Aiden added closer matching look
This commit is contained in:
@@ -38,6 +38,8 @@ The property request uses `READ_ACCESS` and omits `propertyName`, which asks Unr
|
|||||||
|
|
||||||
Details payloads are cached in browser `localStorage` for five minutes per object path. When you reselect an object, cached properties render immediately and the app refreshes them from Unreal in the background.
|
Details payloads are cached in browser `localStorage` for five minutes per object path. When you reselect an object, cached properties render immediately and the app refreshes them from Unreal in the background.
|
||||||
|
|
||||||
|
Asset-looking property values also request `/remote/object/thumbnail` and cache thumbnail data in `localStorage`, so mesh/material fields can render closer to Unreal's Details panel.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
Start Unreal Editor, enable the Remote Control API, and make sure it is listening on `127.0.0.1:30010`.
|
Start Unreal Editor, enable the Remote Control API, and make sure it is listening on `127.0.0.1:30010`.
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ChevronDown, Search, Settings, SlidersHorizontal, X } from "lucide-react";
|
import { Box, ChevronDown, Grid2X2, RotateCcw, Search, Settings, SlidersHorizontal, Star, X } from "lucide-react";
|
||||||
import type { DetailProperty, ObjectDetails, TreeNode } from "../types";
|
import type { DetailProperty, ObjectDetails, TreeNode } from "../types";
|
||||||
import { basename } from "../lib/remoteValues";
|
import { basename } from "../lib/remoteValues";
|
||||||
import { formatDetailValue } from "../services/details";
|
import {
|
||||||
|
detailAssetPath,
|
||||||
|
formatDetailValue,
|
||||||
|
isBooleanProperty,
|
||||||
|
isVectorLikeProperty,
|
||||||
|
normalizeThumbnailPayload,
|
||||||
|
vectorParts,
|
||||||
|
} from "../services/details";
|
||||||
|
import { readRemoteObjectThumbnail } from "../services/remoteControl";
|
||||||
|
import { readCachedThumbnail, writeCachedThumbnail } from "../services/thumbnailCache";
|
||||||
import { NodeIcon } from "./NodeIcon";
|
import { NodeIcon } from "./NodeIcon";
|
||||||
|
|
||||||
const emptyNode: TreeNode = {
|
const emptyNode: TreeNode = {
|
||||||
@@ -15,6 +24,80 @@ const emptyNode: TreeNode = {
|
|||||||
children: [],
|
children: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const categoryTabs = ["General", "Actor", "LOD", "Misc", "Physics", "Rendering", "Streaming", "All"];
|
||||||
|
|
||||||
|
function DetailValue({ property }: { property: DetailProperty }) {
|
||||||
|
const assetPath = detailAssetPath(property);
|
||||||
|
const [thumbnail, setThumbnail] = React.useState(() => (assetPath ? readCachedThumbnail(assetPath) : ""));
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let isCancelled = false;
|
||||||
|
|
||||||
|
if (!assetPath) {
|
||||||
|
setThumbnail("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = readCachedThumbnail(assetPath);
|
||||||
|
if (cached) {
|
||||||
|
setThumbnail(cached);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
readRemoteObjectThumbnail(assetPath)
|
||||||
|
.then((payload) => {
|
||||||
|
const dataUrl = normalizeThumbnailPayload(payload);
|
||||||
|
if (!isCancelled && dataUrl) {
|
||||||
|
setThumbnail(dataUrl);
|
||||||
|
writeCachedThumbnail(assetPath, dataUrl);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!isCancelled) {
|
||||||
|
setThumbnail("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCancelled = true;
|
||||||
|
};
|
||||||
|
}, [assetPath]);
|
||||||
|
|
||||||
|
if (assetPath) {
|
||||||
|
return (
|
||||||
|
<div className="asset-value">
|
||||||
|
<div className="asset-thumb">{thumbnail ? <img src={thumbnail} alt="" /> : <Box size={28} />}</div>
|
||||||
|
<div className="asset-control">
|
||||||
|
<span>{formatDetailValue(property.value)}</span>
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVectorLikeProperty(property)) {
|
||||||
|
return (
|
||||||
|
<div className="vector-value">
|
||||||
|
{vectorParts(property.value).map((part, index) => (
|
||||||
|
<span className={`axis axis-${index}`} key={part.label}>
|
||||||
|
{part.value}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBooleanProperty(property)) {
|
||||||
|
return <span className={`checkbox-value ${property.value ? "checked" : ""}`} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-value">
|
||||||
|
<span>{formatDetailValue(property.value)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function DetailsPanel({
|
export function DetailsPanel({
|
||||||
node,
|
node,
|
||||||
details,
|
details,
|
||||||
@@ -27,10 +110,13 @@ export function DetailsPanel({
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}) {
|
}) {
|
||||||
const [query, setQuery] = React.useState("");
|
const [query, setQuery] = React.useState("");
|
||||||
|
const [activeCategory, setActiveCategory] = React.useState("All");
|
||||||
|
|
||||||
const groupedProperties = React.useMemo(() => {
|
const groupedProperties = React.useMemo(() => {
|
||||||
const lowerQuery = query.toLowerCase();
|
const lowerQuery = query.toLowerCase();
|
||||||
const visibleProperties = (details?.properties ?? []).filter((property) => {
|
const visibleProperties = (details?.properties ?? []).filter((property) => {
|
||||||
|
const matchesCategory = activeCategory === "All" || property.category.toLowerCase().includes(activeCategory.toLowerCase());
|
||||||
|
if (!matchesCategory) return false;
|
||||||
if (!lowerQuery) return true;
|
if (!lowerQuery) return true;
|
||||||
return [property.displayName, property.name, property.type, property.category, formatDetailValue(property.value)].some((field) =>
|
return [property.displayName, property.name, property.type, property.category, formatDetailValue(property.value)].some((field) =>
|
||||||
field.toLowerCase().includes(lowerQuery),
|
field.toLowerCase().includes(lowerQuery),
|
||||||
@@ -43,7 +129,7 @@ export function DetailsPanel({
|
|||||||
groups[category].push(property);
|
groups[category].push(property);
|
||||||
return groups;
|
return groups;
|
||||||
}, {});
|
}, {});
|
||||||
}, [details, query]);
|
}, [activeCategory, details, query]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="details-panel" aria-label="Details">
|
<aside className="details-panel" aria-label="Details">
|
||||||
@@ -67,16 +153,47 @@ export function DetailsPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="component-list">
|
||||||
|
<div className="component-row selected">
|
||||||
|
<NodeIcon node={node ?? emptyNode} />
|
||||||
|
<span>{node ? `${node.label} (Instance)` : "No Selection"}</span>
|
||||||
|
</div>
|
||||||
|
<div className="component-row child">
|
||||||
|
<Box size={18} />
|
||||||
|
<span>{node?.type ? `${node.type}Component (${node.type}Component0)` : "Component"}</span>
|
||||||
|
<a>Edit in C++</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="details-search-row">
|
<div className="details-search-row">
|
||||||
<label className="details-search">
|
<label className="details-search">
|
||||||
<Search size={18} />
|
<Search size={18} />
|
||||||
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search" />
|
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search" />
|
||||||
</label>
|
</label>
|
||||||
<button className="details-tool" type="button" title="View Options">
|
<button className="details-tool" type="button" title="View Options">
|
||||||
|
<Grid2X2 size={18} />
|
||||||
|
</button>
|
||||||
|
<button className="details-tool" type="button" title="Favorites">
|
||||||
|
<Star size={18} />
|
||||||
|
</button>
|
||||||
|
<button className="details-tool" type="button" title="Settings">
|
||||||
<Settings size={18} />
|
<Settings size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="category-tabs">
|
||||||
|
{categoryTabs.map((category) => (
|
||||||
|
<button
|
||||||
|
className={activeCategory === category ? "active" : ""}
|
||||||
|
key={category}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveCategory(category)}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="details-categories">
|
<div className="details-categories">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="details-empty">Loading properties...</div>
|
<div className="details-empty">Loading properties...</div>
|
||||||
@@ -96,7 +213,12 @@ export function DetailsPanel({
|
|||||||
{properties.map((property) => (
|
{properties.map((property) => (
|
||||||
<div className="detail-row" key={property.name} title={property.description || property.type}>
|
<div className="detail-row" key={property.name} title={property.description || property.type}>
|
||||||
<span className="detail-name">{property.displayName}</span>
|
<span className="detail-name">{property.displayName}</span>
|
||||||
<span className="detail-value">{formatDetailValue(property.value)}</span>
|
<span className="detail-value">
|
||||||
|
<DetailValue property={property} />
|
||||||
|
<button className="reset-button row-reset" type="button" title="Reset to default">
|
||||||
|
<RotateCcw size={15} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -109,3 +109,49 @@ export function formatDetailValue(value: unknown): string {
|
|||||||
|
|
||||||
return JSON.stringify(value);
|
return JSON.stringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function detailAssetPath(property: DetailProperty): string {
|
||||||
|
const valuePath = extractObjectPath(property.value);
|
||||||
|
if (valuePath.startsWith("/Game/") || valuePath.startsWith("/Engine/")) {
|
||||||
|
return valuePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof property.value === "string" && (property.value.startsWith("/Game/") || property.value.startsWith("/Engine/"))) {
|
||||||
|
return property.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBooleanProperty(property: DetailProperty): boolean {
|
||||||
|
return typeof property.value === "boolean" || property.type.toLowerCase().includes("bool");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVectorLikeProperty(property: DetailProperty): boolean {
|
||||||
|
if (!property.value || typeof property.value !== "object" || Array.isArray(property.value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = asRecord(property.value);
|
||||||
|
return ["X", "Y", "Z"].every((key) => key in record) || ["Roll", "Pitch", "Yaw"].every((key) => key in record);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vectorParts(value: unknown): Array<{ label: string; value: string }> {
|
||||||
|
const record = asRecord(value);
|
||||||
|
const keys = ["X", "Y", "Z"].every((key) => key in record) ? ["X", "Y", "Z"] : ["Roll", "Pitch", "Yaw"];
|
||||||
|
return keys.map((key) => ({ label: key, value: formatDetailValue(record[key]) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeThumbnailPayload(payload: unknown): string {
|
||||||
|
if (typeof payload === "string") {
|
||||||
|
return payload.startsWith("data:") ? payload : `data:image/png;base64,${payload}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = asRecord(payload);
|
||||||
|
const value = firstString(record, ["Thumbnail", "thumbnail", "Image", "image", "Data", "data", "Base64", "base64"]);
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.startsWith("data:") ? value : `data:image/png;base64,${value}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -223,3 +223,7 @@ export async function readRemoteObjectProperties(objectPath: string): Promise<un
|
|||||||
access: "READ_ACCESS",
|
access: "READ_ACCESS",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readRemoteObjectThumbnail(objectPath: string): Promise<unknown> {
|
||||||
|
return callRemoteHttpRoute("/remote/object/thumbnail", "PUT", { objectPath });
|
||||||
|
}
|
||||||
|
|||||||
106
src/services/thumbnailCache.ts
Normal file
106
src/services/thumbnailCache.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
const CACHE_PREFIX = "unreal-outliner.thumbnail.v1.";
|
||||||
|
const CACHE_TTL_MS = 60 * 60 * 1000;
|
||||||
|
const MAX_CACHE_ENTRIES = 100;
|
||||||
|
|
||||||
|
type CachedThumbnail = {
|
||||||
|
objectPath: string;
|
||||||
|
savedAt: number;
|
||||||
|
dataUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function cacheKey(objectPath: string): string {
|
||||||
|
return `${CACHE_PREFIX}${objectPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCachedThumbnail(value: unknown): value is CachedThumbnail {
|
||||||
|
if (!value || typeof value !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = value as CachedThumbnail;
|
||||||
|
return typeof candidate.objectPath === "string" && typeof candidate.savedAt === "number" && typeof candidate.dataUrl === "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
function storageAvailable(): boolean {
|
||||||
|
try {
|
||||||
|
const key = `${CACHE_PREFIX}probe`;
|
||||||
|
window.localStorage.setItem(key, "1");
|
||||||
|
window.localStorage.removeItem(key);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readCachedThumbnail(objectPath: string): string {
|
||||||
|
if (!storageAvailable()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(cacheKey(objectPath));
|
||||||
|
if (!raw) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!isCachedThumbnail(parsed) || parsed.objectPath !== objectPath) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() - parsed.savedAt > CACHE_TTL_MS) {
|
||||||
|
window.localStorage.removeItem(cacheKey(objectPath));
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.dataUrl;
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeCachedThumbnail(objectPath: string, dataUrl: string): void {
|
||||||
|
if (!storageAvailable() || !dataUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(cacheKey(objectPath), JSON.stringify({ objectPath, savedAt: Date.now(), dataUrl }));
|
||||||
|
pruneThumbnailCache();
|
||||||
|
} catch {
|
||||||
|
pruneThumbnailCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneThumbnailCache(): void {
|
||||||
|
const entries: Array<{ key: string; savedAt: number }> = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < window.localStorage.length; index += 1) {
|
||||||
|
const key = window.localStorage.key(index);
|
||||||
|
if (!key?.startsWith(CACHE_PREFIX)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(window.localStorage.getItem(key) ?? "");
|
||||||
|
if (isCachedThumbnail(parsed)) {
|
||||||
|
entries.push({ key, savedAt: parsed.savedAt });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
window.localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiredBefore = Date.now() - CACHE_TTL_MS;
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.savedAt < expiredBefore) {
|
||||||
|
window.localStorage.removeItem(entry.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries
|
||||||
|
.filter((entry) => entry.savedAt >= expiredBefore)
|
||||||
|
.sort((a, b) => b.savedAt - a.savedAt)
|
||||||
|
.slice(MAX_CACHE_ENTRIES)
|
||||||
|
.forEach((entry) => window.localStorage.removeItem(entry.key));
|
||||||
|
}
|
||||||
243
src/styles.css
243
src/styles.css
@@ -281,7 +281,7 @@ input {
|
|||||||
.details-panel {
|
.details-panel {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 38px 58px 44px 1fr;
|
grid-template-rows: 38px 48px 118px 44px 40px 1fr;
|
||||||
border-top: 2px solid #0d0d0d;
|
border-top: 2px solid #0d0d0d;
|
||||||
background: #202020;
|
background: #202020;
|
||||||
color: #cfcfcf;
|
color: #cfcfcf;
|
||||||
@@ -323,7 +323,7 @@ input {
|
|||||||
grid-template-columns: 28px 1fr;
|
grid-template-columns: 28px 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 9px 14px;
|
padding: 7px 14px;
|
||||||
border-bottom: 1px solid #171717;
|
border-bottom: 1px solid #171717;
|
||||||
background: #242424;
|
background: #242424;
|
||||||
}
|
}
|
||||||
@@ -339,7 +339,7 @@ input {
|
|||||||
|
|
||||||
.details-object strong {
|
.details-object strong {
|
||||||
color: #f1f1f1;
|
color: #f1f1f1;
|
||||||
font-size: 18px;
|
font-size: 17px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,9 +349,56 @@ input {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.component-list {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 6px 8px 8px;
|
||||||
|
background: #181818;
|
||||||
|
border: 1px solid #242424;
|
||||||
|
border-left: 0;
|
||||||
|
border-right: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-row {
|
||||||
|
height: 34px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 0 10px;
|
||||||
|
color: #d2d2d2;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-row.selected {
|
||||||
|
background: #3d5770;
|
||||||
|
color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-row.child {
|
||||||
|
padding-left: 42px;
|
||||||
|
background: #151515;
|
||||||
|
color: #c9c9c9;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-row span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-row a {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #d9d9d9;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.details-search-row {
|
.details-search-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(140px, 1fr) 34px;
|
grid-template-columns: minmax(140px, 1fr) 34px 34px 34px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
@@ -396,6 +443,38 @@ input {
|
|||||||
background: #363636;
|
background: #363636;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.category-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
background: #262626;
|
||||||
|
border-bottom: 1px solid #141414;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tabs button {
|
||||||
|
height: 29px;
|
||||||
|
min-width: 84px;
|
||||||
|
padding: 0 14px;
|
||||||
|
color: #c7c7c7;
|
||||||
|
background: #2c2c2c;
|
||||||
|
border: 1px solid #151515;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tabs button:hover {
|
||||||
|
background: #343434;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tabs button.active {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #0877d9;
|
||||||
|
border-color: #0a5ea9;
|
||||||
|
}
|
||||||
|
|
||||||
.details-categories {
|
.details-categories {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -420,7 +499,7 @@ input {
|
|||||||
.detail-row {
|
.detail-row {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(180px, 0.45fr) minmax(220px, 0.55fr);
|
grid-template-columns: minmax(180px, 0.48fr) minmax(220px, 0.52fr);
|
||||||
border-bottom: 1px solid #171717;
|
border-bottom: 1px solid #171717;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,11 +518,165 @@ input {
|
|||||||
.detail-name {
|
.detail-name {
|
||||||
color: #c8c8c8;
|
color: #c8c8c8;
|
||||||
border-right: 1px solid #151515;
|
border-right: 1px solid #151515;
|
||||||
|
background: #242424;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-value {
|
.detail-value {
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
background: #242424;
|
background: #242424;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 10px 5px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-value {
|
||||||
|
min-width: 0;
|
||||||
|
width: min(380px, 100%);
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
color: #e5e5e5;
|
||||||
|
background: #101010;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 0 0 0 1px #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-value span,
|
||||||
|
.asset-control span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vector-value {
|
||||||
|
min-width: 0;
|
||||||
|
width: min(420px, 100%);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(64px, 1fr));
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.axis {
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: #f0f0f0;
|
||||||
|
background: #101010;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 0 0 0 1px #050505;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.axis-0 {
|
||||||
|
border-left-color: #df3b1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.axis-1 {
|
||||||
|
border-left-color: #76b900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.axis-2 {
|
||||||
|
border-left-color: #0d7fe8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-value {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border: 1px solid #353535;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #111;
|
||||||
|
box-shadow: inset 0 0 0 1px #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-value.checked::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 13px;
|
||||||
|
height: 7px;
|
||||||
|
margin: 7px 0 0 5px;
|
||||||
|
border-left: 2px solid #d7d7d7;
|
||||||
|
border-bottom: 2px solid #d7d7d7;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-value {
|
||||||
|
min-width: 0;
|
||||||
|
width: min(430px, 100%);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 54px minmax(120px, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-thumb {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #bdbdbd;
|
||||||
|
background:
|
||||||
|
linear-gradient(45deg, #1a1a1a 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, #1a1a1a 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, #1a1a1a 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, #1a1a1a 75%),
|
||||||
|
#101010;
|
||||||
|
background-position:
|
||||||
|
0 0,
|
||||||
|
0 8px,
|
||||||
|
8px -8px,
|
||||||
|
-8px 0;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
border: 1px solid #313131;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: inset 0 -3px 0 #16bfc4;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-thumb img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-control {
|
||||||
|
min-width: 0;
|
||||||
|
height: 31px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 20px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px 0 10px;
|
||||||
|
color: #e5e5e5;
|
||||||
|
background: #101010;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: inset 0 0 0 1px #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #bcbcbc;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button:hover {
|
||||||
|
background: #343434;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-reset {
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details-empty {
|
.details-empty {
|
||||||
|
|||||||
Reference in New Issue
Block a user