Enhance site navigation with improved search highlighting, refined navbar offset handling, query-based search term extraction, and additional docs layout adjustments. Also, add new resources and simplify deployment script paths.
This commit is contained in:
parent
d307ed2a04
commit
215c7245a0
@ -28,7 +28,7 @@ function checkState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# default target settings
|
# default target settings
|
||||||
case $1 in
|
case "com" in
|
||||||
com)
|
com)
|
||||||
SSH_HOST=sergeych@lynglang.com # host to deploy to
|
SSH_HOST=sergeych@lynglang.com # host to deploy to
|
||||||
SSH_PORT=22 # ssh port on it
|
SSH_PORT=22 # ssh port on it
|
||||||
@ -48,11 +48,7 @@ die() { echo "ERROR: $*" 1>&2 ; exit 1; }
|
|||||||
#rm build/distributions/*.js > /dev/null
|
#rm build/distributions/*.js > /dev/null
|
||||||
#rm build/distributions/*.js.map > /dev/null
|
#rm build/distributions/*.js.map > /dev/null
|
||||||
|
|
||||||
#./gradlew clean incrementRevision jsBrowserDistribution || die "compilation failed"
|
./gradlew site:clean site:jsBrowserDistribution || die "compilation failed"
|
||||||
#./gradlew incrementRevision jsBrowserDistribution
|
|
||||||
#./gradlew jsBrowserDistribution
|
|
||||||
./gradlew incrementRevision jsBrowserDistribution
|
|
||||||
#./gradlew jsBrowserDistribution
|
|
||||||
|
|
||||||
if [[ $? != 0 ]]; then
|
if [[ $? != 0 ]]; then
|
||||||
echo
|
echo
|
||||||
@ -78,7 +74,7 @@ ssh -p ${SSH_PORT} ${SSH_HOST} "
|
|||||||
";
|
";
|
||||||
|
|
||||||
# sync files
|
# sync files
|
||||||
SRC=./build/dist/js/productionExecutable
|
SRC=./site/build/dist/js/productionExecutable
|
||||||
rsync -e "ssh -p ${SSH_PORT}" -avz -r -d --delete ${SRC}/* ${SSH_HOST}:${ROOT}/build/dist
|
rsync -e "ssh -p ${SSH_PORT}" -avz -r -d --delete ${SRC}/* ${SSH_HOST}:${ROOT}/build/dist
|
||||||
checkState
|
checkState
|
||||||
rsync -e "ssh -p ${SSH_PORT}" -avz ./static/* ${SSH_HOST}:${ROOT}/build/dist
|
rsync -e "ssh -p ${SSH_PORT}" -avz ./static/* ${SSH_HOST}:${ROOT}/build/dist
|
||||||
|
|||||||
@ -102,6 +102,123 @@ external object JSON {
|
|||||||
|
|
||||||
// JS global encodeURI binding (to safely request paths that may contain non-ASCII)
|
// JS global encodeURI binding (to safely request paths that may contain non-ASCII)
|
||||||
external fun encodeURI(uri: String): String
|
external fun encodeURI(uri: String): String
|
||||||
|
// URL encoding helpers for query parameters
|
||||||
|
external fun encodeURIComponent(s: String): String
|
||||||
|
external fun decodeURIComponent(s: String): String
|
||||||
|
|
||||||
|
// Ensure global scroll offset styles and keep a CSS var with the real fixed-top navbar height.
|
||||||
|
// This guarantees that scrolling to anchors or search hits is not hidden underneath the top bar.
|
||||||
|
private fun ensureScrollOffsetStyles() {
|
||||||
|
try {
|
||||||
|
val doc = window.document
|
||||||
|
if (doc.getElementById("scroll-offset-style") == null) {
|
||||||
|
val style = doc.createElement("style") as org.w3c.dom.HTMLStyleElement
|
||||||
|
style.id = "scroll-offset-style"
|
||||||
|
style.textContent = (
|
||||||
|
"""
|
||||||
|
/* Keep a dynamic CSS variable with the measured navbar height */
|
||||||
|
:root { --navbar-offset: 56px; }
|
||||||
|
|
||||||
|
/* Make native hash jumps and programmatic scroll account for the fixed header */
|
||||||
|
html, body { scroll-padding-top: calc(var(--navbar-offset) + 8px); }
|
||||||
|
|
||||||
|
/* When scrolled into view, keep headings and any id-targeted element below the topbar */
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2,
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6,
|
||||||
|
.markdown-body [id] { scroll-margin-top: calc(var(--navbar-offset) + 8px); }
|
||||||
|
|
||||||
|
/* Also offset search highlights as they can be the initial scroll target */
|
||||||
|
mark.search-hit { scroll-margin-top: calc(var(--navbar-offset) + 8px) !important; }
|
||||||
|
"""
|
||||||
|
.trimIndent()
|
||||||
|
)
|
||||||
|
doc.head?.appendChild(style)
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// Best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure the current fixed-top navbar height and update the CSS variable
|
||||||
|
private fun updateNavbarOffsetVar(): Int {
|
||||||
|
return try {
|
||||||
|
val doc = window.document
|
||||||
|
val nav = doc.querySelector("nav.navbar.fixed-top") as? HTMLElement
|
||||||
|
val px = if (nav != null) kotlin.math.round(nav.getBoundingClientRect().height).toInt() else 0
|
||||||
|
doc.documentElement?.let { root ->
|
||||||
|
root.asDynamic().style?.setProperty?.invoke(root.asDynamic().style, "--navbar-offset", "${'$'}{px}px")
|
||||||
|
}
|
||||||
|
px
|
||||||
|
} catch (_: Throwable) { 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure global CSS for search highlights is present (bright, visible everywhere)
|
||||||
|
private fun ensureSearchHighlightStyles() {
|
||||||
|
try {
|
||||||
|
val doc = window.document
|
||||||
|
if (doc.getElementById("search-hit-style") == null) {
|
||||||
|
val style = doc.createElement("style") as org.w3c.dom.HTMLStyleElement
|
||||||
|
style.id = "search-hit-style"
|
||||||
|
// Use strong colors and !important to outshine theme/code styles
|
||||||
|
style.textContent = (
|
||||||
|
"""
|
||||||
|
mark.search-hit {
|
||||||
|
background: #ffeb3b !important; /* bright yellow */
|
||||||
|
color: #000 !important;
|
||||||
|
padding: 0 .1em;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 0 0 2px #ffeb3b inset !important;
|
||||||
|
}
|
||||||
|
code mark.search-hit, pre mark.search-hit {
|
||||||
|
background: #ffd54f !important; /* slightly deeper in code blocks */
|
||||||
|
color: #000 !important;
|
||||||
|
box-shadow: 0 0 0 2px #ffd54f inset !important;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
.trimIndent()
|
||||||
|
)
|
||||||
|
doc.head?.appendChild(style)
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// Best-effort; if styles can't be injected we still proceed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure docs layout tweaks (reduce markdown body top margin to align with TOC)
|
||||||
|
private fun ensureDocsLayoutStyles() {
|
||||||
|
try {
|
||||||
|
val doc = window.document
|
||||||
|
if (doc.getElementById("docs-layout-style") == null) {
|
||||||
|
val style = doc.createElement("style") as org.w3c.dom.HTMLStyleElement
|
||||||
|
style.id = "docs-layout-style"
|
||||||
|
style.textContent = (
|
||||||
|
"""
|
||||||
|
/* Align the markdown content top edge with the TOC */
|
||||||
|
.markdown-body {
|
||||||
|
margin-top: 0 !important; /* remove extra outer spacing */
|
||||||
|
padding-top: 0 !important; /* override GitHub markdown default top padding */
|
||||||
|
}
|
||||||
|
/* Ensure the first element inside markdown body doesn't add extra space */
|
||||||
|
.markdown-body > :first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
/* Some markdown renderers give H1 extra top margin; neutralize when first */
|
||||||
|
.markdown-body h1:first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
.trimIndent()
|
||||||
|
)
|
||||||
|
doc.head?.appendChild(style)
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// Best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App() {
|
fun App() {
|
||||||
@ -127,6 +244,24 @@ fun App() {
|
|||||||
initTopSearch()
|
initTopSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure global docs layout styles are present once
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
ensureDocsLayoutStyles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure scroll offset styles exist and keep navbar offset in sync
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
ensureScrollOffsetStyles()
|
||||||
|
updateNavbarOffsetVar()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute navbar offset on resize and when effect is disposed
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
val handler: (org.w3c.dom.events.Event) -> Unit = { updateNavbarOffsetVar() }
|
||||||
|
window.addEventListener("resize", handler)
|
||||||
|
onDispose { window.removeEventListener("resize", handler) }
|
||||||
|
}
|
||||||
|
|
||||||
// Listen to hash changes (routing)
|
// Listen to hash changes (routing)
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
val listener: (org.w3c.dom.events.Event) -> Unit = {
|
val listener: (org.w3c.dom.events.Event) -> Unit = {
|
||||||
@ -248,13 +383,97 @@ private fun UnsafeRawHtml(html: String) {
|
|||||||
fun currentRoute(): String = window.location.hash.removePrefix("#/")
|
fun currentRoute(): String = window.location.hash.removePrefix("#/")
|
||||||
|
|
||||||
fun routeToPath(route: String): String {
|
fun routeToPath(route: String): String {
|
||||||
val noFrag = stripFragment(route)
|
val noParams = stripQuery(stripFragment(route))
|
||||||
return if (noFrag.startsWith("docs/")) noFrag else "docs/$noFrag"
|
return if (noParams.startsWith("docs/")) noParams else "docs/$noParams"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip trailing fragment from a route like "docs/file.md#anchor" -> "docs/file.md"
|
// Strip trailing fragment from a route like "docs/file.md#anchor" -> "docs/file.md"
|
||||||
fun stripFragment(route: String): String = route.substringBefore('#')
|
fun stripFragment(route: String): String = route.substringBefore('#')
|
||||||
|
|
||||||
|
// Strip query from a route like "docs/file.md?q=term" -> "docs/file.md"
|
||||||
|
fun stripQuery(route: String): String = route.substringBefore('?')
|
||||||
|
|
||||||
|
// Extract lowercase search terms from route query string (?q=...)
|
||||||
|
fun extractSearchTerms(route: String): List<String> {
|
||||||
|
val queryPart = route.substringAfter('?', "")
|
||||||
|
if (queryPart.isEmpty()) return emptyList()
|
||||||
|
val params = queryPart.split('&')
|
||||||
|
val qParam = params.firstOrNull { it.startsWith("q=") }?.substringAfter('=') ?: return emptyList()
|
||||||
|
val decoded = try { decodeURIComponent(qParam) } catch (_: dynamic) { qParam }
|
||||||
|
return decoded.trim().split(Regex("\\s+"))
|
||||||
|
.map { it.lowercase() }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight words in root that start with any of the terms (case-insensitive). Returns count of hits.
|
||||||
|
fun highlightSearchHits(root: HTMLElement, terms: List<String>): Int {
|
||||||
|
if (terms.isEmpty()) return 0
|
||||||
|
// Make sure CSS for highlighting is injected
|
||||||
|
ensureSearchHighlightStyles()
|
||||||
|
// Remove previous highlights
|
||||||
|
try {
|
||||||
|
val prev = root.getElementsByClassName("search-hit")
|
||||||
|
// Because HTMLCollection is live, copy to array first
|
||||||
|
val arr = (0 until prev.length).mapNotNull { prev.item(it) as? HTMLElement }.toList()
|
||||||
|
arr.forEach { mark ->
|
||||||
|
val parent = mark.parentNode
|
||||||
|
val textNode = root.ownerDocument!!.createTextNode(mark.textContent ?: "")
|
||||||
|
parent?.replaceChild(textNode, mark)
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {}
|
||||||
|
|
||||||
|
// Allow highlighting even inside CODE and PRE per request; still skip scripts, styles, and keyboard samples
|
||||||
|
val skipTags = setOf("SCRIPT", "STYLE", "KBD", "SAMP")
|
||||||
|
var hits = 0
|
||||||
|
|
||||||
|
fun processNode(node: org.w3c.dom.Node) {
|
||||||
|
when (node.nodeType) {
|
||||||
|
org.w3c.dom.Node.ELEMENT_NODE -> {
|
||||||
|
val el = node.unsafeCast<HTMLElement>()
|
||||||
|
if (skipTags.contains(el.tagName)) return
|
||||||
|
if (el.classList.contains("no-search")) return
|
||||||
|
// copy list as array, because modifying tree during iteration
|
||||||
|
val children = mutableListOf<org.w3c.dom.Node>()
|
||||||
|
val cn = el.childNodes
|
||||||
|
for (i in 0 until cn.length) children.add(cn.item(i)!!)
|
||||||
|
children.forEach { processNode(it) }
|
||||||
|
}
|
||||||
|
org.w3c.dom.Node.TEXT_NODE -> {
|
||||||
|
val text = node.nodeValue ?: return
|
||||||
|
if (text.isBlank()) return
|
||||||
|
// Tokenize by words and rebuild
|
||||||
|
val parent = node.parentNode ?: return
|
||||||
|
val doc = root.ownerDocument!!
|
||||||
|
val container = doc.createDocumentFragment()
|
||||||
|
var pos = 0
|
||||||
|
val wordRegex = Regex("[A-Za-z0-9_]+")
|
||||||
|
for (m in wordRegex.findAll(text)) {
|
||||||
|
val start = m.range.first
|
||||||
|
val end = m.range.last + 1
|
||||||
|
if (start > pos) container.appendChild(doc.createTextNode(text.substring(pos, start)))
|
||||||
|
val token = m.value
|
||||||
|
val tokenLower = token.lowercase()
|
||||||
|
val match = terms.any { tokenLower.startsWith(it) }
|
||||||
|
if (match) {
|
||||||
|
val mark = doc.createElement("mark") as HTMLElement
|
||||||
|
mark.className = "search-hit"
|
||||||
|
mark.textContent = token
|
||||||
|
container.appendChild(mark)
|
||||||
|
hits++
|
||||||
|
} else {
|
||||||
|
container.appendChild(doc.createTextNode(token))
|
||||||
|
}
|
||||||
|
pos = end
|
||||||
|
}
|
||||||
|
if (pos < text.length) container.appendChild(doc.createTextNode(text.substring(pos)))
|
||||||
|
parent.replaceChild(container, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processNode(root)
|
||||||
|
return hits
|
||||||
|
}
|
||||||
|
|
||||||
fun renderMarkdown(src: String): String =
|
fun renderMarkdown(src: String): String =
|
||||||
highlightLyngHtml(
|
highlightLyngHtml(
|
||||||
ensureBootstrapCodeBlocks(
|
ensureBootstrapCodeBlocks(
|
||||||
@ -450,6 +669,19 @@ private fun DocsPage(
|
|||||||
val initialId = frag ?: newToc.firstOrNull()?.id
|
val initialId = frag ?: newToc.firstOrNull()?.id
|
||||||
setActiveTocId(initialId)
|
setActiveTocId(initialId)
|
||||||
|
|
||||||
|
// Highlight search hits if navigated via search (?q=...) and scroll to the first occurrence
|
||||||
|
val terms = extractSearchTerms(route)
|
||||||
|
if (terms.isNotEmpty()) {
|
||||||
|
// Perform highlighting after typesetting and TOC build
|
||||||
|
val count = try { highlightSearchHits(el, terms) } catch (_: Throwable) { 0 }
|
||||||
|
if (count > 0 && frag == null) {
|
||||||
|
try {
|
||||||
|
val first = el.getElementsByClassName("search-hit").item(0) as? HTMLElement
|
||||||
|
first?.scrollIntoView()
|
||||||
|
} catch (_: Throwable) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!frag.isNullOrBlank()) {
|
if (!frag.isNullOrBlank()) {
|
||||||
val target = el.ownerDocument?.getElementById(frag)
|
val target = el.ownerDocument?.getElementById(frag)
|
||||||
(target as? HTMLElement)?.scrollIntoView()
|
(target as? HTMLElement)?.scrollIntoView()
|
||||||
@ -766,8 +998,11 @@ private fun renderSearchResults(input: HTMLInputElement, menu: HTMLDivElement, q
|
|||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
val path = a.getAttribute("data-path")
|
val path = a.getAttribute("data-path")
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
dlog("nav", "search click -> navigate #/$path")
|
val inputEl = input
|
||||||
window.location.hash = "#/$path"
|
val q = inputEl.value
|
||||||
|
val suffix = if (q.isNotBlank()) "?q=" + encodeURIComponent(q) else ""
|
||||||
|
dlog("nav", "search click -> navigate #/$path$suffix")
|
||||||
|
window.location.hash = "#/$path$suffix"
|
||||||
menu.classList.remove("show")
|
menu.classList.remove("show")
|
||||||
closeNavbarCollapse()
|
closeNavbarCollapse()
|
||||||
}
|
}
|
||||||
@ -870,8 +1105,9 @@ private fun initTopSearch(attempt: Int = 0) {
|
|||||||
val results = performSearch(q)
|
val results = performSearch(q)
|
||||||
val best = results.maxByOrNull { scoreQuery(q, it) }
|
val best = results.maxByOrNull { scoreQuery(q, it) }
|
||||||
if (best != null) {
|
if (best != null) {
|
||||||
dlog("nav", "Enter -> navigate #/${best.path}")
|
val suffix = if (q.isNotBlank()) "?q=" + encodeURIComponent(q) else ""
|
||||||
window.location.hash = "#/${best.path}"
|
dlog("nav", "Enter -> navigate #/${best.path}$suffix")
|
||||||
|
window.location.hash = "#/${best.path}$suffix"
|
||||||
hideSearchResults(menu)
|
hideSearchResults(menu)
|
||||||
closeNavbarCollapse()
|
closeNavbarCollapse()
|
||||||
} else {
|
} else {
|
||||||
@ -1028,7 +1264,7 @@ assertEquals([4, 16], evens)
|
|||||||
// Short features list
|
// Short features list
|
||||||
Div({ classes("row", "g-4", "mt-1") }) {
|
Div({ classes("row", "g-4", "mt-1") }) {
|
||||||
listOf(
|
listOf(
|
||||||
Triple("Fast to learn", "Familiar constructs and readable patterns — be productive in minutes.", "bolt"),
|
Triple("Fast to learn", "Familiar constructs and readable patterns — be productive in minutes.", "lightning"),
|
||||||
Triple("Portable", "Runs wherever Kotlin runs: reuse logic across platforms.", "globe2"),
|
Triple("Portable", "Runs wherever Kotlin runs: reuse logic across platforms.", "globe2"),
|
||||||
Triple("Pragmatic", "A standard library that solves real problems without ceremony.", "gear-fill")
|
Triple("Pragmatic", "A standard library that solves real problems without ceremony.", "gear-fill")
|
||||||
).forEach { (title, text, icon) ->
|
).forEach { (title, text, icon) ->
|
||||||
|
|||||||
@ -68,9 +68,9 @@
|
|||||||
:root { --navbar-offset: 56px; }
|
:root { --navbar-offset: 56px; }
|
||||||
body {
|
body {
|
||||||
/* Fallback padding; JS will keep this equal to the real navbar height */
|
/* Fallback padding; JS will keep this equal to the real navbar height */
|
||||||
padding-top: var(--navbar-offset);
|
/*padding-top: var(--navbar-offset);*/
|
||||||
/* Make native hash jumps account for the fixed header */
|
/* Make native hash jumps account for the fixed header */
|
||||||
scroll-padding-top: calc(var(--navbar-offset) + 8px);
|
/*scroll-padding-top: calc(var(--navbar-offset) + 8px);*/
|
||||||
}
|
}
|
||||||
/* Also offset scroll for headings and any element targeted by an id */
|
/* Also offset scroll for headings and any element targeted by an id */
|
||||||
.markdown-body h1,
|
.markdown-body h1,
|
||||||
@ -80,7 +80,7 @@
|
|||||||
.markdown-body h5,
|
.markdown-body h5,
|
||||||
.markdown-body h6,
|
.markdown-body h6,
|
||||||
[id] {
|
[id] {
|
||||||
scroll-margin-top: calc(var(--navbar-offset) + 8px);
|
/*scroll-margin-top: calc(var(--navbar-offset) + 8px);*/
|
||||||
}
|
}
|
||||||
/* Unify markdown and page backgrounds with Bootstrap theme variables */
|
/* Unify markdown and page backgrounds with Bootstrap theme variables */
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
@ -226,7 +226,7 @@
|
|||||||
// Keep legacy inline padding for older browsers
|
// Keep legacy inline padding for older browsers
|
||||||
document.body.style.paddingTop = px;
|
document.body.style.paddingTop = px;
|
||||||
// And expose as a CSS variable used by scroll-padding/scroll-margin rules
|
// And expose as a CSS variable used by scroll-padding/scroll-margin rules
|
||||||
document.documentElement.style.setProperty('--navbar-offset', px);
|
// document.documentElement.style.setProperty('--navbar-offset', px);
|
||||||
}
|
}
|
||||||
window.addEventListener('load', adjustPadding);
|
window.addEventListener('load', adjustPadding);
|
||||||
window.addEventListener('resize', adjustPadding);
|
window.addEventListener('resize', adjustPadding);
|
||||||
|
|||||||
37
site/src/jsMain/resources/student-cap-black.svg
Normal file
37
site/src/jsMain/resources/student-cap-black.svg
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
-
|
||||||
|
- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
- you may not use this file except in compliance with the License.
|
||||||
|
- You may obtain a copy of the License at
|
||||||
|
-
|
||||||
|
- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
-
|
||||||
|
- Unless required by applicable law or agreed to in writing, software
|
||||||
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
- See the License for the specific language governing permissions and
|
||||||
|
- limitations under the License.
|
||||||
|
-
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg height="800px" width="800px" version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 512 512" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#000000;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M505.837,180.418L279.265,76.124c-7.349-3.385-15.177-5.093-23.265-5.093c-8.088,0-15.914,1.708-23.265,5.093
|
||||||
|
L6.163,180.418C2.418,182.149,0,185.922,0,190.045s2.418,7.896,6.163,9.627l226.572,104.294c7.349,3.385,15.177,5.101,23.265,5.101
|
||||||
|
c8.088,0,15.916-1.716,23.267-5.101l178.812-82.306v82.881c-7.096,0.8-12.63,6.84-12.63,14.138c0,6.359,4.208,11.864,10.206,13.618
|
||||||
|
l-12.092,79.791h55.676l-12.09-79.791c5.996-1.754,10.204-7.259,10.204-13.618c0-7.298-5.534-13.338-12.63-14.138v-95.148
|
||||||
|
l21.116-9.721c3.744-1.731,6.163-5.504,6.163-9.627S509.582,182.149,505.837,180.418z"/>
|
||||||
|
<path class="st0" d="M256,346.831c-11.246,0-22.143-2.391-32.386-7.104L112.793,288.71v101.638
|
||||||
|
c0,22.314,67.426,50.621,143.207,50.621c75.782,0,143.209-28.308,143.209-50.621V288.71l-110.827,51.017
|
||||||
|
C278.145,344.44,267.25,346.831,256,346.831z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
37
site/src/jsMain/resources/student-cap-white.svg
Normal file
37
site/src/jsMain/resources/student-cap-white.svg
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
-
|
||||||
|
- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
- you may not use this file except in compliance with the License.
|
||||||
|
- You may obtain a copy of the License at
|
||||||
|
-
|
||||||
|
- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
-
|
||||||
|
- Unless required by applicable law or agreed to in writing, software
|
||||||
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
- See the License for the specific language governing permissions and
|
||||||
|
- limitations under the License.
|
||||||
|
-
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg height="800px" width="800px" version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 512 512" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#000000;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M505.837,180.418L279.265,76.124c-7.349-3.385-15.177-5.093-23.265-5.093c-8.088,0-15.914,1.708-23.265,5.093
|
||||||
|
L6.163,180.418C2.418,182.149,0,185.922,0,190.045s2.418,7.896,6.163,9.627l226.572,104.294c7.349,3.385,15.177,5.101,23.265,5.101
|
||||||
|
c8.088,0,15.916-1.716,23.267-5.101l178.812-82.306v82.881c-7.096,0.8-12.63,6.84-12.63,14.138c0,6.359,4.208,11.864,10.206,13.618
|
||||||
|
l-12.092,79.791h55.676l-12.09-79.791c5.996-1.754,10.204-7.259,10.204-13.618c0-7.298-5.534-13.338-12.63-14.138v-95.148
|
||||||
|
l21.116-9.721c3.744-1.731,6.163-5.504,6.163-9.627S509.582,182.149,505.837,180.418z"/>
|
||||||
|
<path class="st0" d="M256,346.831c-11.246,0-22.143-2.391-32.386-7.104L112.793,288.71v101.638
|
||||||
|
c0,22.314,67.426,50.621,143.207,50.621c75.782,0,143.209-28.308,143.209-50.621V288.71l-110.827,51.017
|
||||||
|
C278.145,344.44,267.25,346.831,256,346.831z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
Loading…
x
Reference in New Issue
Block a user