2021-11-11 23:37:42 +01:00
|
|
|
<script lang="ts">
|
|
|
|
import filesize from "filesize";
|
2023-06-16 16:30:17 +02:00
|
|
|
import { formatRelative, fromUnixTime, parseISO } from "date-fns";
|
2021-12-21 20:02:47 +01:00
|
|
|
import Ellipsis from "../utils/Ellipsis.svelte";
|
|
|
|
import UpObject from "../display/UpObject.svelte";
|
2022-01-30 16:50:23 +01:00
|
|
|
import { createEventDispatcher } from "svelte";
|
2022-01-09 21:46:52 +01:00
|
|
|
import type { AttributeChange, AttributeUpdate } from "../../types/base";
|
2022-02-06 23:37:05 +01:00
|
|
|
import type { UpEntry, UpListing } from "upend";
|
2021-12-31 00:46:19 +01:00
|
|
|
import IconButton from "../utils/IconButton.svelte";
|
|
|
|
import Selector from "../utils/Selector.svelte";
|
2022-01-09 21:24:49 +01:00
|
|
|
import type { IValue } from "upend/types";
|
|
|
|
import Editable from "../utils/Editable.svelte";
|
2022-02-06 13:41:47 +01:00
|
|
|
import { query } from "../../lib/entity";
|
2022-08-01 21:08:34 +02:00
|
|
|
import { type Readable, readable } from "svelte/store";
|
2022-02-15 22:17:40 +01:00
|
|
|
import { defaultEntitySort, entityValueSort } from "../../util/sort";
|
2022-03-03 10:10:44 +01:00
|
|
|
import { attributeLabels } from "../../util/labels";
|
2022-10-23 18:24:07 +02:00
|
|
|
import { formatDuration } from "../../util/fragments/time";
|
2022-10-25 21:47:17 +02:00
|
|
|
import { i18n } from "../../i18n";
|
2023-01-07 11:00:55 +01:00
|
|
|
import UpLink from "../display/UpLink.svelte";
|
2023-06-24 16:26:14 +02:00
|
|
|
import { ATTR_ADDED, ATTR_LABEL } from "upend/constants";
|
2023-08-28 18:14:06 +02:00
|
|
|
import api from "../../lib/api";
|
|
|
|
import { AddressComponents } from "upend/api";
|
|
|
|
import Icon from "../utils/Icon.svelte";
|
2021-12-02 18:45:29 +01:00
|
|
|
const dispatch = createEventDispatcher();
|
2021-11-11 23:37:42 +01:00
|
|
|
|
2022-02-17 16:02:17 +01:00
|
|
|
export let columns: string | undefined = undefined;
|
2021-12-01 15:39:26 +01:00
|
|
|
export let header = true;
|
2022-02-15 22:38:29 +01:00
|
|
|
|
2022-02-15 22:17:40 +01:00
|
|
|
export let orderByValue = false;
|
2023-06-16 16:30:17 +02:00
|
|
|
export let columnWidths: string[] | undefined = undefined;
|
2021-11-30 22:59:34 +01:00
|
|
|
|
2021-12-19 19:20:09 +01:00
|
|
|
export let entries: UpEntry[];
|
2023-08-28 18:14:06 +02:00
|
|
|
export let attributes: string[] = [];
|
2023-06-16 16:30:17 +02:00
|
|
|
export let attributeOptions: string[] | undefined = undefined;
|
2021-11-11 23:37:42 +01:00
|
|
|
|
2021-12-01 15:39:26 +01:00
|
|
|
// Display
|
2023-06-19 11:53:35 +02:00
|
|
|
$: displayColumns = (columns || "entity, attribute, value")
|
2022-02-15 21:08:07 +01:00
|
|
|
.split(",")
|
|
|
|
.map((c) => c.trim());
|
|
|
|
|
2023-04-23 19:08:44 +02:00
|
|
|
const TIMESTAMP_COL = "timestamp";
|
|
|
|
const PROVENANCE_COL = "provenance";
|
2022-02-15 21:08:07 +01:00
|
|
|
const ENTITY_COL = "entity";
|
|
|
|
const ATTR_COL = "attribute";
|
|
|
|
const VALUE_COL = "value";
|
2021-12-01 15:39:26 +01:00
|
|
|
|
2021-11-30 23:18:39 +01:00
|
|
|
// Editing
|
2021-12-31 00:46:19 +01:00
|
|
|
let newEntryAttribute = "";
|
2022-01-09 21:24:49 +01:00
|
|
|
let newEntryValue: IValue | undefined;
|
2021-11-30 23:18:39 +01:00
|
|
|
|
2021-11-11 23:37:42 +01:00
|
|
|
async function addEntry() {
|
2021-12-02 18:45:29 +01:00
|
|
|
dispatch("change", {
|
|
|
|
type: "create",
|
|
|
|
attribute: newEntryAttribute,
|
|
|
|
value: newEntryValue,
|
|
|
|
} as AttributeChange);
|
|
|
|
newEntryAttribute = "";
|
2022-01-09 21:24:49 +01:00
|
|
|
newEntryValue = undefined;
|
2021-11-11 23:37:42 +01:00
|
|
|
}
|
2022-01-09 21:46:52 +01:00
|
|
|
async function removeEntry(address: string) {
|
2022-10-25 21:47:17 +02:00
|
|
|
if (confirm($i18n.t("Are you sure you want to remove the attribute?"))) {
|
2022-01-09 21:46:52 +01:00
|
|
|
dispatch("change", { type: "delete", address } as AttributeChange);
|
2021-12-02 18:45:29 +01:00
|
|
|
}
|
2021-11-11 23:37:42 +01:00
|
|
|
}
|
2022-01-28 23:24:30 +01:00
|
|
|
async function updateEntry(
|
|
|
|
address: string,
|
|
|
|
attribute: string,
|
2023-08-25 23:35:29 +02:00
|
|
|
value: IValue,
|
2022-01-28 23:24:30 +01:00
|
|
|
) {
|
2021-12-02 18:45:29 +01:00
|
|
|
dispatch("change", {
|
|
|
|
type: "update",
|
2022-01-09 21:46:52 +01:00
|
|
|
address,
|
2021-12-02 18:45:29 +01:00
|
|
|
attribute,
|
|
|
|
value,
|
2022-01-09 21:46:52 +01:00
|
|
|
} as AttributeUpdate);
|
2021-11-11 23:37:42 +01:00
|
|
|
}
|
|
|
|
|
2022-02-06 13:41:47 +01:00
|
|
|
// Labelling
|
2022-02-06 23:37:05 +01:00
|
|
|
let labelListing: Readable<UpListing> = readable(undefined);
|
2022-02-06 14:39:39 +01:00
|
|
|
$: {
|
|
|
|
const addresses = [];
|
|
|
|
entries
|
|
|
|
.flatMap((e) =>
|
2023-08-25 23:35:29 +02:00
|
|
|
e.value.t === "Address" ? [e.entity, e.value.c] : [e.entity],
|
2022-02-06 14:39:39 +01:00
|
|
|
)
|
|
|
|
.forEach((addr) => {
|
|
|
|
if (!addresses.includes(addr)) {
|
|
|
|
addresses.push(addr);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-03-30 10:20:27 +02:00
|
|
|
const addressesString = addresses.map((addr) => `@${addr}`).join(" ");
|
2022-02-06 14:39:39 +01:00
|
|
|
|
2023-06-24 16:26:14 +02:00
|
|
|
labelListing = query(
|
2023-08-25 23:35:29 +02:00
|
|
|
`(matches (in ${addressesString}) "${ATTR_LABEL}" ? )`,
|
2023-06-24 16:26:14 +02:00
|
|
|
).result;
|
2022-02-06 14:39:39 +01:00
|
|
|
}
|
2022-02-06 13:41:47 +01:00
|
|
|
|
2021-11-30 23:18:39 +01:00
|
|
|
// Sorting
|
2023-04-23 16:37:26 +02:00
|
|
|
let sortedEntries = entries;
|
2021-12-20 14:04:44 +01:00
|
|
|
|
2021-12-20 13:22:30 +01:00
|
|
|
let sortKeys: { [key: string]: string[] } = {};
|
2023-07-29 19:24:08 +02:00
|
|
|
function addSortKeys(key: string, vals: string[], resort: boolean) {
|
2021-12-20 13:22:30 +01:00
|
|
|
if (!sortKeys[key]) {
|
|
|
|
sortKeys[key] = [];
|
|
|
|
}
|
2021-12-20 14:11:53 +01:00
|
|
|
let changed = false;
|
2021-12-20 13:22:30 +01:00
|
|
|
vals.forEach((val) => {
|
|
|
|
if (!sortKeys[key].includes(val)) {
|
2021-12-20 14:11:53 +01:00
|
|
|
changed = true;
|
2021-12-20 13:22:30 +01:00
|
|
|
sortKeys[key].push(val);
|
|
|
|
}
|
|
|
|
});
|
2021-12-20 14:11:53 +01:00
|
|
|
|
2023-04-23 16:37:26 +02:00
|
|
|
if (resort && changed) sortEntries();
|
2021-12-20 13:22:30 +01:00
|
|
|
}
|
|
|
|
|
2023-04-23 16:37:26 +02:00
|
|
|
function sortEntries() {
|
|
|
|
sortedEntries = orderByValue
|
2023-04-23 16:42:52 +02:00
|
|
|
? entityValueSort(entries, Object.assign(sortKeys, $attributeLabels))
|
|
|
|
: defaultEntitySort(entries, Object.assign(sortKeys, $attributeLabels));
|
2021-12-20 14:04:44 +01:00
|
|
|
}
|
|
|
|
|
2022-02-06 23:37:05 +01:00
|
|
|
$: {
|
|
|
|
if ($labelListing) {
|
|
|
|
entries.forEach((entry) => {
|
|
|
|
addSortKeys(
|
|
|
|
entry.entity,
|
2023-07-29 19:24:08 +02:00
|
|
|
$labelListing.getObject(entry.entity).identify(),
|
2023-08-25 23:35:29 +02:00
|
|
|
false,
|
2022-02-06 23:37:05 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
if (entry.value.t === "Address") {
|
|
|
|
addSortKeys(
|
|
|
|
entry.value.c,
|
2023-07-29 19:24:08 +02:00
|
|
|
$labelListing.getObject(String(entry.value.c)).identify(),
|
2023-08-25 23:35:29 +02:00
|
|
|
false,
|
2022-02-06 23:37:05 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
2023-04-23 16:37:26 +02:00
|
|
|
sortEntries();
|
2022-02-06 23:37:05 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-20 14:04:44 +01:00
|
|
|
entries.forEach((entry) => {
|
2023-07-29 19:24:08 +02:00
|
|
|
addSortKeys(
|
|
|
|
entry.entity,
|
|
|
|
entry.listing.getObject(entry.entity).identify(),
|
2023-08-25 23:35:29 +02:00
|
|
|
false,
|
2023-07-29 19:24:08 +02:00
|
|
|
);
|
2021-12-20 14:04:44 +01:00
|
|
|
if (entry.value.t === "Address") {
|
|
|
|
addSortKeys(
|
2022-01-28 20:51:34 +01:00
|
|
|
entry.value.c,
|
2023-07-29 19:24:08 +02:00
|
|
|
entry.listing.getObject(String(entry.value.c)).identify(),
|
2023-08-25 23:35:29 +02:00
|
|
|
false,
|
2021-12-20 14:04:44 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
2023-07-29 19:24:08 +02:00
|
|
|
sortEntries();
|
2021-11-30 23:18:39 +01:00
|
|
|
|
2023-07-29 16:45:29 +02:00
|
|
|
// Visibility
|
|
|
|
let visible: Set<string> = new Set();
|
2023-07-29 19:24:08 +02:00
|
|
|
let observer = new IntersectionObserver((intersections) => {
|
|
|
|
intersections.forEach((intersection) => {
|
|
|
|
const address = (intersection.target as HTMLElement).dataset["address"];
|
|
|
|
if (!address) {
|
|
|
|
console.warn("Intersected wrong element?");
|
|
|
|
return;
|
|
|
|
}
|
2023-07-29 16:45:29 +02:00
|
|
|
|
2023-07-29 19:24:08 +02:00
|
|
|
if (intersection.isIntersecting) {
|
|
|
|
visible.add(address);
|
|
|
|
}
|
|
|
|
visible = visible;
|
2023-07-29 16:45:29 +02:00
|
|
|
});
|
2023-07-29 19:24:08 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
function observe(node: HTMLElement) {
|
|
|
|
observer.observe(node);
|
2023-07-29 16:45:29 +02:00
|
|
|
|
2023-07-29 19:24:08 +02:00
|
|
|
return {
|
|
|
|
destroy() {
|
|
|
|
observer.unobserve(node);
|
|
|
|
},
|
|
|
|
};
|
2023-07-29 16:45:29 +02:00
|
|
|
}
|
|
|
|
|
2021-11-30 23:18:39 +01:00
|
|
|
// Formatting & Display
|
2022-02-15 21:08:07 +01:00
|
|
|
const COLUMN_LABELS: { [key: string]: string } = {
|
2023-04-23 19:08:44 +02:00
|
|
|
timestamp: $i18n.t("Added at"),
|
|
|
|
provenance: $i18n.t("Provenance"),
|
2022-10-25 21:47:17 +02:00
|
|
|
entity: $i18n.t("Entity"),
|
|
|
|
attribute: $i18n.t("Attribute"),
|
|
|
|
value: $i18n.t("Value"),
|
2022-02-15 21:08:07 +01:00
|
|
|
};
|
|
|
|
|
2022-01-08 19:05:01 +01:00
|
|
|
function formatValue(value: string | number, attribute: string): string {
|
2022-02-15 21:39:28 +01:00
|
|
|
switch (attribute) {
|
|
|
|
case "FILE_SIZE":
|
|
|
|
return filesize(parseInt(String(value), 10), { base: 2 });
|
2023-06-24 16:26:14 +02:00
|
|
|
case ATTR_ADDED:
|
2022-02-15 21:39:28 +01:00
|
|
|
case "LAST_VISITED":
|
2022-02-15 21:53:34 +01:00
|
|
|
return formatRelative(
|
|
|
|
fromUnixTime(parseInt(String(value), 10)),
|
2023-08-25 23:35:29 +02:00
|
|
|
new Date(),
|
2022-02-15 21:53:34 +01:00
|
|
|
);
|
2022-03-24 12:32:49 +01:00
|
|
|
case "NUM_VISITED":
|
|
|
|
return `${value} times`;
|
2022-10-23 18:24:07 +02:00
|
|
|
case "MEDIA_DURATION":
|
|
|
|
return formatDuration(parseInt(String(value), 10));
|
2022-02-15 21:39:28 +01:00
|
|
|
default:
|
|
|
|
return String(value);
|
2021-11-11 23:37:42 +01:00
|
|
|
}
|
2022-01-08 19:05:01 +01:00
|
|
|
}
|
2023-08-28 18:14:06 +02:00
|
|
|
|
|
|
|
// Unused attributes
|
|
|
|
let unusedAttributes = [];
|
|
|
|
|
|
|
|
$: (async () => {
|
|
|
|
unusedAttributes = await Promise.all(
|
|
|
|
attributes
|
|
|
|
.filter((attr) => !entries.some((entry) => entry.attribute === attr))
|
|
|
|
.map(async (attribute) => {
|
|
|
|
const components = new AddressComponents("Attribute", attribute);
|
|
|
|
return await api.componentsToAddress(components);
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
})();
|
2021-11-11 23:37:42 +01:00
|
|
|
</script>
|
|
|
|
|
2023-07-29 12:29:53 +02:00
|
|
|
<div class="container">
|
2023-08-25 23:35:29 +02:00
|
|
|
<table>
|
2023-07-29 12:29:53 +02:00
|
|
|
<colgroup>
|
|
|
|
{#each displayColumns as column, idx}
|
|
|
|
{#if columnWidths?.length}
|
|
|
|
<col
|
|
|
|
class="{column}-col"
|
|
|
|
style="width: {columnWidths[idx] || 'unset'}"
|
|
|
|
/>
|
|
|
|
{:else}
|
|
|
|
<col class="{column}-col" />
|
|
|
|
{/if}
|
2022-02-15 21:08:07 +01:00
|
|
|
{/each}
|
2023-08-28 18:14:06 +02:00
|
|
|
<col class="action-col" />
|
2023-07-29 12:29:53 +02:00
|
|
|
</colgroup>
|
2021-11-30 22:59:34 +01:00
|
|
|
|
2023-07-29 12:29:53 +02:00
|
|
|
{#if header}
|
|
|
|
<tr>
|
|
|
|
{#each displayColumns as column}
|
|
|
|
<th>{COLUMN_LABELS[column] || $attributeLabels[column] || column}</th>
|
|
|
|
{/each}
|
2023-08-28 18:14:06 +02:00
|
|
|
<th />
|
2023-07-29 12:29:53 +02:00
|
|
|
</tr>
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
{#each sortedEntries as entry (entry.address)}
|
2023-07-29 19:24:08 +02:00
|
|
|
<tr data-address={entry.address} use:observe>
|
2023-07-29 16:45:29 +02:00
|
|
|
{#if visible.has(entry.address)}
|
|
|
|
{#each displayColumns as column}
|
|
|
|
{#if column == TIMESTAMP_COL}
|
|
|
|
<td title={entry.timestamp}
|
|
|
|
>{formatRelative(parseISO(entry.timestamp), new Date())}</td
|
|
|
|
>
|
|
|
|
{:else if column == PROVENANCE_COL}
|
|
|
|
<td>{entry.provenance}</td>
|
|
|
|
{:else if column == ENTITY_COL}
|
2023-07-30 16:00:03 +02:00
|
|
|
<td class="entity mark-entity">
|
2023-07-29 16:45:29 +02:00
|
|
|
<UpObject
|
|
|
|
link
|
|
|
|
labels={$labelListing
|
|
|
|
?.getObject(String(entry.entity))
|
|
|
|
?.identify() || []}
|
|
|
|
address={entry.entity}
|
|
|
|
on:resolved={(event) => {
|
2023-07-29 19:24:08 +02:00
|
|
|
addSortKeys(entry.entity, event.detail, true);
|
2023-07-29 16:45:29 +02:00
|
|
|
}}
|
2021-12-11 21:37:12 +01:00
|
|
|
/>
|
2023-07-29 16:45:29 +02:00
|
|
|
</td>
|
|
|
|
{:else if column == ATTR_COL}
|
|
|
|
<td
|
|
|
|
class:formatted={Boolean(
|
2023-08-25 23:35:29 +02:00
|
|
|
Object.keys($attributeLabels).includes(entry.attribute),
|
2023-07-29 16:45:29 +02:00
|
|
|
)}
|
2023-07-30 16:00:03 +02:00
|
|
|
class="mark-attribute"
|
2023-07-29 12:29:53 +02:00
|
|
|
>
|
2023-07-29 16:45:29 +02:00
|
|
|
<UpLink to={{ attribute: entry.attribute }}>
|
|
|
|
<Ellipsis
|
|
|
|
value={$attributeLabels[entry.attribute] || entry.attribute}
|
|
|
|
title={$attributeLabels[entry.attribute]
|
|
|
|
? `${$attributeLabels[entry.attribute]} (${
|
|
|
|
entry.attribute
|
|
|
|
})`
|
|
|
|
: entry.attribute}
|
2022-02-15 21:08:07 +01:00
|
|
|
/>
|
2023-07-29 16:45:29 +02:00
|
|
|
</UpLink>
|
|
|
|
</td>
|
|
|
|
{:else if column == VALUE_COL}
|
2023-07-30 16:00:03 +02:00
|
|
|
<td class="value mark-value">
|
2023-07-29 16:45:29 +02:00
|
|
|
<Editable
|
|
|
|
attribute={entry.attribute}
|
|
|
|
value={entry.value}
|
|
|
|
on:edit={(ev) =>
|
|
|
|
updateEntry(entry.address, entry.attribute, ev.detail)}
|
|
|
|
>
|
|
|
|
{#if entry.value.t === "Address"}
|
|
|
|
<UpObject
|
|
|
|
link
|
|
|
|
address={String(entry.value.c)}
|
|
|
|
labels={$labelListing
|
|
|
|
?.getObject(String(entry.value.c))
|
|
|
|
?.identify() || []}
|
|
|
|
on:resolved={(event) => {
|
2023-07-29 19:24:08 +02:00
|
|
|
addSortKeys(String(entry.value.c), event.detail, true);
|
2023-07-29 16:45:29 +02:00
|
|
|
}}
|
2023-07-29 12:29:53 +02:00
|
|
|
/>
|
2023-07-29 16:45:29 +02:00
|
|
|
{:else}
|
|
|
|
<div
|
|
|
|
class:formatted={Boolean(
|
2023-08-25 23:35:29 +02:00
|
|
|
formatValue(entry.value.c, entry.attribute),
|
2023-07-29 16:45:29 +02:00
|
|
|
)}
|
|
|
|
>
|
|
|
|
<Ellipsis
|
|
|
|
value={formatValue(entry.value.c, entry.attribute) ||
|
|
|
|
String(entry.value.c)}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
</Editable>
|
|
|
|
</td>
|
|
|
|
{:else}
|
|
|
|
<td>?</td>
|
|
|
|
{/if}
|
|
|
|
{/each}
|
2023-08-28 18:14:06 +02:00
|
|
|
<td class="attr-action">
|
|
|
|
<IconButton
|
|
|
|
subdued
|
|
|
|
name="x-circle"
|
|
|
|
on:click={() => removeEntry(entry.address)}
|
|
|
|
/>
|
|
|
|
</td>
|
2023-07-29 16:45:29 +02:00
|
|
|
{:else}
|
|
|
|
<tr>
|
|
|
|
<td colspan="99">
|
|
|
|
<div class="skeleton" style="text-align: center">...</div>
|
2023-07-29 12:29:53 +02:00
|
|
|
</td>
|
2023-07-29 16:45:29 +02:00
|
|
|
</tr>
|
|
|
|
{/if}
|
2023-07-29 12:29:53 +02:00
|
|
|
</tr>
|
|
|
|
{/each}
|
|
|
|
|
2023-08-28 18:14:06 +02:00
|
|
|
{#each unusedAttributes as attributeAddress}
|
|
|
|
<tr>
|
|
|
|
{#each displayColumns as column}
|
|
|
|
{#if column == ATTR_COL}
|
|
|
|
<td>
|
|
|
|
<UpObject link address={attributeAddress} />
|
|
|
|
</td>
|
|
|
|
{:else if column == VALUE_COL}
|
|
|
|
<!-- <Selector type="value" bind:value={newEntryValue} /> -->
|
|
|
|
<td class="unset">{$i18n.t("(unset)")}</td>
|
|
|
|
{:else}
|
|
|
|
<td></td>
|
|
|
|
{/if}
|
|
|
|
{/each}
|
|
|
|
<td class="attr-action"></td>
|
|
|
|
</tr>
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
{#if !attributes.length}
|
2023-07-29 12:29:53 +02:00
|
|
|
<tr class="add-row">
|
|
|
|
{#if displayColumns.includes(ATTR_COL)}
|
|
|
|
<td>
|
|
|
|
<Selector
|
|
|
|
type="attribute"
|
|
|
|
bind:attribute={newEntryAttribute}
|
|
|
|
attributeOptions={attributeOptions || []}
|
|
|
|
/>
|
2022-02-15 21:08:07 +01:00
|
|
|
</td>
|
|
|
|
{/if}
|
2023-07-29 12:29:53 +02:00
|
|
|
{#if displayColumns.includes(VALUE_COL)}
|
|
|
|
<td>
|
|
|
|
<Selector type="value" bind:value={newEntryValue} />
|
|
|
|
</td>
|
|
|
|
{/if}
|
2023-08-28 18:14:06 +02:00
|
|
|
<td class="attr-action">
|
|
|
|
<IconButton name="plus-circle" on:click={addEntry} />
|
|
|
|
</td>
|
2023-07-29 12:29:53 +02:00
|
|
|
</tr>
|
|
|
|
{/if}
|
|
|
|
</table>
|
|
|
|
</div>
|
2021-11-11 23:37:42 +01:00
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2023-07-29 12:29:53 +02:00
|
|
|
.container {
|
2023-08-27 12:20:56 +02:00
|
|
|
overflow: hidden;
|
2023-07-29 12:29:53 +02:00
|
|
|
}
|
|
|
|
|
2021-11-11 23:37:42 +01:00
|
|
|
table {
|
|
|
|
table-layout: fixed;
|
|
|
|
|
2023-07-29 12:29:53 +02:00
|
|
|
// gaps between columns, but hug the edges
|
|
|
|
width: calc(100% + 1em);
|
|
|
|
margin-left: -0.5em;
|
2021-12-06 14:35:18 +01:00
|
|
|
border-spacing: 0.5em 0;
|
2021-12-05 13:29:06 +01:00
|
|
|
|
2021-11-11 23:37:42 +01:00
|
|
|
th {
|
|
|
|
text-align: left;
|
|
|
|
}
|
|
|
|
|
|
|
|
td {
|
|
|
|
font-family: var(--monospace-font);
|
|
|
|
line-break: anywhere;
|
|
|
|
|
2021-12-06 15:24:00 +01:00
|
|
|
border-radius: 4px;
|
|
|
|
padding: 2px;
|
|
|
|
|
2021-11-11 23:37:42 +01:00
|
|
|
&.attr-action {
|
|
|
|
max-width: 1em;
|
|
|
|
}
|
|
|
|
|
2021-12-01 16:04:23 +01:00
|
|
|
&.formatted,
|
|
|
|
.formatted {
|
2021-11-11 23:37:42 +01:00
|
|
|
font-family: var(--default-font);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-15 21:08:07 +01:00
|
|
|
.action-col {
|
2021-11-11 23:37:42 +01:00
|
|
|
width: 1.5em;
|
|
|
|
}
|
|
|
|
|
2022-02-15 21:08:07 +01:00
|
|
|
.attribute-col {
|
2021-11-11 23:37:42 +01:00
|
|
|
width: 33%;
|
|
|
|
}
|
2023-08-28 18:14:06 +02:00
|
|
|
|
|
|
|
.unset {
|
|
|
|
opacity: 0.66;
|
|
|
|
pointer-events: none;
|
|
|
|
}
|
2021-11-11 23:37:42 +01:00
|
|
|
}
|
|
|
|
</style>
|