Background: #fff
Foreground: #000
PrimaryPale: #8cf
PrimaryLight: #18f
PrimaryMid: #04b
PrimaryDark: #014
SecondaryPale: #ffc
SecondaryLight: #fe8
SecondaryMid: #db4
SecondaryDark: #841
TertiaryPale: #eee
TertiaryLight: #ccc
TertiaryMid: #999
TertiaryDark: #666
Error: #f88
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::EditToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='editor' macro='edit title'></div>
<div macro='annotations'></div>
<div class='editor' macro='edit text'></div>
<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser excludeLists'></span></div>
<!--}}}-->
When getting started, you may want to:
* Set your username for signing your edits: <<option txtUserName>>
* Change the page [[title|SiteTitle]] (now "<<tiddler SiteTitle>>") and [[subtitle|SiteSubtitle]] (now "<<tiddler SiteSubtitle>>"); they also set the browser tab title
* Create a tiddler where your content "starts"
** Use the button on the sidebar or [[link|My first tiddler]] it here, follow the link, edit, and click "done"
** It will be shown in the Timeline (usually on the right), but you may want to link it in the MainMenu (usually on the left)
** and/or make it open when the ~TiddlyWiki is opened by editing the list of [[DefaultTiddlers]] (separate links with spaces or linebreaks)
* Save your ~TiddlyWiki
** Although "download saving" works in any browser, it may be not that convenient, so you'll probably want to use [[a dedicated saver|https://classic.tiddlywiki.com/#%5B%5BSetting up saving%5D%5D]]
<<importTiddlers>>
<!--{{{-->
<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
<!--}}}-->
These [[InterfaceOptions]] for customising [[TiddlyWiki]] are saved in your browser

Your username for signing your edits. Write it as a [[WikiWord]] (eg [[JoeBloggs]])

<<option txtUserName>>
<<option chkSaveBackups>> [[SaveBackups]]
<<option chkAutoSave>> [[AutoSave]]
<<option chkRegExpSearch>> [[RegExpSearch]]
<<option chkCaseSensitiveSearch>> [[CaseSensitiveSearch]]
<<option chkAnimate>> [[EnableAnimations]]

----
Also see [[AdvancedOptions]]
<!--{{{-->
<div class='header' role='banner'>
  <div class='headerShadow'>
    <span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
    <span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
  </div>
  <div class='headerForeground'>
    <span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
    <span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
  </div>
</div>
<div id='mainMenu' role='navigation' refresh='content' tiddler='MainMenu'></div>
<div id='sidebar'>
  <div id='sidebarOptions' role='navigation' refresh='content' tiddler='SideBarOptions'></div>
  <div id='sidebarTabs' role='complementary' refresh='content' force='true' tiddler='SideBarTabs'></div>
</div>
<div id='displayArea' role='main'>
<div id='messageArea'></div>
<div id='tiddlerDisplay'></div>
</div>
<!--}}}-->
/*{{{*/
body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}

a {color:[[ColorPalette::PrimaryMid]];}
a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
a img {border:0;}

h1, h2, h3, h4, h5, h6 { color: [[ColorPalette::SecondaryDark]]; }
h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}

.txtOptionInput {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}

.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}

.header {
	background: -moz-linear-gradient(to bottom, [[ColorPalette::PrimaryLight]], [[ColorPalette::PrimaryMid]]);
	background: linear-gradient(to bottom, [[ColorPalette::PrimaryLight]], [[ColorPalette::PrimaryMid]]);
}
.header a:hover {background:transparent;}
.headerShadow {color:[[ColorPalette::Foreground]];}
.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
.headerForeground {color:[[ColorPalette::Background]];}
.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}

.tabSelected {
	color:[[ColorPalette::Foreground]];
	background:[[ColorPalette::Background]];
	border-left:1px solid [[ColorPalette::TertiaryLight]];
	border-top:1px solid [[ColorPalette::TertiaryLight]];
	border-right:1px solid [[ColorPalette::TertiaryLight]];
}
.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
.tabContents {border:1px solid [[ColorPalette::TertiaryLight]];}
.tabContents .button {border:0;}

#sidebar {}
#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}

.wizard { background:[[ColorPalette::PrimaryPale]]; }
.wizard__title    { color:[[ColorPalette::PrimaryDark]]; border:none; }
.wizard__subtitle { color:[[ColorPalette::Foreground]]; border:none; }
.wizardStep { background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]]; }
.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
.wizardFooter .status a { color: [[ColorPalette::PrimaryPale]]; }
.wizard .button {
	color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
	border-color:[[ColorPalette::SecondaryDark]];
}
.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
.wizard .button:active {
	color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];
}

.wizard .notChanged {background:transparent;}
.wizard .changedLocally {background:#80ff80;}
.wizard .changedServer {background:#8080ff;}
.wizard .changedBoth {background:#ff8080;}
.wizard .notFound {background:#ffff80;}
.wizard .putToServer {background:#ff80ff;}
.wizard .gotFromServer {background:#80ffff;}

#messageArea { background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; box-shadow: 1px 2px 5px [[ColorPalette::TertiaryMid]]; }
.messageToolbar__button { color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none; }
.messageToolbar__button_withIcon { background:inherit; }
.messageToolbar__button_withIcon:active { background:inherit; border:none; }
.tw-icon line { stroke: [[ColorPalette::TertiaryDark]]; }
.messageToolbar__button:hover .tw-icon line { stroke: [[ColorPalette::Foreground]]; }

.popup {
	background: [[ColorPalette::Background]];
	color: [[ColorPalette::TertiaryDark]];
	box-shadow: 1px 2px 5px [[ColorPalette::TertiaryMid]];
}
.popup li a, .popup li a:visited, .popup li a:hover, .popup li a:active {
	color:[[ColorPalette::Foreground]]; border: none;
}
.popup li a:hover { background:[[ColorPalette::SecondaryLight]]; }
.popup li a:active { background:[[ColorPalette::SecondaryPale]]; }
.popup li.disabled { color:[[ColorPalette::TertiaryMid]]; }
.popupHighlight {color:[[ColorPalette::Foreground]];}
.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}

.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}

.tiddler .defaultCommand {font-weight:bold;}

.shadow .title {color:[[ColorPalette::TertiaryDark]];}

.title {color:[[ColorPalette::SecondaryDark]];}
.subtitle {color:[[ColorPalette::TertiaryDark]];}

.toolbar {color:[[ColorPalette::PrimaryMid]];}
.toolbar a {color:[[ColorPalette::TertiaryLight]];}
.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}

.tagging, .tagged { background: [[ColorPalette::Background]]; border: 2px solid [[ColorPalette::TertiaryPale]]; }
.selected .tagging, .selected .tagged { border: 2px solid [[ColorPalette::TertiaryLight]]; }
.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
.tagging .button, .tagged .button { border:none; }

.footer {color:[[ColorPalette::TertiaryLight]];}
.selected .footer {color:[[ColorPalette::TertiaryMid]];}

.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
.lowlight {background:[[ColorPalette::TertiaryLight]];}

.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}

.imageLink, #displayArea .imageLink {background:transparent;}

.annotation { background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }

.viewer .listTitle {list-style-type:none; margin-left:-2em;}
.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}

.twtable { background: [[ColorPalette::Background]]; }
.viewer th, .viewer thead td, .twtable th, .twtable thead td { background: [[ColorPalette::SecondaryMid]]; color: [[ColorPalette::Background]]; }
.viewer td, .viewer tr, .twtable td, .twtable tr { border: 1px solid [[ColorPalette::TertiaryLight]]; }
.twtable caption { color: [[ColorPalette::TertiaryMid]]; }

.viewer pre {background:[[ColorPalette::SecondaryPale]];}
.viewer code {color:[[ColorPalette::SecondaryDark]];}
.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}

.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}

.editor input {border:1px solid [[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%; background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
.editorFooter {color:[[ColorPalette::TertiaryMid]];}
.readOnly {background:[[ColorPalette::TertiaryPale]];}

#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:alpha(opacity=60);}
/*}}}*/
/*{{{*/
body { font-size:.75em; font-family:arial,helvetica,sans-serif; margin:0; padding:0; }

* html .tiddler {height:1%;}

h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
h4,h5,h6 {margin-top:1em;}
h1 {font-size:1.35em;}
h2 {font-size:1.25em;}
h3 {font-size:1.1em;}
h4 {font-size:1em;}
h5 {font-size:.9em;}

hr {height:1px;}

dt {font-weight:bold;}

ol {list-style-type:decimal;}
ol ol {list-style-type:lower-alpha;}
ol ol ol {list-style-type:lower-roman;}
ol ol ol ol {list-style-type:decimal;}
ol ol ol ol ol {list-style-type:lower-alpha;}
ol ol ol ol ol ol {list-style-type:lower-roman;}
ol ol ol ol ol ol ol {list-style-type:decimal;}

.txtOptionInput {width:11em; border-width: 1px; }

#contentWrapper .chkOptionInput {border:0;}

.indent {margin-left:3em;}
.outdent {margin-left:3em; text-indent:-3em;}
code.escaped {white-space:nowrap;}


a {text-decoration:none;}

.externalLink {text-decoration:underline;}

.tiddlyLinkExisting {font-weight:bold;}
.tiddlyLinkNonExisting {font-style:italic;}

/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
a.tiddlyLinkNonExisting.shadow {font-weight:bold;}

#mainMenu .tiddlyLinkExisting,
#mainMenu .tiddlyLinkNonExisting,
#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}


.header {position:relative;}
.headerShadow {position:relative; padding:3em 0 1em 1em; left:-1px; top:-1px;}
.headerForeground {position:absolute; padding:3em 0 1em 1em; left:0; top:0;}

.siteTitle {font-size:3em;}
.siteSubtitle {font-size:1.2em;}

#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}

#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
#sidebarOptions {padding-top:0.3em;}
#sidebarOptions a {margin:0 0.2em; padding:0.2em 0.3em; display:block;}
#sidebarOptions input {margin:0.4em 0.5em;}
#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
#sidebarOptions .sliderPanel input {margin:0 0 0.3em 0;}
#sidebarTabs .tabContents {width:15em; overflow:hidden;}
#sidebarTabs li:not(:last-child) { margin-bottom: 0.3em; }
#sidebarTabs ul:not(:last-child) { margin-bottom: 0.5em; }

.wizard { padding:0.1em 2em 0; }
.wizard__title    { font-size:2em; }
.wizard__subtitle { font-size:1.2em; }
.wizard__title, .wizard__subtitle { font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em; }
.wizardStep { padding:1em; }
.wizardFooter { padding: 0.8em 0; }
.wizardFooter .status { display: inline-block; line-height: 1.5; padding: 0.3em 1em; }
.wizardFooter .button { margin:0.5em 0 0; font-size:1.2em; padding:0.2em 0.5em; }

#messageArea { position:fixed; top:2em; right:0; margin:0.5em; padding:0.7em 1em; z-index:2000; }
.messageToolbar { text-align:right; padding:0.2em 0; }
.messageToolbar__button { text-decoration:underline; }
.messageToolbar__button_withIcon { display: inline-block; }
.tw-icon { height: 1em; width: 1em; } /* width for IE */
.tw-icon line { stroke-width: 1; stroke-linecap: round; }
.messageArea__text:not(:last-child) { margin-bottom: 0.3em; }
.messageArea__text a { text-decoration:underline; }

.popup {position:absolute; z-index:300; font-size:.9em; padding:0.3em 0; list-style:none; margin:0;}
.popup .popupMessage, .popup li.disabled, .popup li a { padding: 0.3em 0.7em; }
.popup li a {display:block; font-weight:normal; cursor:pointer;}
.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0;}
.listBreak {font-size:1px; line-height:1px;}
.listBreak div {margin:2px 0;}

.tiddlerPopupButton {padding:0.2em;}
.popupTiddler {position: absolute; z-index:300; padding:1em; margin:0;}

.tabset {padding:1em 0 0 0.5em;}
.tab {display: inline-block; white-space: nowrap; position: relative; bottom: -0.7px; margin: 0 0.25em 0 0; padding:0.2em;}
.tabContents {padding:0.5em;}
.tabContents ul, .tabContents ol {margin:0; padding:0;}
.txtMainTab .tabContents li {list-style:none;}
.tabContents li.listLink { margin-left:.75em;}

#contentWrapper {display:block;}
#splashScreen {display:none;}

#displayArea {margin:1em 17em 0 14em;}

.toolbar {text-align:right; font-size:.9em;}

.tiddler { padding: 1em; }

.title { font-size: 1.6em; font-weight: bold; }
.subtitle { font-size: 1.1em; }

.missing .viewer, .missing .title { font-style: italic; }
.missing .subtitle { display: none; }

.tiddler .button {padding:0.2em 0.4em;}

.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
.isTag .tagging {display:block;}
.tagged {margin:0.5em; float:right;}
.tagging, .tagged {font-size:0.9em; padding:0.25em;}
.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
.tagged li, .tagging li { margin: 0.3em 0; }
.tagClear {clear:both;}

.footer {font-size:.9em;}
.footer li {display:inline;}

.annotation { padding: 0.5em 0.8em; margin: 0.5em 1px; }

.viewer {line-height:1.4em; padding-top:0.5em;}
.viewer .button {margin:0 0.25em; padding:0 0.25em;}
.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}

.viewer table, table.twtable { border-collapse: collapse; margin: 0.8em 0; }
.viewer th, .viewer td, .viewer tr, .viewer caption, .twtable th, .twtable td, .twtable tr, .twtable caption { padding: 0.2em 0.4em; }
.twtable caption { font-size: 0.9em; }
table.listView { margin: 0.8em 1.0em; }
table.listView th, table.listView td, table.listView tr { text-align: left; }
.listView > thead { position: sticky; top: 0; }

* html .viewer pre {width:99%; padding:0 0 1em 0;}
.viewer pre {padding:0.5em; overflow:auto;}
pre, code { font-family: monospace, monospace; font-size: 1em; }
.viewer pre, .viewer code { line-height: 1.4em; }

.editor {font-size:1.1em; line-height:1.4em;}
.editor input, .editor textarea { display: block; width: 100%; box-sizing: border-box; font: inherit; padding: 0.1em 0.4em; }
.editorFooter {padding:0.25em 0; font-size:.9em;}
.editorFooter .button {padding-top:0; padding-bottom:0;}

.fieldsetFix {border:0; padding:0; margin:1px 0;}

.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
.zoomer div {padding:1em;}

* html #backstage {width:99%;}
* html #backstageArea {width:99%;}
#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em;}
#backstageToolbar {position:relative;}
#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em;}
#backstageButton {display:none; position:absolute; z-index:175; top:0; right:0;}
#backstageButton a {padding: 0.3em 0.5em; display: inline-block;}
#backstage {position:relative; width:100%; z-index:50;}
#backstagePanel { display:none; z-index:100; position:absolute; width:90%; margin:0 5%; }
.backstagePanelFooter {padding-top:0.2em; float:right;}
.backstagePanelFooter a {padding:0.2em 0.4em;}
#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}

.whenBackstage {display:none;}
.backstageVisible .whenBackstage {display:block;}
/*}}}*/
/***
StyleSheet for use when a translation requires any css style changes.
This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
***/
/*{{{*/
body {font-size:0.8em;}
#sidebarOptions {font-size:1.05em;}
#sidebarOptions a {font-style:normal;}
#sidebarOptions .sliderPanel {font-size:0.95em;}
.subtitle {font-size:0.8em;}
.viewer table.listView {font-size:0.95em;}
/*}}}*/
/*{{{*/
@media print {
  #mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea { display: none !important; }
  #displayArea { margin: 1em 1em 0em; }
}
/*}}}*/
<!--{{{-->
<div class='toolbar' role='navigation' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
<div class='tagging' macro='tagging'></div>
<div class='tagged' macro='tags'></div>
<div class='viewer' macro='view text wikified'></div>
<div class='tagClear'></div>
<!--}}}-->
[[Tiddlyhost|https://tiddlyhost.com]] is a hosting service for ~TiddlyWiki.
no
Background: #fff
Foreground: #000
PrimaryPale: #8cf
PrimaryLight: #18f
PrimaryMid: #04b
PrimaryDark: #014
SecondaryPale: #ffc
SecondaryLight: #fe8
SecondaryMid: #db4
SecondaryDark: #841
TertiaryPale: #eee
TertiaryLight: #ccc
TertiaryMid: #999
TertiaryDark: #666
Error: #f88
/***
|Name       |DarkModePlugin|
|Description|This plugin introduces "dark mode" (changes styles) and switching it by the {{{darkMode}}} macro and operating system settings|
|Documentation|https://yakovl.github.io/TiddlyWiki_DarkModePlugin/|
|Version    |1.3.3|
|Author     |Yakov Litvin|
|Source     |https://github.com/YakovL/TiddlyWiki_DarkModePlugin/blob/master/DarkModePlugin.js|
|License    |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
!!!Demo
<<darkMode>>
<<darkMode label:"☀️/🌘">>
!!!Syntax
{{{
<<darkMode>> (<<switchNightMode>> also works, for backward compatibility)
<<darkMode label:"☀️/🌘">>
}}}
!!!Installation
Is as usual: import or copy the plugin with the {{{systemConfig}}} tag, reload. Note: for the plugin to work correctly, you should keep its name (DarkModePlugin).

!!!Optional configuration
When the dark mode is applied, the {{{darkMode}}} class is added to the {{{html}}} element. This allows to add ''styles for dark mode'' only, like this:
{{{
.darkMode code { color:red }
code { color: green }
}}}
Ordinary styles are applied to both modes, but {{{.darkMode}}} ones have higher precedence and "overwrite" the oridinary ones.

The palette applied for the dark mode can be ''customized'' by editing ColorPaletteDark (removing it restores the default values).

!!!Additional notes
Styles of some browser interface bits (like <html><button class="button" onclick='alert("this is known as an alert")'>alert</button</html>) are only affected by OS/browser's "dark mode"/theme, so for good UI it is recommended to switch OS dark mode (DarkModePlugin will follow). For Windows users, [[switching by hotkey|https://superuser.com/a/1724237/576393]] may be useful.

The plugin ''adds extra styles'' (see ~FollowDarkMode and ~FewerColors sections) which are not yet configurable.

The option {{{chkDarkMode}}} is now ''deprecated'': later it will be either removed or re-implemented.
!!!Code
***/
//{{{
config.macros.switchNightMode = // backward compatibility
config.macros.darkMode = {
	pluginName: "DarkModePlugin",
	optionName: "chkDarkMode",
	getDarkPaletteText: function() {
		return store.getTiddlerText(this.darkPaletteTitle)
	},
	// this helper may become more complex for custom themes
	getMainPaletteTitle: function() {
		return "ColorPalette"
	},
	lightPaletteTitle: "ColorPaletteLight",
	darkPaletteTitle: "ColorPaletteDark",

	// setDark, setLight, and applyAdjustments are "governed outside": they don't check or change the cookie-parameter
	setDark: function() {
		var paletteTitle = this.getMainPaletteTitle()

		var lightPaletteTiddler = new Tiddler(this.lightPaletteTitle)
		lightPaletteTiddler.text = store.getTiddlerText(paletteTitle) || "shadow"
		store.saveTiddler(lightPaletteTiddler)

		var darkPaletteTiddler = new Tiddler(paletteTitle)
		darkPaletteTiddler.text = this.getDarkPaletteText()
		// attach the tiddler, recalc slices, invoke notifiers
		store.saveTiddler(darkPaletteTiddler)

		this.applyAdjustments(true)
	},
	setLight: function() {
		var paletteTitle = this.getMainPaletteTitle()

		var lightPaletteText = store.getTiddlerText(this.lightPaletteTitle)
		if(!lightPaletteText || lightPaletteText === "shadow")
			store.removeTiddler(paletteTitle) // to recalc slices of ColorPalette
		else
			store.saveTiddler(paletteTitle, paletteTitle, lightPaletteText)

		store.deleteTiddler(this.lightPaletteTitle)

		this.applyAdjustments(false)
	},
	applySectionCSS: function(sectionName) {
		var sectionText = store.getRecursiveTiddlerText(this.pluginName + "##" + sectionName, "", 1)
		var css = sectionText.replace(/^\s*{{{((?:.|\n)*?)}}}\s*$/, "$1")
		return setStylesheet(css, sectionName)
	},
	applyAdjustments: function(isDarkMode) {
		if(isDarkMode) {
			jQuery('html').addClass('darkMode')
			this.applySectionCSS("FollowDarkMode")
			this.applySectionCSS("~FewerColors")
		} else {
			jQuery('html').removeClass('darkMode')
			removeStyleSheet("FollowDarkMode")
			removeStyleSheet("~FewerColors")
		}
	},

	// "governance" methods
	isDarkMode: function() {
		return !!store.fetchTiddler(this.lightPaletteTitle)
	},
	switchMode: function() {
		var me = config.macros.darkMode
		config.options[me.optionName] = !config.options[me.optionName]

		config.options[me.optionName] ? me.setDark() : me.setLight()

// "baking" doesn't work yet..
		if(saveOption)
			saveOption(me.optionName)
		else
			saveOptionCookie(me.optionName)

		refreshColorPalette()
	},
	followOsMode: function(followLight) {
		// old browsers may fail to detect
		var isOsDarkModeDetected = window.matchMedia &&
			window.matchMedia('(prefers-color-scheme: dark)').matches

		if(isOsDarkModeDetected && !this.isDarkMode()) {
			config.options[this.optionName] = false
			this.switchMode()
		}

		if(!isOsDarkModeDetected && this.isDarkMode() && followLight) {
			config.options[this.optionName] = true
			this.switchMode()
		}
	},
	restoreSavedMode: function() {
		if(!this.isDarkMode()) return

		// TODO: check if styles are really missing (avoid applying twice)
		this.applyAdjustments(true)
		config.options[this.optionName] = true
	},
	handler: function(place, macroName, params, wikifier, paramString, tiddler) {
		var pParams = paramString.parseParams("anon", null, true, false, true)
		var label = getParam(pParams, "label", "switch")
		var tooltip = ""

		createTiddlyButton(place, label, tooltip, this.switchMode, 'button darkModeSwitcher')
	}
}

// We avoid using .init to support installation via SharedTiddlersPlugin, TiddlerInFilePlugin, and reinstalling via CookTiddlerPlugin.
// This also helps to avoid extra refreshing.
;(function(macro) {
	// Save the palette as shadow so that one can cusomize it
	config.shadowTiddlers[macro.darkPaletteTitle] =
		store.getTiddlerText(macro.pluginName + "##DarkModeColorPalette")

	// Set dark mode on start if OS dark mode is set or dark mode was saved previously
	macro.followOsMode(false)
	macro.restoreSavedMode()

	// install only once
	if(!config.extensions.DarkModePlugin) {
		// prevent sites to ask about unsaved changes after switching mode
		config.extensions.DarkModePlugin = {
			orig_confirmExit: confirmExit
		}
		window.confirmExit = function() {
			if(readOnly) return
			return config.extensions.DarkModePlugin.orig_confirmExit ?
				config.extensions.DarkModePlugin.orig_confirmExit() : undefined
		}

		// Detect OS mode change, apply
		if(window.matchMedia) window.matchMedia('(prefers-color-scheme: dark)')
			.addEventListener('change', function(event) { macro.followOsMode(true) })
	}
})(config.macros.darkMode)
//}}}
/***
!!!FollowDarkMode
{{{
input, select, textarea {
	color:[[ColorPalette::Foreground]];
	background-color:[[ColorPalette::Background]];
}

.darkMode {
	color-scheme: dark;
}
}}}
!!!~FewerColors
{{{
.title, h1, h2, h3, h4, h5, h6 {
	color: [[ColorPalette::PrimaryDark]];
}
::selection {
	background: [[ColorPalette::TertiaryMid]];
}
}}}
!!!DarkModeColorPalette
Background: #000
Foreground: #ddd
~PrimaryPale: #730
~PrimaryLight: #e70
~PrimaryMid: #fb4
~PrimaryDark: #feb
~SecondaryPale: #003
~SecondaryLight: #017
~SecondaryMid: #24b
~SecondaryDark: #7be
~TertiaryPale: #111
~TertiaryLight: #333
~TertiaryMid: #666
~TertiaryDark: #999
Error: #f44
!!!
***/
[[What to check out here]]
[[Setting up this page]]
|Source     |https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsCollection.txt|
|Description|This is a central collection for ExtensionsExplorerPlugin. It is meant to gather collections of existing extensions, and also to help new authors make their work more explorable.|
|Version    |0.2.2|
Current status is "under construction", meaning that there's a lot of collections and extensions to add. Other things should be considered as well, like quality guidelines, handling forks, etc. Instructions for authors will be published in a separate readme and linked here.
//{{{
[
  {
    "url": "https://github.com/YakovL/TiddlyWiki_YL_ExtensionsIndex/blob/master/YLExtensionsCollection.txt",
    "description": "Extensions created or heavily modified by Yakov Litvin",
    "type": "collection"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_abego/blob/master/maintained/AbegoCollection.txt",
    "description": "Extensions created by Udo Borkowski",
    "type": "collection"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_Extensions/blob/master/TranslationsCollection.txt",
    "description": "TiddlyWiki Translations",
    "type": "collection"
  },
  {
    "url": "https://github.com/YakovL/TiddlyThemes/blob/master/ThemesCollection.txt",
    "description": "TiddlyWiki themes (see also https://yakovl.github.io/TiddlyThemes/)",
    "type": "collection"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_Extensions/blob/master/FND/SimpleSearchPlugin.js",
    "description": "Displays search results as a simple list of matching tiddlers"
  },
  {
    "url": "https://github.com/PengjuYan/TiddlyWiki_SwitchPalettePlugin/blob/master/SwitchPalettePlugin.js",
    "description": "Switches among your color palettes"
  },
  {
    "url": "https://github.com/wangyenshu/TiddlyWikiClassicPluginsArchives/blob/main/TiddlyWikiClassicPluginsArchives.txt",
    "description": "Dedicated extensions archives, covering most of the existing plugins (collection per TW, like TiddlyTools or PeachTW)",
    "type": "collection"
  }
]
//}}}
/***
|Description|checks and reports updates of installed extensions on startup, introduces a macro/backstage button to explore, install and update extensions|
|Version    |0.7.0|
|Author     |Yakov Litvin|
|Source     |https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsExplorerPlugin.js|
|License    |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
!!!Installation & configuration
Installation of the plugin is as usual: import the tiddler or copy and tag it with {{{systemConfig}}}; reload TW.

!!!What EEP does, how to use it
Once you install this plugin, on startup, it will try to check if installed extensions have any updates available and report if it finds any. An update of a particular extension is looked up by the url in the Source slice (see this tiddler for example). EEP will recognize an "update" if it finds the content by that url, and that content has a Version slice and the version is higher than the installed one (like: 0.4.2 is higher than 0.3.9; 0.0.1 is also higher than none).

It also adds "explore extensions" in the backstage (and the {{{<<extensionsExplorer>>}}} macro with the same interface) that shows some extensions available for installation and the list of installed plugins with buttons to check for updates.

Note: With some TW savers/servers, loading an extension may fail if its author hasn't enabled CORS on the server pointed by Source.

!!!For extension authors: how to prepare extensions and repositories
To make EEP find updates for your extensions, you have to
# put it somewhere in the internet:
** the server should have CORS enabled (~GitHub is fine);
** the extension should be in either form: "plain text" (.js or .txt file extension) or a tiddler in a TW (.html extension);
# ensure that the extension has a Source slice with a url that points to itself (i.e. where to look for the latest version):
** for plain text, one can use a direct url, like: https://raw.githubusercontent.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/master/ShowUnsavedPlugin.js;
** for ~GitHub, one can also use the url of the UI page (i.e. navigate to it via ~GitHub UI and copy the address): https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js;
** for a tiddler inside a TW, use a permalink, like: https://TiddlyTools.com/Classic/#NestedSlidersPlugin (note that the Source slice in this plugin is in fact outdated: http://www.TiddlyTools.com/#NestedSlidersPlugin – you should avoid that as this will break the updating flow);
** for a tiddler inside a TW on ~GitHub, use ~GitHub Pages (this is in fact how ~TiddlyTools is served, they just use a custom domain; an example of an "ordinary" url: https://yakovl.github.io/TiddlyWiki_ExtraFilters/#ExtraFiltersPlugin);
** for your dev flow, it may be useful to put the plugin to ~GitHub as a .js file and load it into the demo TW via [[TiddlerInFilePlugin|https://github.com/YakovL/TiddlyWiki_TiddlerInFilePlugin]]. An example of such setup can be found [[here|https://github.com/YakovL/TiddlyWiki_FromPlaceToPlacePlugin]].

***/
//{{{
// Returns the slice value if it is present or defaultText otherwise
//
Tiddler.prototype.getSlice = Tiddler.prototype.getSlice || function(sliceName, defaultText) {
	let re = TiddlyWiki.prototype.slicesRE, m
	re.lastIndex = 0
	while(m = re.exec(this.text)) {
		if(m[2]) {
			if(m[2] == sliceName) return m[3]
		} else {
			if(m[5] == sliceName) return m[6]
		}
	}
	return defaultText
}

const centralSourcesListName = "AvailableExtensions"

config.macros.extensionsExplorer = {
	lingo: {
		backstageButtonLabel: "explore extensions",
		backstageButtonTooltip: "See if there are any updates or install new ones",
		installButtonLabel: "install",
		installButtonPrompt: "get and install this extension",
		otherActionsPrompt: "show other actions",
		getFailedToLoadMsg: name => "failed to load " + name,
		getSucceededToLoadMsg: name => `loaded ${name}, about to import and install...`,
		noSourceUrlAvailable: "no source url",
		getEvalSuccessMsg: name => `Successfully installed ${name} (reload is not necessary)`,
		getEvalFailMsg: (name, error) => `${name} failed with error: ${error}`,
		getImportSuccessMsg: (title, versionString, isUpdated) => isUpdated ?
			`Updated ${title}${versionString ? " to " + versionString : ""}` :
			`Imported ${title}${versionString ? " v" + versionString : ""}`,

		updateButtonCheckLabel: "check",
		updateButtonCheckPrompt: "check for updates",
		updateButtonUpdateLabel: "update",
		updateButtonUpdatePrompt: "install available update",
		getUpdateAvailableMsg: name => `update of ${name} is available!`,
		getUpdateAvailableAndVersionsMsg: (existingTiddler, newTiddler) => {
			const getVersionString = config.macros.extensionsExplorer.getVersionString
			return `update of ${existingTiddler.title} is available ` +
				"(current version: " + getVersionString(existingTiddler) +
				", available version: " + getVersionString(newTiddler) + ")"
		},
		updateNotAvailable: "update is not available",
		getUpdateConfirmMsg: (title, loadedVersion, presentVersion) => {
			const loadedVersionString = loadedVersion ? formatVersion(loadedVersion) : ""
			const presentVersionString = presentVersion ? formatVersion(presentVersion) : ""
			return `Would you like to update ${title}` +
				` (new version: ${loadedVersionString || "unknown"}, ` +
			 	`current version: ${presentVersionString || "unknown"})?`
		},

		centralSourcesListAnnotation: "The JSON here describes extensions so that ExtensionsExplorerPlugin can install them"
	},

	// helpers specific to tiddler format
	guessExtensionType: function(tiddler) {
		if(tiddler.tags.contains('systemConfig') ||
		   tiddler.getSlice('Type', '').toLowerCase() == 'plugin' ||
		   /Plugin$/.exec(tiddler.title)
		)
			return 'plugin'
	},
	// We use the server.host field a bit different than the core does (see importing):
	// we keep #TiddlerName part which won't hurt except for the plugin https://github.com/TiddlyWiki/tiddlywiki/blob/master/plugins/Sync.js (which we kinda substitute anyway),
	// we also don't set server.type and server.page.revision fields yet (unlike import); see also server.workspace, wikiformat fields.
	sourceUrlField: 'server.host',
	getSourceUrl: function(tiddler) {
		return tiddler.fields[this.sourceUrlField] || tiddler.getSlice('Source')
		//# try also the field set by import (figure the name by experiment)
	},
	setSourceUrl: function(tiddler, url) {
		//# simple implementation, not sure if setValue should be used instead
		tiddler.fields[this.sourceUrlField] = url
	},
	getDescription: tiddler => tiddler.getSlice('Description', ''),
	getVersionString: tiddler => tiddler.getSlice('Version', ''),
	getVersion: function(tiddler) {
		const versionString = this.getVersionString(tiddler)
		//# should use a helper from core instead
		const parts = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(versionString)
		return parts ? {
			major: parseInt(parts[1]),
			minor: parseInt(parts[2]),
			revision: parseInt(parts[3] || '0')
		} : {}
	},

	// helpers to get stuff from external repos
	//# start from hardcoding 1 (.oO data sctructures needed
	//  for getAvailableExtensions and various user scenarios),
	//  then several (TW/JSON, local/remote)
	availableRepositories: [],
	getAvailableRepositories: function() {
		return this.availableRepositories
	},
	// fallback used when AvailableExtensions is empty
	defaultAvailableExtensions: [
		{
			url: 'https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsCollection.txt',
			description: 'A central extensions collection for ExtensionsExplorerPlugin meant to both gather collections of existing extensions and help new authors make their work more explorable',
			type: 'collection'
		},
		{
			// js file @ github - worked /# simplify url to be inserted?
			name: 'ShowUnsavedPlugin',
			sourceType: 'txt',
			url: 'https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js',
			description: 'highlights saving button (bold red by default) and the document title (adds a leading "*") when there are unsaved changes',
			type: 'plugin',
			text: ''
		},
		{
			url: 'https://github.com/YakovL/TiddlyWiki_DarkModePlugin/blob/master/DarkModePlugin.js',
			description: 'This plugin introduces "dark mode" (changes styles) and switching it by the {{{darkMode}}} macro and operating system settings'
		},
		{
			// in TW @ remote (CORS-enabled) – worked
			name: 'FieldsEditorPlugin',
			sourceType: 'tw',
			url: 'https://yakovl.github.io/VisualTW2/VisualTW2.html#FieldsEditorPlugin',
			description: 'adds controls (create/edit/rename/delete) to the "fields" toolbar dropdown',
			type: 'plugin'
		},
		{
			// txt file @ remote without CORS – worked with _
			url: 'http://yakovlitvin.pro/TW/pre-releases/Spreadsheets.html#HandsontablePlugin',
			description: 'a test plugin on a site without CORS'
		},
		{
			url: 'https://github.com/tobibeer/TiddlyWikiPlugins/blob/master/plugins/ListFiltrPlugin.js'
		}
	],
	guessNameByUrl: function(extension) {
		if(!extension.url) return undefined
		const urlParts = extension.url.split('#')

		// site.domain/path/tw.html#TiddlerName  or  site.domain/path/#TiddlerName
		if(urlParts.length > 1 && /(\.html|\/)$/.exec(urlParts[0])) return urlParts[1]

		// <url part>/TiddlerName.txt or <url part>/TiddlerName.js
		const textPathMatch = /\/([^\/]+)\.(js|txt)$/.exec(urlParts[0])
		return textPathMatch ? textPathMatch[1] : undefined
	},
	collectionTag: 'systemExtensionsCollection',
	parseCollection: function(text) {
		/* expected format:

		< additional info, like |Source|...| and other metadata >
		//{{{
		< extensions as JSON >
		//}}}

		*/
		const match = /(\/\/{{{)\s+((?:.|\n)+)\s+(\/\/}}})\s*$/.exec(text)
		if(match) try {
			const list = JSON.parse(match[2])
			return list.map(extension => ({
				name: extension.name || this.guessNameByUrl(extension),
				...extension
			}))
		} catch (e) {
			console.log(`problems with parsing ${centralSourcesListName}:`, e)
			return null
		}
	},
	// reads .centralSourcesListName, .defaultAvailableExtensions, collections
	getAvailableExtensions: function() {
		const listText = store.getTiddlerText(centralSourcesListName)
		const availableExtensions = this.parseCollection(listText)
			|| this.defaultAvailableExtensions

		const otherCollections = store.filterTiddlers("[tag[" + this.collectionTag + "]]")
		for(const collectionTiddler of otherCollections) {
			const extensions = this.parseCollection(collectionTiddler.text)
			// for now, just merge
			if(extensions) for(const extension of extensions) {
				availableExtensions.push(extension)
			}
		}

		return availableExtensions
	},
	availableUpdatesCache: {},
	cacheAvailableUpdate: function(sourceUrl, tiddler) {
		this.availableUpdatesCache[sourceUrl] = { tiddler: tiddler }
	},
	// github urls like https://github.com/tobibeer/TiddlyWikiPlugins/blob/master/plugins/FiltrPlugin.js
	// are urls of user interface; to get raw code, we use the official githubusercontent.com service
	// also, we change the old urls https://raw.github.com/tobibeer/TiddlyWikiPlugins/master/plugins/FiltrPlugin.js
	getUrlOfRawIfGithub: function(url) {
		const ghUrlRE = /^https:\/\/github\.com\/(\w+?)\/(\w+?)\/blob\/(.+)$/
		const oldGhRawUrlRE = /^https:\/\/raw.github.com\/(\w+?)\/(\w+?)\/(.+)$/
//# test
		const match = ghUrlRE.exec(url) || oldGhRawUrlRE.exec(url)
		if(match) return 'https://raw.githubusercontent.com/' + match[1] + // username
			'/' + match[2] + // repository name
			'/' + match[3] // path
		return url
	},
	twsCache: {}, // map of strings
	/*
	@param sourceType: 'tw' | string | fasly (default = 'txt') -
	 of the tiddler source (a TW or a text file)
	@param url: string - either url of the text file or url#TiddlerName
	 for a TW (TiddlerName defines the title of the tiddler to load)
	@param title: string - is assigned to the loaded tiddler
	@param callback: tiddler | null => void
	 support second param of callback? (error/xhr)
	*/
	loadExternalTiddler: function(sourceType, url, title, callback, useCache) {
		sourceType = sourceType || this.guessSourceType(url)
		//# if sourceType is uknown, we can load file and guess afterwards
		if(sourceType == 'tw') {
			const tiddlerName = url.split('#')[1] || title
			const requestUrl = url.split('#')[0]
			const cache = this.twsCache
			const onTwLoad = function(success, params, responseText, url, xhr) {
				//# pass more info? outside: warn?
				if(!success) return callback(null)
				if(!useCache) cache[requestUrl] = responseText

				const externalTW = new TiddlyWiki()
				const result = externalTW.importTiddlyWiki(responseText)
				//# pass more info? outside: warn?
				if(!result) return callback(null)

				const tiddler = externalTW.fetchTiddler(tiddlerName)
				tiddler.title = title
				callback(tiddler)

				// above is a simple "from scratch" implementation
				//# should we reuse existing core code? (see import)
				//  currently, this only loads and passes tiddler,
				//  actual import is done in 
				const context = {
					adaptor: {},
					complete: function() {}
				}
//				FileAdaptor.loadTiddlyWikiSuccess(context, );
				//# import, see ...
				//# tiddler.title = title;
				//# callback(tiddler);
			}
			if(useCache && cache[requestUrl])
				onTwLoad(true, null, cache[requestUrl])
			else
				httpReq('GET', requestUrl, onTwLoad)
		} else {
			url = this.getUrlOfRawIfGithub(url)
			httpReq('GET', url, function(success, params, responseText, url, xhr) {
				//# pass more info? outside: warn?
				if(!success) return callback(null)

				const tiddler = new Tiddler(title)
				// remove \r originating from Windows
				tiddler.text = responseText.replace(/\r\n/g, '\n')
				tiddler.generatedByTextOnly = true
				callback(tiddler)
			})
		}
	},

	getInstalledExtensions: function() {
		//# instead of returning tiddlers, create extension objects,
		//  those should have ~isInstalled, ~isEnabled, ~hasUpdates flags
		//  (and change refresh accordingly)
		return store.filterTiddlers(`[tag[systemConfig]] ` +
			`[tag[${this.collectionTag}]] [[${centralSourcesListName}]]`)
		//# implement others: themes, transclusions
	},
	// for each installed extension, check for update and reports (now: displays message)
	init: function() {
		//# set delegated handlers of install, update buttons
		const extensionTiddlers = this.getInstalledExtensions()
		if(!config.options.chkSkipExtensionsUpdatesCheckOnStartup && !readOnly)
			for(const eTiddler of extensionTiddlers) {
				const url = this.getSourceUrl(eTiddler)
				if(!url) continue
				this.checkForUpdate(url, eTiddler, result => {
		console.log('checkForUpdate for ' + url +
			',', eTiddler, 'result is:', result)
					if(result.tiddler && !result.noUpdateMessage) {
						displayMessage(this.lingo.getUpdateAvailableAndVersionsMsg(eTiddler, result.tiddler))
					}
					//# either report each one at once,
					//   (see onUpdateCheckResponse)
					//  create summary and report,
					//   (use availableUpdates)
					//  create summary and just show "+4" or alike (better something diminishing),
					//  or even update (some of) ext-s silently
					//# start with creating summary
				})
			}

		const taskName = "explorePlugins"
		config.backstageTasks.push(taskName)
		config.tasks[taskName] = {
			text: this.lingo.backstageButtonLabel,
			tooltip: this.lingo.backstageButtonTooltip,
			content: '<<tiddler ExtensionsInBackstage>>',
		}
	},
	handler: function(place, macroName, params, wikifier, paramString) {
		// parse param "[type:installed|available]"
		const pParams = paramString.parseParams("type", null, true, false, true)
		const type = getParam(pParams, "type", "")

		const tableHeaderMarkup = "|name|description|version||h"
		// name is supposted to be a link to the repo; 3d row – for "install" button
		wikify(tableHeaderMarkup, place)
		const table = place.lastChild

		jQuery(table).attr({ refresh: 'macro', macroName: macroName })
			.addClass('extensionsExplorer').append('<tbody>')
			.attr({ 'data-eep-type': type })

		this.refresh(table)
	},
	// grabs list of available extensions and shows with buttons to install;
	// for each installed plugin, shows a button to check update or "no url" message,
	refresh: function(table) {
		const $tbody = jQuery(table).find('tbody')
			.empty()
		const type = jQuery(table).attr('data-eep-type')

		// safe method (no wikification, innerHTML etc)
		const appendRow = function(cells) {
			const row = document.createElement('tr')
			const nameCell = createTiddlyElement(row, 'td')
			if(cells.url)
				createExternalLink(nameCell, cells.url, cells.name)
			else
				createTiddlyLink(nameCell, cells.name, true)

			createTiddlyElement(row, 'td', null, null, cells.description)

			createTiddlyElement(row, 'td', null, null, cells.version)

			const actionsCell = createTiddlyElement(row, 'td', null, 'actionsCell')
			const actionsWrapper = createTiddlyElement(actionsCell, 'div', null, 'actionsWrapper')
			if(cells.actionElements.length > 0) {
				actionsWrapper.appendChild(cells.actionElements[0])
				actionsWrapper.firstChild.classList.add('mainButton')
			}
			if(cells.actionElements.length > 1) {
				const { lingo } = config.macros.extensionsExplorer
				const otherActionEls = cells.actionElements.slice(1)
				createTiddlyButton(actionsWrapper, '▾',
					lingo.otherActionsPrompt,
					function(event) {
						const popup = Popup.create(actionsWrapper)
						for(const e of otherActionEls) {
							const li = createTiddlyElement(popup, 'li')
							li.appendChild(e)
						}
						popup.style.minWidth = actionsWrapper.offsetWidth + 'px'
						Popup.show()
						event.stopPropagation()
						return false
					},
					'button otherActionsButton')
			}

			$tbody.append(row)
		}

		//# when implemented: load list of available extensions (now hardcoded)

		const installedExtensionsTiddlers = this.getInstalledExtensions()
			.sort((e1, e2) => {
				const up1 = this.availableUpdatesCache[this.getSourceUrl(e1)]
				const up2 = this.availableUpdatesCache[this.getSourceUrl(e2)]
				return	up1 && up2 ? 0 :
					up1 && !up2 ? -1 :
					up2 && !up1 ? +1 :
					!this.getSourceUrl(e1) ? +1 :
					!this.getSourceUrl(e2) ? -1 : 0
			})

		// show extensions available to install
		if(!type || type == 'available') {
			const availableExtensions = this.getAvailableExtensions()

			for(const extension of availableExtensions) {
				// skip installed
				if(installedExtensionsTiddlers.some(tid =>
					tid.title === extension.name
					&& this.getSourceUrl(tid) === extension.url)
				) continue

				if(!extension.name && extension.sourceType == 'tw')
					extension.name = extension.url.split('#')[1]

				appendRow({
					name:        extension.name,
					url:         extension.url,
					description: extension.description,
					version:     extension.version,
					actionElements: [
						createTiddlyButton(null,
							this.lingo.installButtonLabel,
							this.lingo.installButtonPrompt,
							() => this.grabAndInstall(extension) )
					]
				})
			}
		}
		//# add link to open, update on the place of install – if installed

		// show installed ones.. # or only those having updates?
		if(!type) $tbody.append(jQuery(
			`<tr><td colspan="4" style="text-align: center;">Installed</td></tr>`))
		if(!type || type == 'installed') {
			for(const extensionTiddler of installedExtensionsTiddlers) {
				//# limit the width of the table|Description column
				const updateUrl = this.getSourceUrl(extensionTiddler)
					//# check also list of extensions to install
				const onUpdateCheckResponse = (result, isAlreadyReported) => {
					if(!result.tiddler) {
						displayMessage(this.lingo.updateNotAvailable)
						//# use result.error
						return
					}
					const versionOfLoaded = this.getVersion(result.tiddler)
					const versionOfPresent = this.getVersion(extensionTiddler)

					if(compareVersions(versionOfLoaded, versionOfPresent) >= 0) {
						displayMessage(this.lingo.updateNotAvailable)
						//# use result.error
						return
					}
					if(!isAlreadyReported) displayMessage(this.lingo.getUpdateAvailableMsg(extensionTiddler.title), updateUrl)

					//# later: better than confirm? option for silent?
					if(confirm(this.lingo.getUpdateConfirmMsg(
						extensionTiddler.title,
						versionOfLoaded, versionOfPresent))
					) {
						this.updateExtension(result.tiddler, updateUrl)
					}
				}

				const checkUpdateButton = createTiddlyButton(null,
					this.lingo.updateButtonCheckLabel,
					this.lingo.updateButtonCheckPrompt,
					() => this.checkForUpdate(updateUrl, extensionTiddler,
						onUpdateCheckResponse))

				const cachedUpdate = this.availableUpdatesCache[updateUrl]
				const installUpdateButton = createTiddlyButton(null,
					this.lingo.updateButtonUpdateLabel,
					this.lingo.updateButtonUpdatePrompt,
					() => onUpdateCheckResponse(cachedUpdate, true))

				appendRow({
					name: extensionTiddler.title,
					description: this.getDescription(extensionTiddler),
					version: this.getVersionString(extensionTiddler),
					actionElements: [
						!updateUrl ? createTiddlyElement(null, 'div', null, 'actionsLabel', this.lingo.noSourceUrlAvailable) :
						cachedUpdate ? installUpdateButton :
						checkUpdateButton
					]
				})
			}
		}
	},
	grabAndInstall: function(extension) {
		if(!extension) return
		if(extension.text) {
			const extensionTiddler = new Tiddler(extension.name)
			extensionTiddler.text = extension.text
			extensionTiddler.generatedByTextOnly = true
			//# share 3 ↑ lines as ~internalize helper (with loadExternalTiddler)
			this.install(extensionTiddler, extension.type, extension.url)
			return
		}
		this.loadExternalTiddler(
			extension.sourceType,
			extension.url,
			extension.name,
			tiddler => {
				if(!tiddler) {
					displayMessage(this.lingo.getFailedToLoadMsg(extension.name))
					return
				}
				displayMessage(this.lingo.getSucceededToLoadMsg(tiddler.title))
				this.install(tiddler, extension.type ||
					this.guessExtensionType(tiddler), extension.url)
			}
		)
	},
	// evaluate if a plugin, import
	//# simple unsafe version, no dependency handling, registering as installed,
	//  _install-only-once check_, result reporting, refreshing/notifying, ..
	install: function(extensionTiddler, extensionType, sourceUrl) {
		if(!extensionTiddler) return

		const { text, title } = extensionTiddler
		switch(extensionType) {
			case 'plugin':
				// enable at once
				try {
					eval(text)
					displayMessage(this.lingo.getEvalSuccessMsg(title))
				} catch(e) {
					displayMessage(this.lingo.getEvalFailMsg(title, e))
					//# don't import? only on confirm?
				}
				// import preparation
				extensionTiddler.tags.pushUnique('systemConfig')
			break;

			case 'collection':
				extensionTiddler.tags.pushUnique(this.collectionTag)
			break;

			//# add _ tag for themes?
		}

		// actually import etc
		this.updateExtension(extensionTiddler, sourceUrl)
		//# what if exists already? (by the same name; other name)
	},
	updateExtension: function(extensionTiddler, sourceUrl) {
		// import
		var existingTiddler = store.fetchTiddler(extensionTiddler.title)
		if(extensionTiddler.generatedByTextOnly && existingTiddler) {
			existingTiddler.text = extensionTiddler.text
			existingTiddler.modified = new Date()
			//# update also modifier? changecount?
		} else {
			store.addTiddler(extensionTiddler)
		}
		if(sourceUrl && this.getSourceUrl(extensionTiddler) !== sourceUrl) {
			this.setSourceUrl(extensionTiddler, sourceUrl)
		}

		delete this.availableUpdatesCache[sourceUrl]
		store.setDirty(true)
		//# store url for updating if slice is not present?
		// make explorer and other stuff refresh
		store.notify(extensionTiddler.title, true)
		//# .oO reloading, hot reinstalling
		displayMessage(this.lingo.getImportSuccessMsg(extensionTiddler.title,
			this.getVersionString(extensionTiddler), !!existingTiddler))
	},
	guessSourceType: function(url) {
		if(/\.(txt|js)$/.exec(url.split('#')[0])) return 'txt'
		//# guess by url instead, fall back to 'txt'
		return 'tw'
	},
//# careful: extension keyword is overloaded (extension object/tiddler)
	/*
	  tries to load update for tiddler, if succeeds calls callback with
	   argument depending on whether it has newer version than the existing one
	  @param url: _
	  @param extensionTiddler: _
	  @param callback: is called [not always yet..] with argument
		{ tiddler: Tiddler | null, error?: string, noUpdateMessage?: string }
		if update is found and it has version newer than extensionTiddler,
		it is called with { tiddler: Tiddler }
	*/
	checkForUpdate: function(url, extensionTiddler, callback) {
		if(!url) return
		const title = extensionTiddler.title
		this.loadExternalTiddler(null, url, title, loadedTiddler => {
			if(!loadedTiddler) return callback({
				tiddler: null,
				error: "" //# specify
			})
			if(compareVersions(this.getVersion(loadedTiddler),
					   this.getVersion(extensionTiddler)
					  ) >= 0)
			//# also get and compare modified dates?
			{
				//# what about undefined?
				console.log('loaded is not newer')
				callback({
					tiddler: loadedTiddler,
					noUpdateMessage: "current version is up-to-date"
				})
			} else {
				this.cacheAvailableUpdate(url, loadedTiddler)
				callback({ tiddler: loadedTiddler })
			}
		})
	}
}

config.shadowTiddlers.ExtensionsInBackstage = `<<tabs txtTabExtensionsExplorer
	"check and update" "" ExtensionsExplorer
	"explore and install" "" ExtensionsOutThere
	contribute "" ContributeToExtensionsEcosystem
>>`

config.shadowTiddlers.ExtensionsExplorer = `<<extensionsExplorer type:installed>>`

config.shadowTiddlers.ExtensionsOutThere = `<<extensionsExplorer type:available>>

Some repositories not yet indexed by EEP that may be worth checking:
|[[TiddlyTools|https://tiddlytools.com/Classic]]|The largest extensions repository created mostly by a single developer, Eric Shulman. Source slice is currently outdated in all the extensions, be sure to change it to the up-to-date urls|
||..more repositories will be added, check the "contribute" tab for more|

Old indexes of existing extensions (EEP is meant to eventually substitute them):
|[[Customize|https://yakovlitvin.pro/TW/TS_backups/customize.tiddlyspace.com%20(24.02.2016).html]]|archive of the big index created by Tobias Beer and contributors|`

config.shadowTiddlers.ContributeToExtensionsEcosystem = `Indexing estensions and repositories for EEP is work in progress. You can suggest changes in [[Github|https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin]] (via issues or ~PRs) or in the [[Google Group|https://groups.google.com/g/tiddlywikiclassic]].

Things that we encourage you to do include:
* Reporting missing repos for the "explore and install" tab (repositories not yet indexed by EEP);
* Creating collections and indexing existing extensions (either yours or created by others);
* Asking questions about contributing and making it as simple as possible for others.`

config.shadowTiddlers[centralSourcesListName] = '//{{{\n' +
	JSON.stringify(config.macros.extensionsExplorer.defaultAvailableExtensions, null, 2) +
	'\n//}}}'
config.annotations[centralSourcesListName] =
	config.macros.extensionsExplorer.lingo.centralSourcesListAnnotation

// Add styles
const css = `
.actionsLabel, .actionsCell .button {
	padding: 0.2em;
	display: inline-block;
	border: none;
	white-space: normal;
}
td.actionsCell {
	padding: 0;
}

.actionsWrapper {
	white-space: nowrap;
}
.button.mainButton {
	padding-left: 0.7em;
}`

const shadowName = 'ExtensionsExplorerStyles'
if(!config.shadowTiddlers[shadowName]) {
	config.shadowTiddlers[shadowName] = css
	store.addNotification(shadowName, refreshStyles)
	store.addNotification("ColorPalette", function(_, doc) { refreshStyles(shadowName, doc) })
}
//}}}
/***
|Description|Adds an interface and hotkeys for jumping between tiddlers and more|
|Source     |https://github.com/YakovL/TiddlyWiki_JumpKeysPlugin/blob/master/JumpKeysPlugin.js|
|Author     |Yakov Litvin|
|Version    |1.2.2|
|License    |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
!!!Usage
The plugin works more or less like the tab switching in a browser: press {{{ctrl + j}}} or the "jump" command in tiddler toolbar to open the jumping interface and:
* hold {{{ctrl}}} and press {{{j}}} or ↑/↓ arrows to select a tiddler (if more than one is open);
* unhold {{{ctrl}}} or click a row to jump.
It also substitutes the jump toolbar command dropdown with the same jumper interface.

As a development of the idea, it also supports hotkeys for some other actions on the tiddler, selected in the jumping interface. Currently, they are:
* {{{x}}} to close the selected tiddler;
* {{{e}}} to edit it.
***/
//{{{
if(!config.jumper) config.jumper = {}
merge(config.jumper, {
	getOpenTiddlersData: function() {
		const list = []
		story.forEachTiddler(function(title, tiddlerElement) {
			list.push({
				title: title, element: tiddlerElement,
				isEditable: !!jQuery(tiddlerElement).has('.editor').length,
				isShadow: tiddlerElement.classList.contains('shadow'),
				isMissing: tiddlerElement.classList.contains('missing')
			})
		})
		this.sortInAccordWithTouchedTiddlersStack(list)
		return list
	},
	getOpenTiddlerDataByIndex: function(index) {
		const list = this.getOpenTiddlersData()
		if(index >= list.length || index < 0) return null
		return list[index]
	},
	jumpToAnOpenTiddler: function(index) {
		const tiddlerData = this.getOpenTiddlerDataByIndex(index)
		if(!tiddlerData) return

		// for compatibility with TiddlersBarPlugin
		if(config.options.chkDisableTabsBar !== undefined)
			story.displayTiddler(null, tiddlerData.title)
		if(tiddlerData.isEditable) {
			const $editor = jQuery(tiddlerData.element).find('.editor textarea')
			// works with CodeMirror as well!
			$editor.focus()
		} else {
			const $title = jQuery(tiddlerData.element).find('.title')
			if($title[0] && $title[0].tabIndex > -1) {
				$title.focus()
			} else {
				window.scrollTo(0, ensureVisible(tiddlerData.element))
				// remove focus from element edited previously
				// (also fixes a problem with handsontable that steals focus on pressing ctrl)
				// will be substitited with focusing an editor when one is to be focused
				if(document.activeElement) document.activeElement.blur()
			}
		}
		this.pushTouchedTiddler({ title: tiddlerData.title })
	},
	callCommand: function(toolbarCommandName, index) {
		const tiddlerData = this.getOpenTiddlerDataByIndex(index)
		if(!tiddlerData) return
		const command = config.commands[toolbarCommandName]
		if(!command || !command.handler) return

		// disable animation so that this methods finishes after closeTiddler etc finishes
		const chkAnimate = config.options.chkAnimate
		config.options.chkAnimate = false
		command.handler(null/*event*/, null/*src*/, tiddlerData.title)
		config.options.chkAnimate = chkAnimate
	},

	touchedTiddlersStack: [], // of { title: string }
	pushTouchedTiddler: function(tiddlerStackElement) {
		this.removeTouchedTiddler(tiddlerStackElement)
		this.touchedTiddlersStack.push(tiddlerStackElement)
	},
	removeTouchedTiddler: function(tiddlerStackElement) {
		this.touchedTiddlersStack = this.touchedTiddlersStack
			.filter(item => item.title != tiddlerStackElement.title)
	},
	sortInAccordWithTouchedTiddlersStack: function(itemsWithTitles) {
		for(var i = 0; i < this.touchedTiddlersStack.length; i++) {
			var touchedTitle = this.touchedTiddlersStack[i].title
			for(var j = 0; j < itemsWithTitles.length; j++)
				if(itemsWithTitles[j].title == touchedTitle)
					itemsWithTitles.unshift(
						itemsWithTitles.splice(j, 1)[0]
					)
		}
	},

	css: store.getTiddlerText("JumpKeysPlugin##Jumper styles", "")
		.replace("//{{{", "/*{{{*/").replace("//}}}", "/*}}}*/"),
	modalClass: 'jump-modal',
	itemClass: 'jump-modal__item',
	selectedItemClass: 'jump-modal__item_selected',
	modal: null,
	isJumperOpen: function() {
		return !!this.modal
	},
	showJumper: function() {
		const openTiddlersData = this.getOpenTiddlersData()
		if(openTiddlersData.length < 2) return false
		if(!this.isJumperOpen()) {
			// TODO: try "modal" element
			this.modal = createTiddlyElement(document.body, 'div', null, this.modalClass)
			this.refreshJumper()
			return true
		} else
			return false
		// return value indicates whether the modal was opened by this call
	},
	refreshJumper: function() {
		if(!this.isJumperOpen()) return
		const openTiddlersData = this.getOpenTiddlersData()
		const $modal = jQuery(this.modal)
			.empty()
		const list = createTiddlyElement(this.modal, 'div', null, this.modalClass + '__list')

		//# find where are we (inside an editor; focus inside tiddlerElement;
		//  scroll between .. and ..)
			for(let i = 0; i < openTiddlersData.length; i++) {
				var listItem = createTiddlyElement(list, 'div', null,
					this.itemClass + (i != 1 ? '' :
					' ' + this.selectedItemClass) +
					(openTiddlersData[i].isShadow ?
						' ' + this.itemClass + '_shadow' :
						openTiddlersData[i].isMissing ?
						' ' + this.itemClass + '_missing' : '') +
						(openTiddlersData[i].isEditable ? 
						' ' + this.itemClass + '_editable' : ''),
					openTiddlersData[i].title)
				listItem.onclick = () => {
					this.selectByIndex(i)
					this.hideJumperAndJump()
				}
			}
		//# or append list after forming
	},
	hideJumper: function() {
		if(!this.isJumperOpen()) return
		this.modal.parentElement.removeChild(this.modal)
		//# ..or hide? (keep isJumperOpen coherent)
		this.modal = null
	},

	isCtrlHold: false,
	getSelectedIndex: function() {
		if(!this.isJumperOpen() || !this.modal.firstElementChild) return -1
		return Array.from(this.modal.firstElementChild.children)
			.findIndex(option => option.classList.contains(this.selectedItemClass))
	},
	selectByIndex: function(index) {
		if(!this.isJumperOpen() || !this.modal.firstElementChild) return
		const list = this.modal.firstElementChild

		jQuery(list.children[this.getSelectedIndex()])
			.removeClass(this.selectedItemClass)
		const option = list.children[index]
		jQuery(option).addClass(this.selectedItemClass)

		const stickOutBottom = option.offsetTop + option.offsetHeight - list.offsetHeight
		const stickOutTop = list.scrollTop - option.offsetTop
		if(stickOutBottom > 0) list.scrollTop += stickOutBottom
		if(stickOutTop > 0)    list.scrollTop -= stickOutTop
	},
	selectPrev: function() {
		var currentIndex = this.getSelectedIndex()
		var optionsCount = this.getOpenTiddlersData().length
		this.selectByIndex((currentIndex - 1 + optionsCount) % optionsCount)
	},
	selectNext: function() {
		var currentIndex = this.getSelectedIndex()
		var optionsCount = this.getOpenTiddlersData().length
		this.selectByIndex((currentIndex + 1) % optionsCount)
	},
	hideJumperAndJump: function() {
		if(!this.isJumperOpen()) return

		const index = this.getSelectedIndex()
		this.jumpToAnOpenTiddler(index)
		this.hideJumper()
	},
	handleKeydown: function(e) {
		const self = config.jumper
		if(e.key === 'Control') self.isCtrlHold = true
		if(self.isCtrlHold) self.handleKeydownOnCtrlHold(e)
	},
	// next: make configurable via UI
	defaultCommandsKeys: {
		x: "closeTiddler",
		e: "editTiddler"
	},
	getCommandsKeys: function() {
		const json = store.getTiddlerText('JumpKeysSettings')
		try {
			return JSON.parse(json)
		} catch (error) {
			//# how/where to notify? ..probably after modifying JumpKeysSettings
			// return this.defaultCommandsKeys
		}
	},
	handleKeyup: function(e) {
		const self = config.jumper

		if(e.key === 'Control') {
			self.isCtrlHold = false
			self.hideJumperAndJump()
			return
		}

		const normalizedKeyCode = !e.originalEvent.code ? null :
			/^(Key)?(\w+)$/.exec(e.originalEvent.code)[2].toLowerCase()

		const commandsKeys = self.getCommandsKeys()
		if(self.isCtrlHold && self.isJumperOpen() && normalizedKeyCode in commandsKeys) {

			const index = self.getSelectedIndex()
			self.callCommand(commandsKeys[normalizedKeyCode], index)
			const numberOfOpen = self.getOpenTiddlersData().length
			if(numberOfOpen < 1) { // or < 2 ?
				self.hideJumper()
			} else {
				self.refreshJumper()
				self.selectByIndex(index < numberOfOpen ? index : index - 1)
			}
			if(e.preventDefault) e.preventDefault()
			return false // prevent _
		}
	},
	handleKeydownOnCtrlHold: function(e) {
		// make this work in different keyboard locale layouts:
		if(e.originalEvent.code == "KeyJ") {
			if(!this.showJumper()) this.selectNext()
			if(e.preventDefault) e.preventDefault()
			return false // prevent _
		}
		if(!this.isJumperOpen()) return

		switch(e.key) {
			case 'ArrowUp':   this.selectPrev(); break
			case 'ArrowDown': this.selectNext(); break
			case 'ArrowLeft': this.hideJumper(); break

			default: return
		}
		if(e.preventDefault) e.preventDefault()
		return false // prevent _
	},

	substituteJumpCommand: function() {
		config.commands.jump.type = null
		config.commands.jump.handler = function() {
			config.jumper.showJumper()
		}
	}
})

config.shadowTiddlers['JumpKeysStyleSheet'] = config.jumper.css
config.shadowTiddlers['JumpKeysSettings'] = JSON.stringify(config.jumper.defaultCommandsKeys, null, 2)

// reinstall-safe decorating and setting handlers
if(!config.jumper.orig_story_displayTiddler) {
	config.jumper.orig_story_displayTiddler = story.displayTiddler
	config.jumper.orig_editHandler = config.macros.edit.handler

	store.addNotification('JumpKeysStyleSheet', refreshStyles)
	store.addNotification("ColorPalette", (unused, doc) => refreshStyles('JumpKeysStyleSheet', doc))

	jQuery(document)
		.on('click', event => {
			const element = config.jumper.modal
			if (element && !element.contains(event.target))
				config.jumper.hideJumper()
		})
		//# these are not updated on reinstalling
		.on('keydown', config.jumper.handleKeydown)
		.on('keyup', config.jumper.handleKeyup)
	// avoid stucking ctrl as "hold" on ctrl + f etc
	//# doesn't seem to work anymore
	window.addEventListener('blur', () => config.jumper.isCtrlHold = false)

	config.jumper.substituteJumpCommand()
}

// a very simplistic implementation:
story.displayTiddler = function(srcElement, tiddler, template, animate, unused, customFields, toggle, animationSrc) {
	config.jumper.pushTouchedTiddler({
		title: (tiddler instanceof Tiddler) ? tiddler.title : tiddler
		//# ...: template == DEFAULT_EDIT_TEMPLATE
	})
	return config.jumper.orig_story_displayTiddler.apply(this, arguments)
}

// once an element is focused in a tiddler, add the latter to the history top
jQuery(document).on('focusin', '.tiddler', (event) => {
	const $tiddler = jQuery(event.currentTarget)
	config.jumper.pushTouchedTiddler({ title: $tiddler.attr('tiddler') })
})
//}}}
/***
!!!Jumper styles
//{{{
.jump-modal {
	position: fixed;
	top: 50vh;
	left: 50vw;
	transform: translate(-50%, -50%);
	z-index: 100;

	max-width: 80vw;
	max-height: 80vh;
	box-sizing: border-box;

	box-shadow: 1px 1px 10px #ccc;
	border-radius: 1em;
	background: [[ColorPalette::Background]];
	padding: 1em;
	display: flex;
}
.jump-modal__list {
	position: relative;
	overflow: auto;

	list-style: none;
	padding: 0;
	margin: 0;
}
.jump-modal__item {
	padding: 0.3em 0.8em;
	border-radius: 0.5em;
	margin-bottom: 0.5em;
	cursor: pointer;
}
.jump-modal__item_shadow {
	font-weight: bold;
	font-style: italic;
}
.jump-modal__item_missing {
	font-style: italic;
}
.jump-modal__item_editable {
	text-decoration: underline;
}
.jump-modal__item:hover {
	background: [[ColorPalette::SecondaryPale]];
}
.jump-modal__item_selected,
.jump-modal__item_selected:hover {
	background: [[ColorPalette::SecondaryLight]];
}
.darkMode .jump-modal__item:hover {
	background: rgba(0,0,255,0.35);;
}
.jump-modal__item:last-child {
	margin-bottom: 0;
}

.jump-modal ::-webkit-scrollbar {
	background-color: transparent;
	width: 1.5em;
}
.jump-modal ::-webkit-scrollbar-thumb {
	background: [[ColorPalette::TertiaryLight]];
	border-radius: 1em;
	width: 1em;
	border-left: 0.5em solid [[ColorPalette::Background]];
}
//}}}
!!!
***/
/***
|Author  |Yakov Litvin|
|Version |0.14.0|
|Status  |pre-release ([[demo|https://a11y.tiddlyhost.com]])|
|Requires|FoldTiddlerPlugin|
|~|↑ should be installed first, if present (it's not required; "example for custom commands from FoldTiddlerPlugin" can be put into a separate plugin instead)|
TODO:
* (+) retest focus after: edit, cancel, close, delete, new t, jump/links (in a tiddler; sidebar), more/less, fold/un~
** retest each on click and hotkey (should work now)
** problem: click on long tiddler → jump to top due to focus title → ?
* delivery: pre-release as KeyboardNavPrototype; merge into the JKP? ..or into the core?
later:
* add hotkeys to jump (move focus) to tiddlers/main menu/sidebar/backstage
* add arrow navigation (and more a11y) to popups, tabs, backstage; fix tab order for popups (and in backstage?)
* do we need aria-label s for navigation via (shift-)tab on screen readers?
* review editableSelector, extend (contenteditable, ..)
* add tabindex to insertEditable areas
* deal with ~pre elements by CM: they become selectable, too
* rewrite `jQuery(document).on('keydown'` with a delegated handler
* note: tiddler toolbar is after title, which makes navigatable via tab less intuitive
***/
//{{{
// 1. make tiddler titles focusable (by adding tabindex to them)
if(!story.orig_HNP_refreshTiddler) {
	story.orig_HNP_refreshTiddler = story.refreshTiddler
	story.refreshTiddler = function() {
		const tidElement = story.orig_HNP_refreshTiddler.apply(this, arguments)
		const title = jQuery(tidElement).find('.title')[0]
		if(title) title.tabIndex = 0
		return tidElement
	}
}

// 1.1 style the tiddlers with focused titles
const pluginName = "KeyboardNavPrototype"
const setStyles = () => {
	const css = store.getRecursiveTiddlerText(pluginName + "##CSS", "", 1)
	const sheetName = "StyleSheet" + pluginName
	setStylesheet(css, sheetName)
}
setStyles()
store.addNotification("ColorPalette", setStyles)

// 2.4 support extended hotkey format ('Ctrl+Shift+S')
const modifierKeys = ['ctrl', 'alt', 'shift', 'meta']
// doesn't validate stuff like 'Ctrl+Shift+Invalid', 'Ctrl+Shift+S+H'
const stringToHotkey = (hotkeyString) => {
	if(!hotkeyString || !hotkeyString.split) return null
	const parts = hotkeyString.split('+')

	const hotkey = {}
	for(const mod of modifierKeys) hotkey[mod] = false

	for(const part of parts) {
		const maybeModifier = part.toLowerCase()
		if(modifierKeys.includes(maybeModifier))
			hotkey[maybeModifier] = true
		else {
			// * if(hotkey.keycode) already, ... (throw? log? return null?)
			hotkey.keycode = part.length != 1 ? part :
				'Key' + part
		}
	}
	return hotkey
}
const isHotkeyMatch = (event, hotkey) => {
	if(!hotkey || !event) return false
	for(const mod of modifierKeys) {
		const hasEventMod = !!event[mod + 'Key']
		const hasHotkeyMod = !!hotkey[mod]
		if(hasEventMod != hasHotkeyMod) return false
	}

	return hotkey.keycode &&
		event.code.toLowerCase() == hotkey.keycode.toLowerCase()
}

// 2. define hotkeys for commands and after which ones to refocus
config.commands.closeTiddler.hotkeys = [{ scope: "tiddlerFocus", keys: 'KeyX' }]
config.commands.closeTiddler.tooltip += ' (press x to close the active tiddler)'
config.commands.deleteTiddler.hotkeys = [{ scope: "tiddlerFocus", keys: 'KeyD' },
	{ scope: "tiddlerEdit", keys: 'Ctrl+Shift+KeyD' }]
config.commands.deleteTiddler.tooltip += ' (press d to delete the active tiddler)'
for(const command of [
	'cancelTiddler', 'saveTiddler', 'closeTiddler', 'deleteTiddler'
]) {
	config.commands[command].shouldRetainFocus = true
}
// example for custom commands from FoldTiddlerPlugin (how to extend)
if(config.commands.collapseTiddler) {
	config.commands.collapseTiddler.hotkeys = [{ scope: "tiddlerFocus", keys: 'KeyF' }]
	config.commands.expandTiddler  .hotkeys = [{ scope: "tiddlerFocus", keys: 'KeyU' }]
	config.commands.collapseTiddler.shouldRetainFocus = true
	config.commands.expandTiddler  .shouldRetainFocus = true
}

// 1.2: when a title is in focus, ctrl+↑/↓ moves focus between tiddlers
//  (skipping the inner elements, unlike tab/shift-tab; ctrl is used to prevent problems with scolling a long tiddler by arrows)
const tiddlerSelector = '.tiddler'
const editableSelector = 'textarea, input'
jQuery(document).on('keydown', function($event) {
	const $focused = jQuery(document.activeElement)
	const $tiddler = $focused.closest(tiddlerSelector)
	const event = $event.originalEvent

	const isTiddlerInFocus = $tiddler.length &&
		!$focused.is(editableSelector)
	if(!isTiddlerInFocus) return

	// relies on tiddlers being siblings
	const $nextTiddler = $tiddler.next(tiddlerSelector).find('.title')
	const $prevTiddler = $tiddler.prev(tiddlerSelector).find('.title')

	if(isHotkeyMatch(event, stringToHotkey('Ctrl+ArrowDown'))) {
		if($nextTiddler.length) $nextTiddler.focus()
		return stopEvent(event)
	}
	if(isHotkeyMatch(event, stringToHotkey('Ctrl+ArrowUp'))) {
		if($prevTiddler.length) $prevTiddler.focus()
		return stopEvent(event)
	}

	if(!$focused.is(tiddlerSelector + ' .title')) return

// 2.2 support per-command hotkeys (but don't handle ctrl+f as f)
	for(const cName in config.commands) {
		const command = config.commands[cName]
		if(command.hotkeys && command.hotkeys.some(h => h.scope == "tiddlerFocus" &&
			isHotkeyMatch(event, stringToHotkey(h.keys)))
		) {
			$tiddler.find(`[commandname=${cName}]`).click()
		}
	}

// 2.3 support Enter → defaultCommand
	if(isHotkeyMatch(event, stringToHotkey('Enter'))) {
		$tiddler.find('.defaultCommand').click()
		return stopEvent(event)
	}
})

// 3: focus the containing tiddler on cancel/.., next/prev on close/delete
const focusTiddlerIfOpen = (title, fallbackTiddlerTitle) => {
	// jQuery('#' + story.tiddlerId(title) + ' .title') fails for titles containing ",", ">", etc
	const getByTitle = title => jQuery(document.getElementById(story.tiddlerId(title))).find('.title')
	let $tiddler = getByTitle(title)
	if(!$tiddler.length) $tiddler = getByTitle(fallbackTiddlerTitle)
	$tiddler.focus()
}
for(const cName in config.commands) {
	const cmd = config.commands[cName]
	if(!cmd.shouldRetainFocus || cmd.orig_HNP_handler) continue

	cmd.orig_HNP_handler = cmd.handler
	cmd.handler = function(event, src, title) {
		const $tiddler = jQuery(src).closest(tiddlerSelector)
		const $nextTiddler = $tiddler.next(tiddlerSelector)
		const $prevTiddler = $tiddler.prev(tiddlerSelector)
		const $fallbackTiddler = $nextTiddler.length ? $nextTiddler : $prevTiddler
		const fallbackTiddlerTitle = $fallbackTiddler.attr('tiddler') // may be undefined

		const result = cmd.orig_HNP_handler.apply(this, arguments)

		focusTiddlerIfOpen(title, fallbackTiddlerTitle)

		return result
	}
}
for(const actionName of ["onClickLess", "onClickMore"]) {
	const tb = config.macros.toolbar
	if(tb["orig_HNP_"+actionName]) continue

	tb["orig_HNP_"+actionName] = tb[actionName]
	tb[actionName] = function(event) {
		const result = tb["orig_HNP_"+actionName].apply(this, arguments)

		const $tiddler = jQuery(event.target).closest(tiddlerSelector)
		const title = $tiddler.attr('tiddler')
		if(title) focusTiddlerIfOpen(title)

		return result
	}
}

// 3.1 focus on jump, open link (note: focus is not visible yet, which may be fine for backward compatibility)
if(!window.orig_HNP_onClickTiddlerLink) {
	window.orig_HNP_onClickTiddlerLink = window.onClickTiddlerLink
	window.onClickTiddlerLink = function(ev) {
		orig_HNP_onClickTiddlerLink.apply(this, arguments)

		const link = resolveTarget(ev || window.event)
		focusTiddlerIfOpen(link.getAttribute("tiddlyLink"))

		// prevent click from getting handled by our "focus tiddler title" handler
		return stopEvent(ev)
	}
}

// 4. allow to navigate from textareas (and inputs) via ctrl+↓/↑ (tab is blocked when chkInsertTabs is enabled)
jQuery(document).on('keydown', editableSelector, function($event) {
	const isArrowDown = isHotkeyMatch($event.originalEvent, stringToHotkey('Ctrl+ArrowDown'))
	const isArrowUp = isHotkeyMatch($event.originalEvent, stringToHotkey('Ctrl+ArrowUp'))
	if(isArrowDown || isArrowUp) {
		// focus ~next focusable~
		// this is a partial solution to cover the simplest cases
		// if elaborating is needed, see github.com/e-hein/emulate-tab
		const focusableElements = document.querySelectorAll(
			(editableSelector + ', select, a[href], button, [tabindex]')
				.split(',').map(sel => sel + ':not([tabindex="-1"])').join(',')
		)
		const currentIndex = Array.from(focusableElements)
			.indexOf(document.activeElement)
		const shiftStep = isArrowDown ? +1 : -1
		if(currentIndex != -1) {
			const candidate = focusableElements[currentIndex + shiftStep]
			// timeout to apply after the global keydown handler
			if(candidate) setTimeout(() => candidate.focus(), 0)
		}
	}
})

// 5. support commands in tiddler edit mode (currently: same hotkey as in the view mode, but with ctrl+shift)
if(!Story.prototype.orig_HNP_onTiddlerKeyPress) {
	Story.prototype.orig_HNP_onTiddlerKeyPress = Story.prototype.onTiddlerKeyPress
	Story.prototype.onTiddlerKeyPress = function(event) {
		const $tiddler = jQuery(event.currentTarget)
		const isEditMode = $tiddler.attr('template') == 'EditTemplate'

// 5.1 in view mode, on escape move focus to the tiddler
		if(!isEditMode && isHotkeyMatch(event, stringToHotkey('Escape'))) {
			$tiddler.find('.title').focus()
			return stopEvent(event)
		}

		for(const cName in config.commands) {
			const command = config.commands[cName]
			if(command.hotkeys && command.hotkeys.some(h => h.scope == "tiddlerEdit" &&
				isHotkeyMatch(event, stringToHotkey(h.keys)))
			) {
				$tiddler.find(`[commandname=${cName}]`).click()
				return stopEvent(event.originalEvent)
			}
		}

		return Story.prototype.orig_HNP_onTiddlerKeyPress.apply(this, arguments)
	}
}

// 6. introduce global commands
config.commands.saveChanges = {
	handler: function() {
		saveChanges()
	},
	hotkeys: [{ scope: 'global', keys: 'Ctrl+S' }],
}

// why null: http://stackoverflow.com/questions/11000826/ctrls-preventdefault-in-chrome
jQuery(document).on('keydown', null, function($event) {
	const event = $event.originalEvent
	for(const cName in config.commands) {
		const command = config.commands[cName]
		if(command.hotkeys && command.hotkeys.some(h =>
			h.scope == 'global' && isHotkeyMatch(event, stringToHotkey(h.keys))
		)) {
			command.handler()
			return stopEvent(event)
		}
	}
})

// 7. extend core popups (__)
if(!config.options.chkDisableAugmentedPopups && !Popup.orig_HNP_show) {
	Popup.orig_HNP_show = Popup.show
	Popup.show = function(valign, halign, offset) {
		Popup.orig_HNP_show.apply(this, arguments)

		var curr = Popup.stack[Popup.stack.length - 1]
		const $focusableElements = jQuery(curr.popup).find(
			// TODO: reuse with focusableElements above; skip disabled
			(editableSelector + ', select, a[href], button, [tabindex]')
				.split(',').map(sel => sel + ':not([tabindex="-1"])').join(',')
		)

		const firstFocusable = $focusableElements[0]
		if(firstFocusable) firstFocusable.focus()
	}
}

// focus tiddler title on click on a tiddler
jQuery(document).on('click', tiddlerSelector, function($event) {
	const $tiddler = jQuery($event.currentTarget)
	const noFocusWithinTiddler = $tiddler.find(document.activeElement).length == 0
	const titleElement = $tiddler.find('.title')[0]
	const noTextIsSelected = !(window.getSelection && getSelection().toString())
	if(noFocusWithinTiddler /*&& noTextIsSelected*/ && titleElement)
		titleElement.focus({ preventScroll: true })
})

// fixup for Vivaldi and maybe others: add tabindex to buttons and links
if(!window.orig_HNP_createTiddlyButton) {
	window.orig_HNP_createTiddlyButton = createTiddlyButton
	window.createTiddlyButton = function() {
		const btn = orig_HNP_createTiddlyButton.apply(this, arguments)
		if(btn.getAttribute('tabindex') === null)
			btn.setAttribute('tabindex', 0)
		return btn
	}
}
if(!window.orig_HNP_createExternalLink) {
	window.orig_HNP_createExternalLink = createExternalLink
	window.createExternalLink = function() {
		const link = orig_HNP_createExternalLink.apply(this, arguments)
		if(link.getAttribute('tabindex') === null)
			link.setAttribute('tabindex', 0)
		return link
	}
}
//}}}
/***
/%
!CSS notes
* :focus for .popup a, #backstageButton a, etc are _copied_ from core :hover styles
* outline: none only in specific cases; using for .button:focus produces a risk of making focus "invisible"
!CSS
.tiddler:focus-within {
	outline: thin dashed currentColor;
	border-radius: 5px;
}

.title:focus {
	outline: none;
}

.popup a:focus {
	background: [[ColorPalette::SecondaryLight]];
	outline: none;
}
#backstageButton a:focus {
	background: [[ColorPalette::Foreground]];
	color: [[ColorPalette::Background]];
	outline: none;
}
.wizard .button:focus {
	background: [[ColorPalette::Background]];
	color: [[ColorPalette::Foreground]];
	outline: none;
}

.button:focus {
	color: [[ColorPalette::PrimaryDark]];
	background: [[ColorPalette::SecondaryLight]];
	border-color: [[ColorPalette::SecondaryMid]];
}
!%/
***/
<!--{{{-->
<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--}}}-->

<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>👨‍🦯</text></svg>">
/***
|Name        |ResponsiveThemePlugin|
|Description |A plugin + theme to make TW responsive (desktop/mobile)|
|Version     |0.6.3|
|Author      |Yakov Litvin|
|Source      |https://responsive.tiddlyhost.com#ResponsiveThemePlugin (won't work for checking/updating via EEP, will be changed)|
|License     |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
|PageTemplate|ResponsiveThemePlugin##Page Template|
|StyleSheet  |ResponsiveStyleSheet|
|Other config|[[TopLineMenuMiddle]] TopLineMenu|
!Installation & configuration
Install this as a usual plugin: copy, tag with {{{systemConfig}}}, save, reload. The only difference is that after the first reload, MarkupPreHead will be adjusted automatically, and to apply it, you have to save and reload again.

These parts can be edited to customize the theme:
* ResponsiveStyleSheet, StyleSheet are both applied, customizing the latter is preferrable
* TopLineMenu, TopLineMenuMiddle define the content in the top menu, customizing the latter (empty by default) is preferrable
As this is both a plugin and a theme, it's currently not possible to combine this with another theme.
!Page Template
<!--{{{-->
<header class='header' role='banner'>
  <div class='headerForeground'>
    <span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
    <span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
  </div>
</header>
<div id='topLineMenu' refresh='content' tiddler='TopLineMenu'></div>
<div class="body">
  <nav id='mainMenu' class='body__nav' role='navigation' refresh='content' tiddler='MainMenu'></nav>
  <main id='displayArea' class='body__main' role='main'>
    <div id='messageArea' class="messageArea"></div>
    <div id='tiddlerDisplay'></div>
  </main>
  <aside id='sidebar' class='body__sidebar'>
    <div id='sidebarOptions' role='navigation' refresh='content' tiddler='SideBarOptions'></div>
    <div id='sidebarTabs' role='complementary' refresh='content' force='true' tiddler='SideBarTabs'></div>
  </aside>
</div>
<!--}}}-->

!Code
***/
//{{{
config.options.txtTheme = 'ResponsiveThemePlugin'

// a fix: when name is set to TiddlerTitle##SectionName, should still find it
// TODO: move this to the core, add version check here
TiddlyWiki.prototype.notify = function(title, doBlanket)
{
	if(!this.notificationLevel) {
		for(var i = 0; i < this.namedNotifications.length; i++) {
			var n = this.namedNotifications[i];

			var nTitle = n.name
			if(nTitle) {
				var separatorIndex = nTitle.indexOf(config.textPrimitives.sectionSeparator)
				if(separatorIndex > -1) nTitle = nTitle.substring(0, separatorIndex)
			}

			if((n.name == null && doBlanket) || (nTitle == title))
				n.notify(n.name || title);
		}
	}
};

var isInstalled = !!document.getElementById('fullContentWrapper')

if(!isInstalled) {
	// Wrap backstage elements into a single element #backstageWrapper,
	// wrap #backstageWrapper and #contentWrapper into #fullContentWrapper
	// (as contentWrapper's innerHTML is defined in refreshPageTemplate, we can't just move backstageWrapper inside contentWrapper, so we create a common wrapper instead)
	const commonWrapper = createTiddlyElement(null, 'div', 'fullContentWrapper')
	const backstageWrapper = createTiddlyElement(commonWrapper, 'div', 'backstageWrapper')
	const contentWrapper = document.getElementById('contentWrapper')

	contentWrapper.parentNode.insertBefore(commonWrapper, contentWrapper)
	commonWrapper.appendChild(contentWrapper)

	for(let id of ['backstageButton', 'backstageArea', 'backstage', 'backstageCloak']) {
		// move inside wrapper
		backstageWrapper.appendChild(document.getElementById(id))
	}


	// make sure editing StyleSheet for custom styles causes updating css at once
	store.addNotification("StyleSheet", function(title, doc) {
		refreshStyles("ResponsiveStyleSheet", doc)
	})
}

const viewportHtml = '<meta name="viewport" content="width=device-width, initial-scale=1" />'
const preHeadMarkup = store.getTiddlerText('MarkupPreHead')
if(preHeadMarkup.indexOf(viewportHtml) == -1) {
	const preHeadTiddler = store.fetchTiddler('MarkupPreHead') || new Tiddler('MarkupPreHead')
	const closeMarkerPosition = preHeadMarkup.indexOf('<!--}}}-->')
	// TODO: test
	preHeadTiddler.text = closeMarkerPosition == -1 ? preHeadMarkup + '\n' + viewportHtml
		: preHeadMarkup.substring(0, closeMarkerPosition) + viewportHtml + '\n'
		+ preHeadMarkup.substring(closeMarkerPosition)
	store.saveTiddler(preHeadTiddler)
}

// close menus on click elsewhere
jQuery('body').on('click', function(event) {
	// except on mobile
	if(!window.matchMedia || !window.matchMedia("(max-width: 768px)").matches) return

	const $mainMenu = jQuery('#mainMenu')
	const $sidebar = jQuery('#sidebar')
	if(!isDescendant(event.target, $mainMenu[0])) $mainMenu.hide()
	if(!isDescendant(event.target, $sidebar[0])) $sidebar.hide()
})
//}}}

// /%
/***
!ResponsiveStyleSheet
***/
///*{{{*/
//body {
//	/* prevent scroll on backstage clock, right? ..better set width by JS instead
//	overflow-x: hidden; */
//	/* increased compared to core; should be increased further */
//	font-size: .8em;
//}
//
//#fullContentWrapper {
//	box-shadow: 0px 1px 4px [[ColorPalette::TertiaryMid]];
//	position: relative;
//	/* to position #messageArea, see https://stackoverflow.com/a/67776640/3995261
//	contain: content; */
//
//	max-width: 80em;
//	margin-inline-start: auto;
//	margin-inline-end: auto;
//}
//
//#backstageWrapper {
//	position: absolute;
//	top: 0;
//	left: 0;
//	right: 0;
//}
//#contentWrapper {
//	min-height: 100vh;
//	background: [[ColorPalette::Background]];
//}
//#backstageCloak {
//	/* from https://css-tricks.com/full-bleed/ */
//	width: 100vw;
//	left: 50%;
//	right: 50%;
//	margin-left: -50vw;
//	margin-right: -50vw;
//	top: 0;
//}
//
//#backstagePanel {
//	width: unset;
//	/* 0 auto  doesn't work here */
//	margin: 0;
//}
//
//.header {
//	background: -moz-linear-gradient(to bottom, [[ColorPalette::PrimaryLight]], [[ColorPalette::PrimaryMid]]);
//	background: linear-gradient(to bottom, [[ColorPalette::PrimaryLight]], [[ColorPalette::PrimaryMid]]);
//}
//.headerForeground {
//	padding: 3em 1em 1em;
//	position: relative;
//	text-shadow: -1px -1px [[ColorPalette::Foreground]];
//}
//.siteTitle {
//	/* decreased compared to the core; may be decreased more */
//	font-size: 2.5em;
//}
//@media (max-width: 768px) {
//	.siteTitle { font-size: 1.8em; }
//}
//
//#topLineMenu {
//	position: sticky;
//	top: 0;
//	z-index: 1;
//	box-shadow: 0px 1px 4px [[ColorPalette::TertiaryMid]];
//	background: [[ColorPalette::Background]];
//	padding: .5em;
///*padding-block-start: .5em;
//padding-inline-start: .5em;
//inline-size: max-content;*/
//}
//.topLineMenu__wrapper {
//	display: flex;
//	align-items: center;
//}
//.topLineMenu__center {
//	flex: 1;
//	text-align: center;
//	padding: 0 1em;
//}
//#topLineMenu a.button {
//	padding: 0.3em 0.5em;
//}
//
///* remake columns using flex */
//.body {
//	display: flex;
//	/* to allow MainMenu hover */
//	position: relative;
//}
///* undoing styles for body__nav, body__sidebar; body__main */
//#mainMenu, #sidebar {
//	position: relative;
//}
//#mainMenu {
//	padding: 1em;
//}
//#sidebar {
//	margin-inline-start: 1em;
//	padding-block-start: 1em;
//	font-size: 1em;
//}
//#sidebarOptions {
//	/* overwriting defaults */
//	padding-top: 0;
//}
//#displayArea {
//	/* prevent stretching the body horizontally (by code blocks etc), idea from:
//	   https://chaiyihein.medium.com/fixing-flexbox-child-element-overflows-the-power-of-min-width-1c7af87314da */
//	min-width: 0;
//	margin: 0;
//}
//.body__main {
//	flex: 1;
//}
//
//@media (max-width: 768px) {
//	#mainMenu, #sidebar {
//		display: none;
//		position: absolute;
//		top: 0;
//		bottom: 0;
//		background: [[ColorPalette::Background]];
//		box-shadow: 1px 2px 5px [[ColorPalette::TertiaryMid]];
//		/* height: ?? (full?) stick to the top? */
//	}
//	#mainMenu {
//		left: 0;
//	}
//	#sidebar {
//		right: 0;
//	}
//}
//
//.button_button {
//	background: [[ColorPalette::Background]];
//	border: 1px solid [[ColorPalette::SecondaryMid]];
//	padding: 0;
//	line-height: 1;
//	border-radius: 5px;
//}
//.button_button svg {
//	vertical-align: middle;
//	/* accessability */
//	min-width: 24px;
//	min-height: 24px;
//}
//.button__shape {
//	fill: none;
//	stroke: [[ColorPalette::Foreground]];
//	stroke-width: 3;
//}
//
//[[StyleSheet]]
///*}}}*/
/***
!end of ResponsiveStyleSheet
***/
// %/ //
//{{{
;(function() {
var cssName = "ResponsiveStyleSheet",
    css = store.getTiddlerText("ResponsiveThemePlugin" + "##" + cssName).replace(/^\/\//gm, "");
css = css.substring(5, css.length - 5); // cut leading \n***/ and trailing /***\n of the section
config.shadowTiddlers[cssName] = css;
})();
//}}}
// /%
/***
!TopLineMenu
***/
//{{topLineMenu__wrapper{
//<html>
//    <button class="button button_button" title="toggle main menu" onclick='
//	const $nav = jQuery(".body__nav")
//	$nav.is(":hidden") ? $nav.show() : $nav.hide()
//	if(event && event.stopPropagation) event.stopPropagation()
//    '>
//      <svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 100 100">
//	<rect x="20" width="60" y="20" height="10" rx="5" ry="5" class="button__shape" />
//	<rect x="20" width="60" y="45" height="10" rx="5" ry="5" class="button__shape" />
//	<rect x="20" width="60" y="70" height="10" rx="5" ry="5" class="button__shape" />
//      </svg>
//    </button>
//</html>
//{{topLineMenu__center{
//<<tiddler [[TopLineMenuMiddle]]>>}}}
//<html>
//    <button class="button button_button" title="toggle sidebar" onclick='
//	const $nav = jQuery(".body__sidebar")
//	$nav.is(":hidden") ? $nav.show() : $nav.hide()
//	if(event && event.stopPropagation) event.stopPropagation()
//    '>
//      <svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 100 100">
//	<circle cx="50" cy="25" r="7" class="button__shape" />
//	<circle cx="50" cy="50" r="7" class="button__shape" />
//	<circle cx="50" cy="75" r="7" class="button__shape" />
//      </svg>
//    </button>
//</html>
//}}}
/***
!end of TopLineMenu
***/
// %/ //
//{{{
;(function() {
var cssName = "TopLineMenu",
    css = store.getTiddlerText("ResponsiveThemePlugin" + "##" + cssName).replace(/^\/\//gm, "");
css = css.substring(5, css.length - 5); // cut leading \n***/ and trailing /***\n of the section
config.shadowTiddlers[cssName] = css;
})();
//}}}
TODO:
* .oO keyboard nav for backstage, sidebar, main menu, topline (custom) menu
* template for TH? (↓) ..also save on blur?
done: add [[EEP|ExtensionsExplorerPlugin]], [[DMP|DarkModePlugin]], [[JKP|JumpKeysPlugin]]; [[responsive|ResponsiveThemePlugin]], [[better|ThostGoodSavingPlugin]] [[saving|ShowUnsavedPluginPatched]][[*|https://yllab.tiddlyhost.com/]] (for now, manually)
/***
|Description|highlights saving button (bold red by default) and the document title (adds a leading "*") when there are unsaved changes (also toggles {{{hasUnsavedChanges}}} class of the root element for hackability)|
|Version|1.5.1'|
|Author|Yakov Litvin|
|Source|https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js|
|License|[[MIT|https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/LICENSE]]|
<<option chkShowDirtyStory>> show unsaved if any tiddler is opened for editing
Styles applied to unsaved TW can be adjusted in StyleSheetUnsaved
***/
//{{{
config.macros.showDirtyPlugin = {
	// styles that highlight save button when there's something to save
	showDirtyCss: ".saveChangesButton { font-weight: bold; color: red !important; }",
	styleSheetName: "suggestSavingOnDirty",
	containerClassName: "hasUnsavedChanges",
	showDrity: function(dirty) {
		const css = store.getTiddlerText('StyleSheetUnsaved')
		if(dirty) {
			jQuery('html').addClass(this.containerClassName)
			setStylesheet(css, this.styleSheetName)
			document.title = "*" + getPageTitle()
		} else {
			jQuery('html').removeClass(this.containerClassName)
			removeStyleSheet(this.styleSheetName)
			document.title = getPageTitle()
		}
	},
	checkDirty: function() {
		return store.isDirty() ||
			(config.options.chkShowDirtyStory && story.areAnyDirty())
	},
	init: function() {
		config.shadowTiddlers.StyleSheetUnsaved = this.showDirtyCss

		// add the "saveChangesButton" class to the save changes button
var macro = config.macros.thostUpload || config.macros.saveChanges
		macro.SCM_orig_handler = macro.handler
		macro.handler = function(place, macroName, params) {
			this.SCM_orig_handler.apply(this, arguments)
			place.lastChild.classList.add("saveChangesButton")
		}

		// regularly check and indicate unsaved
		setInterval(function() {
			const isDirty = config.macros.showDirtyPlugin.checkDirty()
			config.macros.showDirtyPlugin.showDrity(isDirty)
		}, 500)
	}
}
//}}}
proof-of-concept demos
A11y for TWC
/***
|Version|0.1.0|
allows to quickly set the favicon using an (ugly) input ({{{<<faviconSymbol>>}}} macro): <<faviconSymbol>> (applies on enter)
***/
//{{{
config.macros.faviconSymbol = {
	// UI prototype:
	handler: function(place, macroName, params/*, wikifier, paramString, tiddler*/) {
		var value = this.getUsedFavicon() || ''
		var attributes = {
			placeholder: 'insert emoji and hit Enter'
		}
		var editor = createTiddlyElement(place, 'input', null, null, null, attributes)
		var setFavicon = this.setFavicon.bind(this)
		jQuery(editor)
		  .val(value)
		  .on('keyup', function(event) {
			if(event.key.toLowerCase() === 'enter') {
				// TODO: leave just 1 ~symbol, including emoji 💡
				var char = editor.value ? editor.value/*[0]*/ : ''
				setFavicon(char)
				displayMessage('set favicon to ' + char)
			}
		})
		// TODO: *add autosuggestions
	},

	getUsedFavicon: function() {
		var markupPreHead = this.getMarkupTiddlerText()
		if(!markupPreHead) return null
		var existingBit = this.findFaviconInMarkup(markupPreHead)
		return existingBit ? existingBit.iconText : null
	},
	setFavicon: function(iconChar) {
		var markupPreHead = this.getMarkupTiddlerText()
		if(markupPreHead !== null) var existingBit = this.findFaviconInMarkup(markupPreHead)

		if(markupPreHead === null || !existingBit) {
			// TODO: don't just append, insert before \n<!--}}}-->
			this.setMarkupTiddlerText(this.getMarkupText() +
				'\n' + this.getFaviconMarkup(iconChar))
		} else {
			var newMarkup = markupPreHead.substring(0, existingBit.markupStart) +
				this.getFaviconMarkup(iconChar) +
				markupPreHead.substring(existingBit.markupEnd)
			this.setMarkupTiddlerText(newMarkup)
		}
	},
	setMarkupTiddlerText: function(newText) {
		var tiddler = store.fetchTiddler('MarkupPreHead') || new Tiddler('MarkupPreHead')
		// without setting second param, .saveTiddler tries to save a tiddler with an empty title
		store.saveTiddler('MarkupPreHead', 'MarkupPreHead', newText, 'SymbolsPlugin', new Date())
	},
	getMarkupTiddlerText: function() {
		var tiddler = store.fetchTiddler('MarkupPreHead')
		return tiddler ? tiddler.text : null
	},
	// tiddler or shadow
	getMarkupText: function() {
		return store.getTiddlerText('MarkupPreHead')
	},

	faviconMarkupStart: '<link rel="icon" href="data:image/svg+xml,' +
		'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="80">'.replace(/"/g,'%22'),
	faviconMarkupEnd: '</text></svg>">',
	// ..where's the link to the tweak source?
	// TODO: report again: doesn't work in Vivaldi desktop yet (5.7)
	getFaviconMarkup: function(iconChar) {
		return this.faviconMarkupStart +
				iconChar +
			this.faviconMarkupEnd
	},
	findFaviconInMarkup: function(markup) {
		var startIndex = markup.indexOf(this.faviconMarkupStart)
		if(startIndex === -1) return null
		var endPartIndex = markup.indexOf(this.faviconMarkupEnd, startIndex + this.faviconMarkupStart.length)
		if(endPartIndex === -1) return null
		return {
			iconText: markup.substring(startIndex + this.faviconMarkupStart.length, endPartIndex),
			markupStart: startIndex,
			markupEnd: endPartIndex + this.faviconMarkupEnd.length
		}
	}
}
//}}}
/***
|Version|0.4.0|
|Source|https://yllab.tiddlyhost.com#ThostGoodSavingPlugin|
|Requires|ThostUploadPlugin|
|~|should be run after ThostUploadPlugin to update SideBarOptions properly|
should we make "save to tiddlyhost" have the same label as the usual button ("save changes")?
* test download: <<downloadMain>>
* test upload: <<uploadMain>>
***/
//{{{
// keep for downloading functionality
config.orig_tHost_saveChanges = window.saveChanges

window.saveChanges = function(onlyIfDirty, tiddlers) {
	config.macros.thostUpload.action()
}

config.macros.downloadMain = {
	handler: function(place, macroName, params, wikifier, paramString, tiddler) {
		const label = "save from web"
		const tooltip = "download this TiddlyWiki"
		createTiddlyButton(place, label, tooltip, function() {
			config.orig_tHost_saveChanges()
		})
	}
}

config.macros.uploadMain = {
	handler: function(place, macroName, params, wikifier, paramString, tiddler) {
		const label = "upload"
		const tooltip = "upload a TiddlyWiki (a backup) – it will substitute the current one; to see the changes, reload the page"
		createTiddlyButton(place, label, tooltip, function() {
			config.macros.uploadMain.upload()
		})
	},
	getFileText: async function() {
		if(window.showOpenFilePicker) {
			const pickerOptions = { types: [ { accept: { 'text/html': ['.html', '.hta'] } } ] }
			const handles = await window.showOpenFilePicker(pickerOptions)
			if(!handles[0]) return null
			const file = await handles[0].getFile()
			return await file.text()
		} else {
			const tempFileInput = document.createElement('input')
			tempFileInput.type = "file"
			tempFileInput.style.display = "none"
			document.body.appendChild(tempFileInput)
			const cleanUp = () => document.body.removeChild(tempFileInput)

			return new Promise((resolve, reject) => {
				tempFileInput.addEventListener('change', function handleFileSelect(event) {
					const selectedFile = event.target.files[0]
					if(selectedFile) {
						const reader = new FileReader()
						reader.onload = function (fileEvent) {
							cleanUp()
							resolve(fileEvent.target.result)
						}
						reader.readAsText(selectedFile)
					} else {
						cleanUp()
						resolve(null)
					}
				})
				tempFileInput.click()
			})
		}
	},
	upload: async function() {
		const newHtml = await this.getFileText()
		if(!newHtml) return alert("Looks like the file were not picked or it's empty")

		// this will help if one tries to upload a non-TW file
		const posDiv = locateStoreArea(newHtml)
		if((posDiv[0] == -1) || (posDiv[1] == -1)) return alert(config.messages.invalidFileError.format([localPath]))

		displayMessage("Started uploading...")
		const uploadParams = ['https://' + config.options.txtThostSiteName + '.tiddlyhost.com']
		bidix.thostUpload.httpUpload(uploadParams, newHtml, function reportResult(status, params, responseText, url, xhr) {
			if(status) {
				displayMessage(bidix.thostUpload.messages.mainSaved)
				store.setDirty(false)
				displayMessage("To see the changes applied, reload the page")
			} else {
				alert(bidix.thostUpload.messages.mainFailed)
				displayMessage(bidix.thostUpload.messages.mainFailed)
			}
		}, uploadParams)
	}
}

// TODO: decide if using backstage instead is more proper
config.shadowTiddlers.SideBarOptions = config.shadowTiddlers.SideBarOptions.replace(
	/(<<saveChanges>><<thostUpload>>)/,
	"<<thostUpload>><<downloadMain>>")
//}}}
/***
|Name         |ThostUploadPlugin |
|Description  |Support saving to Tiddlyhost.com |
|Version      |1.0.1 |
|Date         |March 06, 2021 |
|Source       |https://github.com/tiddlyhost/tiddlyhost-com/tree/main/rails/tw_content/plugins |
|Author       |BidiX, Simon Baird, Yakov Litvin |
|License      |BSD open source license |
|~CoreVersion |2.9.2 |
***/
//{{{

version.extensions.ThostUploadPlugin = { major: 1, minor: 0, revision: 1 };

//
// Environment
//

if (!window.bidix) window.bidix = {};

// To change these defaults, create a tiddler named "ThostOptions" with tag
// "systemConfig" and the following content:
// window.bidix = { "editModeAlways": false, "uploadButtonAlways": false };

// Set false if you want the chkHttpReadOnly cookie to decide whether to
// render in read-only mode or edit mode when you're not logged in or when
// the site is being viewed by others. Default true.
if (!("editModeAlways" in bidix)) { bidix.editModeAlways = true; }

// Set false to hide the "upload to tiddlyhost" button when you're not logged
// in or when the site is being viewed by others. Default true.
if (!("uploadButtonAlways" in bidix)) { bidix.uploadButtonAlways = true; }

// For debugging. Default false.
if (!("debugMode" in bidix)) { bidix.debugMode = false; }

//
// Upload Macro
//

config.macros.thostUpload = {
  handler: function(place,macroName,params) {
    createTiddlyButton(place, "save to tiddlyhost",
      "save this TiddlyWiki to a site on Tiddlyhost.com",
      this.action, null, null, this.accessKey);
  },

  action: function(params) {
    var siteName = config.options.txtThostSiteName.trim();
    if (!siteName) {
      alert("Tiddlyhost site name is missing!");
      clearMessage();
    }
    else {
      bidix.thostUpload.uploadChanges('https://' + siteName + '.tiddlyhost.com');
    }
    return false;
  }
};

//
// Upload functions
//

if (!bidix.thostUpload) bidix.thostUpload = {};

if (!bidix.thostUpload.messages) bidix.thostUpload.messages = {
  invalidFileError: "The original file '%0' does not appear to be a valid TiddlyWiki",
  mainSaved: "Main TiddlyWiki file uploaded",
  mainFailed: "Failed to upload main TiddlyWiki file. Your changes have not been saved",
  loadOriginalHttpPostError: "Can't get original file",
  aboutToSaveOnHttpPost: 'About to upload on %0 ...',
  storePhpNotFound: "The store script '%0' was not found."
};

bidix.thostUpload.uploadChanges = function(storeUrl) {
  var callback = function(status, uploadParams, original, url, xhr) {
    if (!status) {
      displayMessage(bidix.thostUpload.messages.loadOriginalHttpPostError);
      return;
    }
    if (bidix.debugMode) {
      alert(original.substr(0,500)+"\n...");
    }

    var posDiv = locateStoreArea(original);
    if ((posDiv[0] == -1) || (posDiv[1] == -1)) {
      alert(config.messages.invalidFileError.format([localPath]));
      return;
    }

    bidix.thostUpload.uploadMain(uploadParams, original, posDiv);
  };

  clearMessage();

  // get original
  var uploadParams = [storeUrl];
  var originalPath = document.location.toString();
  var dest = 'index.html';
  displayMessage(bidix.thostUpload.messages.aboutToSaveOnHttpPost.format([dest]));

  if (bidix.debugMode) {
    alert("about to execute Http - GET on "+originalPath);
  }

  var r = doHttp("GET", originalPath, null, null, null, null, callback, uploadParams, null);

  if (typeof r == "string") {
    displayMessage(r);
  }

  return r;
};

bidix.thostUpload.uploadMain = function(uploadParams, original, posDiv) {
  var callback = function(status, params, responseText, url, xhr) {
    if (status) {
      displayMessage(bidix.thostUpload.messages.mainSaved);
      store.setDirty(false);
    }
    else {
      alert(bidix.thostUpload.messages.mainFailed);
      displayMessage(bidix.thostUpload.messages.mainFailed);
    }
  };

  var revised = updateOriginal(original, posDiv);
  bidix.thostUpload.httpUpload(uploadParams, revised, callback, uploadParams);
};

bidix.thostUpload.httpUpload = function(uploadParams, data, callback, params) {
  var localCallback = function(status, params, responseText, url, xhr) {
    if (xhr.status == 404) {
      alert(bidix.thostUpload.messages.storePhpNotFound.format([url]));
    }

    var saveNotOk = responseText.charAt(0) != '0';

    if (bidix.debugMode || saveNotOk) {
      alert(responseText);
    }

    if (saveNotOk) {
      status = null;
    }

    callback(status, params, responseText, url, xhr);
  };

  // do httpUpload
  var boundary = "---------------------------"+"AaB03x";
  var uploadFormName = "UploadPlugin";
  // compose headers data
  var sheader = "";
  sheader += "--" + boundary + "\r\nContent-disposition: form-data; name=\"";
  sheader += uploadFormName +"\"\r\n\r\n";
  sheader += "backupDir=x" +
        ";user=x" +
        ";password=x" +
        ";uploaddir=x";
  if (bidix.debugMode) {
    sheader += ";debug=1";
  }
  sheader += ";;\r\n";
  sheader += "\r\n" + "--" + boundary + "\r\n";
  sheader += "Content-disposition: form-data; name=\"userfile\"; filename=\"index.html\"\r\n";
  sheader += "Content-Type: text/html;charset=UTF-8" + "\r\n";
  sheader += "Content-Length: " + data.length + "\r\n\r\n";
  // compose trailer data
  var strailer = "";
  strailer = "\r\n--" + boundary + "--\r\n";
  data = sheader + data + strailer;
  if (bidix.debugMode) {
    alert("about to execute Http - POST on " + uploadParams[0]+ "\n with \n" + data.substr(0,500) + " ... ");
  }
  var r = doHttp("POST", uploadParams[0], data,
    "multipart/form-data; ;charset=UTF-8; boundary=" + boundary, 'x','x', localCallback, params, null);

  if (typeof r == "string") {
    displayMessage(r);
  }

  return r;
};

// a fix for versions before 2.9.2 (updateOriginal used conversions irrelevant for Tiddlyhost)
convertUnicodeToFileFormat = function(s) { return s };

//
// Site config
//

bidix.initOption = function(name,value) {
  if (!config.options[name]) {
    config.options[name] = value;
  }
};

merge(config.optionsDesc, {
  txtThostSiteName: "Site name for uploads to Tiddlyhost.com",
});

bidix.initOption('txtThostSiteName','a11y');

//
// Tiddlyhost stuff
//

bidix.ownerLoggedIn = (config.shadowTiddlers.TiddlyHostIsLoggedIn &&
  config.shadowTiddlers.TiddlyHostIsLoggedIn == "yes")

if (bidix.editModeAlways || bidix.ownerLoggedIn) {
  // If user is logged in to Tiddlyhost and viewing their own site then
  // we disregard the original value of the chkHttpReadOnly cookie
  config.options.chkHttpReadOnly = false
  // window.readOnly gets set before plugins are loaded, so we need to
  // set it here to make sure TW is editable, unlike window.showBackstage
  // which is set after
  window.readOnly = false
}

if (bidix.uploadButtonAlways || bidix.ownerLoggedIn) {
  // Add the 'save to tiddlyhost' button after the regular save button
  config.shadowTiddlers.SideBarOptions = config.shadowTiddlers.SideBarOptions
    .replace(/(<<saveChanges>>)/,"$1<<thostUpload>>");
}

//}}}
* Keyboard navigation provided by KeyboardNavPrototype, including:
** Focusing tiddlers and navigating between them with {{{ctrl+↑/↓}}} (also {{{esc}}} moves focus from an element inside a tiddler to the "tiddler", re-enabling this navigation);
** Hotkeys to edit a tiddler (enter when in focus), support for arbitrary commands (like {{{x}}} for closing in view mode or {{{ctrl+shift+d}}} in edit mode and {{{d}}} in view mode);
** Support for focus when following a link (including jumping), saving tiddler changes, canceling, closing, or deleting them;
** Both hotkeys and "focus after command" can be set for custom commands, too;
** "Global" commands are also supported, and as an example, the {{{saveChanges}}} command is added with the {{{Ctrl+S}}} hotkey;
** To avoid "trapping" of focus within textareas, {{{ctrl+↑/↓}}} now also move focus from one input field to another, if currently is in one (i.e. you can jump between title/text/tags editors, etc).
* To focus a tiddler in the first place, click on it (but not on an interactive element in it), tab to it, or follow a link.
* Jumping between tiddlers and more from JumpKeysPlugin:
** {{{ctrl + j}}} to start jumping (works similarly to {{{atl+tab}}} or {{{ctrl+tab}}}: hold {{{ctrl}}} and repeat {{{j}}} to move more, can also use {{{↑/↓}}} until {{{ctrl}}} is released);
** For other hotkeys, see plugin [[description|JumpKeysPlugin]].
|Source |https://github.com/YakovL/TiddlyWiki_YL_ExtensionsIndex/blob/master/YLExtensionsCollection.txt|
|Version|0.2.7|
//{{{
[
  {
    "url": "https://github.com/YakovL/TiddlyWiki_YL_ExtensionsIndex/blob/master/YLPrereleasesAndExperimentsCollection.txt",
    "description": "Pre-releases and experiments by Yakov Litvin",
    "type": "collection"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_DisqusPlugin/blob/master/DisqusPlugin.js",
    "description": "Add Disqus threads (comments) to your tiddlers"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_HandsontablePlugin/blob/master/HandsontablePlugin.js",
    "description": "Create spreadsheet-like tables with inline editing and keyboard hotkeys for manipulating rows and columns"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_EncryptionPlugin/blob/master/EncryptionPlugin.js",
    "description": "Save selected parts of content with encryption"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_TwFormulaPlugin/blob/master/TwFormulaPlugin.js",
    "description": "Render beautiful formulas using LaTeX syntax (also provides WYSIWYGish editing with MathQuill)"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_GraphTools/blob/master/VisGraphPlugin.js",
    "description": "View and edit graphs in a WYSIWYGish manner with the <<graph>> macro (see additional installation steps inside the plugin)"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_JumpKeysPlugin/blob/master/JumpKeysPlugin.js",
    "description": "Jump between tiddlers and do more via hotkeys and a UI similar to what browsers use for tabs"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_QuickSavePlugin/blob/master/QuickSavePlugin.js",
    "description": "Save changes via Ctrl + S hotkey (without getting browser default action)"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ContinuousSavingPlugin/blob/main/ContinuousSavingPlugin.js",
    "description": "Get loading and saving work via just one file picking per session"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js",
    "description": "See when TW has unsaved changes: in the tab/window title (adds '*'), on the saveChanges button (bold red), and more"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_DarkModePlugin/blob/master/DarkModePlugin.js",
    "description": "Introduces \"dark mode\" (styles) and switching it by the darkMode macro and operating system settings"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_TiddlerInFilePlugin/blob/master/TiddlerInFilePlugin.js",
    "description": "Store specific tiddlers as external files and more"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ExtraFilters/blob/master/ExtraFiltersPlugin.js",
    "description": "Various additional filters, including 'all', 'and', 'not', 'tagTree', 'unclassified', 'taggedOnly', 'hasPart', 'from', 'sortByText', and more"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_FromPlaceToPlacePlugin/blob/master/FromPlaceToPlacePlugin.js",
    "description": "Open a tiddler or a page in place of the current one (as opposed to opening in addition) via hotkeys"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_SimplifiedUpgradingPlugin/blob/master/SimplifiedUpgradingPlugin.js",
    "description": "Get core upgrading work with savers with I/O limitations, like Timimi, Tiddloid, or MTS 1.7.0 and above; optionally get notified on start if an upgrade is available; have upgrading more git-friendly"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsExplorerPlugin.js",
    "description": "Get notified about extensions updates; explore, install and update extensions"
  },
  {
    "url": "https://yakovl.github.io/TiddlyWiki_SharedTiddlersPlugin/#SharedTiddlersPlugin",
    "description": "Load and use tiddlers from other TiddlyWikis (both content and settings, optionally with importing them)"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ImageGalleries/blob/master/FancyBox2Plugin.js",
    "description": "Create image galleries"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_MarkdeepPlugin/blob/master/MarkdeepPlugin.js",
    "description": "Create diagrams using the Markdeep syntax"
  }
]
//}}}