Make Huginn graph first
This commit is contained in:
parent
75b8770f24
commit
6098b3ffd1
2 changed files with 550 additions and 79 deletions
|
|
@ -89,7 +89,11 @@ function HuginnApp() {
|
|||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
setInspection(inspectCultCacheBytes(file.name, bytes, file.size));
|
||||
setSelection({ record: 0, catalog: 0 });
|
||||
setGraphSelection(null);
|
||||
setGraphSelection({
|
||||
kind: "node",
|
||||
graphKey: "dataflow",
|
||||
nodeId: "store",
|
||||
});
|
||||
setErrorMessage("");
|
||||
} catch (error) {
|
||||
setInspection(undefined);
|
||||
|
|
@ -120,49 +124,81 @@ function HuginnApp() {
|
|||
}}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<section className="sidebar">
|
||||
<div className="brand">
|
||||
<img src={assetPath("hugin-64.png")} alt="" width="64" height="64" />
|
||||
<div>
|
||||
<h1>Huginn</h1>
|
||||
<p>CultCache state inspection for <code>.cc</code> files.</p>
|
||||
</div>
|
||||
</div>
|
||||
<label className="dropzone">
|
||||
<input
|
||||
type="file"
|
||||
accept=".cc,.msgpack,.mpack,application/octet-stream"
|
||||
onChange={(event) => {
|
||||
const file = event.currentTarget.files?.item(0);
|
||||
if (file) {
|
||||
void inspectFile(file);
|
||||
}
|
||||
}}
|
||||
{inspection
|
||||
? (
|
||||
<InspectionView
|
||||
errorMessage={errorMessage}
|
||||
inspection={inspection}
|
||||
graphProjection={graphProjection}
|
||||
graphSelection={graphSelection}
|
||||
setGraphSelection={setGraphSelection}
|
||||
selection={selection}
|
||||
setSelection={setSelection}
|
||||
onInspectFile={inspectFile}
|
||||
/>
|
||||
<span className="drop-title">Drop CultCache file</span>
|
||||
<span className="drop-copy">or choose one from disk</span>
|
||||
</label>
|
||||
{errorMessage ? <div className="error">{errorMessage}</div> : null}
|
||||
{inspection ? <Summary inspection={inspection} /> : <EmptySummary />}
|
||||
</section>
|
||||
<section className="content">
|
||||
{inspection
|
||||
? (
|
||||
<InspectionView
|
||||
inspection={inspection}
|
||||
graphProjection={graphProjection}
|
||||
graphSelection={graphSelection}
|
||||
setGraphSelection={setGraphSelection}
|
||||
selection={selection}
|
||||
setSelection={setSelection}
|
||||
/>
|
||||
)
|
||||
: <NoFile />}
|
||||
</section>
|
||||
)
|
||||
: (
|
||||
<EmptyWorkspace
|
||||
errorMessage={errorMessage}
|
||||
onInspectFile={inspectFile}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyWorkspace({
|
||||
errorMessage,
|
||||
onInspectFile,
|
||||
}: {
|
||||
errorMessage: string;
|
||||
onInspectFile: (file: File) => Promise<void>;
|
||||
}) {
|
||||
return (
|
||||
<section className="empty-workspace">
|
||||
<HuginnFieldCanvas imageUrl={assetPath("hugin.png")} />
|
||||
<section className="floating-panel brand-panel is-primary">
|
||||
<BrandBlock />
|
||||
<FilePicker onInspectFile={onInspectFile} />
|
||||
{errorMessage ? <div className="error">{errorMessage}</div> : null}
|
||||
<EmptySummary />
|
||||
</section>
|
||||
<NoFile />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function BrandBlock() {
|
||||
return (
|
||||
<div className="brand">
|
||||
<img src={assetPath("hugin-64.png")} alt="" width="64" height="64" />
|
||||
<div>
|
||||
<h1>Huginn</h1>
|
||||
<p>CultCache state inspection for <code>.cc</code> files.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FilePicker({ onInspectFile }: { onInspectFile: (file: File) => Promise<void> }) {
|
||||
return (
|
||||
<label className="dropzone">
|
||||
<input
|
||||
type="file"
|
||||
accept=".cc,.msgpack,.mpack,application/octet-stream"
|
||||
onChange={(event) => {
|
||||
const file = event.currentTarget.files?.item(0);
|
||||
if (file) {
|
||||
void onInspectFile(file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="drop-title">Drop CultCache file</span>
|
||||
<span className="drop-copy">or choose one from disk</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function Summary({ inspection }: { inspection: CultCacheInspection }) {
|
||||
return (
|
||||
<div className="summary">
|
||||
|
|
@ -205,22 +241,42 @@ function NoFile() {
|
|||
}
|
||||
|
||||
function InspectionView({
|
||||
errorMessage,
|
||||
inspection,
|
||||
graphProjection,
|
||||
graphSelection,
|
||||
setGraphSelection,
|
||||
selection,
|
||||
setSelection,
|
||||
onInspectFile,
|
||||
}: {
|
||||
errorMessage: string;
|
||||
inspection: CultCacheInspection;
|
||||
graphProjection: GraphProjection;
|
||||
graphSelection: ViewerSelection | null;
|
||||
setGraphSelection: (selection: ViewerSelection | null) => void;
|
||||
selection: Selection;
|
||||
setSelection: (selection: Selection) => void;
|
||||
onInspectFile: (file: File) => Promise<void>;
|
||||
}) {
|
||||
const record = inspection.records[selection.record];
|
||||
const catalogEntry = inspection.catalog[selection.catalog];
|
||||
const expandedNode = graphSelection?.kind === "node"
|
||||
? {
|
||||
graphKey: graphSelection.graphKey,
|
||||
nodeId: graphSelection.nodeId,
|
||||
className: "huginn-expanded-node",
|
||||
ariaLabel: "Selected CultCache node",
|
||||
content: (
|
||||
<ExpandedNodePanel
|
||||
catalogEntry={catalogEntry}
|
||||
inspection={inspection}
|
||||
record={record}
|
||||
selection={selection}
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
const selectRaw = (next: RawSelection) => {
|
||||
if (next.kind === "record") {
|
||||
setSelection({ ...selection, record: next.index });
|
||||
|
|
@ -258,9 +314,10 @@ function InspectionView({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="inspector-stack">
|
||||
<div className="inspector-stage">
|
||||
<section className="graph-panel">
|
||||
<EpiphanyGraphViewer
|
||||
className="huginn-graph-shell"
|
||||
state={graphProjection.state}
|
||||
initialGraph="dataflow"
|
||||
selection={graphSelection}
|
||||
|
|
@ -270,8 +327,11 @@ function InspectionView({
|
|||
viewportBackground="#03070a"
|
||||
overlayPanels
|
||||
showSidebar={false}
|
||||
focusSelection
|
||||
selectionFocusMode="preview"
|
||||
expandedNode={expandedNode}
|
||||
graphLabels={{ architecture: "File", dataflow: "Payload" }}
|
||||
style={{ minHeight: 620 }}
|
||||
style={{ minHeight: "100vh" }}
|
||||
/>
|
||||
{graphProjection.truncatedValueNodes > 0 ? (
|
||||
<div className="graph-warning">
|
||||
|
|
@ -279,14 +339,20 @@ function InspectionView({
|
|||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
<div className="columns">
|
||||
<section className="panel">
|
||||
<header><h2>Records</h2><span>{inspection.records.length}</span></header>
|
||||
<div className="overlay-layer">
|
||||
<section className="floating-panel brand-panel">
|
||||
<BrandBlock />
|
||||
<FilePicker onInspectFile={onInspectFile} />
|
||||
{errorMessage ? <div className="error">{errorMessage}</div> : null}
|
||||
<Summary inspection={inspection} />
|
||||
</section>
|
||||
<section className="floating-panel data-panel records-panel">
|
||||
<PanelHeader title="Records" count={inspection.records.length.toString()} />
|
||||
<div className="list">
|
||||
{inspection.records.length
|
||||
? inspection.records.map((entry, index) => (
|
||||
<RecordButton
|
||||
key={`${entry.schemaId}:${entry.key}`}
|
||||
key={`${entry.schemaId}:${entry.key}:${index}`}
|
||||
record={entry}
|
||||
index={index}
|
||||
selected={index === selection.record}
|
||||
|
|
@ -296,14 +362,8 @@ function InspectionView({
|
|||
: <div className="empty">No records</div>}
|
||||
</div>
|
||||
</section>
|
||||
<section className="panel detail">
|
||||
<header><h2>Record Detail</h2><span>{inspection.filePath}</span></header>
|
||||
{record ? <RecordDetail record={record} /> : <div className="empty">No record selected</div>}
|
||||
</section>
|
||||
</div>
|
||||
<div className="columns bottom">
|
||||
<section className="panel">
|
||||
<header><h2>Schema Catalog</h2><span>{inspection.catalog.length}</span></header>
|
||||
<section className="floating-panel data-panel catalog-panel">
|
||||
<PanelHeader title="Schema Catalog" count={inspection.catalog.length.toString()} />
|
||||
<div className="list">
|
||||
{inspection.catalog.length
|
||||
? inspection.catalog.map((entry, index) => (
|
||||
|
|
@ -318,15 +378,82 @@ function InspectionView({
|
|||
: <div className="empty">No schema catalog</div>}
|
||||
</div>
|
||||
</section>
|
||||
<section className="panel detail">
|
||||
<header><h2>Catalog Entry</h2><span>{catalogEntry?.schemaVersion ?? ""}</span></header>
|
||||
{catalogEntry ? <CatalogDetail entry={catalogEntry} /> : <div className="empty">No catalog entry selected</div>}
|
||||
<section className="floating-panel detail-panel">
|
||||
<PanelHeader title="Selected Raw View" count={record?.key ?? catalogEntry?.schemaVersion ?? ""} />
|
||||
<div className="detail-tabs">
|
||||
<section>
|
||||
<h3>Record</h3>
|
||||
{record ? <RecordDetail record={record} /> : <div className="empty">No record selected</div>}
|
||||
</section>
|
||||
<section>
|
||||
<h3>Schema</h3>
|
||||
{catalogEntry ? <CatalogDetail entry={catalogEntry} /> : <div className="empty">No catalog entry selected</div>}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PanelHeader({ title, count }: { title: string; count: string }) {
|
||||
return (
|
||||
<header>
|
||||
<h2>{title}</h2>
|
||||
<span>{count}</span>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedNodePanel({
|
||||
catalogEntry,
|
||||
inspection,
|
||||
record,
|
||||
selection,
|
||||
}: {
|
||||
catalogEntry: InspectedCatalogEntry | undefined;
|
||||
inspection: CultCacheInspection;
|
||||
record: InspectedRecord | undefined;
|
||||
selection: Selection;
|
||||
}) {
|
||||
return (
|
||||
<article className="expanded-content">
|
||||
<div className="expanded-kicker">CultCache Node</div>
|
||||
<header className="expanded-header">
|
||||
<h1>{record?.key ?? catalogEntry?.schemaName ?? inspection.filePath}</h1>
|
||||
<div className="expanded-chips">
|
||||
<span>{inspection.format}</span>
|
||||
<span>{inspection.records.length} records</span>
|
||||
<span>{inspection.catalog.length} schemas</span>
|
||||
</div>
|
||||
</header>
|
||||
<section className="expanded-summary">
|
||||
{record ? (
|
||||
<>
|
||||
<p>{record.schemaName} record stored at {record.storedAt} with {record.payloadBytes} payload bytes.</p>
|
||||
{record.payloadDecodeError ? <div className="error">{record.payloadDecodeError}</div> : null}
|
||||
</>
|
||||
) : (
|
||||
<p>{inspection.filePath} contains a schema catalog and persisted MessagePack record set.</p>
|
||||
)}
|
||||
</section>
|
||||
<section className="expanded-article">
|
||||
<div>
|
||||
<h2>Record Payload</h2>
|
||||
{record ? <RecordDetail record={record} /> : <div className="empty">Select a record node to inspect payload data.</div>}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Schema Entry</h2>
|
||||
{catalogEntry ? <CatalogDetail entry={catalogEntry} /> : <div className="empty">Select a schema node to inspect catalog data.</div>}
|
||||
</div>
|
||||
</section>
|
||||
<footer className="expanded-footer">
|
||||
Raw selection: record {selection.record + 1}, schema {selection.catalog + 1}
|
||||
</footer>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function RecordButton({
|
||||
record,
|
||||
index,
|
||||
|
|
@ -341,7 +468,7 @@ function RecordButton({
|
|||
return (
|
||||
<button className="row" type="button" data-record={index} aria-selected={selected} onClick={onSelect}>
|
||||
<strong>{record.key}</strong>
|
||||
<span>{record.schemaName} · {record.payloadBytes} bytes</span>
|
||||
<span>{record.schemaName} - {record.payloadBytes} bytes</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ body {
|
|||
margin: 0;
|
||||
min-width: 760px;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button,
|
||||
|
|
@ -40,8 +41,8 @@ code {
|
|||
}
|
||||
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-columns: 330px minmax(0, 1fr);
|
||||
position: relative;
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at 78% 18%, rgba(39, 202, 239, 0.12), transparent 30%),
|
||||
|
|
@ -150,22 +151,45 @@ code {
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
.inspector-stack {
|
||||
.empty-workspace,
|
||||
.inspector-stage {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background: #03070a;
|
||||
}
|
||||
|
||||
.empty-workspace {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
place-items: center;
|
||||
padding: clamp(18px, 4vw, 56px);
|
||||
}
|
||||
|
||||
.empty-workspace .huginn-field-canvas {
|
||||
opacity: 0.46;
|
||||
}
|
||||
|
||||
.overlay-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 12;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.graph-panel {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
background: #03070a;
|
||||
box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.graph-panel > section {
|
||||
min-height: 620px !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
|
||||
.huginn-field-canvas {
|
||||
|
|
@ -185,8 +209,98 @@ code {
|
|||
box-shadow: 0 0 28px rgba(51, 230, 243, 0.08), 0 18px 48px rgba(0, 0, 0, 0.28) !important;
|
||||
}
|
||||
|
||||
.huginn-graph-shell {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.huginn-graph-shell .epiphany-graph-overlay-panel,
|
||||
.floating-panel {
|
||||
opacity: 0.28;
|
||||
transition:
|
||||
opacity 160ms ease,
|
||||
transform 160ms ease,
|
||||
box-shadow 160ms ease,
|
||||
background-color 160ms ease;
|
||||
}
|
||||
|
||||
.huginn-graph-shell .epiphany-graph-overlay-panel:hover,
|
||||
.huginn-graph-shell .epiphany-graph-overlay-panel:focus-within,
|
||||
.floating-panel:hover,
|
||||
.floating-panel:focus-within,
|
||||
.floating-panel.is-primary {
|
||||
opacity: 1;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.floating-panel {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
container-type: inline-size;
|
||||
border: 1px solid rgba(51, 230, 243, 0.22);
|
||||
border-radius: 24px;
|
||||
background: rgba(4, 11, 16, 0.72);
|
||||
padding: clamp(10px, 1.4vw, 18px);
|
||||
box-shadow: 0 20px 54px rgba(0, 0, 0, 0.34), 0 0 28px rgba(51, 230, 243, 0.08);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.brand-panel {
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
width: min(330px, calc(100vw - 32px));
|
||||
transform: translateX(-242px);
|
||||
}
|
||||
|
||||
.brand-panel:hover,
|
||||
.brand-panel:focus-within,
|
||||
.brand-panel.is-primary {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.records-panel {
|
||||
left: 16px;
|
||||
bottom: 16px;
|
||||
width: min(360px, calc(50vw - 24px));
|
||||
max-height: min(44vh, 420px);
|
||||
transform: translateY(calc(100% - 48px));
|
||||
}
|
||||
|
||||
.catalog-panel {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
width: min(360px, calc(50vw - 24px));
|
||||
max-height: min(44vh, 420px);
|
||||
transform: translateY(calc(100% - 48px));
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
top: 112px;
|
||||
right: 16px;
|
||||
width: min(480px, calc(100vw - 32px));
|
||||
max-height: calc(100vh - 152px);
|
||||
transform: translateX(calc(100% - 56px));
|
||||
}
|
||||
|
||||
.records-panel:hover,
|
||||
.records-panel:focus-within,
|
||||
.catalog-panel:hover,
|
||||
.catalog-panel:focus-within,
|
||||
.detail-panel:hover,
|
||||
.detail-panel:focus-within {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.graph-warning {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 18px;
|
||||
z-index: 14;
|
||||
max-width: min(720px, calc(100vw - 36px));
|
||||
transform: translateX(-50%);
|
||||
border-top: 1px solid rgba(212, 154, 61, 0.3);
|
||||
border: 1px solid rgba(212, 154, 61, 0.3);
|
||||
border-radius: 18px;
|
||||
background: rgba(72, 45, 12, 0.34);
|
||||
color: #ffd38a;
|
||||
padding: 10px 12px;
|
||||
|
|
@ -195,10 +309,14 @@ code {
|
|||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
min-height: calc(100vh - 36px);
|
||||
max-width: 680px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero img {
|
||||
|
|
@ -214,22 +332,14 @@ code {
|
|||
color: #f0fbff;
|
||||
font-size: 42px;
|
||||
line-height: 1;
|
||||
text-shadow: 0 0 18px rgba(3, 7, 10, 0.92), 0 0 34px rgba(51, 230, 243, 0.28);
|
||||
}
|
||||
|
||||
.hero p {
|
||||
margin: 14px 0 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 0.82fr) minmax(420px, 1.18fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
margin-top: 14px;
|
||||
text-shadow: 0 0 14px rgba(3, 7, 10, 0.96);
|
||||
}
|
||||
|
||||
.panel {
|
||||
|
|
@ -241,7 +351,8 @@ code {
|
|||
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.panel header {
|
||||
.panel header,
|
||||
.floating-panel header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
|
|
@ -251,13 +362,15 @@ code {
|
|||
background: rgba(16, 28, 39, 0.92);
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
.panel h2,
|
||||
.floating-panel h2 {
|
||||
margin: 0;
|
||||
color: #f0fbff;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.panel header span {
|
||||
.panel header span,
|
||||
.floating-panel header span {
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
|
|
@ -268,7 +381,7 @@ code {
|
|||
.list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: 430px;
|
||||
max-height: inherit;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
@ -305,6 +418,28 @@ code {
|
|||
min-height: 360px;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
max-height: calc(100vh - 238px);
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.detail-tabs section {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-tabs h3 {
|
||||
margin: 4px 2px 8px;
|
||||
color: #dffbff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail > dl,
|
||||
.detail > pre,
|
||||
.detail > .error {
|
||||
|
|
@ -328,7 +463,7 @@ dd {
|
|||
}
|
||||
|
||||
pre {
|
||||
max-height: 440px;
|
||||
max-height: min(440px, 42vh);
|
||||
overflow: auto;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #142b39;
|
||||
|
|
@ -361,3 +496,212 @@ pre {
|
|||
padding: 18px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.huginn-expanded-node {
|
||||
scrollbar-color: rgba(51, 230, 243, 0.58) rgba(3, 7, 10, 0.32);
|
||||
transition:
|
||||
border-radius 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
padding 180ms ease;
|
||||
}
|
||||
|
||||
.expanded-content {
|
||||
display: grid;
|
||||
gap: clamp(10px, 1.4vw, 18px);
|
||||
min-height: 100%;
|
||||
padding: clamp(14px, 2.2vw, 30px);
|
||||
color: #e7fbff;
|
||||
}
|
||||
|
||||
.expanded-kicker {
|
||||
color: var(--cyan);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.expanded-header {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.expanded-header h1 {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: #f8fdff;
|
||||
font-size: clamp(1.45rem, 3.4vw, 3rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.expanded-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.expanded-chips span,
|
||||
.expanded-footer {
|
||||
border: 1px solid rgba(51, 230, 243, 0.22);
|
||||
border-radius: 999px;
|
||||
background: rgba(5, 18, 26, 0.68);
|
||||
color: #aeeef5;
|
||||
padding: 6px 9px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.expanded-summary {
|
||||
color: rgba(231, 251, 255, 0.78);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.expanded-article {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.expanded-article h2 {
|
||||
margin: 0 0 8px;
|
||||
color: #f8fdff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.expanded-footer {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-kicker,
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-summary,
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-article,
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-footer,
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-chips {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-header {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-header h1 {
|
||||
overflow: hidden;
|
||||
font-size: 1.02rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-article {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-article > div:nth-child(2),
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] pre {
|
||||
max-height: 170px;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="article"] {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@container (max-width: 260px) {
|
||||
.brand p,
|
||||
.drop-copy,
|
||||
.metric span,
|
||||
.row span,
|
||||
.floating-panel header span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.brand {
|
||||
grid-template-columns: 42px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.brand img {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
min-height: 54px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 180px) {
|
||||
.summary,
|
||||
.dropzone,
|
||||
.list,
|
||||
.detail-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.floating-panel header {
|
||||
border-bottom: 0;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
body {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.brand-panel {
|
||||
width: min(300px, calc(100vw - 24px));
|
||||
transform: translateX(calc(-100% + 58px));
|
||||
}
|
||||
|
||||
.records-panel,
|
||||
.catalog-panel {
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
width: auto;
|
||||
max-height: 32vh;
|
||||
}
|
||||
|
||||
.records-panel {
|
||||
bottom: 76px;
|
||||
}
|
||||
|
||||
.catalog-panel {
|
||||
bottom: 12px;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
top: auto;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
left: 12px;
|
||||
width: auto;
|
||||
max-height: 46vh;
|
||||
transform: translateY(calc(100% - 54px));
|
||||
}
|
||||
|
||||
.expanded-article {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue