Make Zyphos graph the fullscreen site shell
This commit is contained in:
parent
dbc0b38650
commit
799303b847
4 changed files with 518 additions and 35 deletions
|
|
@ -1 +1 @@
|
|||
html,body,#root{min-height:100%;margin:0}body{background:radial-gradient(circle at 72% 12%,rgba(255,174,88,.16),transparent 24%),radial-gradient(circle at 10% 18%,rgba(142,223,176,.16),transparent 28%),linear-gradient(180deg,#03070d,#07110f 52%,#091712);color:#effcf8;font-family:Ubuntu,Segoe UI,sans-serif}.zyphos-epiphany-graph-app{box-sizing:border-box;min-height:100vh;padding:16px}.zyphos-epiphany-graph-app *{box-sizing:border-box}.zyphos-graph-error{white-space:pre-wrap;color:#fecaca;background:#450a0a99;border:1px solid rgba(248,113,113,.45);border-radius:16px;padding:16px}.zyphos-graph-loading{display:grid;min-height:320px;place-items:center;color:#cfeee4;font-size:.95rem}
|
||||
html,body,#root{width:100%;min-height:100%;margin:0}body{background:radial-gradient(circle at 72% 12%,rgba(255,174,88,.16),transparent 24%),radial-gradient(circle at 10% 18%,rgba(142,223,176,.16),transparent 28%),linear-gradient(180deg,#03070d,#07110f 52%,#091712);color:#effcf8;font-family:Ubuntu,Segoe UI,sans-serif}.zyphos-epiphany-graph-app{box-sizing:border-box;width:100vw;min-height:100vh;padding:0}.zyphos-epiphany-graph-app *{box-sizing:border-box}.zyphos-epiphany-graph-root{position:fixed;inset:0;z-index:1000;width:100vw;height:100vh;min-height:100vh;margin:0!important;border:0!important;border-radius:0!important;background:#03070d}.zyphos-graph-error{white-space:pre-wrap;color:#fecaca;background:#450a0a99;border:1px solid rgba(248,113,113,.45);border-radius:16px;padding:16px}.zyphos-graph-loading{display:grid;min-height:320px;place-items:center;color:#cfeee4;font-size:.95rem}body.zyphos-graph-spa-active{overflow:hidden}.zyphos-graph-shell{width:100vw;height:100vh}.zyphos-graph-shell .epiphany-graph-overlay-panel{opacity:.25;transition:opacity .16s ease,transform .16s ease,box-shadow .16s ease}.zyphos-graph-shell .epiphany-graph-overlay-panel:hover,.zyphos-graph-shell .epiphany-graph-overlay-panel:focus-within{opacity:1;transform:translateY(-1px)}.zyphos-graph-shell aside.epiphany-graph-overlay-panel>section{display:none!important}.zyphos-graph-shell aside.epiphany-graph-overlay-panel>section:last-child{display:grid!important;grid-row:1 / -1}.zyphos-spa-panel{min-height:0;max-height:100%;overflow:auto;color:#effcf8}.zyphos-spa-panel h1{margin:0 0 .9rem;color:#f7fffb;font-size:clamp(1.7rem,2.8vw,3.2rem);line-height:1;letter-spacing:0}.zyphos-spa-kicker{margin:0 0 .55rem;color:#8edfb0;font-size:.76rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase}.zyphos-spa-summary,.zyphos-spa-status{color:#effcf8c2;line-height:1.55}.zyphos-spa-status{margin:1rem 0;padding:.75rem .9rem;border:1px solid rgba(142,223,176,.16);border-radius:16px;background:#03070d73}.zyphos-spa-status-error{color:#fecaca;border-color:#f8717159}.zyphos-spa-article-header{margin-bottom:1rem}.zyphos-spa-actions{display:flex;gap:.6rem;flex-wrap:wrap}.zyphos-spa-actions a,.zyphos-spa-note-list button{border:1px solid rgba(142,223,176,.18);border-radius:16px;background:#8edfb014;color:#effcf8}.zyphos-spa-actions a{padding:.5rem .7rem;text-decoration:none}.zyphos-spa-article{color:#effcf8db;line-height:1.65}.zyphos-spa-article h1{display:none}.zyphos-spa-article h2,.zyphos-spa-article h3{color:#f7fffb;line-height:1.12}.zyphos-spa-article a{color:#ffe0a8}.zyphos-spa-article img{max-width:100%;border-radius:18px}.zyphos-spa-note-list{display:grid;gap:.7rem}.zyphos-spa-note-list button{display:grid;gap:.35rem;padding:.85rem;text-align:left;cursor:pointer}.zyphos-spa-note-list button:hover{border-color:#ffae5861;background:#ffae581c}.zyphos-spa-note-list strong{color:#f7fffb}.zyphos-spa-note-list span{color:#effcf8ad;font-size:.92rem;line-height:1.45}@media(max-width:900px){.zyphos-graph-shell aside.epiphany-graph-overlay-panel{left:12px;right:12px;top:auto!important;bottom:12px!important;width:auto!important;max-height:48vh}}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -6,6 +6,8 @@ import type {
|
|||
EpiphanyGraphEdge,
|
||||
EpiphanyGraphNode,
|
||||
EpiphanyGraphsState,
|
||||
GraphKey,
|
||||
ViewerSelection,
|
||||
} from "../../../../EpiphanyGraph/web/epiphany-graph-viewer/src/lib/types"
|
||||
import "./styles.css"
|
||||
|
||||
|
|
@ -20,6 +22,25 @@ type QuartzContentEntry = {
|
|||
|
||||
type QuartzContentIndex = Record<string, QuartzContentEntry>
|
||||
|
||||
type ArticleState =
|
||||
| {
|
||||
status: "idle"
|
||||
html: string | null
|
||||
}
|
||||
| {
|
||||
status: "loading"
|
||||
html: string | null
|
||||
}
|
||||
| {
|
||||
status: "ready"
|
||||
html: string
|
||||
}
|
||||
| {
|
||||
status: "error"
|
||||
html: string | null
|
||||
message: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
fetchData?: Promise<QuartzContentIndex>
|
||||
|
|
@ -87,6 +108,50 @@ function sectionForSlug(slug: string) {
|
|||
return slug.includes("/") ? slug.split("/")[0] : "Root"
|
||||
}
|
||||
|
||||
function slugToArticleUrl(slug: string) {
|
||||
if (slug === "index") {
|
||||
return "/index.html"
|
||||
}
|
||||
|
||||
return `/${slug.split("/").map(encodeURIComponent).join("/")}.html`
|
||||
}
|
||||
|
||||
function slugFromArticleUrl(url: string, slugs: Set<string>) {
|
||||
const parsed = new URL(url, window.location.href)
|
||||
|
||||
if (parsed.origin !== window.location.origin) {
|
||||
return null
|
||||
}
|
||||
|
||||
const decodedPath = decodeURIComponent(parsed.pathname)
|
||||
.replace(/^\//, "")
|
||||
.replace(/\.html$/, "")
|
||||
.replace(/\/$/, "/index")
|
||||
const slug = normalizeSlug(decodedPath || "index")
|
||||
|
||||
return slugs.has(slug) ? slug : null
|
||||
}
|
||||
|
||||
function parseHashSelection(slugs: Set<string>): ViewerSelection | null {
|
||||
const match = window.location.hash.match(/^#note=(.+)$/)
|
||||
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
|
||||
const slug = normalizeSlug(decodeURIComponent(match[1]))
|
||||
|
||||
if (!slugs.has(slug)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "node",
|
||||
graphKey: "architecture",
|
||||
nodeId: slug,
|
||||
}
|
||||
}
|
||||
|
||||
function compareSections(left: string, right: string) {
|
||||
const leftIndex = SECTION_ORDER.indexOf(left)
|
||||
const rightIndex = SECTION_ORDER.indexOf(right)
|
||||
|
|
@ -259,20 +324,83 @@ async function loadGraphState() {
|
|||
? await window.fetchData
|
||||
: await fetch("/static/contentIndex.json").then((response) => response.json() as Promise<QuartzContentIndex>)
|
||||
|
||||
return buildGraphState(contentIndex)
|
||||
return {
|
||||
contentIndex,
|
||||
graphState: buildGraphState(contentIndex),
|
||||
}
|
||||
}
|
||||
|
||||
async function loadArticleHtml(slug: string) {
|
||||
const response = await fetch(slugToArticleUrl(slug))
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not load ${slug}: ${response.status}`)
|
||||
}
|
||||
|
||||
const html = await response.text()
|
||||
const documentHtml = new DOMParser().parseFromString(html, "text/html")
|
||||
const article = documentHtml.querySelector("article")
|
||||
|
||||
if (!article) {
|
||||
throw new Error(`No article body found for ${slug}`)
|
||||
}
|
||||
|
||||
const articleUrl = new URL(slugToArticleUrl(slug), window.location.href)
|
||||
|
||||
article.querySelectorAll<HTMLAnchorElement>("a[href]").forEach((anchor) => {
|
||||
anchor.href = new URL(anchor.getAttribute("href") ?? "", articleUrl).toString()
|
||||
})
|
||||
|
||||
article.querySelectorAll<HTMLImageElement>("img[src]").forEach((image) => {
|
||||
image.src = new URL(image.getAttribute("src") ?? "", articleUrl).toString()
|
||||
})
|
||||
|
||||
return article.innerHTML
|
||||
}
|
||||
|
||||
function selectedNoteSlug(selection: ViewerSelection | null) {
|
||||
return selection?.kind === "node" && selection.graphKey === "architecture" ? selection.nodeId : null
|
||||
}
|
||||
|
||||
function selectedSection(selection: ViewerSelection | null) {
|
||||
if (selection?.kind !== "node" || selection.graphKey !== "dataflow") {
|
||||
return null
|
||||
}
|
||||
|
||||
return selection.nodeId.replace(/^section:/, "")
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [graphState, setGraphState] = React.useState<EpiphanyGraphsState | null>(null)
|
||||
const [contentIndex, setContentIndex] = React.useState<QuartzContentIndex | null>(null)
|
||||
const [selection, setSelection] = React.useState<ViewerSelection | null>(null)
|
||||
const [articleState, setArticleState] = React.useState<ArticleState>({ status: "idle", html: null })
|
||||
const [error, setError] = React.useState<Error | null>(null)
|
||||
const slugs = React.useMemo(() => new Set(Object.keys(contentIndex ?? {}).map(normalizeSlug)), [contentIndex])
|
||||
const noteSlug = selectedNoteSlug(selection)
|
||||
|
||||
React.useEffect(() => {
|
||||
document.body.classList.add("zyphos-graph-spa-active")
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove("zyphos-graph-spa-active")
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
loadGraphState()
|
||||
.then((state) => {
|
||||
.then(({ contentIndex: nextContentIndex, graphState: nextGraphState }) => {
|
||||
if (!cancelled) {
|
||||
setGraphState(state)
|
||||
const nextSlugs = new Set(Object.keys(nextContentIndex).map(normalizeSlug))
|
||||
setContentIndex(nextContentIndex)
|
||||
setGraphState(nextGraphState)
|
||||
setSelection(parseHashSelection(nextSlugs) ?? {
|
||||
kind: "node",
|
||||
graphKey: "architecture",
|
||||
nodeId: nextSlugs.has("index") ? "index" : nextGraphState.architecture.nodes[0]?.id ?? "",
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((caught) => {
|
||||
|
|
@ -286,6 +414,57 @@ function App() {
|
|||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!noteSlug) {
|
||||
setArticleState({ status: "idle", html: null })
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
setArticleState((current) => ({ status: "loading", html: current.html }))
|
||||
|
||||
loadArticleHtml(noteSlug)
|
||||
.then((html) => {
|
||||
if (!cancelled) {
|
||||
setArticleState({ status: "ready", html })
|
||||
}
|
||||
})
|
||||
.catch((caught) => {
|
||||
if (!cancelled) {
|
||||
setArticleState({
|
||||
status: "error",
|
||||
html: null,
|
||||
message: caught instanceof Error ? caught.message : String(caught),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [noteSlug])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!noteSlug) {
|
||||
return
|
||||
}
|
||||
|
||||
window.history.replaceState(null, "", `#note=${encodeURIComponent(noteSlug)}`)
|
||||
}, [noteSlug])
|
||||
|
||||
React.useEffect(() => {
|
||||
const onHashChange = () => {
|
||||
const hashSelection = parseHashSelection(slugs)
|
||||
|
||||
if (hashSelection) {
|
||||
setSelection(hashSelection)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("hashchange", onHashChange)
|
||||
return () => window.removeEventListener("hashchange", onHashChange)
|
||||
}, [slugs])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<pre className="zyphos-graph-error">
|
||||
|
|
@ -310,6 +489,25 @@ function App() {
|
|||
<EpiphanyGraphViewer
|
||||
state={graphState}
|
||||
title="Zyphos Vault Backlink Web"
|
||||
selection={selection}
|
||||
onSelectionChange={setSelection}
|
||||
overlayPanels
|
||||
className="zyphos-graph-shell"
|
||||
sidebarWidth="minmax(360px, 44vw)"
|
||||
sidebar={
|
||||
<ArticlePanel
|
||||
articleState={articleState}
|
||||
contentIndex={contentIndex}
|
||||
selection={selection}
|
||||
onSelectNote={(slug) =>
|
||||
setSelection({
|
||||
kind: "node",
|
||||
graphKey: "architecture",
|
||||
nodeId: slug,
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
graphLabels={{
|
||||
architecture: "Notes",
|
||||
dataflow: "Sections",
|
||||
|
|
@ -324,6 +522,115 @@ function App() {
|
|||
)
|
||||
}
|
||||
|
||||
function ArticlePanel({
|
||||
articleState,
|
||||
contentIndex,
|
||||
selection,
|
||||
onSelectNote,
|
||||
}: {
|
||||
articleState: ArticleState
|
||||
contentIndex: QuartzContentIndex | null
|
||||
selection: ViewerSelection | null
|
||||
onSelectNote: (slug: string) => void
|
||||
}) {
|
||||
const noteSlug = selectedNoteSlug(selection)
|
||||
const section = selectedSection(selection)
|
||||
const slugs = React.useMemo(() => new Set(Object.keys(contentIndex ?? {}).map(normalizeSlug)), [contentIndex])
|
||||
const note = noteSlug && contentIndex ? contentIndex[noteSlug] : null
|
||||
const sectionNotes = React.useMemo(() => {
|
||||
if (!section || !contentIndex) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.entries(contentIndex)
|
||||
.map(([slug, entry]) => [normalizeSlug(entry.slug || slug), entry] as [string, QuartzContentEntry])
|
||||
.filter(([slug]) => sectionForSlug(slug) === section)
|
||||
.sort(([left, leftEntry], [right, rightEntry]) => entryTitle(left, leftEntry).localeCompare(entryTitle(right, rightEntry)))
|
||||
}, [contentIndex, section])
|
||||
|
||||
const onArticleClick = React.useCallback(
|
||||
(event: React.MouseEvent<HTMLElement>) => {
|
||||
const anchor = (event.target as HTMLElement).closest("a[href]")
|
||||
|
||||
if (!(anchor instanceof HTMLAnchorElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
const slug = slugFromArticleUrl(anchor.href, slugs)
|
||||
|
||||
if (!slug) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
onSelectNote(slug)
|
||||
},
|
||||
[onSelectNote, slugs],
|
||||
)
|
||||
|
||||
if (section) {
|
||||
return (
|
||||
<article className="zyphos-spa-panel">
|
||||
<p className="zyphos-spa-kicker">Section Cluster</p>
|
||||
<h1>{section}</h1>
|
||||
<p className="zyphos-spa-summary">
|
||||
{sectionNotes.length} notes live in this section. Pick one and the graph becomes the table of contents it was clearly auditioning to be.
|
||||
</p>
|
||||
<div className="zyphos-spa-note-list">
|
||||
{sectionNotes.map(([slug, entry]) => (
|
||||
<button key={slug} type="button" onClick={() => onSelectNote(slug)}>
|
||||
<strong>{entryTitle(slug, entry)}</strong>
|
||||
<span>{summarizeContent(entry)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
if (!noteSlug || !note) {
|
||||
return (
|
||||
<article className="zyphos-spa-panel">
|
||||
<p className="zyphos-spa-kicker">Graph Atlas</p>
|
||||
<h1>Zyphos</h1>
|
||||
<p className="zyphos-spa-summary">
|
||||
Select a note to read it here. The graph is the site map now, which is probably what it was trying to confess.
|
||||
</p>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="zyphos-spa-panel">
|
||||
<header className="zyphos-spa-article-header">
|
||||
<p className="zyphos-spa-kicker">{sectionForSlug(noteSlug)}</p>
|
||||
<h1>{entryTitle(noteSlug, note)}</h1>
|
||||
<div className="zyphos-spa-actions">
|
||||
<a href={slugToArticleUrl(noteSlug)}>Open page</a>
|
||||
</div>
|
||||
</header>
|
||||
{articleState.status === "loading" && <div className="zyphos-spa-status">Loading article...</div>}
|
||||
{articleState.status === "error" && (
|
||||
<div className="zyphos-spa-status zyphos-spa-status-error">{articleState.message}</div>
|
||||
)}
|
||||
{articleState.status === "ready" && (
|
||||
<div
|
||||
className="zyphos-spa-article"
|
||||
onClick={onArticleClick}
|
||||
dangerouslySetInnerHTML={{ __html: articleState.html }}
|
||||
/>
|
||||
)}
|
||||
{articleState.status !== "ready" && articleState.html && (
|
||||
<div
|
||||
className="zyphos-spa-article"
|
||||
onClick={onArticleClick}
|
||||
dangerouslySetInnerHTML={{ __html: articleState.html }}
|
||||
/>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function mountZyphosGraph() {
|
||||
document.querySelectorAll(".zyphos-epiphany-graph-root").forEach((element) => {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -16,14 +17,28 @@ body {
|
|||
|
||||
.zyphos-epiphany-graph-app {
|
||||
box-sizing: border-box;
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
padding: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.zyphos-epiphany-graph-app * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.zyphos-epiphany-graph-root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
background: #03070d;
|
||||
}
|
||||
|
||||
.zyphos-graph-error {
|
||||
white-space: pre-wrap;
|
||||
color: #fecaca;
|
||||
|
|
@ -40,3 +55,164 @@ body {
|
|||
color: #cfeee4;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
body.zyphos-graph-spa-active {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.zyphos-graph-shell {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.zyphos-graph-shell .epiphany-graph-overlay-panel {
|
||||
opacity: 0.25;
|
||||
transition:
|
||||
opacity 160ms ease,
|
||||
transform 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.zyphos-graph-shell .epiphany-graph-overlay-panel:hover,
|
||||
.zyphos-graph-shell .epiphany-graph-overlay-panel:focus-within {
|
||||
opacity: 1;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.zyphos-graph-shell aside.epiphany-graph-overlay-panel > section {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.zyphos-graph-shell aside.epiphany-graph-overlay-panel > section:last-child {
|
||||
display: grid !important;
|
||||
grid-row: 1 / -1;
|
||||
}
|
||||
|
||||
.zyphos-spa-panel {
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
color: #effcf8;
|
||||
}
|
||||
|
||||
.zyphos-spa-panel h1 {
|
||||
margin: 0 0 0.9rem;
|
||||
color: #f7fffb;
|
||||
font-size: clamp(1.7rem, 2.8vw, 3.2rem);
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.zyphos-spa-kicker {
|
||||
margin: 0 0 0.55rem;
|
||||
color: #8edfb0;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.zyphos-spa-summary,
|
||||
.zyphos-spa-status {
|
||||
color: rgba(239, 252, 248, 0.76);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.zyphos-spa-status {
|
||||
margin: 1rem 0;
|
||||
padding: 0.75rem 0.9rem;
|
||||
border: 1px solid rgba(142, 223, 176, 0.16);
|
||||
border-radius: 16px;
|
||||
background: rgba(3, 7, 13, 0.45);
|
||||
}
|
||||
|
||||
.zyphos-spa-status-error {
|
||||
color: #fecaca;
|
||||
border-color: rgba(248, 113, 113, 0.35);
|
||||
}
|
||||
|
||||
.zyphos-spa-article-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.zyphos-spa-actions {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.zyphos-spa-actions a,
|
||||
.zyphos-spa-note-list button {
|
||||
border: 1px solid rgba(142, 223, 176, 0.18);
|
||||
border-radius: 16px;
|
||||
background: rgba(142, 223, 176, 0.08);
|
||||
color: #effcf8;
|
||||
}
|
||||
|
||||
.zyphos-spa-actions a {
|
||||
padding: 0.5rem 0.7rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.zyphos-spa-article {
|
||||
color: rgba(239, 252, 248, 0.86);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.zyphos-spa-article h1 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.zyphos-spa-article h2,
|
||||
.zyphos-spa-article h3 {
|
||||
color: #f7fffb;
|
||||
line-height: 1.12;
|
||||
}
|
||||
|
||||
.zyphos-spa-article a {
|
||||
color: #ffe0a8;
|
||||
}
|
||||
|
||||
.zyphos-spa-article img {
|
||||
max-width: 100%;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.zyphos-spa-note-list {
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.zyphos-spa-note-list button {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
padding: 0.85rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.zyphos-spa-note-list button:hover {
|
||||
border-color: rgba(255, 174, 88, 0.38);
|
||||
background: rgba(255, 174, 88, 0.11);
|
||||
}
|
||||
|
||||
.zyphos-spa-note-list strong {
|
||||
color: #f7fffb;
|
||||
}
|
||||
|
||||
.zyphos-spa-note-list span {
|
||||
color: rgba(239, 252, 248, 0.68);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.zyphos-graph-shell aside.epiphany-graph-overlay-panel {
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
top: auto !important;
|
||||
bottom: 12px !important;
|
||||
width: auto !important;
|
||||
max-height: 48vh;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue