From b346a15f3d1d9b5bdc66d87152c40a1d519a1db5 Mon Sep 17 00:00:00 2001 From: Ayfri Date: Wed, 8 Jan 2025 01:06:48 +0100 Subject: [PATCH] feat(docs): Enhance documentation with metadata, anchor links, TOC, and search functionality. --- website/build.gradle.kts | 30 ++ .../github/ayfri/kore/website/GlobalStyle.kt | 1 + .../kore/website/components/doc/DocSidebar.kt | 87 +++++ .../kore/website/components/doc/DocTree.kt | 158 ++------ .../kore/website/components/doc/Search.kt | 171 +++++++++ .../components/layouts/MarkdownLayout.kt | 359 +++++++++++++++++- .../kore/website/utils/HTMLComposables.kt | 13 + 7 files changed, 683 insertions(+), 136 deletions(-) create mode 100644 website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/DocSidebar.kt create mode 100644 website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/Search.kt diff --git a/website/build.gradle.kts b/website/build.gradle.kts index 6a385c16..06dd8687 100644 --- a/website/build.gradle.kts +++ b/website/build.gradle.kts @@ -74,6 +74,36 @@ kobweb { """io.github.ayfri.kore.website.components.common.CodeBlock($text, "${code.info.takeIf { it.isNotBlank() }}")""" } + + heading.set { heading -> + val id = heading.children() + .filterIsInstance() + .map { it.literal.lowercase().replace(Regex("[^a-z0-9]+"), "-") } + .joinToString("") + val content = heading.children() + .filterIsInstance() + .map { it.literal.escapeSingleQuotedText() } + .joinToString("") + childrenOverride = emptyList() + val tag = "H${heading.level}" + + val onSubtitles = + if (heading.level > 1) "classes(io.github.ayfri.kore.website.components.layouts.MarkdownLayoutStyle.heading)" + else "" + + """org.jetbrains.compose.web.dom.${tag.replaceFirstChar { it.uppercase() }}({ + | attr("id", "$id") + | $onSubtitles + |}) { + | org.jetbrains.compose.web.dom.A("#$id", { + | classes(io.github.ayfri.kore.website.components.layouts.MarkdownLayoutStyle.anchor) + | }) { + | com.varabyte.kobweb.silk.components.icons.mdi.MdiLink() + | } + | org.jetbrains.compose.web.dom.Text("$content") + |} + """.trimMargin() + } } process = { markdownFiles -> diff --git a/website/src/jsMain/kotlin/io/github/ayfri/kore/website/GlobalStyle.kt b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/GlobalStyle.kt index 8bd65489..c6a0ec88 100644 --- a/website/src/jsMain/kotlin/io/github/ayfri/kore/website/GlobalStyle.kt +++ b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/GlobalStyle.kt @@ -22,6 +22,7 @@ object GlobalStyle : StyleSheet() { val buttonBackgroundColorHover = Color("#0597ba") val linkColor = Color("#0597ba") val linkColorHover = Color("#23cae8") + val inactiveLinkColor = Color("#a7b5bd") val textColor = Color("#fff") val scrollbarThumbColor = Color("#ffffff99") diff --git a/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/DocSidebar.kt b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/DocSidebar.kt new file mode 100644 index 00000000..20fb394c --- /dev/null +++ b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/DocSidebar.kt @@ -0,0 +1,87 @@ +package io.github.ayfri.kore.website.components.doc + +import androidx.compose.runtime.Composable +import com.varabyte.kobweb.compose.css.Cursor +import com.varabyte.kobweb.compose.css.cursor +import com.varabyte.kobweb.compose.css.translateX +import com.varabyte.kobweb.compose.css.zIndex +import com.varabyte.kobweb.silk.components.icons.mdi.MdiClose +import io.github.ayfri.kore.website.GlobalStyle +import io.github.ayfri.kore.website.utils.smMax +import io.github.ayfri.kore.website.utils.smMin +import io.github.ayfri.kore.website.utils.transition +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.Button +import org.jetbrains.compose.web.dom.Div + +@Composable +fun DocSidebar(revealed: Boolean, onClose: () -> Unit) { + Style(DocSidebarStyle) + + Div({ + classes(DocSidebarStyle.sidebar) + if (revealed) classes("reveal") + }) { + Button({ + classes(DocSidebarStyle.closeButton) + onClick { onClose() } + }) { + MdiClose() + } + + Search() + DocTree() + } +} + +object DocSidebarStyle : StyleSheet() { + val sidebar by style { + backgroundColor(GlobalStyle.secondaryBackgroundColor) + display(DisplayStyle.Flex) + flexDirection(FlexDirection.Column) + padding(1.cssRem) + position(Position.Relative) + + smMin(self) { + borderRadius(GlobalStyle.roundingButton) + } + + smMax(self) { + backgroundColor(GlobalStyle.backgroundColor) + height(100.vh) + left(0.px) + paddingBottom(100.vh) + paddingTop(4.cssRem) + position(Position.Fixed) + top(0.px) + translateX((-100).percent) + transition(0.3.s, "translate") + width(100.percent) + zIndex(10) + } + + self + className("reveal") style { + translateX(0.percent) + } + } + + val closeButton by style { + backgroundColor(Color.transparent) + border(0.px) + color(GlobalStyle.textColor) + cursor(Cursor.Pointer) + display(DisplayStyle.None) + padding(0.5.cssRem) + position(Position.Absolute) + right(1.cssRem) + top(1.cssRem) + + smMax(self) { + display(DisplayStyle.Block) + } + + className("material-icons") style { + fontSize(2.cssRem) + } + } +} diff --git a/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/DocTree.kt b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/DocTree.kt index 4361534e..896f6d1b 100644 --- a/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/DocTree.kt +++ b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/DocTree.kt @@ -6,15 +6,12 @@ import com.varabyte.kobweb.compose.css.* import com.varabyte.kobweb.core.rememberPageContext import io.github.ayfri.kore.website.GlobalStyle import io.github.ayfri.kore.website.docEntries -import io.github.ayfri.kore.website.utils.* -import kotlinx.browser.document +import io.github.ayfri.kore.website.utils.A +import io.github.ayfri.kore.website.utils.Span +import io.github.ayfri.kore.website.utils.transition import org.jetbrains.compose.web.ExperimentalComposeWebApi import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.dom.Button -import org.jetbrains.compose.web.dom.Li -import org.jetbrains.compose.web.dom.Text -import org.jetbrains.compose.web.dom.Ul -import org.w3c.dom.HTMLButtonElement +import org.jetbrains.compose.web.dom.* private fun StyleScope.indentation(level: Int) = marginLeft(level * 2.cssRem) @@ -52,145 +49,60 @@ fun DocTree() { val entries = docEntries val currentURL = context.path - - Ul({ + Div({ id("doc-tree") - classes(DocTreeStyle.list) }) { - Button({ - classes(DocTreeStyle.revealButton) - - onClick { - val docTree = document.querySelector("#doc-tree") - docTree?.classList?.toggle("reveal") - it.currentTarget.unsafeCast().classList.toggle("reveal") - } - }) { - Text(">") + H2 { + Text("Pages") } - // Separate the entries that are not in a group. - val (simpleEntriesWithoutGroup, otherEntries) = entries.partition { - it.middleSlugs.isEmpty() && entries.none { entry -> entry.middleSlugs.contains(it.slugs.last()) } - } - // Display these entries first. - simpleEntriesWithoutGroup.forEach { entry -> - Entry(entry, entry.path == currentURL) - } + Ul({ + classes(DocTreeStyle.list) + }) { + // Separate the entries that are not in a group. + val (simpleEntriesWithoutGroup, otherEntries) = entries.partition { + it.middleSlugs.isEmpty() && entries.none { entry -> entry.middleSlugs.contains(it.slugs.last()) } + } + // Display these entries first. + simpleEntriesWithoutGroup.forEach { entry -> + Entry(entry, entry.path == currentURL) + } - // Then group the entries by their middle slugs. - val presentedGroups = mutableSetOf() - otherEntries.groupBy { it.middleSlugs.joinToString("/") }.forEach { (slug, groupEntries) -> - if (slug in presentedGroups || slug.isEmpty()) return@forEach + // Then group the entries by their middle slugs. + val presentedGroups = mutableSetOf() + otherEntries.groupBy { it.middleSlugs.joinToString("/") }.forEach { (slug, groupEntries) -> + if (slug in presentedGroups || slug.isEmpty()) return@forEach + + presentedGroups += slug + val slugName = slug.split("/").last() + if (slugName in entries.map { it.slugs.last() }) { + entries.find { it.slugs.last() == slug }?.let { entry -> + Entry(entry, entry.path == currentURL) + } + } else { + GroupEntry(slugName.kebabCaseToTitleCamelCase(), slug.count { it == '/' }) + } - presentedGroups += slug - val slugName = slug.split("/").last() - if (slugName in entries.map { it.slugs.last() }) { - entries.find { it.slugs.last() == slug }?.let { entry -> + val sortedEntries = groupEntries.sortedBy { it.slugs.size }.filter { it.slugs.last() != slugName } + sortedEntries.forEach { entry -> Entry(entry, entry.path == currentURL) } - } else { - GroupEntry(slugName.kebabCaseToTitleCamelCase(), slug.count { it == '/' }) - } - - val sortedEntries = groupEntries.sortedBy { it.slugs.size }.filter { it.slugs.last() != slugName } - sortedEntries.forEach { entry -> - Entry(entry, entry.path == currentURL) } } } } object DocTreeStyle : StyleSheet() { - val revealButton by style { - position(Position.Fixed) - left(100.percent) - padding(0.4.cssRem, 0.8.cssRem) - top(4.cssRem) - - backgroundColor(GlobalStyle.secondaryBackgroundColor) - borderTopRightRadius(GlobalStyle.roundingButton) - borderBottomRightRadius(GlobalStyle.roundingButton) - borderStyle(LineStyle.None) - color(GlobalStyle.altTextColor) - cursor(Cursor.Pointer) - fontSize(1.2.cssRem) - fontWeight(FontWeight.Bold) - transition(0.3.s, "background-color", "color") - - smMin(self) { - display(DisplayStyle.None) - } - - self + after style { - content("") - position(Position.Absolute) - width(100.vw) - height(100.vh) - left(0.px) - top((-4).cssRem) - transition(0.3.s, "background-color") - pointerEvents(PointerEvents.None) - } - - self + before style { - content("x") - position(Position.Absolute) - property("right", "calc(-100vw + 17rem)") - top((-150).percent) - - color(Color.transparent) - fontWeight(FontWeight.Normal) - transition(0.15.s, 0.s, "color") - zIndex(10) - } - - self + className("reveal") style { - backgroundColor(BackgroundColor.Transparent) - color(Color.transparent) - property("-webkit-tap-highlight-color", "transparent") - - self + before style { - color(GlobalStyle.altTextColor) - transition(0.3.s, 0.2.s, "color") - } - - self + after style { - backgroundColor(GlobalStyle.shadowColor) - pointerEvents(PointerEvents.Auto) - } - } - } - @OptIn(ExperimentalComposeWebApi::class) val list by style { listStyle(ListStyleType.None) padding(0.8.cssRem) + marginTop(0.px) marginRight(1.cssRem) height(100.percent) position(Position.Sticky) left(0.px) - - smMax(self) { - position(Position.Fixed) - top((-1).cssRem) - paddingTop(10.cssRem) - paddingBottom(100.vh) - zIndex(10) - - backgroundColor(GlobalStyle.secondaryBackgroundColor) - transition(0.3.s, "transform") - transform { - translateX((-100).percent) - } - - self + className("reveal") style { - transform { - translateX(0.percent) - } - } - } } val entry by style { diff --git a/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/Search.kt b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/Search.kt new file mode 100644 index 00000000..862b7471 --- /dev/null +++ b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/doc/Search.kt @@ -0,0 +1,171 @@ +package io.github.ayfri.kore.website.components.doc + +import androidx.compose.runtime.* +import com.varabyte.kobweb.compose.css.* +import com.varabyte.kobweb.core.rememberPageContext +import com.varabyte.kobweb.silk.components.icons.mdi.MdiSearch +import io.github.ayfri.kore.website.GlobalStyle +import io.github.ayfri.kore.website.docEntries +import io.github.ayfri.kore.website.utils.Search +import io.github.ayfri.kore.website.utils.transition +import kotlinx.browser.document +import org.jetbrains.compose.web.attributes.InputType +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.css.AlignItems +import org.jetbrains.compose.web.dom.* +import org.w3c.dom.HTMLElement + +@Composable +fun Search() { + var query by remember { mutableStateOf("") } + var showResults by remember { mutableStateOf(false) } + val context = rememberPageContext() + + Style(SearchStyle) + + Search({ + classes(SearchStyle.container) + }) { + Div({ + classes(SearchStyle.inputContainer) + }) { + MdiSearch() + Input(InputType.Search) { + classes(SearchStyle.input) + attr("placeholder", "Search documentation...") + attr("value", query) + onInput { event -> query = event.value } + onFocus { showResults = true } + } + } + + if (showResults && query.isNotEmpty()) { + val results = docEntries.filter { + it.title.contains(query, ignoreCase = true) || + it.desc.contains(query, ignoreCase = true) || + it.keywords.any { keyword -> keyword.contains(query, ignoreCase = true) } + } + + Div({ + classes(SearchStyle.results) + }) { + if (results.isEmpty()) { + P({ + classes(SearchStyle.noResults) + }) { + Text("No results found") + } + } else { + results.take(5).forEach { entry -> + A(entry.path, { + classes(SearchStyle.result) + onClick { + context.router.navigateTo(entry.path) + showResults = false + query = "" + } + }) { + H4 { Text(entry.title) } + P { Text(entry.desc) } + } + } + } + } + } + } + + // Click outside to close results + LaunchedEffect(Unit) { + document.addEventListener("click", { event -> + val target = event.target + if (target !is HTMLElement || target.closest(".${SearchStyle.container}") == null) { + showResults = false + } + }) + } +} + +object SearchStyle : StyleSheet() { + val container by style { + position(Position.Relative) + width(100.percent) + marginBottom(1.cssRem) + } + + val inputContainer by style { + alignItems(AlignItems.Center) + backgroundColor(GlobalStyle.secondaryBackgroundColor) + border(1.px, LineStyle.Solid, GlobalStyle.tertiaryBackgroundColor) + borderRadius(GlobalStyle.roundingButton) + display(DisplayStyle.Flex) + gap(0.5.cssRem) + padding(0.8.cssRem) + transition(0.2.s, "border-color") + + self + focus style { + borderColor(GlobalStyle.linkColor) + } + + child(self, type("span")) style { + color(GlobalStyle.altTextColor) + fontSize(1.5.cssRem) + } + } + + val input by style { + backgroundColor(Color.transparent) + border(0.px) + color(GlobalStyle.textColor) + fontSize(1.cssRem) + outline(0.px) + width(100.percent) + } + + val results by style { + backgroundColor(GlobalStyle.secondaryBackgroundColor) + border(1.px, LineStyle.Solid, GlobalStyle.tertiaryBackgroundColor) + borderRadius(GlobalStyle.roundingButton) + boxShadow(0.px, 4.px, 12.px, 0.px, rgba(0, 0, 0, 0.1)) + left(0.px) + marginTop(0.5.cssRem) + maxHeight(400.px) + overflowY(Overflow.Auto) + position(Position.Absolute) + right(0.px) + top(100.percent) + zIndex(100) + } + + val result by style { + backgroundColor(Color.transparent) + color(GlobalStyle.textColor) + cursor(Cursor.Pointer) + display(DisplayStyle.Block) + padding(1.cssRem) + textDecorationLine(TextDecorationLine.None) + transition(0.2.s, "background-color") + + self + hover style { + backgroundColor(GlobalStyle.tertiaryBackgroundColor) + } + + child(self, type("h4")) style { + color(GlobalStyle.linkColor) + margin(0.px) + marginBottom(0.2.cssRem) + } + + child(self, type("p")) style { + color(GlobalStyle.altTextColor) + fontSize(0.9.cssRem) + margin(0.px) + } + } + + val noResults by style { + color(GlobalStyle.altTextColor) + margin(0.px) + padding(1.cssRem) + textAlign(TextAlign.Center) + } +} diff --git a/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/layouts/MarkdownLayout.kt b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/layouts/MarkdownLayout.kt index e377a49e..ea0a151c 100644 --- a/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/layouts/MarkdownLayout.kt +++ b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/components/layouts/MarkdownLayout.kt @@ -1,17 +1,98 @@ package io.github.ayfri.kore.website.components.layouts -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import com.varabyte.kobweb.compose.css.* +import com.varabyte.kobweb.compose.css.functions.blur import com.varabyte.kobweb.core.rememberPageContext +import com.varabyte.kobweb.silk.components.icons.mdi.MdiChevronRight import com.varabyte.kobwebx.markdown.markdown import io.github.ayfri.kore.website.GlobalStyle import io.github.ayfri.kore.website.components.common.setDescription -import io.github.ayfri.kore.website.components.doc.DocTree -import io.github.ayfri.kore.website.utils.marginX -import io.github.ayfri.kore.website.utils.marginY -import io.github.ayfri.kore.website.utils.smMax +import io.github.ayfri.kore.website.components.doc.DocSidebar +import io.github.ayfri.kore.website.docEntries +import io.github.ayfri.kore.website.utils.* +import kotlinx.browser.document +import kotlinx.browser.window import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.css.JustifyContent +import org.jetbrains.compose.web.dom.* +import org.w3c.dom.HTMLElement +import org.w3c.dom.asList + +@Composable +fun TableOfContents() { + var headings by remember { mutableStateOf(listOf()) } + + LaunchedEffect(Unit) { + headings = document.querySelectorAll(".${MarkdownLayoutStyle.mainContent} .${MarkdownLayoutStyle.heading}") + .asList() + .map { it as HTMLElement } + } + + LaunchedEffect(Unit) { + window.addEventListener("hashchange", { + val hash = window.location.hash + if (hash.isNotEmpty()) { + val element = document.querySelector(hash) + element?.classList?.remove(MarkdownLayoutStyle.highlight) + element?.classList?.add(MarkdownLayoutStyle.highlight) + } + }) + } + + Nav({ + classes(MarkdownLayoutStyle.toc) + }) { + H3 { Text("On this page") } + Ul { + headings.forEach { heading -> + Li({ + classes(MarkdownLayoutStyle.tocEntry) + style { + marginLeft((heading.tagName.last().toString().toInt() - 2) * 1.25.cssRem) + } + onClick { + val id = heading.id + if (id.isNotEmpty()) { + window.location.hash = "#$id" + heading.classList.remove(MarkdownLayoutStyle.highlight) + heading.offsetWidth + heading.classList.add(MarkdownLayoutStyle.highlight) + } + } + }) { + Text(heading.innerText.removePrefix("link").trim()) + } + } + } + } +} + +@Composable +fun PageNavigation(currentPath: String) { + val currentIndex = docEntries.indexOfFirst { it.path == currentPath } + if (currentIndex == -1) return + + Div({ + classes(MarkdownLayoutStyle.pageNav) + }) { + if (currentIndex > 0) { + A(docEntries[currentIndex - 1].path, { + classes(MarkdownLayoutStyle.prevPage) + }) { + Text("← ${docEntries[currentIndex - 1].navTitle}") + } + } + + if (currentIndex < docEntries.size - 1) { + A(docEntries[currentIndex + 1].path, { + classes(MarkdownLayoutStyle.nextPage) + }) { + Text("${docEntries[currentIndex + 1].navTitle} →") + } + } + } +} @Composable fun MarkdownLayout(content: @Composable () -> Unit) { @@ -20,8 +101,23 @@ fun MarkdownLayout(content: @Composable () -> Unit) { val context = rememberPageContext() val markdownData = context.markdown!!.frontMatter + var onMobile by remember { mutableStateOf(false) } + var revealed by remember { mutableStateOf(false) } + + onMobile = window.innerWidth < 768 + window.onresize = { + onMobile = window.innerWidth < 768 + } + PageLayout(markdownData["nav-title"]?.get(0) ?: "Untitled") { - DocTree() + Button({ + classes(MarkdownLayoutStyle.revealButton) + onClick { revealed = !revealed } + }) { + MdiChevronRight() + } + + DocSidebar(revealed, onClose = { revealed = false }) Div({ classes(MarkdownLayoutStyle.content) @@ -30,7 +126,42 @@ fun MarkdownLayout(content: @Composable () -> Unit) { setDescription(markdownData["description"]!![0]) } - content() + if (onMobile) { + Div({ + classes(MarkdownLayoutStyle.overlay) + if (revealed) classes("reveal") + }) {} + } + + Div({ + classes(MarkdownLayoutStyle.contentWrapper) + }) { + Div({ + classes(MarkdownLayoutStyle.mainContent) + }) { + content() + + Div({ + classes(MarkdownLayoutStyle.metadata) + }) { + markdownData["date-modified"]?.get(0)?.let { date -> + P { Text("Last updated: $date") } + } + + A( + "https://github.com/Ayfri/Kore/edit/master/website/src/jsMain/resources/markdown/${context.markdown!!.path}", + { + classes(MarkdownLayoutStyle.editLink) + }) { + Text("Edit this page on GitHub") + } + } + + PageNavigation(context.route.path) + } + + TableOfContents() + } } } } @@ -43,6 +174,10 @@ object MarkdownLayoutStyle : StyleSheet() { minHeight(100.vh) } + "html" style { + scrollBehavior(ScrollBehavior.Smooth) + } + "main" style { display(DisplayStyle.Flex) flexDirection(FlexDirection.Row) @@ -51,14 +186,17 @@ object MarkdownLayoutStyle : StyleSheet() { "h1" style { fontSize(3.cssRem) - marginY(3.cssRem) + marginY(2.cssRem) textAlign(TextAlign.Center) } + child(type("h1"), type("a")) style { + display(DisplayStyle.None) + } + "h2" style { fontSize(2.cssRem) - marginTop(2.5.cssRem) - marginBottom(1.5.cssRem) + marginY(1.5.cssRem) } "blockquote" style { @@ -67,6 +205,15 @@ object MarkdownLayoutStyle : StyleSheet() { borderLeft(3.px, LineStyle.Solid, rgba(1, 1, 1, 0.1)) } + "code" + not(attrContains("class", "language")) style { + fontFamily("Consolas", "Monaco", "Andale Mono", "Ubuntu Mono", "monospace") + fontSize(0.9.cssRem) + backgroundColor(GlobalStyle.secondaryBackgroundColor) + borderRadius(GlobalStyle.roundingButton) + paddingX(0.2.cssRem) + paddingY(0.1.cssRem) + } + child(type("div") + className("code-toolbar"), type("pre")) style { borderRadius(GlobalStyle.roundingButton) } @@ -74,15 +221,73 @@ object MarkdownLayoutStyle : StyleSheet() { smMax { "h1" { fontSize(2.5.cssRem) - marginY(2.cssRem) + lineHeight(LineHeight.Normal) + marginY(1.cssRem) } "h2" { - fontSize(2.cssRem) + fontSize(1.75.cssRem) + marginY(0.75.cssRem) + wordBreak(WordBreak.BreakAll) + } + + "h3" { + fontSize(1.25.cssRem) + marginY(0.5.cssRem) } } } + val revealButton by style { + backdropFilter(blur(5.px)) + backgroundColor(GlobalStyle.secondaryBackgroundColor) + border(0.px) + borderTopRightRadius(GlobalStyle.roundingButton) + borderBottomRightRadius(GlobalStyle.roundingButton) + color(GlobalStyle.textColor) + cursor(Cursor.Pointer) + display(DisplayStyle.None) + padding(0.2.cssRem) + position(Position.Fixed) + top(4.5.cssRem) + zIndex(10) + + smMax(self) { + display(DisplayStyle.Block) + } + + child(self, type("span")) style { + fontSize(2.cssRem) + } + } + + val overlay by style { + backgroundColor(rgba(0, 0, 0, 0.5)) + height(100.percent) + left(0.px) + opacity(0) + position(Position.Fixed) + top(0.px) + transition(0.2.s, "opacity") + width(100.percent) + zIndex(5) + + self + className("reveal") style { + opacity(1) + } + } + + val anchor by style { + color(GlobalStyle.inactiveLinkColor) + opacity(0.2) + textDecorationLine(TextDecorationLine.None) + transition(0.2.s, "opacity", "color") + + self + hover style { + opacity(1) + } + } + val content by style { display(DisplayStyle.Flex) flexDirection(FlexDirection.Column) @@ -92,4 +297,132 @@ object MarkdownLayoutStyle : StyleSheet() { width(100.percent) overflowX(Overflow.Auto) } + + val contentWrapper by style { + marginRight(16.cssRem) + + smMax(self) { + marginRight(0.px) + } + } + + val highlightAnimation by keyframes { + from { backgroundColor(rgba(0, 120, 215, 0.2)) } + to { backgroundColor(rgba(0, 120, 215, 0)) } + } + + val highlight by style { + animation(highlightAnimation) { + delay(0.6.s) + duration(1.s) + timingFunction(AnimationTimingFunction.Ease) + } + } + + val mainContent by style { + minWidth(0.px) + + "img" style { + borderRadius(GlobalStyle.roundingButton) + marginY(0.5.cssRem) + maxWidth(100.percent) + } + } + + val toc by style { + backgroundColor(GlobalStyle.secondaryBackgroundColor) + borderRadius(GlobalStyle.roundingButton) + maxHeight(80.vh - 4.cssRem) + maxWidth(16.cssRem) + overflowY(Overflow.Auto) + padding(1.5.cssRem) + position(Position.Fixed) + right(1.cssRem) + top(15.vh) + + smMax(self) { + display(DisplayStyle.None) + } + + "ul" style { + paddingLeft(1.5.cssRem) + } + } + + val tocEntry by style { + cursor(Cursor.Pointer) + padding(0.2.cssRem, 0.px) + color(GlobalStyle.textColor) + listStyle(ListStyleType.None) + transition(0.2.s, "color") + overflow(Overflow.Hidden) + textOverflow(TextOverflow.Ellipsis) + whiteSpace(WhiteSpace.NoWrap) + userSelect(UserSelect.None) + + self + hover style { + color(GlobalStyle.linkColor) + } + } + + val metadata by style { + marginTop(4.cssRem) + paddingTop(1.cssRem) + borderTop(1.px, LineStyle.Solid, GlobalStyle.tertiaryBackgroundColor) + color(GlobalStyle.altTextColor) + fontSize(0.9.cssRem) + } + + val editLink by style { + color(GlobalStyle.linkColor) + textDecorationLine(TextDecorationLine.None) + + self + hover style { + textDecorationLine(TextDecorationLine.Underline) + } + } + + val pageNav by style { + display(DisplayStyle.Flex) + justifyContent(JustifyContent.SpaceBetween) + marginTop(2.cssRem) + gap(1.cssRem) + } + + val prevPage by style { + color(GlobalStyle.linkColor) + textDecorationLine(TextDecorationLine.None) + + self + hover style { + textDecorationLine(TextDecorationLine.Underline) + } + } + + val nextPage by style { + color(GlobalStyle.linkColor) + textDecorationLine(TextDecorationLine.None) + marginLeft(autoLength) + + self + hover style { + textDecorationLine(TextDecorationLine.Underline) + } + } + + val heading by style { + borderRadius(GlobalStyle.roundingButton) + display(DisplayStyle.Flex) + flexDirection(FlexDirection.RowReverse) + justifyContent(JustifyContent.FlexEnd) + gap(0.5.cssRem) + padding(0.5.cssRem) + scrollMarginTop(1.5.cssRem) + + smMax(self) { + padding(0.35.cssRem) + } + + child(self + hover, className(anchor)) style { + opacity(1) + } + } } diff --git a/website/src/jsMain/kotlin/io/github/ayfri/kore/website/utils/HTMLComposables.kt b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/utils/HTMLComposables.kt index 13e0083b..03ca2e33 100644 --- a/website/src/jsMain/kotlin/io/github/ayfri/kore/website/utils/HTMLComposables.kt +++ b/website/src/jsMain/kotlin/io/github/ayfri/kore/website/utils/HTMLComposables.kt @@ -6,6 +6,7 @@ import org.jetbrains.compose.web.dom.ContentBuilder import org.jetbrains.compose.web.dom.ElementBuilder import org.jetbrains.compose.web.dom.TagElement import org.w3c.dom.HTMLDetailsElement +import org.w3c.dom.HTMLElement @Composable fun Details( @@ -30,3 +31,15 @@ fun Summary( content = content ) } + +@Composable +fun Search( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null, +) { + TagElement( + elementBuilder = ElementBuilder.createBuilder("search"), + applyAttrs = attrs, + content = content + ) +}