First commit.

This commit is contained in:
2025-11-30 14:11:42 +05:30
commit f5b4556120
30 changed files with 2244 additions and 0 deletions

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2019, Kailash Nadh
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

6
README.md Normal file
View File

@@ -0,0 +1,6 @@
# dictmaker-alar
This repository is the [dictmaker](https://dict.press) site theme for the Kannada-English dictionary site [Alar.ink](https://alar.ink)
This can be cloned and used as a theme for building dictionary site themes for dictpress.
Licensed under the MIT License.

110
base.html Normal file
View File

@@ -0,0 +1,110 @@
{{- define "header" -}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
{{- block "meta" . -}}
<title>
{{- if eq .Data.PageType "/" }} {{- .L.T "global.siteName" -}}
{{- else if eq .Data.PageType "glossary" }}{{- .L.T "public.glossaryTitle" -}}
{{- else if eq .Data.PageType "search" }}{{- .L.Ts "public.searchTitle" "query" .Data.Query.Query -}}
{{- else if ne .Data.Title "" }}{{ .Data.Title }}
{{- end -}}
</title>
<meta name="description" value="
{{- if eq .Data.PageType "/" }}Dictionary website
{{- else if eq .Data.PageType "glossary" }}Glossary of words.
{{- else if eq .Data.PageType "search" }}{{ .Data.Query.Query }} meaning.
{{- else if ne .Data.Description "" }}{{ .Data.Description }}
{{- else }}{{ block "description" . }}{{end}}
{{- end -}}" />
{{- end -}}
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script>
window._ROOT_URL = "";
window._MAX_CONTENT_ITEMS = {{ .Consts.SiteMaxEntryContentItems }};
</script>
<link rel="shortcut icon" href="/static/favicon.png?v={{ .AssetVer }}" type="image/x-icon" />
<link href="/static/flexit.css?v={{ .AssetVer }}" rel="stylesheet" type="text/css" />
<link href="/static/style.css?v={{ .AssetVer }}" rel="stylesheet" type="text/css" />
</head>
<body class="{{ if eq .Data.PageType "/"}}home{{ end }}">
<div class="container">
<section class="main">
<header class="header">
<div class="row">
<div class="logo four columns">
<a href="/"><img src="/static/logo.svg?v={{ .AssetVer }}" alt="Dictionary logo" /></a>
<h3 class="intro">
<p>ಶ್ರೀ. ವಿ. ಕೃಷ್ಣ ಅವರ ಕನ್ನಡ - ಇಂಗ್ಲಿಷ್ ನಿಘಂಟು</p>
“Alar” V. Krishna's Kannada → English dictionary
</h3>
</div><!-- logo col -->
<div class="search eight columns">
<form class="search-form" method="get" action="">
<div>
<select name="lang" aria-label="Select dictionary">
{{ range $d := .Dicts }}
{{ $from := index $d 0}}
{{ $to := index $d 1}}
<option value="{{ $from.ID }}/{{ $to.ID }}">
{{ $from.Name }} - {{ $to.Name }}
</option>
{{ end }}
</select>
<div class="input-group">
<input autofocus autocomplete="off" required placeholder="" aria-label="Search word"
type="text" id="q" name="q" value="{{ if .Data.Query }}{{ .Data.Query.Query }}{{ end }}" />
<button type="submit" aria-label="Search"><img src="/static/search.svg?v={{ .AssetVer }}" alt="{{- .L.T "global.btnSearch" -}}" /></button>
</div>
</div>
</form>
</div>
</div>
</header>
{{ end}}
{{ define "footer" }}
<nav class="nav">
{{ if .Consts.EnableSubmissions }}
<a href="{{ $.Consts.RootURL }}/submit">{{- .L.T "public.submitEntry" -}}</a>
{{ end }}
{{ if .Consts.EnableGlossary }}
{{ range $d := .Dicts }}
{{ $from := index $d 0}}
{{ $to := index $d 1}}
<a href="{{ $.Consts.RootURL }}/glossary/{{ $from.ID }}/{{ $to.ID }}/*" class="tab">
{{ title ($.L.Ts "public.glossary" "lang" "") }}
</a>
{{ end }}
{{ end }}
<a href="/p/about">{{ .L.T "public.about" }}</a>
</nav>
</section>
<footer class="footer">
<a href="https://indicarchive.org">IDAF</a>
<a href="https://zerodha.tech/blog/alar-the-making-of-an-open-source-dictionary/">Support by <img src="/static/zerodha-logo.svg" alt="Zerodha"></a>
</footer>
</div><!-- container -->
<form class="box form-comments" data-success="{{ .L.T "public.suggestSubmitted" }}">
<div>
<h4>{{ .L.T "public.submitTitle" }}</h4>
<textarea name="comments" autofocus maxlength="500"></textarea>
<p>
<button type="submit">{{ .L.T "public.suggestSubmitBtn" }}</button>
<button class="button-outline close">{{ .L.T "global.btnClose" }}</button>
</p>
</div>
</form>
<script type="module" src="/static/_bundle.js?f=autocomp.js&f=share.js&f=main.js&f=alar.js&v={{ .AssetVer }}"></script>
</body>
</html>
{{ end }}

13
glossary-words.html Normal file
View File

@@ -0,0 +1,13 @@
{{ define "glossary-words" }}
{{ $g := .Data.Glossary }}
{{ if not $g.Words }}
<h2>{{ $.L.T "public.noResultsTitle" }}</h2>
<p>{{ $.L.T "public.noResults" }}</p>
{{ else }}
<ul class="noul words">
{{ range $i, $w := $g.Words }}
<li><a href="{{ $.Consts.RootURL }}/dictionary/{{ UnicodeURL $g.FromLang }}/{{ UnicodeURL $g.ToLang }}/{{ UnicodeURL (index $w.Content 0) }}">{{ $w.Content | join ", " }}</a></li>
{{ end }}
</ul>
{{ end }}
{{ end }}

22
glossary.html Normal file
View File

@@ -0,0 +1,22 @@
{{ define "glossary" }}
{{ template "header" . }}
<section class="content glossary">
{{ if not .Data.Initials }}
<h2>{{ .L.T "public.glossaryTitle" }}</h2>
<p>{{ .L.T "public.noResults" }}</p>
{{ else }}
<nav class="index">
{{ range $k, $a := .Data.Initials }}
<a href="{{ UnicodeURL $a }}"{{ if eq $a $.Data.Initial }} class="sel"{{ end }}>{{ $a }}</a>
{{ end }}
</nav>
<nav class="pagination top">{{ .Data.PgBar }}</nav>
{{ template "glossary-words" . }}
<nav class="pagination bottom">{{ .Data.PgBar }}</nav>
{{ end}}
</section>
{{ template "footer" . }}
{{ end }}

4
index.html Normal file
View File

@@ -0,0 +1,4 @@
{{ define "index" }}
{{ template "header" . }}
{{ template "footer" . }}
{{ end }}

36
lang.json Normal file
View File

@@ -0,0 +1,36 @@
{
"_.code": "en",
"_.name": "English",
"global.btnClose": "Close",
"global.btnDelete": "Delete",
"global.btnSearch": "Search",
"global.siteName": "Dictionary",
"public.errorMessage": "An error occurred. Please try later.",
"public.errorTitle": "Error",
"public.about": "About",
"public.glossary": "{lang} glossary",
"public.glossaryTitle": "Glossary of words",
"public.mainTitle": "Dictionary website",
"public.noResults": "No results where found for the query.",
"public.noResultsTitle": "No results",
"public.searchTitle": "{query} English meaning - Alar",
"public.similarTitle": "Similar words",
"public.playAudio": "Play audio",
"public.subTitle": "Kannada-English dictionary",
"public.submitEntry": "Suggest new entry",
"public.submitEntryTitle": "Suggest new entry",
"public.submitTitle": "Comments and suggestions",
"public.suggestAddDefBtn": "Add another definition",
"public.suggestContent": "Content",
"public.suggestDefLang": "Definition language",
"public.suggestDefsTitle": "Definitions",
"public.suggestEdit": "Suggest edit for \"{word}\"",
"public.suggestEntryLang": "Entry language",
"public.suggestPhones": "Phonetic notations (pronunciation)",
"public.suggestPhonesPlaceholder": "eg: pɛts, pɛt (commma separated)",
"public.suggestSubmitBtn": "Submit for review",
"public.suggestSubmitted": "Submitted for review successfully",
"public.suggestTitle": "Suggest a new entry",
"public.viewMore": "View {num} more",
"public.sourceTag": "Source of the entry"
}

10
message.html Normal file
View File

@@ -0,0 +1,10 @@
{{ define "message" }}
{{ template "header" . }}
<section class="content">
<h1>{{ .Data.Title }}</h1>
<p>{{ .Data.Description }}</p>
</section>
{{ template "footer" . }}
{{ end }}

73
pages/about.html Normal file
View File

@@ -0,0 +1,73 @@
{{ define "meta" }}
<title>About V. Krishna's Alar (Kannada-English dictionary)</title>
<meta name="description" content="About V. Krishna's Alar (Kannada-English dictionary)" />
{{ end }}
{{ define "page-about" }}
{{ template "header" . }}
<section class="page box">
<h1>About</h1>
<p>
<em>Alar</em> is an authoritative Kannada-English dictionary corpus
created by V. Krishna. It contains over 150,000 Kannada words
with over 240,000 English definitions. It is released
as an open data corpus licensed under
the Open Database License (<a href="https://opendatacommons.org/licenses/odbl/summary/index.html">ODC-ODbL</a>).
</p>
<hr />
<h3>V. Krishna</h3>
<p>
<img src="/static/vkrishna.png" alt="Photo of V. Krishna" class="photo" style="float: left; max-width: 200px; margin: 0 15px 15px 0; vertical-align: middle;" />
V. Krishna started building his Kannada-English dictionary in the 1970s
as a hobby project. This incredible endeavour spanning four decades
has now evolved into an invaluable contribution to Kannada language.
In addition to authoring the colossal dictionary, he single-handedly digitised
his original manuscripts. In 2019, he open sourced the entire dictionary.
<a href="https://zerodha.tech/blog/alar-the-making-of-an-open-source-dictionary">Read the full story here</a>.
</p>
<p>
He is a resident of Bengaluru and spends his time working on his dictionary
and other Kannada literature projects. He has recently started laying
foundations for a new English-Kannada dictionary. He can be reached at
<a href="mailto:vkrishna1411&#064;yahoo.co.in">vkrishna1411&#064;yahoo.co.in</a>.
</p>
<br /><br />
<hr />
<h3>Data</h3>
<p>
The corpus is available on the <a href="https://github.com/alar-dict">Alar repository</a>.
It is licensed under <a href="https://opendatacommons.org/licenses/odbl/summary/index.html">ODC-ODbL</a>.
</p>
<h3>Website</h3>
<p>
This website is published using <a href="https://github.com/knadh/dictpress">dictpress</a>,
and uses <a href="https://github.com/knadh/knphone">knphone</a>, a Kannada phonetic
indexing algorithm, for search. Search suggestions and transliterations are powered by <a href="https://github.com/varnamproject">Varnam</a>.
The website's source is <a href="https://github.com/alar-dict/alar.ink">available here</a>.
</p>
<p>
Technical feedback can be sent to <a href="mailto:kailash&#064;nadh.in">kailash&#064;nadh.in</a>.
</p>
<hr />
<p><a href="https://zerodha.com"><img height="18px" src="/static/zerodha-logo.svg"
alt="Zerodha" /></a></p>
<p>In 2019, <a href="https://zerodha.tech/blog/alar-the-making-of-an-open-source-dictionary">Zerodha collaborated</a> with
V. Krishna to open source and publish his dictionary online and
awarded him a grant to support his work.
</p>
<hr />
<p><a href="https://indicarchive.org"><img height="72px" src="/static/idaf-logo.svg"
alt="Indic Archive" /></a></p>
<p>Since 2023, Alar is a project under Indic Digital Archive Foundation.</p>
</section>
{{ template "footer" . }}
{{ end }}

97
results.html Normal file
View File

@@ -0,0 +1,97 @@
{{ define "results" }}
{{ $maxContentItems := .Consts.SiteMaxEntryContentItems }}
{{ $numResults := (min (len .Data.Results.Entries) 10) }}
<div class="row">
<div class="eight columns">
<ol class="entries">
{{- range $k, $r := (mustSlice .Data.Results.Entries 0 $numResults) -}}
<li class="entry" data-guid="{{ $r.GUID }}" data-head="{{ $r.Content | join ", " }}">
<header class="head">
<div>
<div class="title-wrap">
<h3 class="title">{{ $r.Content | join ", " }}</h3>
{{- if $r.Meta.audio -}}
<a href="{{ $r.Meta.audio }}" class="audio" data-audio><img src="/static/audio.svg?v={{ $.AssetVer }}" alt="{{ $.L.T "public.playAudio" }}"></a>
{{- end -}}
{{- if $.Consts.EnableSubmissions -}}
<a href="#" data-from="{{ $r.GUID }}" class="edit" title="{{ $.L.Ts "public.suggestEdit" "word" (index $r.Content 0) }}">✏️</a>
{{- end -}}
</div>
{{- if $r.Phones -}}
<span class="pronun">♪ {{ $r.Phones | join "," }}</span>
{{- end -}}
</div>
<div class="meta">
{{- if $r.Tags -}}
<span class="tags">
{{- range $tag := $r.Tags -}}
<span class="tag" {{- if hasPrefix "src:" $tag }} title="{{ $.L.T "public.sourceTag" }}"{{- end }}>{{ $tag }}</span>
{{- end -}}
</span>
{{- end -}}
<a href="#" title="Share screenshot" class="export" data-guid="{{ $r.GUID }}"><img src="/static/export.svg?v={{ $.AssetVer }}" alt="Share screenshot"></a>
</div>
</header>
{{- if $r.Relations -}}
{{- $lastType := "" -}}
{{- range $k, $d := $r.Relations -}}
{{- $types := ($d.RelationTypes | join ", ") -}}
{{- if ne $lastType $types -}}
{{- if $lastType -}}</ol>{{- end }}
<ol class="defs">
{{ if $d.RelationTypes }}
<li class="types">
{{- range $t := $d.RelationTypes -}}
<span>
{{- $dType := index (index $.Langs $r.Lang).Types $t }}
{{- if $dType }}{{ $dType }} {{ end -}}
{{- $rType := index (index $.Langs $d.Lang).Types $t }}
{{- if $rType }}({{ $rType }}){{ end -}}
</span>
{{- end -}}
</li>
{{ end }}
{{- end -}}
<li><div data-guid="{{ $d.GUID }}" class="def">
{{ $d.Content | join ", " }}
{{ if or (gt $d.ContentLength $maxContentItems) $d.Meta.synonyms -}}
<a href="#" class="more-toggle" data-id="{{ $r.GUID }}-{{ $d.GUID }}" title="{{ $.L.Ts "public.viewMore" "num" $d.ContentLength }}"
data-entry-guid="{{ $d.GUID }}" data-from-lang="{{ $r.Lang }}" data-to-lang="{{ $d.Lang }}"
role="button" aria-expanded="false" aria-controls="{{ $r.GUID }}-{{ $d.GUID }}">
</a>
{{- end -}}
{{- if $.Consts.EnableSubmissions }}
<a href="#" data-from="{{ $r.GUID }}" data-to="{{ $d.GUID }}" class="edit" title="{{ $.L.Ts "public.suggestEdit" "word" (index $d.Content 0) }}">✏️</a>
{{ end -}}
<!-- This is dynamically populated by JS when the "more" link is clicked -->
<div class="more" id="{{ $r.GUID }}-{{ $d.GUID }}" role="region" aria-hidden="true"></div>
</div></li>
{{ $lastType = $types }}
{{- end -}}
</ol>
{{ end }}
</li>
{{- end -}}
</ol>
</div>
<nav class="three columns col-offset-1 related">
{{ if gt (len .Data.Results.Entries) $numResults }}
<h3>{{ .L.T "public.similarTitle" }}</h3>
{{ range $k, $r := (mustSlice .Data.Results.Entries $numResults) }}
<a href="{{ UnicodeURL (index $r.Content 0) }}" class="word">{{ index $r.Content 0 }}</a>
{{ end }}
{{ end }}
</nav>
</div>
{{ end }}

16
search.html Normal file
View File

@@ -0,0 +1,16 @@
{{ define "search" }}
{{ template "header" . }}
<section class="content results">
{{ if not .Data.Results.Entries }}
<h2>{{ .L.T "public.noResultsTitle" }}</h2>
<p>
{{ .L.T "public.noResults" }}
</p>
{{ else }}
{{ template "results" . }}
{{ end }}
</section>
{{ template "footer" . }}
{{ end }}

55
static/alar.js Normal file
View File

@@ -0,0 +1,55 @@
function hasKannadaChar(str) {
return /[\u0C80-\u0CFF]/.test(str);
}
(function () {
const reMatchNonKannadaBlobs = new RegExp(/[^\u0C80-\u0CFF]+/g);
// In the results (definitions), if there are Kannada words, hyperlink
// them to search.
let defs = document.querySelectorAll(".defs li");
if (!defs || defs.length === 0) {
return;
}
for (let i = 0; i < defs.length; i++) {
// Go through each definition. Ignore the ASCII ones.
if (!hasKannadaChar(defs[i].innerText)) {
continue;
}
// Split the word and iterate through it, turning non-ASCII words into
// hyperlinks;
const parts = defs[i].innerText.split(" ");
const s = document.createElement("span");
parts.forEach((v) => {
if (!hasKannadaChar(v)) {
// ASCII word. Append the text as-is.
s.appendChild(document.createTextNode(v));
} else {
// Non-ASCII word. Turn into a link.
const a = document.createElement("a");
// Some Kannada words have non-Kannada characters around them,
// the hyperlink should be formed only for Kannada words, while retaining all the characters order
const kannadaWord = v.replace(reMatchNonKannadaBlobs, "");
const nonKannadaWords = v.split(kannadaWord);
nonKannadaWords[0] && s.appendChild(document.createTextNode(nonKannadaWords[0]));
a.setAttribute("href", kannadaWord);
a.appendChild(document.createTextNode(kannadaWord));
s.appendChild(a);
nonKannadaWords[1] && s.appendChild(document.createTextNode(nonKannadaWords[1]));
}
// Append a space.
s.appendChild(document.createTextNode(" "));
});
defs[i].innerHTML = "";
defs[i].appendChild(s);
}
})();

130
static/alar.svg Normal file
View File

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="7.25in"
height="10.5in"
viewBox="0 0 184.15 266.7"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="alar.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="399.34639"
inkscape:cy="680.28571"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="70"
inkscape:window-y="27"
inkscape:window-maximized="1"
showguides="false"
inkscape:guide-bbox="true"
units="in">
<sodipodi:guide
position="35.189583,91.016669"
orientation="1,0"
id="guide868"
inkscape:locked="false" />
<sodipodi:guide
position="179.12291,98.689586"
orientation="1,0"
id="guide870"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-30.3)">
<g
aria-label="ಅಲರ್"
style="font-style:normal;font-weight:normal;font-size:61.74640656px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.54366004"
id="text817"
inkscape:export-filename="/home/kailash/code/go/my/knadh/dictionary-site/templates/static/logo.svg.png"
inkscape:export-xdpi="44.132771"
inkscape:export-ydpi="44.132771">
<path
d="m 41.731897,96.304657 q 0,1.447237 0.78392,2.472362 0.78392,1.025126 1.929649,1.025126 0.964825,0 1.628141,-1.025126 0.723619,-1.025125 0.723619,-2.472362 0,-1.447237 -0.723619,-2.472363 -0.663316,-1.085427 -1.628141,-1.085427 -1.145729,0 -1.929649,1.085427 -0.78392,1.025126 -0.78392,2.472363 z m 25.025134,6.271363 v 5.48743 H 53.068585 v -5.48743 h 6.753771 Q 58.556024,101.973 57.349993,99.621241 56.505772,97.9931 56.505772,96.304657 q 0,-3.798996 2.894473,-6.331661 2.592966,-2.291458 6.994977,-2.291458 4.522615,0 7.537691,4.100504 3.015076,4.100504 3.015076,11.035178 0,9.46734 -6.150755,15.19599 -6.150756,5.72864 -15.37689,5.72864 -9.045229,0 -14.532668,-5.66834 -5.427137,-5.66835 -5.427137,-15.13569 v 0 q 0,-6.934671 2.532664,-11.095476 2.592965,-4.160806 6.874374,-4.160806 3.075378,0 5.246233,2.532665 2.170855,2.532664 2.170855,6.090454 0,3.55779 -2.291458,6.090453 -2.291458,2.53267 -5.547741,2.53267 -0.663317,0 -1.56784,-0.18091 -0.844221,-0.24121 -1.929648,-0.60301 0,6.75377 3.9799,10.25126 5.246233,4.70351 10.793974,4.2814 8.140706,-0.60301 11.095481,-4.34171 6.814072,-8.56281 3.678393,-16.944726 -1.507538,-3.9799 -3.738695,-4.160805 -3.015076,-0.241206 -4.34171,2.231156 -0.78392,1.386936 -0.241206,3.376886 0.663317,2.592969 4.582916,3.738699 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:61.74876404px;font-family:'Lohit Kannada';-inkscape-font-specification:'Lohit Kannada Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-1.32291663px;writing-mode:lr-tb;text-anchor:start;stroke-width:1.54366004"
id="path866"
inkscape:connector-curvature="0" />
<path
d="m 90.881358,95.520737 q 0,1.326634 0.904523,2.231157 0.904523,0.904522 2.231157,0.904522 1.326633,0 2.231156,-0.904522 0.904523,-0.904523 0.904523,-2.231157 0,-1.326634 -0.904523,-2.231157 -0.904523,-0.904522 -2.231156,-0.904522 -1.326634,0 -2.231157,0.904522 -0.904523,0.904523 -0.904523,2.231157 z m 9.829152,22.733673 q 6.93467,0 11.09548,-4.0402 4.1608,-4.1005 4.1608,-10.85427 0,-3.37689 -2.17085,-6.331665 -2.11055,-2.954775 -5.66834,-4.643217 l 3.13567,-4.70352 q 4.64322,2.170855 7.41709,6.452264 2.77387,4.221107 2.77387,9.226138 0,9.22613 -5.66834,14.83417 -5.66834,5.54774 -15.07538,5.54774 -9.467343,0 -15.135686,-5.54774 -5.668344,-5.60804 -5.668344,-14.83417 0,-6.512569 4.100504,-11.095485 4.160806,-4.582917 10.010054,-4.582917 3.55779,0 6.090452,2.291458 2.53267,2.291459 2.53267,5.547741 0,3.376886 -2.53267,5.788943 -2.532662,2.41206 -6.090452,2.41206 -2.834172,0 -4.944726,-1.44723 -2.110553,-1.44724 -2.472362,-3.618094 -0.603015,0.723619 -0.904523,1.929654 -0.301508,1.20603 -0.301508,2.77387 0,6.75377 4.160806,10.85427 4.221107,4.0402 11.155785,4.0402 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:61.74876404px;font-family:'Lohit Kannada';-inkscape-font-specification:'Lohit Kannada Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-1.32291663px;writing-mode:lr-tb;text-anchor:start;stroke-width:1.54366004"
id="path868"
inkscape:connector-curvature="0" />
<path
d="m 130.08107,105.7117 q 0,5.18593 3.43719,8.86432 3.43718,3.67839 8.32161,3.67839 4.88442,0 8.32161,-3.67839 3.43719,-3.67839 3.43719,-8.86432 0,-5.18594 -3.43719,-8.864329 -3.43719,-3.678394 -8.32161,-3.678394 -4.88443,0 -8.32161,3.678394 -3.43719,3.678389 -3.43719,8.864329 z m 0.36181,-12.542723 h -5.84925 v -4.703519 h 37.38695 q 0.36181,-4.34171 3.19598,-7.236183 2.83417,-2.954775 6.57286,-2.954775 2.47237,0 4.52262,1.507538 2.05025,1.507539 2.95477,3.979901 l -3.91959,2.35176 q -0.84423,-1.447237 -1.80905,-2.291458 -0.96483,-0.844222 -1.74875,-0.844222 -1.92964,0 -3.31658,1.628142 -1.38693,1.567839 -1.38693,3.859297 h 2.35176 q 3.25628,0 5.54774,2.291458 2.29145,2.291458 2.29145,5.547741 0,3.256282 -2.29145,5.547743 -2.29146,2.29146 -5.54774,2.29146 -3.25629,0 -5.54775,-2.65327 -2.29145,-2.653269 -2.29145,-6.391964 v -1.929649 h -8.38192 q 2.83418,2.35176 4.34171,5.668344 1.56784,3.256279 1.56784,6.874379 0,7.47738 -5.06532,12.78392 -5.06533,5.24623 -12.18091,5.24623 -7.11558,0 -12.18091,-5.24623 -5.06533,-5.30654 -5.06533,-12.78392 0,-3.6181 1.50754,-6.874379 1.56784,-3.316584 4.34171,-5.668344 z m 35.81911,1.929649 q 0,1.809046 0.90452,3.075378 0.90452,1.266332 2.23116,1.266332 1.32663,0 2.23115,-1.145729 0.90452,-1.145729 0.90452,-2.77387 0,-0.964824 -0.90452,-1.628141 -0.90452,-0.723619 -2.23115,-0.723619 h -3.13568 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:61.74876404px;font-family:'Lohit Kannada';-inkscape-font-specification:'Lohit Kannada Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-1.32291663px;writing-mode:lr-tb;text-anchor:start;stroke-width:1.54366004"
id="path870"
inkscape:connector-curvature="0" />
</g>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:5.35675001px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.13391875"
x="46.978397"
y="146.70674"
id="text866"
transform="scale(0.9958963,1.0041206)"><tspan
sodipodi:role="line"
id="tspan864"
x="46.978397"
y="146.70674"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.76250029px;font-family:Inter;-inkscape-font-specification:Inter;fill:#666666;stroke-width:0.13391875"><tspan
style="font-style:italic;font-size:4.76250029px;stroke-width:0.13391875"
id="tspan872">Alar -</tspan> V Krishna's Kannada - English dictionary</tspan></text>
<text
id="text894"
y="136.81509"
x="47.357018"
style="font-style:normal;font-weight:normal;font-size:6.20058775px;line-height:1.25;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15501469"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Inter;-inkscape-font-specification:Inter;text-align:start;text-anchor:start;fill:#666666;stroke-width:0.15501469"
y="136.81509"
x="47.357018"
id="tspan892"
sodipodi:role="line">ಶ್ರೀ . ವಿ ಕ್ರಿಷ್ಣ ರವರ ಕನ್ನಡ - ಇಂಗ್ಲಿಷ್ ನಿಘಂಟು</tspan></text>
<text
id="text864"
y="141.99811"
x="234.03926"
style="font-style:normal;font-weight:normal;font-size:13.59828949px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33995721"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:13.59880829px;font-family:'Lohit Kannada';-inkscape-font-specification:'Lohit Kannada Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-0.2913433px;writing-mode:lr-tb;text-anchor:start;stroke-width:0.33995721"
y="141.99811"
x="234.03926"
id="tspan862"
sodipodi:role="line">ಅಲರ್</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

1
static/audio.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15.298" height="11.711" viewBox="0 0 2.295 1.756" stroke="#999" stroke-width=".163" xmlns:v="https://vecta.io/nano"><path d="M1.168.081L.61.564H.081v.621h.52l.566.49z" fill="#999" stroke-linejoin="round"/><path d="M1.67.531c.139.211.139.485 0 .696M1.935.089a1.26 1.26 0 0 1 0 1.581" fill="none" stroke-linecap="round"/></svg>

After

Width:  |  Height:  |  Size: 372 B

137
static/autocomp.js Normal file
View File

@@ -0,0 +1,137 @@
function autocomp(el, options = {}) {
const opt = {
onQuery: null, onNavigate: null, onSelect: null, onRender: null, debounce: 100, autoSelect: true,...options
};
let box, cur = opt.autoSelect ? 0 : -1, items = [], val, req;
// Disable browser's default autocomplete behaviour on the input.
el.autocomplete = "off";
// Attach all the events required for the interactions in one go.
["input", "keydown", "blur"].forEach(k => el.addEventListener(k, handleEvent));
function handleEvent(e) {
if (e.type === "keydown" && !handleKeydown(e)) {
return;
}
if (e.type === "blur") {
return destroy();
}
const newVal = e.target.value;
if (!newVal) {
destroy();
val = null;
return;
}
if (newVal === val && box) {
return;
}
val = newVal;
// Clear (debounce) any existing pending requests and queue
// the next search request.
clearTimeout(req);
req = setTimeout(query, opt.debounce);
}
function handleKeydown(e) {
if (!box) {
return e.keyCode === 38 || e.keyCode === 40
}
switch (e.keyCode) {
case 38: return navigate(-1, e); // Up arrow.
case 40: return navigate(1, e); // Down arrow
case 9: // Tab
case 13: // Enter
e.preventDefault();
select(cur);
destroy();
return;
case 27: // Escape.
destroy();
return;
}
}
async function query() {
if (!val) {
return;
}
items = await opt.onQuery(val);
if (!items.length) {
return destroy();
}
if (!box) {
createBox();
}
renderResults();
}
function createBox() {
box = document.createElement("div");
Object.assign(box.style, {
width: window.getComputedStyle(el).width,
position: "absolute",
left: `${el.offsetLeft}px`,
top: `${el.offsetTop + el.offsetHeight}px`
});
box.classList.add("autocomp");
el.parentNode.insertBefore(box, el.nextSibling);
}
function renderResults() {
box.innerHTML = "";
items.forEach((item, idx) => {
const div = document.createElement("div");
div.classList.add("autocomp-item");
// If there's a custom renderer callback, use it, else, simply insert the value/text as-is.
opt.onRender ? div.appendChild(opt.onRender(item)) : div.innerText = item;
if (idx === cur) {
div.classList.add("autocomp-sel");
}
div.addEventListener("mousedown", () => select(idx));
box.appendChild(div);
});
}
function navigate(direction, e) {
e.preventDefault();
// Remove the previous item's highlight;
const prev = box.querySelector(`:nth-child(${cur + 1})`);
prev?.classList.remove("autocomp-sel");
// Increment the cursor and highlight the next item, cycled between [0, n].
cur = (cur + direction + items.length) % items.length;
box.querySelector(`:nth-child(${cur + 1})`).classList.add("autocomp-sel");
}
function select(idx) {
if (!opt.onSelect) {
return;
}
val = opt.onSelect(items[idx]);
el.value = val || items[idx];
}
function destroy() {
items = [];
cur = opt.autoSelect ? 0 : -1;
if (box) {
box.remove();
box = null;
}
}
}

1
static/down.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="6.857" viewBox="0 0 0.36 0.206" fill="none" xmlns:v="https://vecta.io/nano"><path d="M.026.026L.18.18.334.026" stroke="#000" stroke-width=".051" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 258 B

2
static/export.svg Normal file
View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#aaa" width="18px" height="18px" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"><path d="M30.3 13.7L25 8.4l-5.3 5.3-1.4-1.4L25 5.6l6.7 6.7z"/><path d="M24 7h2v21h-2z"/><path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z"/></svg>

After

Width:  |  Height:  |  Size: 485 B

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

135
static/flexit.css Normal file
View File

@@ -0,0 +1,135 @@
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
box-sizing: border-box;
}
.row {
box-sizing: border-box;
display: flex;
flex: 0 1 auto;
flex-flow: row wrap;
}
.row.nogutter {
margin-left: 0;
margin-right: 0;
}
.row.nogutter > .columns {
padding-left: 0;
padding-right: 0;
}
.columns {
box-sizing: border-box;
flex: 0 1 auto;
min-width: 0;
}
.one {
flex-basis: 8.33333333%;
}
.two {
flex-basis: 16.66666667%;
}
.three {
flex-basis: 25%;
}
.four {
flex-basis: 33.3333333333%;
}
.five {
flex-basis: 41.66666667%;
}
.six {
flex-basis: 50%;
}
.seven {
flex-basis: 58.33333333%;
}
.eight {
flex-basis: 66.66666667%;
}
.nine {
flex-basis: 75%;
}
.ten {
flex-basis: 83.33333333%;
}
.eleven {
flex-basis: 91.66666667%;
}
.twelve {
flex-basis: 100%;
}
.col-offset-0 {
margin-left: 0;
}
.col-offset-1 {
margin-left: 8.33333333%;
}
.col-offset-2 {
margin-left: 16.66666667%;
}
.col-offset-3 {
margin-left: 25%;
}
.col-offset-4 {
margin-left: 33.33333333%;
}
.col-offset-5 {
margin-left: 41.66666667%;
}
.col-offset-6 {
margin-left: 50%;
}
.col-offset-7 {
margin-left: 58.33333333%;
}
.col-offset-8 {
margin-left: 66.66666667%;
}
.col-offset-9 {
margin-left: 75%;
}
.col-offset-10 {
margin-left: 83.33333333%;
}
.col-offset-11 {
margin-left: 91.66666667%;
}
.between {
justify-content: space-between;
}
.end {
justify-content: flex-end;
}
.around {
justify-content: space-around;
}
.row-align-center {
align-items: center;
}
.space-right {
margin-right: 10px;
}
.space-left {
margin-left: 10px;
}
.space-bottom {
margin-bottom: 10px;
}
.space-top {
margin-top: 10px;
}
@media (max-width: 980px) {
.columns {
flex: 1 1 auto;
}
.offset-0,
.offset-1,
.offset-2 {
margin: unset;
}
.container {
padding: 0 15px;
}
}

1
static/idaf-logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

1
static/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="418.375" height="132.314" viewBox="0 0 110.695 35.008" xmlns:v="https://vecta.io/nano"><path d="M4.829 13.883q0 1.114.604 1.904.604.789 1.486.789.743 0 1.254-.789.557-.789.557-1.904 0-1.114-.557-1.904-.511-.836-1.254-.836-.882 0-1.486.836-.604.789-.604 1.904zm19.269 4.829v4.225h-10.54v-4.225h5.2q-.975-.464-1.904-2.275-.65-1.254-.65-2.554 0-2.925 2.229-4.875 1.996-1.764 5.386-1.764 3.482 0 5.804 3.157 2.322 3.157 2.322 8.497 0 7.29-4.736 11.7-4.736 4.411-11.84 4.411-6.965 0-11.19-4.364Q0 26.279 0 18.99h0q0-5.339 1.95-8.543 1.996-3.204 5.293-3.204 2.368 0 4.039 1.95 1.671 1.95 1.671 4.689 0 2.739-1.764 4.689-1.764 1.95-4.272 1.95-.511 0-1.207-.139-.65-.186-1.486-.464 0 5.2 3.064 7.893 4.039 3.622 8.311 3.297 6.268-.464 8.543-3.343 5.247-6.593 2.832-13.047-1.161-3.064-2.879-3.204-2.322-.186-3.343 1.718-.604 1.068-.186 2.6.511 1.997 3.529 2.879zm18.574-5.433q0 1.021.696 1.718.696.696 1.718.696 1.021 0 1.718-.696.696-.696.696-1.718 0-1.021-.696-1.718-.696-.696-1.718-.696-1.021 0-1.718.696-.696.696-.696 1.718zm7.568 17.504q5.339 0 8.543-3.111 3.204-3.157 3.204-8.357 0-2.6-1.671-4.875-1.625-2.275-4.364-3.575l2.414-3.622q3.575 1.671 5.711 4.968 2.136 3.25 2.136 7.104 0 7.104-4.364 11.422-4.364 4.272-11.608 4.272-7.29 0-11.654-4.272-4.364-4.318-4.364-11.422 0-5.014 3.157-8.543 3.204-3.529 7.707-3.529 2.739 0 4.689 1.764 1.95 1.764 1.95 4.272 0 2.6-1.95 4.457-1.95 1.857-4.689 1.857-2.182 0-3.807-1.114-1.625-1.114-1.904-2.786-.464.557-.696 1.486-.232.929-.232 2.136 0 5.2 3.204 8.357 3.25 3.111 8.59 3.111zm22.615-9.657q0 3.993 2.647 6.825 2.647 2.832 6.407 2.832 3.761 0 6.407-2.832 2.647-2.832 2.647-6.825 0-3.993-2.647-6.825-2.647-2.832-6.407-2.832-3.761 0-6.407 2.832-2.647 2.832-2.647 6.825zm.279-9.657H68.63V7.847h28.787q.279-3.343 2.461-5.572Q102.059 0 104.938 0q1.904 0 3.482 1.161 1.579 1.161 2.275 3.064l-3.018 1.811q-.65-1.114-1.393-1.764-.743-.65-1.346-.65-1.486 0-2.554 1.254-1.068 1.207-1.068 2.972h1.811q2.507 0 4.272 1.764 1.764 1.764 1.764 4.272 0 2.507-1.764 4.272-1.764 1.764-4.272 1.764-2.507 0-4.272-2.043-1.764-2.043-1.764-4.922v-1.486h-6.454q2.182 1.811 3.343 4.364 1.207 2.507 1.207 5.293 0 5.757-3.9 9.843-3.9 4.039-9.379 4.039-5.479 0-9.379-4.039-3.9-4.086-3.9-9.843 0-2.786 1.161-5.293 1.207-2.554 3.343-4.364zm27.58 1.486q0 1.393.696 2.368.696.975 1.718.975 1.021 0 1.718-.882.696-.882.696-2.136 0-.743-.696-1.254-.696-.557-1.718-.557h-2.414z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

304
static/main.js Normal file
View File

@@ -0,0 +1,304 @@
async function screenshotElement(element) {
const canvas = await html2canvas(element);
canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'screenshot.png';
a.click();
URL.revokeObjectURL(url);
});
}
(() => {
const elForm = document.querySelector("form.search-form");
const elQ = document.querySelector("#q");
const elSelDict = document.querySelector("form.search-form select");
const defaultLang = elSelDict.value;
// ===================================
// Helper functions.
function selectDict(dict) {
// dict is in the format '$fromLang/$toLang'.
const langs = dict.split("/");
Object.assign(localStorage, { dict, from_lang: langs[0], to_lang: langs[1] });
elSelDict.value = dict;
elForm.setAttribute("action", `${_ROOT_URL}/dictionary/${dict}`);
}
// Capture the form submit and send it as a canonical URL instead
// of the ?q query param.
function search(q) {
let val = q.trim();
const uri = elForm.getAttribute("action");
document.location.href = `${uri}/${encodeURIComponent(val).replace(/%20/g, "+")}`;
}
// ===================================
// Events
// On ~ press, focus search input.
document.onkeydown = (function (e) {
if (e.key !== "`" && e.key !== "~") {
return;
}
e.preventDefault();
q.focus();
q.select();
});
// Bind to language change.
elSelDict.addEventListener("change", function (e) {
selectDict(e.target.value);
});
// Bind to form submit.
elForm.addEventListener("submit", function (e) {
e.preventDefault();
search(elQ.value);
});
// "More" link next to entries/defs that expands an entry.
document.querySelectorAll(".more-toggle").forEach((el) => {
el.onclick = (e) => {
e.preventDefault();
let state = "block";
if (el.dataset.open) {
delete (el.dataset.open);
state = "none";
} else {
el.dataset.open = true;
state = "block";
}
const container = document.getElementById(el.dataset.id);
container.style.display = state;
// fetch() content from the dataset data.guid from /api/entry/{guid}
// and populate the div.more innerHTML with the content.
if (state === "block" && !el.dataset.fetched) {
container.innerHTML = `<div class="loader"></div>`;
fetch(`${window._ROOT_URL}/api/dictionary/entries/${el.dataset.entryGuid}`)
.then((resp) => resp.json())
.then((data) => {
const d = data.data;
container.innerHTML = "";
// If there's data.data.meta.synonyms, add them to the synonyms div.
if (d.meta.synonyms) {
const syns = document.createElement("div");
syns.classList.add("synonyms");
syns.innerHTML = d.meta.synonyms.map((s) => `<a href="${window._ROOT_URL}/dictionary/${el.dataset.fromLang}/${el.dataset.toLang}/${s}">${s}</a>`).join("");
container.appendChild(syns);
}
// Each data.content[] word, add to target.
const more = document.createElement("div");
more.classList.add("words");
more.innerHTML = d.content.slice(window._MAX_CONTENT_ITEMS).map((c) => `<span class="word">${c}</span>`).join("");
container.appendChild(more);
el.dataset.fetched = true;
})
.catch((err) => {
console.error("Error fetching entry content:", err);
});
}
};
});
// ===================================
// Select a language based on the page URL.
let dict = localStorage.dict || defaultLang;
const uri = /(dictionary)\/((.+?)\/(.+?))\//i.exec(document.location.href);
if (uri && uri.length == 5) {
dict = uri[2];
}
selectDict(dict);
elQ.focus();
elQ.select();
})();
// Screenshot sharing.
(() => {
document.querySelectorAll("a.export").forEach((el) => {
el.onclick = async (e) => {
e.preventDefault();
const guid = el.dataset.guid;
const entryEl = document.querySelector(`.entry[data-guid='${guid}']`);
if (!entryEl) {
alert("Could not find entry to export");
return;
}
const title = entryEl.dataset.head;
// Make the filename by stripping spaces from the head word(s).
const filename = title.replace(/\s+/g, "_").toLowerCase();
try {
await shareDOM(entryEl, `${title} meaning`, `${localStorage.from_lang} to ${localStorage.to_lang} meaning\n\n${window.location.href}`, `${filename}.png`);
} catch (err) {
console.error("Error sharing entry:", err);
alert(`Error sharing entry: ${err?.message || err}`);
}
};
});
})();
// Play audio.
(() => {
document.querySelectorAll("a[data-audio]").forEach((el) => {
el.onclick = (e) => {
e.preventDefault();
const audio = new Audio(el.getAttribute("href"));
audio.play().catch((err) => {
console.error("error playing audio:", err);
alert("error playing audio");
});
};
});
})();
// Submission form.
(() => {
function filterTypes(e) {
// Filter the types select field with elements that are supported by the language.
const types = e.target.closest("fieldset").querySelector("select[name=relation_type]");
types.querySelectorAll("option").forEach((o) => o.style.display = "none");
types.querySelectorAll(`option[data-lang=${e.target.value}]`).forEach((o) => o.style.display = "block");
types.selectedIndex = 1;
}
if (document.querySelector(".form-submit")) {
document.querySelectorAll("select[name=relation_lang]").forEach((e) => {
e.onchange = filterTypes;
});
// +definition button.
document.querySelector(".btn-add-relation").onclick = (e) => {
e.preventDefault();
if (document.querySelectorAll(".add-relations li").length >= 20) {
return false;
}
// Clone and add a relation fieldset.
const d = document.querySelector(".add-relations li").cloneNode(true);
d.dataset.added = true
d.querySelector("select[name=relation_lang]").onchange = filterTypes;
document.querySelector(".add-relations").appendChild(d);
// Remove definition link.
d.querySelector(".btn-remove-relation").onclick = (e) => {
e.preventDefault();
d.remove();
};
};
}
})();
// Edit form.
(() => {
document.querySelectorAll(".edit").forEach((o) => {
o.onclick = ((e) => {
e.preventDefault();
const btn = e.target;
// Form is already open.
if (btn.close) {
btn.close();
return;
}
const form = document.querySelector(".form-comments").cloneNode(true);
o.parentNode.appendChild(form);
form.style.display = "block";
const txt = form.querySelector("textarea");
txt.focus();
txt.onkeydown = (e) => {
if (e.key === "Escape" && txt.value === "") {
btn.close();
}
};
btn.close = () => {
btn.close = null;
form.remove();
};
// Handle form submission.
form.onsubmit = () => {
fetch(`${window._ROOT_URL}/api/submissions/comments`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
from_guid: btn.dataset.from,
to_guid: btn.dataset.to,
comments: txt.value
})
}).catch((err) => {
alert(`Error submitting: ${err}`);
});
alert(form.dataset.success);
btn.close();
};
form.querySelector("button.close").onclick = btn.close;
});
})
})();
// Autocomplete.
(() => {
if(!autocomp) {
return;
}
const elForm = document.querySelector("form.search-form");
const elQ = document.querySelector("#q");
let debounce;
autocomp(elQ, {
autoSelect: false,
onQuery: async (val) => {
const langCode = localStorage.from_lang;
clearTimeout(debounce);
return new Promise(resolve => {
debounce = setTimeout(async () => {
const response = await fetch(`${_ROOT_URL}/atl/${langCode}/${val.toLowerCase()}`);
const data = await response.json();
const a = data.greedy_tokenized.map(item => item.word).slice(0, 3).sort((a, b) => a.length - b.length);
const b = data.dictionary_suggestions.map(item => item.word).slice(0, 6).sort((a, b) => a.length - b.length);
debounce = null;
resolve([...new Set(a.concat(b))]);
}, 50);
});
},
onSelect: (val) => {
// autocomp search isn't complete. Use the user's input instead of autocomp selection.
if (val) {
elQ.value = val;
}
elForm.dispatchEvent(new Event("submit", { cancelable: true }));
return elQ.value;
}
});
})();

1
static/search.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20.962" height="21" xmlns:v="https://vecta.io/nano"><path d="M14.508 8.857c0-1.565-.535-2.902-1.642-4.009s-2.444-1.642-4.009-1.642-2.902.535-4.009 1.642-1.642 2.444-1.642 4.009.535 2.902 1.642 4.009 2.444 1.642 4.009 1.642 2.902-.535 4.009-1.642c1.069-1.145 1.642-2.482 1.642-4.009zm6.452 10.499c0 .42-.153.802-.496 1.145s-.687.496-1.145.496a1.45 1.45 0 0 1-1.145-.496l-4.276-4.353c-1.527 1.069-3.207 1.565-5.04 1.565a8.63 8.63 0 0 1-3.436-.687c-1.107-.458-2.062-1.107-2.825-1.909a10.51 10.51 0 0 1-1.909-2.825C.229 11.186 0 10.041 0 8.857S.229 6.49.687 5.421c.458-1.107 1.107-2.062 1.909-2.825A10.51 10.51 0 0 1 5.421.687C6.528.229 7.674 0 8.857 0s2.367.229 3.436.687c1.107.458 2.062 1.107 2.825 1.909a10.51 10.51 0 0 1 1.909 2.825c.458 1.107.687 2.253.687 3.436 0 1.833-.535 3.513-1.565 5.04l4.314 4.314a1.45 1.45 0 0 1 .496 1.145z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 901 B

265
static/share.js Normal file
View File

@@ -0,0 +1,265 @@
async function screenshotDOM(el, opts = {}) {
const {
scale = 2,
type = 'image/png',
quality = 0.92,
background = '#fff',
// if you want to *allow* splitting (and risk the bug), set to false
preventCapsuleSplit = true
} = opts;
if (!(el instanceof Element)) throw new TypeError('Expected a DOM Element');
if (document.fonts && document.fonts.status !== 'loaded') {
try { await document.fonts.ready; } catch { }
}
const { width, height } = (() => {
const r = el.getBoundingClientRect();
return { width: Math.ceil(r.width), height: Math.ceil(r.height) };
})();
if (!width || !height) throw new Error('Element has zero width/height.');
const clone = el.cloneNode(true);
await inlineEverything(el, clone);
if (background !== 'transparent') clone.style.background = background;
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
const xhtml = new XMLSerializer().serializeToString(clone);
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
<foreignObject x="0" y="0" width="100%" height="100%">${xhtml}</foreignObject>
</svg>`;
const svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(svgBlob);
try {
const img = await loadImage(url);
const canvas = document.createElement('canvas');
canvas.width = Math.ceil(width * scale);
canvas.height = Math.ceil(height * scale);
const ctx = canvas.getContext('2d');
if (background !== 'transparent' || type !== 'image/png') {
ctx.fillStyle = background === 'transparent' ? '#0000' : background;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const blob = await new Promise(res => canvas.toBlob(res, type, quality));
if (!blob) throw new Error('Canvas.toBlob returned null.');
return blob;
} finally {
URL.revokeObjectURL(url);
}
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.decoding = 'async';
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
async function inlineEverything(srcNode, dstNode) {
copyComputedStyle(srcNode, dstNode);
// Remove interactive states that may be captured on touch devices
if (dstNode instanceof Element) {
// Reset outline from :focus state
dstNode.style.setProperty('outline', 'none', 'important');
// Reset any pointer-events to ensure no hover states
const cs = window.getComputedStyle(srcNode);
// Only override cursor if it's pointer (indicating interactivity)
if (cs.cursor === 'pointer') {
dstNode.style.setProperty('cursor', 'default', 'important');
}
}
if (srcNode instanceof HTMLTextAreaElement) {
dstNode.textContent = srcNode.value;
} else if (srcNode instanceof HTMLInputElement) {
dstNode.setAttribute('value', srcNode.value);
if ((srcNode.type === 'checkbox' || srcNode.type === 'radio') && srcNode.checked) {
dstNode.setAttribute('checked', '');
}
} else if (srcNode instanceof HTMLSelectElement) {
const sel = Array.from(srcNode.options).filter(o => o.selected).map(o => o.value);
Array.from(dstNode.options).forEach(o => (o.selected = sel.includes(o.value)));
}
if (srcNode instanceof HTMLCanvasElement) {
try {
const dataURL = srcNode.toDataURL();
const img = document.createElement('img');
img.src = dataURL;
copyBoxSizing(dstNode, img);
dstNode.replaceWith(img);
dstNode = img;
} catch { }
}
// Handle img elements to ensure they render properly
if (srcNode instanceof HTMLImageElement && dstNode instanceof HTMLImageElement) {
// Convert image to data URL for inlining
if (srcNode.complete && srcNode.naturalWidth > 0) {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = srcNode.naturalWidth;
canvas.height = srcNode.naturalHeight;
ctx.drawImage(srcNode, 0, 0);
dstNode.src = canvas.toDataURL('image/png');
// Also set width/height to ensure proper rendering
const cs = window.getComputedStyle(srcNode);
dstNode.style.width = cs.width;
dstNode.style.height = cs.height;
dstNode.style.objectFit = cs.objectFit;
} catch (err) {
// If cross-origin or other error, keep original src
console.log('Could not inline image:', err);
dstNode.src = srcNode.src;
}
} else {
// Image not loaded or has no dimensions, use src as-is
dstNode.src = srcNode.src || srcNode.getAttribute('src') || '';
}
// Remove alt text from being rendered
dstNode.removeAttribute('alt');
}
materializePseudo(srcNode, dstNode, '::before');
materializePseudo(srcNode, dstNode, '::after');
const sKids = srcNode.childNodes;
const dKids = dstNode.childNodes;
for (let i = 0; i < sKids.length; i++) {
const s = sKids[i], d = dKids[i];
if (s && d && s.nodeType === 1 && d.nodeType === 1) await inlineEverything(s, d);
}
function copyComputedStyle(src, dst) {
const cs = window.getComputedStyle(src);
let cssText = '';
for (const prop of cs) cssText += `${prop}:${cs.getPropertyValue(prop)};`;
dst.setAttribute('style', (dst.getAttribute('style') || '') + cssText);
// Always keep transform origin consistent
if (cs.transformOrigin) dst.style.transformOrigin = cs.transformOrigin;
// Keep each line painting its own decoration (helps some engines)
dst.style.setProperty('box-decoration-break', 'clone', 'important');
dst.style.setProperty('-webkit-box-decoration-break', 'clone', 'important');
// --- CAPSULE FIX: prevent decorated inline from splitting across lines ---
if (preventCapsuleSplit) {
const hasBg = (cs.backgroundImage && cs.backgroundImage !== 'none') ||
(cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent');
const hasRadius = ['borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius']
.some(k => parseFloat(cs[k]) > 0);
const hasPad = ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft']
.some(k => parseFloat(cs[k]) > 0);
const isInlineLevel = cs.display.startsWith('inline');
if (isInlineLevel && (hasBg || hasRadius || hasPad)) {
// Make it behave like a chip for capture: no internal wrapping
dst.style.setProperty('display', 'inline-block', 'important');
dst.style.setProperty('white-space', 'nowrap', 'important');
// In case original allowed breaking long words, emulate by clipping
if (cs.overflowWrap === 'break-word' || cs.wordBreak === 'break-all') {
dst.style.setProperty('max-width', cs.maxWidth && cs.maxWidth !== 'none' ? cs.maxWidth : '100%', 'important');
dst.style.setProperty('overflow', 'hidden', 'important');
dst.style.setProperty('text-overflow', 'ellipsis', 'important');
}
}
}
}
function copyBoxSizing(src, dst) {
const cs = window.getComputedStyle(src);
dst.style.width = cs.width;
dst.style.height = cs.height;
dst.style.display = cs.display;
dst.style.objectFit = 'contain';
}
function materializePseudo(src, dst, which) {
const ps = window.getComputedStyle(src, which);
if (!ps || ps.content === '' || ps.content === 'none') return;
const span = document.createElement('span');
let cssText = '';
for (const prop of ps) cssText += `${prop}:${ps.getPropertyValue(prop)};`;
span.setAttribute('style', cssText);
const content = ps.content;
const quoted = /^(['"]).*\1$/.test(content);
if (quoted) span.textContent = content.slice(1, -1).replace(/\\n/g, '\n');
else if (content.startsWith('attr(')) {
const attrName = content.slice(5, -1).trim();
span.textContent = src.getAttribute(attrName) || '';
} else span.textContent = '';
if (which === '::before') dst.insertBefore(span, dst.firstChild);
else dst.appendChild(span);
}
}
}
async function shareDOM(target, title, text, filename) {
try {
// Remove any active/focus states before capturing
// This is especially important on touch devices (Android)
if (document.activeElement) {
document.activeElement.blur();
}
// Wait a brief moment for any :active states to clear
await new Promise(resolve => setTimeout(resolve, 100));
// Make a crisp image (PNG keeps transparency; use JPEG for smaller files)
const blob = await screenshotDOM(target, {
scale: Math.max(2, window.devicePixelRatio || 2),
type: 'image/png',
background: 'white'
});
const file = new File([blob], filename || 'share.png', {
type: blob.type,
lastModified: Date.now()
});
// Web Share API with files (Android Chrome, iOS/iPadOS Safari 16+).
if (navigator.canShare && navigator.canShare({ files: [file] })) {
await navigator.share({ title, text, files: [file] });
return;
}
// Copy image to clipboard (desktop Chrome/Edge, some Android)
if (navigator.clipboard && window.ClipboardItem) {
try {
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
alert('Screenshot copied to clipboard');
return;
} catch (clipErr) {
// Clipboard write failed (common on Android), fall through to download
console.log('Clipboard write not allowed, downloading instead:', clipErr);
}
}
// Fallback: prompt a download
const dlUrl = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'), {
href: dlUrl,
download: 'share.png'
});
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(dlUrl);
} catch (err) {
console.error(err);
alert(`share failed: ${err?.message || err}`);
} finally { }
}

741
static/style.css Normal file
View File

@@ -0,0 +1,741 @@
:root {
--primary: #111;
--secondary: #333;
--light: #666;
--lighter: #aaa;
--lightest: #eee;
--lightestest: #f4f4f4;
--white: #fff;
--blue: #36c;
}
* {
box-sizing: border-box;
}
body {
background: #fbfbfb;
font-family: "Helvetica Neue", "Segoe UI", Helvetica, sans-serif;
margin: 0;
color: var(--primary);
}
html,
body {
font-size: 1em;
line-height: 1.3;
height: 100%;
min-width: 320px;
}
:focus {
outline-style: solid;
outline-width: 2px;
outline-color: var(--blue);
}
img {
max-width: 100%;
}
hr {
height: 2px;
border: 0;
background: #ddd;
display: block;
margin: 30px 0;
clear: both;
}
h1,
h2,
h3,
h4,
h5 {
margin: 10px 0 10px 0;
}
h4,
h5 {
margin: 0 0 10px 0;
font-weight: normal;
}
a {
color: var(--primary);
}
a:hover {
color: var(--blue);
}
input:not([type="radio"]),
select,
button,
textarea,
.button {
padding: 10px 15px;
font-family: "Helvetica Neue", "Segoe UI", Helvetica, sans-serif;
font-size: 1.2rem;
line-height: 1;
color: var(--primary);
border-radius: 5px;
border: 1px solid #ccc;
}
select {
background-color: var(--white);
font-size: 0.875rem;
}
label {
display: block;
color: var(--light);
}
button,
.button {
background: var(--primary);
border-color: var(--primary);
color: var(--white);
width: auto;
cursor: pointer;
padding: 10px 20px;
}
.button-outline {
background: var(--white);
color: var(--primary);
}
.button-outline:hover {
background: var(--lighter);
color: var(--primary);
}
button:hover,
.button:hover {
border-color: #444;
background: #444;
}
.noul {
list-style-type: none;
margin: 0;
padding: 0;
}
.text-right {
text-align: right;
}
.box {
box-shadow: 2px 2px 1px #f6f6f6;
border: 1px solid #e6e6e6;
padding: 15px;
}
.loader {
width: 16px;
padding: 3px;
aspect-ratio: 1;
border-radius: 50%;
background: #aaa;
--_m:
conic-gradient(#0000 10%, #000),
linear-gradient(#000 0 0) content-box;
-webkit-mask: var(--_m);
mask: var(--_m);
-webkit-mask-composite: source-out;
mask-composite: subtract;
animation: l3 1s infinite linear;
}
@keyframes l3 {
to {
transform: rotate(1turn)
}
}
/* Custom */
.main {
margin: 30px auto 15px auto;
display: flex;
flex-direction: column;
}
.home .container {
max-width: 700px;
margin: auto;
}
.logo a {
display: inline-block;
}
.logo img {
height: auto;
max-height: 40px;
}
.home .logo img {
max-width: 80%;
}
.intro {
display: none;
color: #444;
margin: 30px 0 30px;
font-weight: normal;
}
.search-form {
position: relative;
}
.search-form>div {
display: flex;
align-items: stretch;
}
.search-form .input-group {
display: flex;
flex-grow: 1;
align-self: center;
}
.search-form input {
flex-grow: 1;
padding: 10px 15px;
border-width: 1px 0px;
border-radius: 0;
}
.search-form select {
background-color: var(--white);
padding: 5px 15px;
border-radius: 30px 0 0 30px;
color: var(--light)
}
.search-form select:focus {
z-index: 100;
}
.search-form button {
line-height: 0;
padding: 7px 15px 7px 10px;
border-radius: 0 30px 30px 0;
z-index: 100;
}
.search-form button img {
max-height: 32px;
min-width: 13px;
}
.page {
margin: 30px 0 30px 0;
}
.word {
background: var(--lightest);
padding: 0 5px;
border-radius: 30px;
margin: 0 5px 5px 0;
display: inline-block;
text-decoration: none;
}
.entries {
padding: 0;
}
.entries .entry {
background-color: var(--white);
margin-bottom: 30px;
padding: 15px;
border-radius: 5px;
box-shadow: 2px 2px 1px #f6f6f6;
border: 1px solid #e6e6e6;
list-style-type: none;
}
.entries .head {
position: relative;
margin-bottom: 10px;
display: flex;
align-items: baseline;
justify-content: space-between;
}
.entries .title {
margin: 0;
}
.entries .title-wrap {
display: flex;
align-items: center;
gap: 10px;
}
.entries .audio {
display: inline-block;
vertical-align: middle;
cursor: pointer;
transition: ease-in-out 0.035s;
}
.entries .audio:hover {
transform: scale(1.1);
}
.entries .pronun {
color: var(--light);
}
.entries .defs {
padding: 0 0 0 30px;
}
.entries .defs:not(:last-child) {
margin-bottom: 30px;
}
.entries li::marker {
color: var(--lighter);
}
.entries .defs li {
margin-bottom: 5px;
position: relative;
}
.entries .defs li::marker {
font-size: 0.8em;
}
.entries .defs .more {
display: none;
}
.entries .defs .synonyms {
margin: 5px 0 10px 0;
}
.entries .defs .synonyms a {
display: inline-block;
text-decoration: none;
color: var(--light);
font-size: 0.775rem;
margin-right: 8px;
}
.entries .defs .synonyms a:hover {
color: var(--blue);
}
.entries .more-toggle {
display: inline-block;
text-decoration: none;
transform: translate(0, 3px);
transition: transform 0.15s ease;
position: absolute;
margin: 0 5px;
}
.entries .more-toggle::before {
width: 1em;
line-height: 0;
content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
transform-origin: .5em 50%;
}
.entries .more-toggle[data-open] {
transform: rotate(-180deg) translate(0, 3px);
}
.entries .defs .types {
list-style-type: none;
display: block;
margin: 0 0 10px -15px;
color: var(--light);
text-decoration: underline;
text-decoration-style: dashed;
text-decoration-color: var(--lighter);
font-size: 0.675rem;
font-weight: bold;
}
.entries .defs .types span {
margin-right: 10px;
}
.entry .edit {
padding: 3px;
display: none;
text-decoration: none;
z-index: 1000;
line-height: 1;
text-shadow: 0px 3px 3px #fff;
font-size: 0.8rem;
}
.entries .defs li:hover .edit {
display: inline-block;
}
.entry .head:hover .edit {
display: inline-block;
}
.entries .tags {
display: inline-block;
background-color: var(--lightestest);
border: 1px solid var(--lightest);
border-radius: 3px;
font-size: 0.75rem;
line-height: 1;
padding: 2px 5px;
color: var(--lighter);
}
.entries .meta {
display: flex;
align-items: center;
gap: 10px;
}
.related .word {
margin: 0 5px 8px 0;
padding: 5px 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
.form-submit fieldset {
padding: 0;
margin: 0 0 20px 0;
border: 0;
}
.form-submit select {
margin-bottom: 5px;
}
.form-submit select,
.form-submit textarea {
width: 100%;
}
.form-submit li {
margin-bottom: 20px;
}
.form-submit li:first-child .btn-remove-relation {
display: none;
}
.form-submit input,
.form-submit textarea {
width: 100%;
}
.form-comments {
margin: 10px 0 45px 0;
display: none;
padding: 15px;
clear: both;
}
.form-comments textarea {
width: 100%;
min-height: 200px;
}
.form-comments h3 {
margin: 0 0 15px 0;
}
.form-comments button {
margin: 0 10px 10px 0;
width: 100%;
}
.glossary .index {
background: #f7f7f7;
margin-bottom: 15px;
}
.glossary .index a {
text-decoration: none;
display: inline-block;
padding: 5px;
}
.glossary .index .sel,
.glossary .index a:hover {
background: var(--primary);
color: var(--white);
}
.glossary .words {
/* column-count: auto;
column-gap: 0; */
column-count: 3;
column-gap: 40px;
}
.glossary .words li {
border-bottom: 1px solid #eee;
page-break-inside: avoid;
margin-bottom: 10px;
}
.glossary .words a {
text-decoration: none;
}
.pagination {
margin-top: 30px;
}
.pagination a {
display: inline-block;
text-decoration: none;
}
.pagination .pg-page {
padding: 0 6px;
}
.pagination .pg-selected {
font-weight: bold;
border-bottom: 2px solid var(--primary);
}
.pagination.top {
margin-bottom: 30px;
}
.pagination.bottom {
margin-top: 30px;
}
/* Custom pages */
.page.type textarea {
width: 100%;
min-height: 400px;
}
.page {
background-color: #fff;
padding: 30px;
}
.page li {
margin-bottom: 20px;
}
.page a {
color: var(--blue);
}
.page a:hover {
color: var(--secondary);
}
.nav {
border-bottom: 1px solid #ddd;
padding: 30px 0;
text-align: center;
margin-bottom: 30px;
}
.nav a {
text-decoration: none;
color: var(--light);
margin: 0 15px;
}
.nav a:hover {
color: var(--blue);
}
.header {
margin-bottom: 40px;
}
.footer {
padding-bottom: 30px;
text-align: center;
font-size: 0.75rem;
color: var(--light);
}
.footer a {
color: var(--light);
display: inline-block;
margin: 0 10px;
text-decoration: none;
}
.footer a:hover {
color: var(--blue);
}
.footer img {
max-height: 9px;
}
.footer .slash {
margin: 0 10px;
}
.footer .credit img {
height: 11px;
width: auto;
}
.footer .credit:hover img {
opacity: 0.6;
}
.center {
text-align: center;
}
/* Homepage */
.home {
display: flex;
justify-content: center;
min-height: 100%;
padding: 15px 0;
}
.home .header {
text-align: center;
margin-bottom: 40px;
}
.home .intro {
display: block;
}
.home .logo {
width: 100%;
flex: none;
margin-bottom: 15px;
}
.home .logo img {
max-height: 100%;
}
.home .search {
width: 100%;
flex: none;
}
/* Autocomplete */
.autocomp {
background: #f8f8f8;
border-radius: 0 0 5px 5px;
border: 1px solid #ccc;
border-top: 0;
box-shadow: 2px 2px 2px #eee;
text-align: left;
z-index: 100;
}
.autocomp-item {
padding-bottom: 5px;
padding: 10px;
cursor: pointer;
}
.autocomp-item:hover,
.autocomp-sel {
background: #f1f1f1;
font-weight: bold;
}
@media screen and (max-width: 500px) {
.search-form select {
min-width: 0;
flex-shrink: 1;
}
.search-form .input-group {
min-width: 0;
flex-shrink: 1;
}
.search-form input {
min-width: 0;
}
}
@media screen and (max-width: 720px) {
.logo {
text-align: center;
margin-bottom: 5px;
}
.logo img {
max-height: 42px;
}
.main {
margin-top: 15px;
}
.header {
margin-bottom: 0;
}
.footer a, .nav a {
display: block;
line-height: 1.4rem;
}
.footer a.icon {
display: inline-block;
}
}
@media screen and (max-width: 320px) {
.search-form>div {
flex-wrap: wrap;
}
.search-form select {
flex-basis: 100%;
border-radius: 30px;
padding: 10px 15px;
margin-bottom: 10px;
}
.search-form .input-group {
flex-basis: 100%;
width: 100%;
min-width: 0;
}
.search-form input {
border-width: 1px 0 1px 1px;
border-radius: 30px 0 0 30px;
padding: 10px 15px;
min-width: 0;
}
.entries .entry {
margin-bottom: 15px;
}
.entries .defs:not(:last-child) {
margin-bottom: 15px;
}
}

BIN
static/thumb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

1
static/up.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="6.857" viewBox="0 0 0.36 0.206" fill="none" xmlns:v="https://vecta.io/nano"><path d="M.026.18L.18.026.334.18" stroke="#000" stroke-width=".051" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 257 B

BIN
static/vkrishna.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

1
static/zerodha-logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" baseProfile="tiny" width="609" height="80" xmlns:v="https://vecta.io/nano"><path d="M66.252 21.469c4.404 5.71 8.056 12.124 10.886 19.04V3.931H46.077c7.472 4.037 14.317 9.943 20.175 17.538zM21.473 7.828c-5.754 0-11.289 1.23-16.473 3.506v64.735h68.963c-.534-37.754-23.875-68.241-52.49-68.241" fill-rule="evenodd" fill="#387ed1"/><path fill="#387ed1" d="M118.349 64.918l38.533-46.65H119.56V8.098h52.25v8.49l-38.533 46.651h38.533v10.169h-53.461v-8.49zm67.828-56.82h48.331v10.356H197.56v16.794h32.751v10.357H197.56v17.446h37.413v10.356h-48.796V8.098zm62.512 0h29.11c4.105 0 7.744.576 10.917 1.726s5.814 2.752 7.93 4.805c1.741 1.805 3.08 3.888 4.013 6.251s1.398 4.977 1.398 7.837v.187c0 2.675-.388 5.085-1.165 7.231s-1.853 4.044-3.22 5.691-3.001 3.049-4.898 4.198-3.997 2.038-6.297 2.659l17.633 24.725h-13.529l-16.097-22.765h-.186-14.227v22.765h-11.383V8.098zm28.231 32.375c4.115 0 7.393-.982 9.824-2.951s3.652-4.639 3.652-8.012v-.187c0-3.56-1.188-6.263-3.559-8.106s-5.709-2.764-10.014-2.764h-16.752v22.019h16.849zm70.389 34.055c-5.039 0-9.641-.886-13.807-2.658s-7.744-4.182-10.73-7.232-5.318-6.607-6.996-10.682-2.521-8.413-2.521-13.016v-.187c0-4.603.84-8.941 2.521-13.016s4.041-7.651 7.09-10.73 6.654-5.52 10.824-7.324 8.768-2.706 13.807-2.706 9.641.886 13.809 2.659 7.744 4.184 10.73 7.231 5.316 6.609 6.996 10.683 2.521 8.413 2.521 13.015v.187c0 4.604-.84 8.943-2.521 13.015s-4.043 7.651-7.09 10.73-6.656 5.521-10.824 7.324-8.77 2.707-13.809 2.707zm.187-10.543c3.221 0 6.178-.606 8.871-1.819s5.002-2.86 6.922-4.944 3.422-4.526 4.506-7.325 1.625-5.785 1.625-8.956v-.187c0-3.172-.541-6.173-1.625-9.004s-2.604-5.286-4.553-7.37-4.289-3.747-7.016-4.992-5.697-1.866-8.918-1.866-6.178.606-8.871 1.819-5.002 2.862-6.922 4.945-3.422 4.525-4.508 7.324-1.625 5.785-1.625 8.957v.187c0 3.172.541 6.173 1.625 9.002s2.604 5.289 4.555 7.372 4.289 3.748 7.014 4.991 5.699 1.866 8.92 1.866zm47.955-55.887h24.354c5.1 0 9.779.825 14.043 2.473s7.928 3.934 11.008 6.857 5.457 6.361 7.139 10.311 2.52 8.227 2.52 12.829v.187c0 4.604-.84 8.896-2.52 12.875s-4.059 7.435-7.139 10.356-6.748 5.226-11.008 6.904-8.943 2.519-14.043 2.519h-24.354V8.098zm24.26 54.954c3.42 0 6.531-.545 9.33-1.639s5.18-2.622 7.137-4.589 3.482-4.312 4.572-7.03 1.635-5.667 1.635-8.854v-.187c0-3.186-.547-6.152-1.635-8.9s-2.613-5.106-4.572-7.074-4.338-3.514-7.137-4.639-5.91-1.687-9.33-1.687h-12.877v44.598h12.877zm48.703-54.954h11.383v27.151h31.352V8.098h11.383v65.311h-11.383V45.885h-31.352v27.523h-11.383V8.098zm94.049-.467H573.1l28.736 65.777h-12.129l-6.625-15.768h-30.883l-6.719 15.768h-11.756l28.739-65.777zm16.422 39.841l-11.291-26.125-11.197 26.125h22.488z"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

74
submit-new.html Normal file
View File

@@ -0,0 +1,74 @@
{{ define "submit-entry" }}
{{ template "header" . }}
<h2>{{ .L.T "public.submitEntryTitle" }}</h2>
<hr />
<form method="post" action="" class="form-submit">
<div>
<div class="row">
<fieldset class="columns three">
<label for="entry-lang">{{ .L.T "public.suggestEntryLang" }}</label>
<select name="entry_lang" for="entry-lang">
{{ range $id, $l := .Langs }}
<option value="{{ $id }}">{{ $l.Name }}</option>
{{ end }}
</select>
</fieldset>
<fieldset class="columns one"></fieldset>
<fieldset class="columns eight">
<label for="entry-content">{{ .L.T "public.suggestContent" }}</label>
<textarea name="entry_content" id="entry-content" required minlength="3"></textarea>
</fieldset>
</div>
<fieldset>
<label>{{ .L.T "public.suggestPhones" }}</label>
<input type="text" name="phones" placeholder="{{ .L.T "public.suggestPhonesPlaceholder" }}" />
</fieldset>
<br />
<h3>{{ .L.T "public.suggestDefsTitle" }}</h3>
<ol class="add-relations box">
<li class="row">
<fieldset class="columns three">
<label>{{ .L.T "public.suggestDefLang" }}</label>
<select name="relation_lang">
{{ range $id, $l := .Langs }}
<option value="{{ $id }}">{{ $l.Name }}</option>
{{ end }}
</select>
<select name="relation_type">
{{ range $lid, $l := .Langs }}
{{ range $tid, $t := $l.Types }}
<option data-lang="{{ $lid }}" value="{{ $tid }}">{{ $t }}</option>
{{ end }}
{{ end }}
</select>
<br />
<a href="#" class="btn-remove-relation">{{ .L.T "global.btnDelete" }}</a>
</fieldset>
<fieldset class="columns one"></fieldset>
<fieldset class="columns eight">
<label for="definition-content">{{ .L.T "public.suggestContent" }}</label>
<textarea name="relation_content" required minlength="3"></textarea>
</fieldset>
</li>
</ol>
<p class="text-right">
<a href="#" class="btn-add-relation">+ {{ .L.T "public.suggestAddDefBtn" }}</a>
</p>
<br />
<p>
<button type="submit">{{ .L.T "public.suggestSubmitBtn" }}</button>
</p>
</div>
</form>
<script>
(() => { window.setTimeout(() => { document.querySelector('#entry-content').focus() }, 10); })();
</script>
{{ template "footer" . }}
{{ end }}